From 55fd561ee4e4c5899e2ca1dfd7b66f6d5d67415a Mon Sep 17 00:00:00 2001 From: nlisgo Date: Tue, 14 Jan 2020 12:39:45 +0000 Subject: [PATCH 01/35] Change blanknodes to namednodes --- src/adaptors/in-memory-articles.ts | 14 +++++----- src/articles.ts | 12 ++++----- src/errors/article-not-found.ts | 6 ++--- src/routes/add-article.ts | 8 +++--- src/routes/article-list.ts | 4 +-- test/adaptors/in-memory-articles.test.ts | 34 ++++++++++++------------ test/create-article.ts | 4 +-- test/errors/article-not-found.test.ts | 10 +++---- test/routes/add-article.test.ts | 8 +++--- test/routes/article-list.test.ts | 6 ++--- 10 files changed, 53 insertions(+), 53 deletions(-) diff --git a/src/adaptors/in-memory-articles.ts b/src/adaptors/in-memory-articles.ts index 2c860097..140384f7 100644 --- a/src/adaptors/in-memory-articles.ts +++ b/src/adaptors/in-memory-articles.ts @@ -1,5 +1,5 @@ import { - BlankNode, DatasetCore, Quad, Quad_Object as QuadObject, + DatasetCore, NamedNode, Quad, Quad_Object as QuadObject, } from 'rdf-js'; import Articles from '../articles'; import ArticleNotFound from '../errors/article-not-found'; @@ -7,9 +7,9 @@ import NotAnArticle from '../errors/not-an-article'; import { rdf, schema } from '../namespaces'; export default class InMemoryArticles implements Articles { - private articles: { [id: string]: [BlankNode, DatasetCore] } = {}; + private articles: { [id: string]: [NamedNode, DatasetCore] } = {}; - async set(id: BlankNode, article: DatasetCore): Promise { + async set(id: NamedNode, article: DatasetCore): Promise { if (article.match(id, rdf.type, schema.Article).size === 0) { throw new NotAnArticle([...article.match(id, rdf.type)].map((quad: Quad): QuadObject => quad.object)); } @@ -17,7 +17,7 @@ export default class InMemoryArticles implements Articles { this.articles[id.value] = [id, article]; } - async get(id: BlankNode): Promise { + async get(id: NamedNode): Promise { if (!(id.value in this.articles)) { throw new ArticleNotFound(id); } @@ -25,11 +25,11 @@ export default class InMemoryArticles implements Articles { return this.articles[id.value][1]; } - async remove(id: BlankNode): Promise { + async remove(id: NamedNode): Promise { delete this.articles[id.value]; } - async contains(id: BlankNode): Promise { + async contains(id: NamedNode): Promise { return id.value in this.articles; } @@ -37,7 +37,7 @@ export default class InMemoryArticles implements Articles { return Object.values(this.articles).length; } - async* [Symbol.asyncIterator](): AsyncIterator<[BlankNode, DatasetCore]> { + async* [Symbol.asyncIterator](): AsyncIterator<[NamedNode, DatasetCore]> { yield* Object.values(this.articles); } } diff --git a/src/articles.ts b/src/articles.ts index 3da38f93..4fd1e1e2 100644 --- a/src/articles.ts +++ b/src/articles.ts @@ -1,13 +1,13 @@ -import { BlankNode, DatasetCore } from 'rdf-js'; +import { DatasetCore, NamedNode } from 'rdf-js'; -interface Articles extends AsyncIterable<[BlankNode, DatasetCore]> { - set(id: BlankNode, article: DatasetCore): Promise; +interface Articles extends AsyncIterable<[NamedNode, DatasetCore]> { + set(id: NamedNode, article: DatasetCore): Promise; - get(id: BlankNode): Promise; + get(id: NamedNode): Promise; - remove(id: BlankNode): Promise; + remove(id: NamedNode): Promise; - contains(id: BlankNode): Promise; + contains(id: NamedNode): Promise; count(): Promise; } diff --git a/src/errors/article-not-found.ts b/src/errors/article-not-found.ts index 41771d40..ed39861b 100644 --- a/src/errors/article-not-found.ts +++ b/src/errors/article-not-found.ts @@ -1,10 +1,10 @@ -import { BlankNode } from 'rdf-js'; +import { NamedNode } from 'rdf-js'; import { termToString } from 'rdf-string'; export default class ArticleNotFound extends Error { - readonly id: BlankNode; + readonly id: NamedNode; - constructor(id: BlankNode) { + constructor(id: NamedNode) { super(`Article ${termToString(id)} could not be found`); this.id = id; diff --git a/src/routes/add-article.ts b/src/routes/add-article.ts index 24b3672c..1d0cf6db 100644 --- a/src/routes/add-article.ts +++ b/src/routes/add-article.ts @@ -12,7 +12,7 @@ import Routes from './index'; export default (): AppMiddleware => ( async ({ - articles, dataFactory: { blankNode, quad }, request, response, router, + articles, dataFactory: { namedNode, quad }, request, response, router, }: AppContext, next: Next): Promise => { const id = clownface({ dataset: request.dataset }).has(rdf.type, schema.Article).term; @@ -20,15 +20,15 @@ export default (): AppMiddleware => ( throw new createHttpError.BadRequest(`No ${termToString(schema.Article)} found`); } - if (id.termType !== 'BlankNode') { - throw new createHttpError.BadRequest(`Article must have a blank node identifier (${termToString(id)} given)`); + if (id.termType !== 'NamedNode') { + throw new createHttpError.BadRequest(`Article must have a named node identifier (${termToString(id)} given)`); } if (request.dataset.match(id, schema('name')).size === 0) { throw new createHttpError.BadRequest(`Article must have at least one ${termToString(schema('name'))}`); } - const newId = blankNode(uniqueString()); + const newId = namedNode(uniqueString()); [...request.dataset].forEach((originalQuad: Quad): void => { let newQuad: Quad; diff --git a/src/routes/article-list.ts b/src/routes/article-list.ts index e2f156e9..9290d17a 100644 --- a/src/routes/article-list.ts +++ b/src/routes/article-list.ts @@ -3,7 +3,7 @@ import { constants } from 'http2'; import all from 'it-all'; import { Next } from 'koa'; import { addAll } from 'rdf-dataset-ext'; -import { BlankNode, DatasetCore } from 'rdf-js'; +import { DatasetCore, NamedNode } from 'rdf-js'; import { toRdf } from 'rdf-literal'; import url from 'url'; import { AppContext, AppMiddleware } from '../app'; @@ -21,7 +21,7 @@ export default (): AppMiddleware => ( const listPromise = all(articles) .then((list): void => { - list.forEach(([id, article]: [BlankNode, DatasetCore]): void => { + list.forEach(([id, article]: [NamedNode, DatasetCore]): void => { graph.addOut(hydra.member, id); addAll(graph.dataset, article); }); diff --git a/test/adaptors/in-memory-articles.test.ts b/test/adaptors/in-memory-articles.test.ts index 9894e402..7272cb22 100644 --- a/test/adaptors/in-memory-articles.test.ts +++ b/test/adaptors/in-memory-articles.test.ts @@ -1,7 +1,7 @@ -import { blankNode, literal, quad } from '@rdfjs/data-model'; +import { literal, namedNode, quad } from '@rdfjs/data-model'; import all from 'it-all'; import 'jest-rdf'; -import { BlankNode, DatasetCore } from 'rdf-js'; +import { DatasetCore, NamedNode } from 'rdf-js'; import InMemoryArticles from '../../src/adaptors/in-memory-articles'; import ArticleNotFound from '../../src/errors/article-not-found'; import NotAnArticle from '../../src/errors/not-an-article'; @@ -11,7 +11,7 @@ import createArticle from '../create-article'; describe('in-memory articles', (): void => { it('can add an article', async (): Promise => { const articles = new InMemoryArticles(); - const id = blankNode(); + const id = namedNode('one'); expect(await articles.contains(id)).toBe(false); @@ -22,7 +22,7 @@ describe('in-memory articles', (): void => { it('can add an article with multiple types', async (): Promise => { const articles = new InMemoryArticles(); - const id = blankNode(); + const id = namedNode('one'); const article = createArticle({ id, types: [schema.Article, schema.NewsArticle] }); await articles.set(id, article); @@ -32,7 +32,7 @@ describe('in-memory articles', (): void => { it('can update an article', async (): Promise => { const articles = new InMemoryArticles(); - const id = blankNode(); + const id = namedNode('one'); await articles.set(id, createArticle({ id, name: literal('Original') })); await articles.set(id, createArticle({ id, name: literal('Updated') })); @@ -42,7 +42,7 @@ describe('in-memory articles', (): void => { it('throws an error if it is not an article', async (): Promise => { const articles = new InMemoryArticles(); - const id = blankNode(); + const id = namedNode('one'); const article = createArticle({ id, types: [schema.NewsArticle] }); await expect(articles.set(id, article)).rejects.toThrow(new NotAnArticle([schema.NewsArticle])); @@ -50,7 +50,7 @@ describe('in-memory articles', (): void => { it('throws an error if it has no type', async (): Promise => { const articles = new InMemoryArticles(); - const id = blankNode(); + const id = namedNode('one'); const article = createArticle({ id, types: [] }); await expect(articles.set(id, article)).rejects.toThrow(new NotAnArticle()); @@ -58,7 +58,7 @@ describe('in-memory articles', (): void => { it('can retrieve an article', async (): Promise => { const articles = new InMemoryArticles(); - const id = blankNode(); + const id = namedNode('one'); const article = createArticle({ id }); await articles.set(id, article); @@ -68,7 +68,7 @@ describe('in-memory articles', (): void => { it('throws an error if the article is not found', async (): Promise => { const articles = new InMemoryArticles(); - const id = blankNode(); + const id = namedNode('one'); await expect(articles.get(id)).rejects.toBeInstanceOf(ArticleNotFound); await expect(articles.get(id)).rejects.toHaveProperty('id', id); @@ -76,7 +76,7 @@ describe('in-memory articles', (): void => { it('can remove an article', async (): Promise => { const articles = new InMemoryArticles(); - const id = blankNode(); + const id = namedNode('one'); await articles.set(id, createArticle({ id })); await articles.remove(id); @@ -86,7 +86,7 @@ describe('in-memory articles', (): void => { it('does nothing when trying to remove an article that is not there', async (): Promise => { const articles = new InMemoryArticles(); - const id = blankNode(); + const id = namedNode('one'); await expect(articles.remove(id)).resolves.not.toThrow(); }); @@ -96,8 +96,8 @@ describe('in-memory articles', (): void => { expect(await articles.count()).toBe(0); - const id1 = blankNode(); - const id2 = blankNode(); + const id1 = namedNode('one'); + const id2 = namedNode('two'); await articles.set(id1, createArticle({ id: id1 })); await articles.set(id2, createArticle({ id: id2 })); @@ -109,15 +109,15 @@ describe('in-memory articles', (): void => { it('can iterate through the articles', async (): Promise => { const articles = new InMemoryArticles(); - const id1 = blankNode(); - const id2 = blankNode(); - const id3 = blankNode(); + const id1 = namedNode('one'); + const id2 = namedNode('two'); + const id3 = namedNode('three'); await articles.set(id1, createArticle({ id: id1 })); await articles.set(id3, createArticle({ id: id3 })); await articles.set(id2, createArticle({ id: id2 })); - const ids = (await all(articles)).map((parts: [BlankNode, DatasetCore]): BlankNode => parts[0]); + const ids = (await all(articles)).map((parts: [NamedNode, DatasetCore]): NamedNode => parts[0]); expect(ids).toStrictEqual([id1, id3, id2]); }); diff --git a/test/create-article.ts b/test/create-article.ts index c5449fa0..09f1ad97 100644 --- a/test/create-article.ts +++ b/test/create-article.ts @@ -2,7 +2,7 @@ import { DatasetCore, Literal, NamedNode, Quad_Subject as QuadSubject, } from 'rdf-js'; import { - blankNode, dataset, literal, quad, + dataset, literal, namedNode, quad, } from '../src/data-factory'; import { rdf, schema } from '../src/namespaces'; @@ -13,7 +13,7 @@ type Options = { } export default ({ - id = blankNode(), + id = namedNode('one'), name = literal('Article'), types = [schema.Article], }: Options = {}): DatasetCore => { diff --git a/test/errors/article-not-found.test.ts b/test/errors/article-not-found.test.ts index 16db7c35..6cbced6c 100644 --- a/test/errors/article-not-found.test.ts +++ b/test/errors/article-not-found.test.ts @@ -1,21 +1,21 @@ -import { blankNode } from '@rdfjs/data-model'; +import { namedNode } from '@rdfjs/data-model'; import ArticleNotFound from '../../src/errors/article-not-found'; describe('article not found error', (): void => { it('should be an error', async (): Promise => { - const error = new ArticleNotFound(blankNode()); + const error = new ArticleNotFound(namedNode('1')); expect(error).toBeInstanceOf(Error); }); it('should have a message', async (): Promise => { - const error = new ArticleNotFound(blankNode('12345')); + const error = new ArticleNotFound(namedNode('12345')); - expect(error.message).toBe('Article _:12345 could not be found'); + expect(error.message).toBe('Article 12345 could not be found'); }); it('should have an ID', async (): Promise => { - const id = blankNode(); + const id = namedNode('1'); const error = new ArticleNotFound(id); expect(error.id).toBe(id); diff --git a/test/routes/add-article.test.ts b/test/routes/add-article.test.ts index c293af58..4e28c23a 100644 --- a/test/routes/add-article.test.ts +++ b/test/routes/add-article.test.ts @@ -24,7 +24,7 @@ const makeRequest = async ( describe('add article', (): void => { it('should return a successful response', async (): Promise => { const articles = new InMemoryArticles(); - const id = blankNode(); + const id = namedNode('one'); const name = literal('Article'); const response = await makeRequest(createArticle({ id, name }), undefined, articles); @@ -44,12 +44,12 @@ describe('add article', (): void => { await expect(makeRequest(article)).rejects.toHaveProperty('message', 'No http://schema.org/Article found'); }); - it('should throw an error if the articles does not have a blank node identifier', async (): Promise => { - const id = namedNode('http://example.com/my-article'); + it('should throw an error if the articles does not have a named node identifier', async (): Promise => { + const id = blankNode('one'); const article = createArticle({ id }); await expect(makeRequest(article)).rejects.toBeInstanceOf(createHttpError.BadRequest); - await expect(makeRequest(article)).rejects.toHaveProperty('message', 'Article must have a blank node identifier (http://example.com/my-article given)'); + await expect(makeRequest(article)).rejects.toHaveProperty('message', 'Article must have a named node identifier (_:one given)'); }); it('should throw an error if it has no schema:name', async (): Promise => { diff --git a/test/routes/article-list.test.ts b/test/routes/article-list.test.ts index 3fc249e8..41e7d9e4 100644 --- a/test/routes/article-list.test.ts +++ b/test/routes/article-list.test.ts @@ -1,4 +1,4 @@ -import { blankNode, namedNode, quad } from '@rdfjs/data-model'; +import { namedNode, quad } from '@rdfjs/data-model'; import { Response } from 'koa'; import { toRdf } from 'rdf-literal'; import InMemoryArticles from '../../src/adaptors/in-memory-articles'; @@ -35,8 +35,8 @@ describe('article list', (): void => { it('should return articles in the list', async (): Promise => { const articles = new InMemoryArticles(); - const id1 = blankNode(); - const id2 = blankNode(); + const id1 = namedNode('one'); + const id2 = namedNode('two'); await articles.set(id1, createArticle({ id: id1 })); await articles.set(id2, createArticle({ id: id2 })); From 3d53e4ca3133fb89870f0bbebbba25dfc4a4b03a Mon Sep 17 00:00:00 2001 From: nlisgo Date: Tue, 14 Jan 2020 14:08:24 +0000 Subject: [PATCH 02/35] refactor namedNode calls --- test/errors/article-not-found.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/errors/article-not-found.test.ts b/test/errors/article-not-found.test.ts index 6cbced6c..1c27e36f 100644 --- a/test/errors/article-not-found.test.ts +++ b/test/errors/article-not-found.test.ts @@ -3,7 +3,7 @@ import ArticleNotFound from '../../src/errors/article-not-found'; describe('article not found error', (): void => { it('should be an error', async (): Promise => { - const error = new ArticleNotFound(namedNode('1')); + const error = new ArticleNotFound(namedNode('one')); expect(error).toBeInstanceOf(Error); }); @@ -15,7 +15,7 @@ describe('article not found error', (): void => { }); it('should have an ID', async (): Promise => { - const id = namedNode('1'); + const id = namedNode('one'); const error = new ArticleNotFound(id); expect(error.id).toBe(id); From fbbb80777dc89a8706456abfd4dcc9d9fed3ccf5 Mon Sep 17 00:00:00 2001 From: nlisgo Date: Tue, 14 Jan 2020 15:03:38 +0000 Subject: [PATCH 03/35] Create article with blankNode before namedNode is assigned --- src/routes/add-article.ts | 5 +++-- test/create-article.ts | 4 ++-- test/routes/add-article.test.ts | 8 ++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/routes/add-article.ts b/src/routes/add-article.ts index 1d0cf6db..64bc7687 100644 --- a/src/routes/add-article.ts +++ b/src/routes/add-article.ts @@ -14,14 +14,15 @@ export default (): AppMiddleware => ( async ({ articles, dataFactory: { namedNode, quad }, request, response, router, }: AppContext, next: Next): Promise => { + console.log(request.dataset); const id = clownface({ dataset: request.dataset }).has(rdf.type, schema.Article).term; if (!id) { throw new createHttpError.BadRequest(`No ${termToString(schema.Article)} found`); } - if (id.termType !== 'NamedNode') { - throw new createHttpError.BadRequest(`Article must have a named node identifier (${termToString(id)} given)`); + if (id.termType !== 'BlankNode') { + throw new createHttpError.BadRequest(`Article must have a blank node identifier (${termToString(id)} given)`); } if (request.dataset.match(id, schema('name')).size === 0) { diff --git a/test/create-article.ts b/test/create-article.ts index 09f1ad97..c5449fa0 100644 --- a/test/create-article.ts +++ b/test/create-article.ts @@ -2,7 +2,7 @@ import { DatasetCore, Literal, NamedNode, Quad_Subject as QuadSubject, } from 'rdf-js'; import { - dataset, literal, namedNode, quad, + blankNode, dataset, literal, quad, } from '../src/data-factory'; import { rdf, schema } from '../src/namespaces'; @@ -13,7 +13,7 @@ type Options = { } export default ({ - id = namedNode('one'), + id = blankNode(), name = literal('Article'), types = [schema.Article], }: Options = {}): DatasetCore => { diff --git a/test/routes/add-article.test.ts b/test/routes/add-article.test.ts index 4e28c23a..c293af58 100644 --- a/test/routes/add-article.test.ts +++ b/test/routes/add-article.test.ts @@ -24,7 +24,7 @@ const makeRequest = async ( describe('add article', (): void => { it('should return a successful response', async (): Promise => { const articles = new InMemoryArticles(); - const id = namedNode('one'); + const id = blankNode(); const name = literal('Article'); const response = await makeRequest(createArticle({ id, name }), undefined, articles); @@ -44,12 +44,12 @@ describe('add article', (): void => { await expect(makeRequest(article)).rejects.toHaveProperty('message', 'No http://schema.org/Article found'); }); - it('should throw an error if the articles does not have a named node identifier', async (): Promise => { - const id = blankNode('one'); + it('should throw an error if the articles does not have a blank node identifier', async (): Promise => { + const id = namedNode('http://example.com/my-article'); const article = createArticle({ id }); await expect(makeRequest(article)).rejects.toBeInstanceOf(createHttpError.BadRequest); - await expect(makeRequest(article)).rejects.toHaveProperty('message', 'Article must have a named node identifier (_:one given)'); + await expect(makeRequest(article)).rejects.toHaveProperty('message', 'Article must have a blank node identifier (http://example.com/my-article given)'); }); it('should throw an error if it has no schema:name', async (): Promise => { From 41624dcd0be7b46605f4e4a3ba2a9af8d28b82a6 Mon Sep 17 00:00:00 2001 From: nlisgo Date: Tue, 14 Jan 2020 15:04:22 +0000 Subject: [PATCH 04/35] Remove console.log --- src/routes/add-article.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes/add-article.ts b/src/routes/add-article.ts index 64bc7687..26908683 100644 --- a/src/routes/add-article.ts +++ b/src/routes/add-article.ts @@ -14,7 +14,6 @@ export default (): AppMiddleware => ( async ({ articles, dataFactory: { namedNode, quad }, request, response, router, }: AppContext, next: Next): Promise => { - console.log(request.dataset); const id = clownface({ dataset: request.dataset }).has(rdf.type, schema.Article).term; if (!id) { From 4455043d53dc4567cc26ab7fdd7982560088213b Mon Sep 17 00:00:00 2001 From: nlisgo Date: Thu, 16 Jan 2020 14:59:16 +0000 Subject: [PATCH 05/35] Add Iri as namedNode value and use in response location header --- src/routes/add-article.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/add-article.ts b/src/routes/add-article.ts index 26908683..52f9382d 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, { id: 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 = constants.HTTP_STATUS_CREATED; - response.set('Location', url.resolve(request.origin, router.url(Routes.ArticleList))); + response.set('Location', newId.value); await next(); } From f2bbb6f61aa39cc814238d664fa33a13f66c3693 Mon Sep 17 00:00:00 2001 From: nlisgo Date: Thu, 16 Jan 2020 15:00:04 +0000 Subject: [PATCH 06/35] Look up article if no route found --- src/router.ts | 3 +++ src/routes/article.ts | 32 ++++++++++++++++++++++++++++++++ src/routes/index.ts | 1 + 3 files changed, 36 insertions(+) create mode 100644 src/routes/article.ts diff --git a/src/router.ts b/src/router.ts index 585ed62b..425c6a3d 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'; @@ -13,6 +14,8 @@ export default (): Router => { router.get(Routes.ArticleList, '/articles', articleList()); router.post(Routes.AddArticle, '/articles', addArticle()); router.get(Routes.EntryPoint, '/', entryPoint()); + router.get(Routes.Article, '/articles/:id', article()); + router.get(Routes.Article, /(|^$)/, article()); return router; }; diff --git a/src/routes/article.ts b/src/routes/article.ts new file mode 100644 index 00000000..9be9297d --- /dev/null +++ b/src/routes/article.ts @@ -0,0 +1,32 @@ +import clownface from 'clownface'; +import createHttpError from 'http-errors'; +import { constants } from 'http2'; +import { Next } from 'koa'; +import { addAll } from 'rdf-dataset-ext'; +import url from 'url'; +import { AppContext, AppMiddleware } from '../app'; + +export default (): AppMiddleware => ( + async ({ + dataFactory: { namedNode }, articles, request, response, + }: AppContext, next: Next): Promise => { + if (response.status === constants.HTTP_STATUS_NOT_FOUND) { + const articleNamedNode = namedNode(url.resolve(request.origin, request.url)); + const graph = clownface({ + dataset: response.dataset, + term: articleNamedNode, + }); + try { + await articles.get(articleNamedNode).then((article) => { + addAll(graph.dataset, article); + }); + } catch (error) { + throw new createHttpError.NotFound(error.message); + } + + response.status = constants.HTTP_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', } From 29b9f07c9c8129b0b7cc8a2c545be44cb0bdafa4 Mon Sep 17 00:00:00 2001 From: nlisgo Date: Thu, 16 Jan 2020 15:10:34 +0000 Subject: [PATCH 07/35] Amend expected location header in add-article --- test/routes/add-article.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/routes/add-article.test.ts b/test/routes/add-article.test.ts index c293af58..e8a44122 100644 --- a/test/routes/add-article.test.ts +++ b/test/routes/add-article.test.ts @@ -29,7 +29,7 @@ describe('add article', (): void => { const response = await makeRequest(createArticle({ id, name }), undefined, articles); expect(response.status).toBe(201); - expect(response.get('Location')).toBe('http://example.com/path-to/article-list'); + expect(response.get('Location')).toBe('http://example.com/path-to/article'); expect(await articles.count()).toBe(1); const [newId, dataset] = (await all(articles))[0]; From a19701fcbb268440f39a10e89e4b16d4800323bf Mon Sep 17 00:00:00 2001 From: nlisgo Date: Fri, 17 Jan 2020 10:48:26 +0000 Subject: [PATCH 08/35] Reverse change to location header of add article response --- src/routes/add-article.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/add-article.ts b/src/routes/add-article.ts index 52f9382d..d6d60116 100644 --- a/src/routes/add-article.ts +++ b/src/routes/add-article.ts @@ -46,7 +46,7 @@ export default (): AppMiddleware => ( await articles.set(newId, request.dataset); response.status = constants.HTTP_STATUS_CREATED; - response.set('Location', newId.value); + response.set('Location', url.resolve(request.origin, router.url(Routes.ArticleList))); await next(); } From 381aace4795db3f4bd87761597c64f633d7ed9d1 Mon Sep 17 00:00:00 2001 From: nlisgo Date: Fri, 17 Jan 2020 10:55:48 +0000 Subject: [PATCH 09/35] Reverse test change to location header of add article response --- test/routes/add-article.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/routes/add-article.test.ts b/test/routes/add-article.test.ts index e8a44122..c293af58 100644 --- a/test/routes/add-article.test.ts +++ b/test/routes/add-article.test.ts @@ -29,7 +29,7 @@ describe('add article', (): void => { const response = await makeRequest(createArticle({ id, name }), undefined, articles); expect(response.status).toBe(201); - expect(response.get('Location')).toBe('http://example.com/path-to/article'); + 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]; From 7952d9fc733866fad4adfa28f582aed11625be2d Mon Sep 17 00:00:00 2001 From: nlisgo Date: Fri, 17 Jan 2020 12:00:55 +0000 Subject: [PATCH 10/35] Accept params and id for article endpoint --- test/context.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/context.ts b/test/context.ts index 8d678cf4..c5f2a760 100644 --- a/test/context.ts +++ b/test/context.ts @@ -25,8 +25,8 @@ type Options = { }; const dummyRouter = { - url(name: string): string { - return `/path-to/${name}`; + url(name: string, params = {}): string { + return Router.url(`/path-to/${name}${(name === 'article') ? '/:id' : ''}`, params); }, } as unknown as Router; From 7bb9fbd979ad3844c09021200d8e00c0442b4a89 Mon Sep 17 00:00:00 2001 From: nlisgo Date: Fri, 17 Jan 2020 14:24:21 +0000 Subject: [PATCH 11/35] Add tests for article end point --- src/routes/article.ts | 7 +++-- test/routes/article.test.ts | 53 +++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 test/routes/article.test.ts diff --git a/src/routes/article.ts b/src/routes/article.ts index 9be9297d..a2330089 100644 --- a/src/routes/article.ts +++ b/src/routes/article.ts @@ -3,15 +3,14 @@ import createHttpError from 'http-errors'; import { constants } from 'http2'; import { Next } from 'koa'; import { addAll } from 'rdf-dataset-ext'; -import url from 'url'; import { AppContext, AppMiddleware } from '../app'; export default (): AppMiddleware => ( async ({ - dataFactory: { namedNode }, articles, request, response, + dataFactory: { namedNode }, articles, path, response, }: AppContext, next: Next): Promise => { - if (response.status === constants.HTTP_STATUS_NOT_FOUND) { - const articleNamedNode = namedNode(url.resolve(request.origin, request.url)); + if (!response.status || response.status === constants.HTTP_STATUS_NOT_FOUND) { + const articleNamedNode = namedNode(path); const graph = clownface({ dataset: response.dataset, term: articleNamedNode, diff --git a/test/routes/article.test.ts b/test/routes/article.test.ts new file mode 100644 index 00000000..5695962c --- /dev/null +++ b/test/routes/article.test.ts @@ -0,0 +1,53 @@ +import { + namedNode, +} from '@rdfjs/data-model'; +import createHttpError from 'http-errors'; +import { Response } from 'koa'; +import InMemoryArticles from '../../src/adaptors/in-memory-articles'; +import Articles from '../../src/articles'; +import article from '../../src/routes/article'; +import createContext from '../context'; +import createArticle from '../create-article'; +import runMiddleware, { NextMiddleware } from '../middleware'; +import { WithDataset } from '../../src/middleware/dataset'; + +const makeRequest = async ( + next?: NextMiddleware, + path?: string, + articles?: Articles, +): Promise> => ( + runMiddleware(article(), createContext({ path, articles }), next) +); + +describe('article', (): void => { + it('should return a successful response', async (): Promise => { + const articles = new InMemoryArticles(); + + const id = namedNode('http://example.com/path-to/article/one'); + await articles.set(id, createArticle({ id })); + + const response = await makeRequest(undefined, 'http://example.com/path-to/article/one', articles); + + expect(response.status).toBe(200); + }); + + it('should throw an error if article is not found', async (): Promise => { + const articles = new InMemoryArticles(); + const response = makeRequest(undefined, 'http://example.com/path-to/article/not-found', articles); + + 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 articles = new InMemoryArticles(); + + const id = namedNode('http://example.com/path-to/article/one'); + await articles.set(id, createArticle({ id })); + + const next = jest.fn(); + await makeRequest(next, 'http://example.com/path-to/article/one', articles); + + expect(next).toHaveBeenCalledTimes(1); + }); +}); From 62da38f4e0cf8053eb32626bafe011f9f76b8f73 Mon Sep 17 00:00:00 2001 From: nlisgo Date: Fri, 17 Jan 2020 14:32:52 +0000 Subject: [PATCH 12/35] Adjust location header in add-article response --- src/routes/add-article.ts | 2 +- test/routes/add-article.test.ts | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/routes/add-article.ts b/src/routes/add-article.ts index d6d60116..52f9382d 100644 --- a/src/routes/add-article.ts +++ b/src/routes/add-article.ts @@ -46,7 +46,7 @@ export default (): AppMiddleware => ( await articles.set(newId, request.dataset); response.status = constants.HTTP_STATUS_CREATED; - response.set('Location', url.resolve(request.origin, router.url(Routes.ArticleList))); + response.set('Location', newId.value); await next(); } diff --git a/test/routes/add-article.test.ts b/test/routes/add-article.test.ts index c293af58..2e66720e 100644 --- a/test/routes/add-article.test.ts +++ b/test/routes/add-article.test.ts @@ -5,6 +5,7 @@ import createHttpError from 'http-errors'; import all from 'it-all'; import { Response } from 'koa'; import { DatasetCore } from 'rdf-js'; +import Router from '@koa/router'; import InMemoryArticles from '../../src/adaptors/in-memory-articles'; import Articles from '../../src/articles'; import { schema } from '../../src/namespaces'; @@ -17,8 +18,9 @@ const makeRequest = async ( dataset?: DatasetCore, next?: NextMiddleware, articles: Articles = new InMemoryArticles(), + router?: Router, ): Promise => ( - runMiddleware(addArticle(), createContext({ articles, dataset }), next) + runMiddleware(addArticle(), createContext({ articles, dataset, router }), next) ); describe('add article', (): void => { @@ -26,10 +28,15 @@ describe('add article', (): void => { const articles = new InMemoryArticles(); const id = blankNode(); const name = literal('Article'); - const response = await makeRequest(createArticle({ id, name }), undefined, articles); + const dummyRouter = { + url(): string { + return '/path-to/article/one'; + }, + } as unknown as Router; + const response = await makeRequest(createArticle({ id, name }), undefined, articles, dummyRouter); expect(response.status).toBe(201); - expect(response.get('Location')).toBe('http://example.com/path-to/article-list'); + expect(response.get('Location')).toBe('http://example.com/path-to/article/one'); expect(await articles.count()).toBe(1); const [newId, dataset] = (await all(articles))[0]; From 4d408ff8742ca75d76369e5db8d340959f1bf7c2 Mon Sep 17 00:00:00 2001 From: nlisgo Date: Mon, 20 Jan 2020 16:08:50 +0000 Subject: [PATCH 13/35] Fix 404 --- src/routes/article.ts | 12 +++++++++--- test/routes/article.test.ts | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/routes/article.ts b/src/routes/article.ts index a2330089..8a2dcf19 100644 --- a/src/routes/article.ts +++ b/src/routes/article.ts @@ -3,14 +3,16 @@ import createHttpError from 'http-errors'; import { constants } from 'http2'; import { Next } from 'koa'; import { addAll } from 'rdf-dataset-ext'; +import url from 'url'; import { AppContext, AppMiddleware } from '../app'; +import ArticleNotFound from '../errors/article-not-found'; export default (): AppMiddleware => ( async ({ - dataFactory: { namedNode }, articles, path, response, + dataFactory: { namedNode }, articles, path, request, response, }: AppContext, next: Next): Promise => { if (!response.status || response.status === constants.HTTP_STATUS_NOT_FOUND) { - const articleNamedNode = namedNode(path); + const articleNamedNode = namedNode(url.resolve(request.origin, path)); const graph = clownface({ dataset: response.dataset, term: articleNamedNode, @@ -20,7 +22,11 @@ export default (): AppMiddleware => ( addAll(graph.dataset, article); }); } catch (error) { - throw new createHttpError.NotFound(error.message); + if (error instanceof ArticleNotFound) { + throw new createHttpError.NotFound(error.message); + } + + throw error; } response.status = constants.HTTP_STATUS_OK; diff --git a/test/routes/article.test.ts b/test/routes/article.test.ts index 5695962c..5f872ae2 100644 --- a/test/routes/article.test.ts +++ b/test/routes/article.test.ts @@ -26,7 +26,7 @@ describe('article', (): void => { const id = namedNode('http://example.com/path-to/article/one'); await articles.set(id, createArticle({ id })); - const response = await makeRequest(undefined, 'http://example.com/path-to/article/one', articles); + const response = await makeRequest(undefined, 'path-to/article/one', articles); expect(response.status).toBe(200); }); From d21b19d42aace2e6d6daf07a45d2ceff06c46c80 Mon Sep 17 00:00:00 2001 From: nlisgo Date: Mon, 20 Jan 2020 16:35:21 +0000 Subject: [PATCH 14/35] Reduce code in try block --- src/routes/article.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/routes/article.ts b/src/routes/article.ts index 8a2dcf19..5c78d6fe 100644 --- a/src/routes/article.ts +++ b/src/routes/article.ts @@ -13,14 +13,10 @@ export default (): AppMiddleware => ( }: AppContext, next: Next): Promise => { if (!response.status || response.status === constants.HTTP_STATUS_NOT_FOUND) { const articleNamedNode = namedNode(url.resolve(request.origin, path)); - const graph = clownface({ - dataset: response.dataset, - term: articleNamedNode, - }); + let article; + try { - await articles.get(articleNamedNode).then((article) => { - addAll(graph.dataset, article); - }); + article = await articles.get(articleNamedNode); } catch (error) { if (error instanceof ArticleNotFound) { throw new createHttpError.NotFound(error.message); @@ -29,6 +25,13 @@ export default (): AppMiddleware => ( throw error; } + const graph = clownface({ + dataset: response.dataset, + term: articleNamedNode, + }); + + addAll(graph.dataset, article); + response.status = constants.HTTP_STATUS_OK; } From 06f1e6583e5da135fb7743df43a147b17f0fd6c6 Mon Sep 17 00:00:00 2001 From: nlisgo Date: Tue, 21 Jan 2020 10:54:59 +0000 Subject: [PATCH 15/35] Remove specific article endpoint from router --- src/router.ts | 1 - src/routes/add-article.ts | 2 +- test/context.ts | 4 ++-- test/routes/add-article.test.ts | 14 ++++---------- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/router.ts b/src/router.ts index 425c6a3d..29ca4aef 100644 --- a/src/router.ts +++ b/src/router.ts @@ -14,7 +14,6 @@ export default (): Router => { router.get(Routes.ArticleList, '/articles', articleList()); router.post(Routes.AddArticle, '/articles', addArticle()); router.get(Routes.EntryPoint, '/', entryPoint()); - router.get(Routes.Article, '/articles/:id', article()); router.get(Routes.Article, /(|^$)/, article()); return router; diff --git a/src/routes/add-article.ts b/src/routes/add-article.ts index 52f9382d..996eb428 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(url.resolve(request.origin, router.url(Routes.Article, { id: uniqueString() }))); + const newId = namedNode(url.resolve(request.origin, 'articles/'.concat(uniqueString()))); [...request.dataset].forEach((originalQuad: Quad): void => { let newQuad: Quad; diff --git a/test/context.ts b/test/context.ts index c5f2a760..8d678cf4 100644 --- a/test/context.ts +++ b/test/context.ts @@ -25,8 +25,8 @@ type Options = { }; const dummyRouter = { - url(name: string, params = {}): string { - return Router.url(`/path-to/${name}${(name === 'article') ? '/:id' : ''}`, params); + url(name: string): string { + return `/path-to/${name}`; }, } as unknown as Router; diff --git a/test/routes/add-article.test.ts b/test/routes/add-article.test.ts index 2e66720e..3a367c92 100644 --- a/test/routes/add-article.test.ts +++ b/test/routes/add-article.test.ts @@ -18,25 +18,19 @@ const makeRequest = async ( dataset?: DatasetCore, next?: NextMiddleware, articles: Articles = new InMemoryArticles(), - router?: Router, ): Promise => ( - runMiddleware(addArticle(), createContext({ articles, dataset, router }), next) + runMiddleware(addArticle(), createContext({ articles, dataset }), next) ); describe('add article', (): void => { - it('should return a successful response', async (): Promise => { + it.skip('should return a successful response', async (): Promise => { const articles = new InMemoryArticles(); const id = blankNode(); const name = literal('Article'); - const dummyRouter = { - url(): string { - return '/path-to/article/one'; - }, - } as unknown as Router; - const response = await makeRequest(createArticle({ id, name }), undefined, articles, dummyRouter); + const response = await makeRequest(createArticle({ id, name }), undefined, articles); expect(response.status).toBe(201); - expect(response.get('Location')).toBe('http://example.com/path-to/article/one'); + expect(response.get('Location')).toBe('http://example.com/articles/one'); expect(await articles.count()).toBe(1); const [newId, dataset] = (await all(articles))[0]; From dd0f6e9a5a55a0c6b2437aa1ba947f6f7a79a297 Mon Sep 17 00:00:00 2001 From: nlisgo Date: Tue, 21 Jan 2020 11:45:30 +0000 Subject: [PATCH 16/35] cs --- src/routes/add-article.ts | 3 +-- test/routes/add-article.test.ts | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/routes/add-article.ts b/src/routes/add-article.ts index 996eb428..9d8a073f 100644 --- a/src/routes/add-article.ts +++ b/src/routes/add-article.ts @@ -8,11 +8,10 @@ import uniqueString from 'unique-string'; import url from 'url'; import { AppContext, AppMiddleware } from '../app'; import { rdf, schema } from '../namespaces'; -import Routes from './index'; export default (): AppMiddleware => ( async ({ - articles, dataFactory: { namedNode, quad }, request, response, router, + articles, dataFactory: { namedNode, quad }, request, response, }: AppContext, next: Next): Promise => { const id = clownface({ dataset: request.dataset }).has(rdf.type, schema.Article).term; diff --git a/test/routes/add-article.test.ts b/test/routes/add-article.test.ts index 3a367c92..c97ae5d8 100644 --- a/test/routes/add-article.test.ts +++ b/test/routes/add-article.test.ts @@ -5,7 +5,6 @@ import createHttpError from 'http-errors'; import all from 'it-all'; import { Response } from 'koa'; import { DatasetCore } from 'rdf-js'; -import Router from '@koa/router'; import InMemoryArticles from '../../src/adaptors/in-memory-articles'; import Articles from '../../src/articles'; import { schema } from '../../src/namespaces'; From 74bbf95d5fb1ecbe7f27652a7f6e46b339011c98 Mon Sep 17 00:00:00 2001 From: nlisgo Date: Wed, 22 Jan 2020 10:33:29 +0000 Subject: [PATCH 17/35] Refactor add-article test to check new location header --- test/routes/add-article.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/routes/add-article.test.ts b/test/routes/add-article.test.ts index c97ae5d8..b32996e2 100644 --- a/test/routes/add-article.test.ts +++ b/test/routes/add-article.test.ts @@ -22,18 +22,18 @@ const makeRequest = async ( ); describe('add article', (): void => { - it.skip('should return a successful response', async (): Promise => { + it('should return a successful response', async (): Promise => { const articles = new InMemoryArticles(); const id = blankNode(); const name = literal('Article'); const response = await makeRequest(createArticle({ id, name }), undefined, articles); expect(response.status).toBe(201); - expect(response.get('Location')).toBe('http://example.com/articles/one'); 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); }); From 528ac261c94229b1c276ea27a32e797080593c06 Mon Sep 17 00:00:00 2001 From: nlisgo Date: Wed, 22 Jan 2020 10:33:47 +0000 Subject: [PATCH 18/35] Add article retrieval to middleware --- src/app.ts | 2 ++ src/router.ts | 2 -- src/routes/article.ts | 54 ++++++++++++++++++++++--------------------- src/routes/index.ts | 1 - 4 files changed, 30 insertions(+), 29 deletions(-) diff --git a/src/app.ts b/src/app.ts index 13b4e6f7..e9fcc0e2 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,6 +11,7 @@ import errorHandler from './middleware/error-handler'; import jsonld from './middleware/jsonld'; import routing from './middleware/routing'; import namespaces from './namespaces'; +import article from './routes/article'; export type AppState = DefaultState; @@ -48,6 +49,7 @@ export default ( })); app.use(apiDocumentationLink(apiDocumentationPath)); app.use(errorHandler()); + app.use(article(articles)); app.use(routing(router)); return app; diff --git a/src/router.ts b/src/router.ts index 29ca4aef..585ed62b 100644 --- a/src/router.ts +++ b/src/router.ts @@ -3,7 +3,6 @@ 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'; @@ -14,7 +13,6 @@ export default (): Router => { router.get(Routes.ArticleList, '/articles', articleList()); router.post(Routes.AddArticle, '/articles', addArticle()); router.get(Routes.EntryPoint, '/', entryPoint()); - router.get(Routes.Article, /(|^$)/, article()); return router; }; diff --git a/src/routes/article.ts b/src/routes/article.ts index 5c78d6fe..15ed6558 100644 --- a/src/routes/article.ts +++ b/src/routes/article.ts @@ -1,40 +1,42 @@ import clownface from 'clownface'; import createHttpError from 'http-errors'; import { constants } from 'http2'; -import { Next } from 'koa'; import { addAll } from 'rdf-dataset-ext'; +import { + DefaultStateExtends, Middleware, Next, +} from 'koa'; import url from 'url'; -import { AppContext, AppMiddleware } from '../app'; +import Articles from '../articles'; +import { namedNode } from '../data-factory'; import ArticleNotFound from '../errors/article-not-found'; +import { DatasetContext } from '../middleware/dataset'; -export default (): AppMiddleware => ( - async ({ - dataFactory: { namedNode }, articles, path, request, response, - }: AppContext, next: Next): Promise => { - if (!response.status || response.status === constants.HTTP_STATUS_NOT_FOUND) { - const articleNamedNode = namedNode(url.resolve(request.origin, path)); - let article; +export default (articles: Articles): Middleware => ( + async ({ request, response }: DatasetContext, next: Next): Promise => { + try { + await next(); + } catch (error) { + if (error instanceof createHttpError.NotFound) { + const articleNamedNode = namedNode(url.resolve(request.origin, request.url)); + let article; + try { + article = await articles.get(articleNamedNode); + } catch (getError) { + if (getError instanceof ArticleNotFound) { + throw new createHttpError.NotFound(error.message); + } - try { - article = await articles.get(articleNamedNode); - } catch (error) { - if (error instanceof ArticleNotFound) { - throw new createHttpError.NotFound(error.message); + throw getError; } - + const graph = clownface({ + dataset: response.dataset, + term: articleNamedNode, + }); + addAll(graph.dataset, article); + response.status = constants.HTTP_STATUS_OK; + } else { throw error; } - - const graph = clownface({ - dataset: response.dataset, - term: articleNamedNode, - }); - - addAll(graph.dataset, article); - - response.status = constants.HTTP_STATUS_OK; } - - await next(); } ); diff --git a/src/routes/index.ts b/src/routes/index.ts index 7089f0cb..9f67ecaf 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,7 +1,6 @@ enum Routes { 'AddArticle' = 'add-article', 'ApiDocumentation' = 'api-documentation', - 'Article' = 'article', 'ArticleList' = 'article-list', 'EntryPoint' = 'entry-point', } From 95acaa10b16616e972dd57bd7b9ab8ddbd9c8e94 Mon Sep 17 00:00:00 2001 From: nlisgo Date: Wed, 22 Jan 2020 11:12:03 +0000 Subject: [PATCH 19/35] Throw with correct message --- src/routes/article.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/article.ts b/src/routes/article.ts index 15ed6558..500dc7cf 100644 --- a/src/routes/article.ts +++ b/src/routes/article.ts @@ -23,7 +23,7 @@ export default (articles: Articles): Middleware Date: Wed, 22 Jan 2020 11:18:07 +0000 Subject: [PATCH 20/35] Add url option to dummy request and make next optional --- test/context.ts | 3 +++ test/routes/article.test.ts | 16 ++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/test/context.ts b/test/context.ts index 8d678cf4..39c3f781 100644 --- a/test/context.ts +++ b/test/context.ts @@ -22,6 +22,7 @@ type Options = { method?: string; path?: string; router?: Router; + url?: string; }; const dummyRouter = { @@ -39,6 +40,7 @@ export default ({ method, path, router = dummyRouter, + url, }: Options = {}): AppContext => { const app = new Koa(); app.on('error', errorListener || jest.fn()); @@ -54,6 +56,7 @@ export default ({ host: 'example.com', }, method, + url: typeof url === 'string' ? url : undefined, }); const response = Object.create(app.response) as WithDataset; diff --git a/test/routes/article.test.ts b/test/routes/article.test.ts index 5f872ae2..76d50d6f 100644 --- a/test/routes/article.test.ts +++ b/test/routes/article.test.ts @@ -11,12 +11,16 @@ import createArticle from '../create-article'; import runMiddleware, { NextMiddleware } from '../middleware'; import { WithDataset } from '../../src/middleware/dataset'; +const dummyNext = async (): Promise => { + throw new createHttpError.NotFound(); +}; + const makeRequest = async ( + articles: Articles, + url: string, next?: NextMiddleware, - path?: string, - articles?: Articles, ): Promise> => ( - runMiddleware(article(), createContext({ path, articles }), next) + runMiddleware(article(articles), createContext({ url }), typeof next !== 'undefined' ? next : dummyNext) ); describe('article', (): void => { @@ -26,14 +30,14 @@ describe('article', (): void => { const id = namedNode('http://example.com/path-to/article/one'); await articles.set(id, createArticle({ id })); - const response = await makeRequest(undefined, 'path-to/article/one', articles); + const response = await makeRequest(articles, 'path-to/article/one'); expect(response.status).toBe(200); }); it('should throw an error if article is not found', async (): Promise => { const articles = new InMemoryArticles(); - const response = makeRequest(undefined, 'http://example.com/path-to/article/not-found', articles); + const response = makeRequest(articles, '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'); @@ -46,7 +50,7 @@ describe('article', (): void => { await articles.set(id, createArticle({ id })); const next = jest.fn(); - await makeRequest(next, 'http://example.com/path-to/article/one', articles); + await makeRequest(articles, 'path-to/article/one', next); expect(next).toHaveBeenCalledTimes(1); }); From df2865bba55c359acc5002aa6e685d84c2cef2bb Mon Sep 17 00:00:00 2001 From: nlisgo Date: Wed, 22 Jan 2020 11:58:08 +0000 Subject: [PATCH 21/35] should only attempt article retrieval if next middleware throws not found http error --- test/routes/article.test.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/routes/article.test.ts b/test/routes/article.test.ts index 76d50d6f..c5b4fa9f 100644 --- a/test/routes/article.test.ts +++ b/test/routes/article.test.ts @@ -3,6 +3,7 @@ import { } from '@rdfjs/data-model'; import createHttpError from 'http-errors'; import { Response } from 'koa'; +import { DatasetCore, NamedNode } from 'rdf-js'; import InMemoryArticles from '../../src/adaptors/in-memory-articles'; import Articles from '../../src/articles'; import article from '../../src/routes/article'; @@ -10,6 +11,7 @@ import createContext from '../context'; import createArticle from '../create-article'; import runMiddleware, { NextMiddleware } from '../middleware'; import { WithDataset } from '../../src/middleware/dataset'; +import ArticleNotFound from '../../src/errors/article-not-found'; const dummyNext = async (): Promise => { throw new createHttpError.NotFound(); @@ -35,6 +37,26 @@ describe('article', (): void => { expect(response.status).toBe(200); }); + it('should only attempt article retrieval if next middleware throws not found http error', async (): Promise => { + const mockArticles: Articles = { + set: jest.fn(), + get: jest.fn(async (id: NamedNode): Promise => { + throw new ArticleNotFound(id); + }), + remove: jest.fn(), + contains: jest.fn(), + count: jest.fn(), + [Symbol.asyncIterator]: jest.fn(), + }; + + const next = jest.fn(); + await makeRequest(mockArticles, 'path-to/article/one', next); + expect(mockArticles.get).toHaveBeenCalledTimes(0); + await expect(makeRequest(mockArticles, 'path-to/article/one')).rejects.toBeInstanceOf(createHttpError.NotFound); + expect(mockArticles.get).toHaveBeenCalledTimes(1); + expect(mockArticles.get).toHaveBeenCalledWith(namedNode('http://example.com/path-to/article/one')); + }); + it('should throw an error if article is not found', async (): Promise => { const articles = new InMemoryArticles(); const response = makeRequest(articles, 'path-to/article/not-found'); From 0438de09df0de9f85a3a927c9ef85df743126a75 Mon Sep 17 00:00:00 2001 From: nlisgo Date: Wed, 22 Jan 2020 12:16:03 +0000 Subject: [PATCH 22/35] Use http-status-codes constants --- src/routes/article.ts | 4 ++-- test/routes/article.test.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/routes/article.ts b/src/routes/article.ts index 500dc7cf..c57e4d79 100644 --- a/src/routes/article.ts +++ b/src/routes/article.ts @@ -1,6 +1,6 @@ import clownface from 'clownface'; import createHttpError from 'http-errors'; -import { constants } from 'http2'; +import { OK } from 'http-status-codes'; import { addAll } from 'rdf-dataset-ext'; import { DefaultStateExtends, Middleware, Next, @@ -33,7 +33,7 @@ export default (articles: Articles): Middleware { const response = await makeRequest(articles, 'path-to/article/one'); - expect(response.status).toBe(200); + expect(response.status).toBe(OK); }); it('should only attempt article retrieval if next middleware throws not found http error', async (): Promise => { From a0ab94e24a659b54000640f1685193345d35ea0d Mon Sep 17 00:00:00 2001 From: nlisgo Date: Wed, 22 Jan 2020 12:28:05 +0000 Subject: [PATCH 23/35] should throw error raised in next middleware --- test/routes/article.test.ts | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/test/routes/article.test.ts b/test/routes/article.test.ts index 05bec98a..a9c780c8 100644 --- a/test/routes/article.test.ts +++ b/test/routes/article.test.ts @@ -14,26 +14,26 @@ import runMiddleware, { NextMiddleware } from '../middleware'; import { WithDataset } from '../../src/middleware/dataset'; import ArticleNotFound from '../../src/errors/article-not-found'; +const inMemoryArticles = new InMemoryArticles(); + const dummyNext = async (): Promise => { throw new createHttpError.NotFound(); }; const makeRequest = async ( - articles: Articles, url: string, + articles?: Articles, next?: NextMiddleware, ): Promise> => ( - runMiddleware(article(articles), createContext({ url }), typeof next !== 'undefined' ? next : dummyNext) + runMiddleware(article(typeof articles !== 'undefined' ? articles : inMemoryArticles), createContext({ url }), typeof next !== 'undefined' ? next : dummyNext) ); describe('article', (): void => { it('should return a successful response', async (): Promise => { - const articles = new InMemoryArticles(); - const id = namedNode('http://example.com/path-to/article/one'); - await articles.set(id, createArticle({ id })); + await inMemoryArticles.set(id, createArticle({ id })); - const response = await makeRequest(articles, 'path-to/article/one'); + const response = await makeRequest('path-to/article/one'); expect(response.status).toBe(OK); }); @@ -51,29 +51,32 @@ describe('article', (): void => { }; const next = jest.fn(); - await makeRequest(mockArticles, 'path-to/article/one', next); + await makeRequest('path-to/article/one', mockArticles, next); expect(mockArticles.get).toHaveBeenCalledTimes(0); - await expect(makeRequest(mockArticles, 'path-to/article/one')).rejects.toBeInstanceOf(createHttpError.NotFound); + await expect(makeRequest('path-to/article/one', mockArticles)).rejects.toBeInstanceOf(createHttpError.NotFound); expect(mockArticles.get).toHaveBeenCalledTimes(1); expect(mockArticles.get).toHaveBeenCalledWith(namedNode('http://example.com/path-to/article/one')); }); it('should throw an error if article is not found', async (): Promise => { - const articles = new InMemoryArticles(); - const response = makeRequest(articles, 'path-to/article/not-found'); + 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 articles = new InMemoryArticles(); + it('should throw error raised in next middleware', async (): Promise => { + const next = async (): Promise => { + throw new createHttpError.BadRequest(); + }; + const response = makeRequest('path-to/article/not-found', undefined, next); - const id = namedNode('http://example.com/path-to/article/one'); - await articles.set(id, createArticle({ id })); + await expect(response).rejects.toBeInstanceOf(createHttpError.BadRequest); + }); + it('should call the next middleware', async (): Promise => { const next = jest.fn(); - await makeRequest(articles, 'path-to/article/one', next); + await makeRequest('path-to/article/one', undefined, next); expect(next).toHaveBeenCalledTimes(1); }); From 2805ffc7fa14a30e57aaf8e745ad53c2b061a9ce Mon Sep 17 00:00:00 2001 From: nlisgo Date: Wed, 22 Jan 2020 17:17:36 +0000 Subject: [PATCH 24/35] Address feedback --- src/routes/article.ts | 32 ++++++++++++-------------------- test/routes/article.test.ts | 20 ++++++-------------- 2 files changed, 18 insertions(+), 34 deletions(-) diff --git a/src/routes/article.ts b/src/routes/article.ts index c57e4d79..d679f7ce 100644 --- a/src/routes/article.ts +++ b/src/routes/article.ts @@ -1,7 +1,5 @@ -import clownface from 'clownface'; import createHttpError from 'http-errors'; import { OK } from 'http-status-codes'; -import { addAll } from 'rdf-dataset-ext'; import { DefaultStateExtends, Middleware, Next, } from 'koa'; @@ -16,27 +14,21 @@ export default (articles: Articles): Middleware => { throw new createHttpError.NotFound(); @@ -25,25 +21,24 @@ const makeRequest = async ( articles?: Articles, next?: NextMiddleware, ): Promise> => ( - runMiddleware(article(typeof articles !== 'undefined' ? articles : inMemoryArticles), createContext({ url }), typeof next !== 'undefined' ? next : dummyNext) + runMiddleware(article(typeof articles !== 'undefined' ? articles : new InMemoryArticles()), createContext({ url }), typeof next !== 'undefined' ? next : dummyNext) ); describe('article', (): void => { it('should return a successful response', async (): Promise => { const id = namedNode('http://example.com/path-to/article/one'); - await inMemoryArticles.set(id, createArticle({ id })); + const articles = new InMemoryArticles(); + await articles.set(id, createArticle({ id })); - const response = await makeRequest('path-to/article/one'); + const response = await makeRequest('path-to/article/one', articles); expect(response.status).toBe(OK); }); - it('should only attempt article retrieval if next middleware throws not found http error', async (): Promise => { + it('should not attempt article retrieval when next middleware throws not found http error', async (): Promise => { const mockArticles: Articles = { set: jest.fn(), - get: jest.fn(async (id: NamedNode): Promise => { - throw new ArticleNotFound(id); - }), + get: jest.fn(), remove: jest.fn(), contains: jest.fn(), count: jest.fn(), @@ -53,9 +48,6 @@ describe('article', (): void => { const next = jest.fn(); await makeRequest('path-to/article/one', mockArticles, next); expect(mockArticles.get).toHaveBeenCalledTimes(0); - await expect(makeRequest('path-to/article/one', mockArticles)).rejects.toBeInstanceOf(createHttpError.NotFound); - expect(mockArticles.get).toHaveBeenCalledTimes(1); - expect(mockArticles.get).toHaveBeenCalledWith(namedNode('http://example.com/path-to/article/one')); }); it('should throw an error if article is not found', async (): Promise => { From e02077c2482e1ddedda683cbe41db4076206d259 Mon Sep 17 00:00:00 2001 From: nlisgo Date: Wed, 22 Jan 2020 20:23:37 +0000 Subject: [PATCH 25/35] use AppContext in article request handler --- src/routes/article.ts | 11 ++++++----- test/context.ts | 2 -- test/routes/article.test.ts | 4 ++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/routes/article.ts b/src/routes/article.ts index d679f7ce..e58cea6f 100644 --- a/src/routes/article.ts +++ b/src/routes/article.ts @@ -4,13 +4,14 @@ import { DefaultStateExtends, Middleware, Next, } from 'koa'; import url from 'url'; -import Articles from '../articles'; +import { AppContext } from '../app'; import { namedNode } from '../data-factory'; import ArticleNotFound from '../errors/article-not-found'; -import { DatasetContext } from '../middleware/dataset'; -export default (articles: Articles): Middleware => ( - async ({ request, response }: DatasetContext, next: Next): Promise => { +export default (): Middleware => ( + async ({ + path, articles, request, response, + }: AppContext, next: Next): Promise => { try { await next(); } catch (error) { @@ -19,7 +20,7 @@ export default (articles: Articles): Middleware { const app = new Koa(); app.on('error', errorListener || jest.fn()); @@ -56,7 +55,6 @@ export default ({ host: 'example.com', }, method, - url: typeof url === 'string' ? url : undefined, }); const response = Object.create(app.response) as WithDataset; diff --git a/test/routes/article.test.ts b/test/routes/article.test.ts index 0b021a89..7c67eedd 100644 --- a/test/routes/article.test.ts +++ b/test/routes/article.test.ts @@ -17,11 +17,11 @@ const dummyNext = async (): Promise => { }; const makeRequest = async ( - url: string, + path: string, articles?: Articles, next?: NextMiddleware, ): Promise> => ( - runMiddleware(article(typeof articles !== 'undefined' ? articles : new InMemoryArticles()), createContext({ url }), typeof next !== 'undefined' ? next : dummyNext) + runMiddleware(article(), createContext({ articles, path }), typeof next !== 'undefined' ? next : dummyNext) ); describe('article', (): void => { From 63254c4d78cf333762ed286767f43570ba88e65b Mon Sep 17 00:00:00 2001 From: nlisgo Date: Wed, 22 Jan 2020 20:28:30 +0000 Subject: [PATCH 26/35] No need to pass Articles to article request handler --- src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index e9fcc0e2..c3473e92 100644 --- a/src/app.ts +++ b/src/app.ts @@ -49,7 +49,7 @@ export default ( })); app.use(apiDocumentationLink(apiDocumentationPath)); app.use(errorHandler()); - app.use(article(articles)); + app.use(article()); app.use(routing(router)); return app; From 2f7caa1633fada2f08ae4d6f8a55831fea97fa1d Mon Sep 17 00:00:00 2001 From: Nathan Lisgo Date: Thu, 23 Jan 2020 09:17:16 +0000 Subject: [PATCH 27/35] Remove unneeded context option Co-Authored-By: Chris Wilkinson --- test/context.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/context.ts b/test/context.ts index caa1ee47..8d678cf4 100644 --- a/test/context.ts +++ b/test/context.ts @@ -22,7 +22,6 @@ type Options = { method?: string; path?: string; router?: Router; - url?: string; }; const dummyRouter = { From be71fdcb1cb5c20262d324d8e52e603b605d94cc Mon Sep 17 00:00:00 2001 From: nlisgo Date: Thu, 23 Jan 2020 09:19:01 +0000 Subject: [PATCH 28/35] Simplify with use of AppMiddleware --- src/routes/article.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/routes/article.ts b/src/routes/article.ts index e58cea6f..77cec812 100644 --- a/src/routes/article.ts +++ b/src/routes/article.ts @@ -1,14 +1,12 @@ import createHttpError from 'http-errors'; import { OK } from 'http-status-codes'; -import { - DefaultStateExtends, Middleware, Next, -} from 'koa'; +import { Next } from 'koa'; import url from 'url'; -import { AppContext } from '../app'; +import { AppContext, AppMiddleware } from '../app'; import { namedNode } from '../data-factory'; import ArticleNotFound from '../errors/article-not-found'; -export default (): Middleware => ( +export default (): AppMiddleware => ( async ({ path, articles, request, response, }: AppContext, next: Next): Promise => { From 2174565b123a1db992239d52abc463bd1c1cf00a Mon Sep 17 00:00:00 2001 From: nlisgo Date: Thu, 23 Jan 2020 09:23:44 +0000 Subject: [PATCH 29/35] Avoid embedding try blocks --- src/routes/article.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/routes/article.ts b/src/routes/article.ts index 77cec812..d9a572c3 100644 --- a/src/routes/article.ts +++ b/src/routes/article.ts @@ -12,22 +12,23 @@ export default (): AppMiddleware => ( }: AppContext, next: Next): Promise => { try { await next(); + return; } catch (error) { if (!(error instanceof createHttpError.NotFound)) { throw error; } + } - try { - response.dataset = await articles.get(namedNode(url.resolve(request.origin, path))); - } catch (getError) { - if (getError instanceof ArticleNotFound) { - throw new createHttpError.NotFound(getError.message); - } - - throw getError; + try { + response.dataset = await articles.get(namedNode(url.resolve(request.origin, path))); + } catch (error) { + if (error instanceof ArticleNotFound) { + throw new createHttpError.NotFound(error.message); } - response.status = OK; + throw error; } + + response.status = OK; } ); From 0d43a9a3266b180f31fc88f16e17f8d4c267191c Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Thu, 23 Jan 2020 12:03:13 +0000 Subject: [PATCH 30/35] Try and compromise between the router and named nodes --- src/app.ts | 2 -- src/router.ts | 2 ++ src/routes/add-article.ts | 5 +++-- src/routes/article.ts | 13 +++---------- src/routes/index.ts | 1 + test/routes/article.test.ts | 39 +++++++++---------------------------- 6 files changed, 18 insertions(+), 44 deletions(-) diff --git a/src/app.ts b/src/app.ts index c3473e92..13b4e6f7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,7 +11,6 @@ import errorHandler from './middleware/error-handler'; import jsonld from './middleware/jsonld'; import routing from './middleware/routing'; import namespaces from './namespaces'; -import article from './routes/article'; export type AppState = DefaultState; @@ -49,7 +48,6 @@ export default ( })); app.use(apiDocumentationLink(apiDocumentationPath)); app.use(errorHandler()); - app.use(article()); app.use(routing(router)); return app; 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 328da076..18901032 100644 --- a/src/routes/add-article.ts +++ b/src/routes/add-article.ts @@ -8,10 +8,11 @@ import uniqueString from 'unique-string'; import url from 'url'; import { AppContext, AppMiddleware } from '../app'; import { rdf, schema } from '../namespaces'; +import Routes from './index'; export default (): AppMiddleware => ( async ({ - articles, dataFactory: { namedNode, quad }, request, response, + articles, dataFactory: { namedNode, quad }, request, response, router, }: AppContext, next: Next): Promise => { const id = clownface({ dataset: request.dataset }).has(rdf.type, schema.Article).term; @@ -27,7 +28,7 @@ export default (): AppMiddleware => ( throw new createHttpError.BadRequest(`Article must have at least one ${termToString(schema('name'))}`); } - const newId = namedNode(url.resolve(request.origin, 'articles/'.concat(uniqueString()))); + const newId = namedNode(url.resolve(request.origin, router.url(Routes.Article, uniqueString()))); [...request.dataset].forEach((originalQuad: Quad): void => { let newQuad: Quad; diff --git a/src/routes/article.ts b/src/routes/article.ts index d9a572c3..b39a467f 100644 --- a/src/routes/article.ts +++ b/src/routes/article.ts @@ -8,17 +8,8 @@ import ArticleNotFound from '../errors/article-not-found'; export default (): AppMiddleware => ( async ({ - path, articles, request, response, + articles, request, response, path, }: AppContext, next: Next): Promise => { - try { - await next(); - return; - } catch (error) { - if (!(error instanceof createHttpError.NotFound)) { - throw error; - } - } - try { response.dataset = await articles.get(namedNode(url.resolve(request.origin, path))); } catch (error) { @@ -30,5 +21,7 @@ export default (): AppMiddleware => ( } 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/article.test.ts b/test/routes/article.test.ts index 0c942970..be8bf6a8 100644 --- a/test/routes/article.test.ts +++ b/test/routes/article.test.ts @@ -1,21 +1,17 @@ -import { - namedNode, -} from '@rdfjs/data-model'; +import { namedNode } from '@rdfjs/data-model'; import createHttpError from 'http-errors'; import { OK } from 'http-status-codes'; 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, throwingNext } from '../middleware'; -import { WithDataset } from '../../src/middleware/dataset'; +import runMiddleware, { NextMiddleware } from '../middleware'; const makeRequest = async ( - path: string, - articles?: Articles, - next: NextMiddleware = throwingNext(new createHttpError.NotFound()), + path: string, articles?: Articles, next?: NextMiddleware, ): Promise> => ( runMiddleware(article(), createContext({ articles, path }), next) ); @@ -31,21 +27,6 @@ describe('article', (): void => { expect(response.status).toBe(OK); }); - it('should not attempt article retrieval when next middleware throws not found http error', async (): Promise => { - const mockArticles: Articles = { - set: jest.fn(), - get: jest.fn(), - remove: jest.fn(), - contains: jest.fn(), - count: jest.fn(), - [Symbol.asyncIterator]: jest.fn(), - }; - - const next = jest.fn(); - await makeRequest('path-to/article/one', mockArticles, next); - expect(mockArticles.get).toHaveBeenCalledTimes(0); - }); - it('should throw an error if article is not found', async (): Promise => { const response = makeRequest('path-to/article/not-found'); @@ -53,15 +34,13 @@ describe('article', (): void => { await expect(response).rejects.toHaveProperty('message', 'Article http://example.com/path-to/article/not-found could not be found'); }); - it('should throw error raised in next middleware', async (): Promise => { - const response = makeRequest('path-to/article/not-found', undefined, throwingNext(new createHttpError.BadRequest())); - - await expect(response).rejects.toBeInstanceOf(createHttpError.BadRequest); - }); - 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', undefined, next); + + await makeRequest('path-to/article/one', articles, next); expect(next).toHaveBeenCalledTimes(1); }); From 5cbdaea7043e8ea30a3f56d494e76c5479030b92 Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Thu, 23 Jan 2020 12:49:02 +0000 Subject: [PATCH 31/35] Use data factory from the context --- src/routes/article.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/routes/article.ts b/src/routes/article.ts index b39a467f..3da4aff9 100644 --- a/src/routes/article.ts +++ b/src/routes/article.ts @@ -3,12 +3,11 @@ import { OK } from 'http-status-codes'; import { Next } from 'koa'; import url from 'url'; import { AppContext, AppMiddleware } from '../app'; -import { namedNode } from '../data-factory'; import ArticleNotFound from '../errors/article-not-found'; export default (): AppMiddleware => ( async ({ - articles, request, response, path, + articles, dataFactory: { namedNode }, request, response, path, }: AppContext, next: Next): Promise => { try { response.dataset = await articles.get(namedNode(url.resolve(request.origin, path))); From fa7f1f49299e2030446cfe180f9420b05a6c1331 Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Thu, 23 Jan 2020 13:01:35 +0000 Subject: [PATCH 32/35] Avoid shortcut --- src/routes/article.ts | 4 ++-- test/context.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/routes/article.ts b/src/routes/article.ts index 3da4aff9..33465d69 100644 --- a/src/routes/article.ts +++ b/src/routes/article.ts @@ -7,10 +7,10 @@ import ArticleNotFound from '../errors/article-not-found'; export default (): AppMiddleware => ( async ({ - articles, dataFactory: { namedNode }, request, response, path, + articles, dataFactory: { namedNode }, request, response, }: AppContext, next: Next): Promise => { try { - response.dataset = await articles.get(namedNode(url.resolve(request.origin, path))); + response.dataset = await articles.get(namedNode(url.resolve(request.origin, request.path))); } catch (error) { if (error instanceof ArticleNotFound) { throw new createHttpError.NotFound(error.message); diff --git a/test/context.ts b/test/context.ts index 8d678cf4..806b725c 100644 --- a/test/context.ts +++ b/test/context.ts @@ -54,6 +54,7 @@ export default ({ host: 'example.com', }, method, + url: path, }); const response = Object.create(app.response) as WithDataset; From 6ade9f7907e8c1fcbeddb98c46e4e792b736a94b Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Thu, 23 Jan 2020 13:02:56 +0000 Subject: [PATCH 33/35] Reduce work inside try block --- src/routes/article.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/routes/article.ts b/src/routes/article.ts index 33465d69..34c421c5 100644 --- a/src/routes/article.ts +++ b/src/routes/article.ts @@ -9,8 +9,10 @@ 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(namedNode(url.resolve(request.origin, request.path))); + response.dataset = await articles.get(id); } catch (error) { if (error instanceof ArticleNotFound) { throw new createHttpError.NotFound(error.message); From 701b79025f2c6e89c63aac4ab401540f9596fac3 Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Thu, 23 Jan 2020 13:24:28 +0000 Subject: [PATCH 34/35] Check the article is actually returned --- test/routes/article.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/routes/article.test.ts b/test/routes/article.test.ts index be8bf6a8..7a94a0eb 100644 --- a/test/routes/article.test.ts +++ b/test/routes/article.test.ts @@ -1,6 +1,7 @@ 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'; @@ -27,6 +28,17 @@ describe('article', (): void => { 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'); From 8813756d31c39995e9b06dfe732469773d84f693 Mon Sep 17 00:00:00 2001 From: Chris Wilkinson Date: Thu, 23 Jan 2020 16:01:10 +0000 Subject: [PATCH 35/35] Update Hypertest --- .github/hypertest.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/hypertest.sh b/.github/hypertest.sh index 88feb3af..3d88b5a0 100755 --- a/.github/hypertest.sh +++ b/.github/hypertest.sh @@ -9,4 +9,4 @@ trap finish EXIT make start wait-healthy -docker run --rm --init --network host --mount "type=bind,source=$(pwd)/test/hypertest/,destination=/tests" hydrofoil/hypertest:_0.4.0 --baseUri http://localhost:8080/ +docker run --rm --init --network host --mount "type=bind,source=$(pwd)/test/hypertest/,destination=/tests" hydrofoil/hypertest:_0.4.1 --baseUri http://localhost:8080/