Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Foreign key suffix #570

Merged
merged 5 commits into from
Jun 28, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
})
})
})