Skip to content

Commit

Permalink
Add JSON responses (#250)
Browse files Browse the repository at this point in the history
* Add JSON responses

Return HTML or JSON , with the format chosen to match the request accept
header.

Additionally, rewrite requests with URLs that end in ".json" to their
equivalent document without an extensnion but with the appropriate
accept header to receive a JSON response.

* Update tests

* Update server/routes/pages.js

Co-authored-by: Isaac White <isaacwhite@users.noreply.github.com>

Co-authored-by: SMores <shane@shanemoore.me>
Co-authored-by: SMores <5354254+SMores@users.noreply.github.com>
Co-authored-by: Isaac White <isaacwhite@users.noreply.github.com>
  • Loading branch information
4 people committed Mar 1, 2021
1 parent 4034367 commit b7b068a
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 15 deletions.
9 changes: 9 additions & 0 deletions server/index.js
Expand Up @@ -69,6 +69,15 @@ app.use((req, res, next) => {
next()
})

// treat requests ending in .json as application/json
app.use((req, res, next) => {
if (req.path.endsWith('.json')) {
req.headers.accept = 'application/json'
req.url = req.baseUrl + req.path.slice(0, -5)
}
next()
})

app.use(pages)
app.use(cache)

Expand Down
34 changes: 27 additions & 7 deletions server/routes/categories.js
Expand Up @@ -48,24 +48,44 @@ async function handleCategory(req, res) {

// if this is a folder, just render from the generic data
if (resourceType === 'folder') {
return res.render(template, baseRenderData, (err, html) => {
if (err) throw err
res.end(html)
return res.format({
html: () => {
res.render(template, baseRenderData, (err, html) => {
if (err) throw err
res.end(html)
})
},

json: () => {
const {template, duplicates, ...jsonResponse} = Object.assign({}, baseRenderData, {resourceType})
res.json(jsonResponse)
}
})
}

res.locals.docId = data.id // we need this for history later
// for docs, fetch the html and then combine with the base data
const {html, byline, createdBy, sections} = await fetchDoc(id, resourceType, req)

res.render(template, Object.assign({}, baseRenderData, {
const renderData = Object.assign({}, baseRenderData, {
content: html,
byline,
createdBy,
sections
}), (err, html) => {
if (err) throw err
res.end(html)
})

res.format({
html: () => {
res.render(template, renderData, (err, html) => {
if (err) throw err
res.end(html)
})
},

json: () => {
const {template, duplicates, ...jsonResponse} = Object.assign({}, renderData, {resourceType})
res.json(jsonResponse)
}
})
}

Expand Down
22 changes: 18 additions & 4 deletions server/routes/errors.js
Expand Up @@ -63,9 +63,23 @@ module.exports = async (err, req, res, next) => {
const code = messages[err.message] || 500
log.error(`Serving an error page for ${req.url}`, err)
const inlined = await loadInlineAssets()
res.status(code).render(`errors/${code}`, {
inlineCSS: inlined.css,
err,
template: inlined.stringTemplate
res.status(code)

res.format({
html: () => {
res.render(`errors/${code}`, {
inlineCSS: inlined.css,
err,
template: inlined.stringTemplate
})
},

json: () => {
res.json({
error: true,
code: code,
message: inlined.stringTemplate(`error.${code}.message`)
})
}
})
}
38 changes: 35 additions & 3 deletions server/routes/pages.js
Expand Up @@ -10,7 +10,7 @@ const {getTemplates, sortDocs, stringTemplate, getConfig} = require('../utils')
router.get('/', handlePage)
router.get('/:page', handlePage)

router.get('/filename-listing.json', async (req, res) => {
router.get('/filename-listing', async (req, res) => {
res.header('Cache-Control', 'public, must-revalidate') // override no-cache
const filenames = await getFilenames()
res.json({filenames: filenames})
Expand Down Expand Up @@ -38,7 +38,23 @@ async function handlePage(req, res) {
if (exactMatches.length === 1) return res.redirect(exactMatches[0].path)
}

res.render(template, {q, results, template: stringTemplate})
res.format({
html: () => {
res.render(template, {q, results, template: stringTemplate})
},

json: () => {
res.json(results.map((result) => ({
url: result.path,
title: result.prettyName,
lastUpdatedBy: (result.lastModifyingUser || {}).displayName,
modifiedAt: result.modifiedTime,
createdAt: result.createdTime,
id: result.id,
resourceType: result.resourceType
})))
}
})
})
}

Expand All @@ -47,7 +63,23 @@ async function handlePage(req, res) {
if (page === 'categories' || page === 'index') {
const tree = await getTree()
const categories = buildDisplayCategories(tree)
res.render(template, {...categories, template: stringTemplate})
res.format({
html: () => {
res.render(template, {...categories, template: stringTemplate})
},

json: () => {
res.json(categories.all.map((category) => ({
url: category.path,
title: category.prettyName,
lastUpdatedBy: (category.lastModifyingUser || {}).displayName,
modifiedAt: category.modifiedTime,
createdAt: category.createdTime,
id: category.id,
resourceType: category.resourceType
})))
}
})
return
}

Expand Down
53 changes: 52 additions & 1 deletion test/functional/pages.test.js
Expand Up @@ -113,7 +113,7 @@ describe('Server responses', () => {
describe('that return JSON', () => {
it('should contain a complete filename listing', () => {
return request(app)
.get('/filename-listing.json')
.get('/filename-listing.json')
.expect(200)
.then((res) => {
const {filenames} = res.body
Expand All @@ -122,5 +122,56 @@ describe('Server responses', () => {
expect(filenames.length).to.equal(allFilenames.length)
})
})

it('folder with home doc should render the doc', () => {
return request(app)
.get('/test-folder-9')
.set('Accept', 'application/json')
.expect(200)
.expect('Content-Type', /json/)
.then((res) => {
const data = JSON.parse(res.text)
expect(data).to.deep.include(
{
url: '/test-folder-9',
title: 'Home article 10 for test folder 9',
lastUpdatedBy: 'Foo Bar',
modifiedAt: '2018-03-02T14:13:20.000Z',
createdAt: '2018-02-19T00:26:40.000Z',
id: 'Test10',
resourceType: 'document',
content: '<p> This is a simple test document export.</p>',
byline: 'John Smith',
createdBy: 'John Smith',
sections: []
}
)
})
})

it('folder with home doc should render the doc (.json suffix)', () => {
return request(app)
.get('/test-folder-9.json')
.expect(200)
.expect('Content-Type', /json/)
.then((res) => {
const data = JSON.parse(res.text)
expect(data).to.deep.include(
{
url: '/test-folder-9',
title: 'Home article 10 for test folder 9',
lastUpdatedBy: 'Foo Bar',
modifiedAt: '2018-03-02T14:13:20.000Z',
createdAt: '2018-02-19T00:26:40.000Z',
id: 'Test10',
resourceType: 'document',
content: '<p> This is a simple test document export.</p>',
byline: 'John Smith',
createdBy: 'John Smith',
sections: []
}
)
})
})
})
})
3 changes: 3 additions & 0 deletions test/unit/errors.test.js
Expand Up @@ -8,6 +8,9 @@ const req = {url: 'foo.com/bar'}
const res = {
renderResults: {},
status: () => res,
format: (formats) => {
formats.html()
},
render: (template, opts) => { res.renderResults = {template, opts} }
}

Expand Down

0 comments on commit b7b068a

Please sign in to comment.