Skip to content

Commit

Permalink
Foreign key suffix (#570)
Browse files Browse the repository at this point in the history
  • Loading branch information
typicode authored Jun 28, 2017
1 parent 3ec3dec commit 9bff1b6
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 21 deletions.
3 changes: 2 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
src
src
test
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Change Log

## Unreleased
## [0.10.2][2017-06-28]

* Allow alternate foreign key attribute names (eg snake case `post_id`) [#556](https://github.com/typicode/json-server/pull/556)
* Add `--foreignKeySuffix` option (e.g. snake case `post_id`) to make it easier to fake, for example, Rails APIs

## [0.10.1][2017-05-16]
## [0.10.1][2017-05-16]

* Multiple fields sorting `GET /posts?_sort=user,views&_order=desc,asc`

Expand Down
2 changes: 1 addition & 1 deletion src/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ module.exports = function () {
default: 'id'
},
foreignKeySuffix: {
alias: 'f',
alias: 'fks',
description: 'Set foreign key suffix, (e.g. _id as in post_id)',
default: 'Id'
},
Expand Down
3 changes: 2 additions & 1 deletion src/cli/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ function createApp (source, object, routes, middlewares, argv) {

let router

const { foreignKeySuffix } = argv
try {
router = jsonServer.router(
is.JSON(source) ? source : object,
argv
foreignKeySuffix ? { foreignKeySuffix } : undefined
)
} catch (e) {
console.log()
Expand Down
8 changes: 5 additions & 3 deletions src/server/mixins.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ module.exports = {

// Returns document ids that have unsatisfied relations
// Example: a comment that references a post that doesn't exist
function getRemovable (db) {
function getRemovable (db, opts) {
const _ = this
const removable = []
_.each(db, (coll, collName) => {
_.each(coll, (doc) => {
_.each(doc, (value, key) => {
if (/Id$/.test(key)) {
const refName = pluralize.plural(key.slice(0, -2))
if (new RegExp(`${opts.foreignKeySuffix}$`).test(key)) {
// Remove foreign key suffix and pluralize it
// Example postId -> posts
const refName = pluralize.plural(key.replace(new RegExp(`${opts.foreignKeySuffix}$`), ''))
// Test if table exists
if (db[refName]) {
// Test if references is defined in table
Expand Down
6 changes: 3 additions & 3 deletions src/server/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const nested = require('./nested')
const singular = require('./singular')
const mixins = require('../mixins')

module.exports = (source, argv) => {
module.exports = (source, opts = { foreignKeySuffix: 'Id' }) => {
// Create router
const router = express.Router()

Expand Down Expand Up @@ -50,7 +50,7 @@ module.exports = (source, argv) => {
})

// Handle /:parent/:parentId/:resource
router.use(nested())
router.use(nested(opts))

// Create routes
db.forEach((value, key) => {
Expand All @@ -60,7 +60,7 @@ module.exports = (source, argv) => {
}

if (_.isArray(value)) {
router.use(`/${key}`, plural(db, key, argv))
router.use(`/${key}`, plural(db, key, opts))
return
}

Expand Down
6 changes: 3 additions & 3 deletions src/server/router/nested.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
const express = require('express')
const pluralize = require('pluralize')

module.exports = () => {
module.exports = (opts) => {
const router = express.Router()

// Rewrite URL (/:resource/:id/:nested -> /:nested) and request query
function get (req, res, next) {
const prop = pluralize.singular(req.params.resource)
req.query[`${prop}Id`] = req.params.id
req.query[`${prop}${opts.foreignKeySuffix}`] = req.params.id
req.url = `/${req.params.nested}`
next()
}

// Rewrite URL (/:resource/:id/:nested -> /:nested) and request body
function post (req, res, next) {
const prop = pluralize.singular(req.params.resource)
req.body[`${prop}Id`] = req.params.id
req.body[`${prop}${opts.foreignKeySuffix}`] = req.params.id
req.url = `/${req.params.nested}`
next()
}
Expand Down
7 changes: 4 additions & 3 deletions src/server/router/plural.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const write = require('./write')
const getFullURL = require('./get-full-url')
const utils = require('../utils')

module.exports = (db, name, opts = { foreignKeySuffix: 'Id' }) => {
module.exports = (db, name, opts) => {
// Create router
const router = express.Router()

Expand Down Expand Up @@ -272,8 +272,9 @@ module.exports = (db, name, opts = { foreignKeySuffix: 'Id' }) => {
.value()

// Remove dependents documents
const removable = db._.getRemovable(db.getState())

console.log({opts})
const removable = db._.getRemovable(db.getState(), opts)
console.log(removable)
removable.forEach((item) => {
db.get(item.name)
.removeById(item.id)
Expand Down
12 changes: 10 additions & 2 deletions test/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ describe('cli', () => {
posts: [
{ id: 1 },
{ _id: 2 }
],
comments: [
{ id: 1, post_id: 1 }
]
}),
'db.json'
Expand Down Expand Up @@ -116,16 +119,21 @@ describe('cli', () => {
})
})

describe('db.json -r routes.json -m middleware.js -i _id --read-only', () => {
describe('db.json -r routes.json -m middleware.js -i _id --foreignKeySuffix _id --read-only', () => {
beforeEach((done) => {
child = cli([ dbFile, '-r', routesFile, '-m', middlewareFiles.en, '-i', '_id', '--read-only' ])
child = cli([ dbFile, '-r', routesFile, '-m', middlewareFiles.en, '-i', '_id', '--read-only', '--foreignKeySuffix', '_id' ])
serverReady(PORT, done)
})

it('should use routes.json and _id as the identifier', (done) => {
request.get('/blog/posts/2').expect(200, done)
})

it('should use _id as foreignKeySuffix', async () => {
const response = await request.get('/posts/1/comments')
assert.equal(response.body.length, 1)
})

it('should apply middlewares', (done) => {
request.get('/blog/posts/2').expect('X-Hello', 'World', done)
})
Expand Down
11 changes: 10 additions & 1 deletion test/server/mixins.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,16 @@ describe('mixins', () => {
{ name: 'comments', id: 3 }
]

assert.deepEqual(_.getRemovable(db), expected)
assert.deepEqual(_.getRemovable(db, { foreignKeySuffix: 'Id' }), expected)
})

it('should support custom foreignKeySuffix', () => {
const expected = [
{ name: 'comments', id: 2 },
{ name: 'comments', id: 3 }
]

assert.deepEqual(_.getRemovable(db, { foreignKeySuffix: 'Id' }), expected)
})
})

Expand Down
126 changes: 126 additions & 0 deletions test/server/plural-with-custom-foreign-key.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
const assert = require('assert')
const _ = require('lodash')
const request = require('supertest')
const jsonServer = require('../../src/server')

describe('Server with custom foreign key', () => {
let server
let router
let db

beforeEach(() => {
db = {}

db.posts = [
{ id: 1, body: 'foo' },
{ id: 2, body: 'bar' }
]

db.comments = [
{ id: 1, post_id: 1 },
{ id: 2, post_id: 1 },
{ id: 3, post_id: 2 }
]

server = jsonServer.create()
router = jsonServer.router(db, { foreignKeySuffix: '_id' })
server.use(jsonServer.defaults())
server.use(router)
})

describe('GET /:parent/:parentId/:resource', () => {
it('should respond with json and corresponding nested resources', () => (
request(server)
.get('/posts/1/comments')
.expect('Content-Type', /json/)
.expect([
db.comments[0],
db.comments[1]
])
.expect(200)
))
})

describe('GET /:resource/:id', () => {
it('should respond with json and corresponding resource', () => (
request(server)
.get('/posts/1')
.expect('Content-Type', /json/)
.expect(db.posts[0])
.expect(200)
))
})

describe('GET /:resource?_embed=', () => {
it('should respond with corresponding resources and embedded resources', () => {
const posts = _.cloneDeep(db.posts)
posts[0].comments = [ db.comments[0], db.comments[1] ]
posts[1].comments = [ db.comments[2] ]
return request(server)
.get('/posts?_embed=comments')
.expect('Content-Type', /json/)
.expect(posts)
.expect(200)
})
})

describe('GET /:resource/:id?_embed=', () => {
it('should respond with corresponding resources and embedded resources', () => {
const post = _.cloneDeep(db.posts[0])
post.comments = [ db.comments[0], db.comments[1] ]
return request(server)
.get('/posts/1?_embed=comments')
.expect('Content-Type', /json/)
.expect(post)
.expect(200)
})
})

describe('GET /:resource?_expand=', () => {
it('should respond with corresponding resource and expanded inner resources', () => {
const comments = _.cloneDeep(db.comments)
comments[0].post = db.posts[0]
comments[1].post = db.posts[0]
comments[2].post = db.posts[1]
return request(server)
.get('/comments?_expand=post')
.expect('Content-Type', /json/)
.expect(comments)
.expect(200)
})
})

describe('GET /:resource/:id?_expand=', () => {
it('should respond with corresponding resource and expanded inner resources', () => {
const comment = _.cloneDeep(db.comments[0])
comment.post = db.posts[0]
return request(server)
.get('/comments/1?_expand=post')
.expect('Content-Type', /json/)
.expect(comment)
.expect(200)
})
})

describe('POST /:parent/:parentId/:resource', () => {
it('should respond with json and set parentId', () => (
request(server)
.post('/posts/1/comments')
.send({body: 'foo'})
.expect('Content-Type', /json/)
.expect({id: 4, post_id: 1, body: 'foo'})
.expect(201)
))
})

describe('DELETE /:resource/:id', () => {
it('should respond with empty data, destroy resource and dependent resources', async () => {
await request(server)
.del('/posts/1')
.expect({})
.expect(200)
assert.equal(db.posts.length, 1)
assert.equal(db.comments.length, 1)
})
})
})

0 comments on commit 9bff1b6

Please sign in to comment.