UNPKG

4.96 kBJavaScriptView Raw
1// TODO: We do not consider forks and joins here: We need to
2
3var errors = require('../errors')
4
5module.exports = function filterSafeDeletes (osm, batch, cb) {
6 // A mapping of ids of ways and relations to
7 // the ids of elements that they reference
8 var pendingRefs = {}
9 // A list of all references to each element within the changeset itself
10 // It seems possible that a changeset could modify an element to reference
11 // an element that was not previously referenced, and is then deleted
12 // later in the changeset. This logic checks for that
13 var reverseRefs = {}
14 var filtered = []
15 var inUseErrors = []
16 var pending = batch.length
17
18 batch.forEach(function (change) {
19 var id = change.id
20 if (change.action !== 'delete' && change.type !== 'node') {
21 // Remove previous reverse refs to this element
22 ;(pendingRefs[id] || []).forEach(function (ref) {
23 reverseRefs[ref] = (reverseRefs[ref] || []).filter(function (ref) {
24 return id !== ref
25 })
26 })
27 // If a way or relation is modified, keep a list of the elements
28 // they reference, since this overrides what is in the db
29 if (change.type === 'way') {
30 pendingRefs[id] = change.nodes || []
31 } else if (change.type === 'relation') {
32 pendingRefs[id] = change.members
33 ? change.members.map(function (m) { return m.ref }) : []
34 }
35 // Add new reverse refs
36 ;(pendingRefs[id] || []).forEach(function (ref) {
37 reverseRefs[ref] = reverseRefs[ref] || []
38 reverseRefs[ref].push(id)
39 })
40 }
41 // If action is not delete it is always included
42 if (change.action !== 'delete') {
43 filtered.push(change)
44 return onCheck()
45 }
46
47 /** We only get here for change.action === 'delete' */
48
49 // If a way or relation that appeared in the create or modified block
50 // is subsequently deleted, we can remove the references to it
51 if (change.type !== 'node') {
52 // Remove reverse refs to this element
53 ;(pendingRefs[id] || []).forEach(function (ref) {
54 reverseRefs[ref] = (reverseRefs[ref] || []).filter(function (ref) {
55 return id !== ref
56 })
57 })
58 pendingRefs[id] = []
59 }
60
61 // Check whether the element to be deleted is referenced by any
62 // existing ways or relations in the database
63 refList(osm, id, function (err, refs) {
64 if (err) return cb(err)
65 // Update the refs from the database with pending refs from the changeset
66 var mergedRefs = refs.filter(function (ref) {
67 // If the element is referenced in the database by an element
68 // that is earlier in the changeset, what is important is
69 // whether the new version of the element still references it
70 if (pendingRefs[ref.value]) {
71 return pendingRefs[ref.value].indexOf(id) > -1
72 } else {
73 return true
74 }
75 })
76 if (mergedRefs.length || reverseRefs[id] && reverseRefs[id].length) {
77 // If there are any references in mergedRefs, then element is in use
78 // and cannot be deleted without ...
79 if (change.ifUnused) {
80 // If the ifUnused prop is set, silently skip and continue
81 onCheck()
82 } else {
83 refs.forEach(function (ref) {
84 inUseErrors.push({id: id, usedBy: ref.value})
85 })
86 onCheck()
87 }
88 } else {
89 // If the element is not referenced anywhere, we can safely delete it
90 filtered.push(change)
91 onCheck()
92 }
93 })
94 })
95
96 function onCheck () {
97 if (--pending > 0) return
98 if (!inUseErrors.length) return cb(null, filtered)
99 var msg = ''
100 inUseErrors.forEach(function (inUse) {
101 msg += 'Element #' + inUse.id + ' is still used by element #' + inUse.usedBy + '.'
102 })
103 cb(new errors.InUse(msg))
104 }
105}
106
107// work-around to ensure that reference counts are accurate for ifUnused calculation
108// TODO: Why do we need this?
109function refList (osm, id, cb) {
110 var res = []
111 osm.refs.list(id, function (err, refs) {
112 if (err) return cb(err)
113 var pending = 1
114 refs.forEach(function (r) {
115 var ref = r.value
116 pending++
117 osm.get(ref, function (err, docs) {
118 if (err && !notFound(err)) return cb(err)
119 var contained = false
120 Object.keys(docs || {}).forEach(function (key) {
121 var d = docs[key]
122 if (!d) return
123 if (d.refs && d.refs.indexOf(id) >= 0) {
124 contained = true
125 } else if (d.nodes && d.nodes.indexOf(id) >= 0) {
126 contained = true
127 } else if (d.members && d.members.map(refid).indexOf(id) >= 0) {
128 contained = true
129 }
130 })
131 if (contained) res.push(r)
132 if (--pending === 0) cb(null, res)
133 })
134 })
135 if (--pending === 0) cb(null, res)
136 })
137}
138
139function refid (m) { return m.ref || m.id }
140
141function notFound (err) {
142 return err && (/^notfound/i.test(err) || err.notFound)
143}