From 9bff1b6f3d08e94c43c3641ad07b947428a21ce0 Mon Sep 17 00:00:00 2001 From: typicode Date: Wed, 28 Jun 2017 23:05:21 +0200 Subject: [PATCH] Foreign key suffix (#570) --- .npmignore | 3 +- CHANGELOG.md | 6 +- src/cli/index.js | 2 +- src/cli/run.js | 3 +- src/server/mixins.js | 8 +- src/server/router/index.js | 6 +- src/server/router/nested.js | 6 +- src/server/router/plural.js | 7 +- test/cli/index.js | 12 +- test/server/mixins.js | 11 +- test/server/plural-with-custom-foreign-key.js | 126 ++++++++++++++++++ 11 files changed, 169 insertions(+), 21 deletions(-) create mode 100644 test/server/plural-with-custom-foreign-key.js diff --git a/.npmignore b/.npmignore index e831038..281df39 100644 --- a/.npmignore +++ b/.npmignore @@ -1 +1,2 @@ -src \ No newline at end of file +src +test diff --git a/CHANGELOG.md b/CHANGELOG.md index 413355c..f836706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` diff --git a/src/cli/index.js b/src/cli/index.js index e25fc09..59018b8 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -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' }, diff --git a/src/cli/run.js b/src/cli/run.js index 59b53d6..feeb498 100644 --- a/src/cli/run.js +++ b/src/cli/run.js @@ -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() diff --git a/src/server/mixins.js b/src/server/mixins.js index 7079efb..3d9632e 100644 --- a/src/server/mixins.js +++ b/src/server/mixins.js @@ -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 diff --git a/src/server/router/index.js b/src/server/router/index.js index e577c6f..a4da0b2 100644 --- a/src/server/router/index.js +++ b/src/server/router/index.js @@ -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() @@ -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) => { @@ -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 } diff --git a/src/server/router/nested.js b/src/server/router/nested.js index 61a1a59..46ddef3 100644 --- a/src/server/router/nested.js +++ b/src/server/router/nested.js @@ -1,13 +1,13 @@ 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() } @@ -15,7 +15,7 @@ module.exports = () => { // 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() } diff --git a/src/server/router/plural.js b/src/server/router/plural.js index e22269e..9d0574a 100644 --- a/src/server/router/plural.js +++ b/src/server/router/plural.js @@ -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() @@ -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) diff --git a/test/cli/index.js b/test/cli/index.js index f822f6d..989863a 100644 --- a/test/cli/index.js +++ b/test/cli/index.js @@ -38,6 +38,9 @@ describe('cli', () => { posts: [ { id: 1 }, { _id: 2 } + ], + comments: [ + { id: 1, post_id: 1 } ] }), '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) => { - 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) }) @@ -126,6 +129,11 @@ describe('cli', () => { 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) }) diff --git a/test/server/mixins.js b/test/server/mixins.js index 639a27a..5ab0a90 100644 --- a/test/server/mixins.js +++ b/test/server/mixins.js @@ -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) }) }) diff --git a/test/server/plural-with-custom-foreign-key.js b/test/server/plural-with-custom-foreign-key.js new file mode 100644 index 0000000..7e7b136 --- /dev/null +++ b/test/server/plural-with-custom-foreign-key.js @@ -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) + }) + }) +})