Skip to content

DELETE request: only remove dependent document + add no cascade flag #756

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ Also when doing requests, it's good to know that:
- Your request body JSON should be object enclosed, just like the GET output. (for example `{"name": "Foobar"}`)
- Id values are not mutable. Any `id` value in the body of your PUT or PATCH request wil be ignored. Only a value set in a POST request wil be respected, but only if not already taken.
- A POST, PUT or PATCH request should include a `Content-Type: application/json` header to use the JSON in the request body. Otherwise it will result in a 200 OK but without changes being made to the data.
- DELETE requests will look for dependent documents, and will remove them as well. That means that for the previous `db.json` file, a `DELETE /posts/1` would also delete the dependent comment (with `postId: 1`). You can disable this behavior by raising the 'no-delete-cascade' flag. See [CLI usage](#cli-usage).

## Install

Expand Down Expand Up @@ -369,6 +370,7 @@ Options:
--static, -s Set static files directory
--read-only, --ro Allow only GET requests [boolean]
--no-cors, --nc Disable Cross-Origin Resource Sharing [boolean]
--no-delete-cascade, --ndc Disable delete cacade behavior [boolean]
--no-gzip, --ng Disable GZIP Content-Encoding [boolean]
--snapshots, -S Set snapshots directory [default: "."]
--delay, -d Add delay to responses (ms)
Expand Down
5 changes: 5 additions & 0 deletions src/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ module.exports = function() {
alias: 'nc',
description: 'Disable Cross-Origin Resource Sharing'
},
'no-delete-cascade': {
alias: 'ndc',
description: 'Disable delete cascade behavior'
},
'no-gzip': {
alias: 'ng',
description: 'Disable GZIP Content-Encoding'
Expand Down Expand Up @@ -82,6 +86,7 @@ module.exports = function() {
.boolean('read-only')
.boolean('quiet')
.boolean('no-cors')
.boolean('no-delete-cascade')
.boolean('no-gzip')
.help('help')
.alias('help', 'h')
Expand Down
10 changes: 5 additions & 5 deletions src/cli/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ function createApp(source, object, routes, middlewares, argv) {

let router

const { foreignKeySuffix } = argv
const { foreignKeySuffix, noDeleteCascade } = argv
try {
router = jsonServer.router(
is.JSON(source) ? source : object,
foreignKeySuffix ? { foreignKeySuffix } : undefined
)
router = jsonServer.router(is.JSON(source) ? source : object, {
foreignKeySuffix,
noDeleteCascade
})
} catch (e) {
console.log()
console.error(chalk.red(e.message.replace(/^/gm, ' ')))
Expand Down
56 changes: 27 additions & 29 deletions src/server/mixins.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,38 @@
const nanoid = require('nanoid')
const pluralize = require('pluralize')

module.exports = {
getRemovable,
getDependents,
createId,
deepQuery
}

// Returns document ids that have unsatisfied relations
// Example: a comment that references a post that doesn't exist
function getRemovable(db, opts) {
/**
* Return documents which are dependent on the specified foreign field
* Example: a comment that references a post that doesn't exist
*
* @param {object} db - The entire database object
* @param {string} foreignField - The foreign field name. e.g. "postId"
* @param {string|number} foreignId - The foreign field id to match
* @return {[{name: string, id: string|number]} - Array of dependent objects with resource names and ids
*/
function getDependents(db, foreignField, foreignId) {
const _ = this
const removable = []
_.each(db, (coll, collName) => {
_.each(coll, doc => {
_.each(doc, (value, key) => {
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
const ref = _.getById(db[refName], value)
if (_.isUndefined(ref)) {
removable.push({ name: collName, id: doc.id })
}
}
}
})
})
})

return removable
return _.reduce(
db,
(acc, table, tableName) =>
// only work on arrays; object are irrelevant
!_.isArray(table)
? acc
: table
.filter(
doc =>
// perform a type-insensitive comparison (so we could compare '2' to 2
_.get(doc, foreignField, '').toString() === foreignId.toString()
)
.map(doc => ({ name: tableName, id: doc.id }))
.concat(acc),
[]
)
}

// Return incremented id or uuid
Expand Down
7 changes: 2 additions & 5 deletions src/server/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,8 @@ module.exports = (source, opts = { foreignKeySuffix: 'Id' }) => {
return
}

const msg =
`Type of "${key}" (${typeof value}) ${_.isObject(source)
? ''
: `in ${source}`} is not supported. ` +
`Use objects or arrays of objects.`
const srcDesc = _.isObject(source) ? '' : `in ${source}`
const msg = `Type of "${key}" (${typeof value}) ${srcDesc} is not supported. Use objects or arrays of objects.`

throw new Error(msg)
})
Expand Down
19 changes: 11 additions & 8 deletions src/server/router/plural.js
Original file line number Diff line number Diff line change
Expand Up @@ -296,14 +296,17 @@ module.exports = (db, name, opts) => {
.removeById(req.params.id)
.value()

// Remove dependents documents
const removable = db._.getRemovable(db.getState(), opts)
removable.forEach(item => {
db
.get(item.name)
.removeById(item.id)
.value()
})
if (!opts.noDeleteCascade) {
// Remove dependents documents
const prop = `${pluralize.singular(name)}${opts.foreignKeySuffix}`
const dependents = db._.getDependents(db.getState(), prop, req.params.id)
dependents.forEach(item => {
db
.get(item.name)
.removeById(item.id)
.value()
})
}

if (resource) {
res.locals.data = {}
Expand Down
15 changes: 10 additions & 5 deletions test/server/mixins.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,28 @@ describe('mixins', () => {
}
})

describe('getRemovable', () => {
it('should return removable documents', () => {
describe('getDependents', () => {
it('should return dependent documents', () => {
const expected = [
{ name: 'comments', id: 2 },
{ name: 'comments', id: 3 }
]

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

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

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

it('should not return irrelevant dead documents', () => {
const expected = [{ name: 'comments', id: 1 }]
assert.deepEqual(_.getDependents(db, 'postId', 1), expected)
})
})

Expand Down