mirror of
https://github.com/typicode/json-server.git
synced 2025-07-29 21:23:41 +08:00
Foreign key suffix (#570)
This commit is contained in:
@ -1 +1,2 @@
|
|||||||
src
|
src
|
||||||
|
test
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
# Change Log
|
# 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`
|
* Multiple fields sorting `GET /posts?_sort=user,views&_order=desc,asc`
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ module.exports = function () {
|
|||||||
default: 'id'
|
default: 'id'
|
||||||
},
|
},
|
||||||
foreignKeySuffix: {
|
foreignKeySuffix: {
|
||||||
alias: 'f',
|
alias: 'fks',
|
||||||
description: 'Set foreign key suffix, (e.g. _id as in post_id)',
|
description: 'Set foreign key suffix, (e.g. _id as in post_id)',
|
||||||
default: 'Id'
|
default: 'Id'
|
||||||
},
|
},
|
||||||
|
@ -40,10 +40,11 @@ function createApp (source, object, routes, middlewares, argv) {
|
|||||||
|
|
||||||
let router
|
let router
|
||||||
|
|
||||||
|
const { foreignKeySuffix } = argv
|
||||||
try {
|
try {
|
||||||
router = jsonServer.router(
|
router = jsonServer.router(
|
||||||
is.JSON(source) ? source : object,
|
is.JSON(source) ? source : object,
|
||||||
argv
|
foreignKeySuffix ? { foreignKeySuffix } : undefined
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log()
|
console.log()
|
||||||
|
@ -9,14 +9,16 @@ module.exports = {
|
|||||||
|
|
||||||
// Returns document ids that have unsatisfied relations
|
// Returns document ids that have unsatisfied relations
|
||||||
// Example: a comment that references a post that doesn't exist
|
// Example: a comment that references a post that doesn't exist
|
||||||
function getRemovable (db) {
|
function getRemovable (db, opts) {
|
||||||
const _ = this
|
const _ = this
|
||||||
const removable = []
|
const removable = []
|
||||||
_.each(db, (coll, collName) => {
|
_.each(db, (coll, collName) => {
|
||||||
_.each(coll, (doc) => {
|
_.each(coll, (doc) => {
|
||||||
_.each(doc, (value, key) => {
|
_.each(doc, (value, key) => {
|
||||||
if (/Id$/.test(key)) {
|
if (new RegExp(`${opts.foreignKeySuffix}$`).test(key)) {
|
||||||
const refName = pluralize.plural(key.slice(0, -2))
|
// Remove foreign key suffix and pluralize it
|
||||||
|
// Example postId -> posts
|
||||||
|
const refName = pluralize.plural(key.replace(new RegExp(`${opts.foreignKeySuffix}$`), ''))
|
||||||
// Test if table exists
|
// Test if table exists
|
||||||
if (db[refName]) {
|
if (db[refName]) {
|
||||||
// Test if references is defined in table
|
// Test if references is defined in table
|
||||||
|
@ -11,7 +11,7 @@ const nested = require('./nested')
|
|||||||
const singular = require('./singular')
|
const singular = require('./singular')
|
||||||
const mixins = require('../mixins')
|
const mixins = require('../mixins')
|
||||||
|
|
||||||
module.exports = (source, argv) => {
|
module.exports = (source, opts = { foreignKeySuffix: 'Id' }) => {
|
||||||
// Create router
|
// Create router
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ module.exports = (source, argv) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Handle /:parent/:parentId/:resource
|
// Handle /:parent/:parentId/:resource
|
||||||
router.use(nested())
|
router.use(nested(opts))
|
||||||
|
|
||||||
// Create routes
|
// Create routes
|
||||||
db.forEach((value, key) => {
|
db.forEach((value, key) => {
|
||||||
@ -60,7 +60,7 @@ module.exports = (source, argv) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_.isArray(value)) {
|
if (_.isArray(value)) {
|
||||||
router.use(`/${key}`, plural(db, key, argv))
|
router.use(`/${key}`, plural(db, key, opts))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
const express = require('express')
|
const express = require('express')
|
||||||
const pluralize = require('pluralize')
|
const pluralize = require('pluralize')
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = (opts) => {
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
// Rewrite URL (/:resource/:id/:nested -> /:nested) and request query
|
// Rewrite URL (/:resource/:id/:nested -> /:nested) and request query
|
||||||
function get (req, res, next) {
|
function get (req, res, next) {
|
||||||
const prop = pluralize.singular(req.params.resource)
|
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}`
|
req.url = `/${req.params.nested}`
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
@ -15,7 +15,7 @@ module.exports = () => {
|
|||||||
// Rewrite URL (/:resource/:id/:nested -> /:nested) and request body
|
// Rewrite URL (/:resource/:id/:nested -> /:nested) and request body
|
||||||
function post (req, res, next) {
|
function post (req, res, next) {
|
||||||
const prop = pluralize.singular(req.params.resource)
|
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}`
|
req.url = `/${req.params.nested}`
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ const write = require('./write')
|
|||||||
const getFullURL = require('./get-full-url')
|
const getFullURL = require('./get-full-url')
|
||||||
const utils = require('../utils')
|
const utils = require('../utils')
|
||||||
|
|
||||||
module.exports = (db, name, opts = { foreignKeySuffix: 'Id' }) => {
|
module.exports = (db, name, opts) => {
|
||||||
// Create router
|
// Create router
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
@ -272,8 +272,9 @@ module.exports = (db, name, opts = { foreignKeySuffix: 'Id' }) => {
|
|||||||
.value()
|
.value()
|
||||||
|
|
||||||
// Remove dependents documents
|
// 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) => {
|
removable.forEach((item) => {
|
||||||
db.get(item.name)
|
db.get(item.name)
|
||||||
.removeById(item.id)
|
.removeById(item.id)
|
||||||
|
@ -38,6 +38,9 @@ describe('cli', () => {
|
|||||||
posts: [
|
posts: [
|
||||||
{ id: 1 },
|
{ id: 1 },
|
||||||
{ _id: 2 }
|
{ _id: 2 }
|
||||||
|
],
|
||||||
|
comments: [
|
||||||
|
{ id: 1, post_id: 1 }
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
'db.json'
|
'db.json'
|
||||||
@ -116,9 +119,9 @@ 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) => {
|
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)
|
serverReady(PORT, done)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -126,6 +129,11 @@ describe('cli', () => {
|
|||||||
request.get('/blog/posts/2').expect(200, 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) => {
|
it('should apply middlewares', (done) => {
|
||||||
request.get('/blog/posts/2').expect('X-Hello', 'World', done)
|
request.get('/blog/posts/2').expect('X-Hello', 'World', done)
|
||||||
})
|
})
|
||||||
|
@ -36,7 +36,16 @@ describe('mixins', () => {
|
|||||||
{ name: 'comments', id: 3 }
|
{ 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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
126
test/server/plural-with-custom-foreign-key.js
Normal file
126
test/server/plural-with-custom-foreign-key.js
Normal file
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Reference in New Issue
Block a user