-
Notifications
You must be signed in to change notification settings - Fork 24
/
index.js
213 lines (186 loc) · 6.68 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
'use strict'
let Promise = require('bluebird')
let result = require('lodash.result')
let merge = require('lodash.merge')
/**
* A function that can be used as a plugin for bookshelf
* @param {Object} bookshelf The main bookshelf instance
* @param {Object} [settings] Additional settings for configuring this plugin
* @param {String} [settings.field=deleted_at] The name of the field that stores
* the soft delete information for that model
* @param {String?} [settings.sentinel=null] The name of the field that stores
* the model's active state as a boolean for unique indexing purposes, if any
*/
module.exports = (bookshelf, settings) => {
// Add default settings
settings = merge(
{
field: 'deleted_at',
nullValue: null,
sentinel: null,
events: {
destroying: true,
updating: false,
saving: false,
destroyed: true,
updated: false,
saved: false
}
},
settings
)
/**
* Check if the operation needs to be patched for not retrieving
* soft deleted rows
* @param {Object} model An instantiated bookshelf model
* @param {Object} attrs The attributes that's being queried
* @param {Object} options The operation option
* @param {Boolean} [options.withDeleted=false] Override the default behavior
* and allow querying soft deleted objects
*/
function skipDeleted (model, attrs, options) {
if (!options.isEager || options.parentResponse) {
let softDelete = this.model
? this.model.prototype.softDelete
: this.softDelete
if (softDelete && !options.withDeleted) {
if (settings.nullValue === null) {
options.query.whereNull(`${result(this, 'tableName')}.${settings.field}`)
} else {
options.query.where(`${result(this, 'tableName')}.${settings.field}`, settings.nullValue)
}
}
}
}
// Store prototypes for later
let modelPrototype = bookshelf.Model.prototype
let collectionPrototype = bookshelf.Collection.prototype
// Extends the default collection to be able to patch relational queries
// against a set of models
bookshelf.Collection = bookshelf.Collection.extend({
initialize: function () {
collectionPrototype.initialize.call(this)
this.on('fetching', skipDeleted.bind(this))
this.on('counting', (collection, options) =>
skipDeleted.call(this, null, null, options)
)
}
})
// Extends the default model class
bookshelf.Model = bookshelf.Model.extend({
initialize: function () {
modelPrototype.initialize.call(this)
if (this.softDelete && settings.sentinel) {
this.defaults = merge(
{
[settings.sentinel]: true
},
result(this, 'defaults')
)
}
this.on('fetching', skipDeleted.bind(this))
},
/**
* Override the default destroy method to provide soft deletion logic
* @param {Object} [options] The default options parameters from Model.destroy
* @param {Boolean} [options.hardDelete=false] Override the default soft
* delete behavior and allow a model to be hard deleted
* @param {Number|Date} [options.date=new Date()] Use a client supplied time
* @return {Promise} A promise that's fulfilled when the model has been
* hard or soft deleted
*/
destroy: function (options) {
options = options || {}
if (this.softDelete && !options.hardDelete) {
let query = this.query()
// Add default values to options
options = merge(
{
method: 'update',
patch: true,
softDelete: true,
query: query
},
options
)
const date = options.date ? new Date(options.date) : new Date()
// Attributes to be passed to events
let attrs = { [settings.field]: date }
// Null out sentinel column, since NULL is not considered by SQL unique indexes
if (settings.sentinel) {
attrs[settings.sentinel] = null
}
// Make sure the field is formatted the same as other date columns
attrs = this.format(attrs)
return Promise.resolve()
.then(() => {
// Don't need to trigger hooks if there's no events registered
if (!settings.events) return
let events = []
// Emulate all pre update events
if (settings.events.destroying) {
events.push(
this.triggerThen('destroying', this, options).bind(this)
)
}
if (settings.events.saving) {
events.push(
this.triggerThen('saving', this, attrs, options).bind(this)
)
}
if (settings.events.updating) {
events.push(
this.triggerThen('updating', this, attrs, options).bind(this)
)
}
// Resolve all promises in parallel like bookshelf does
return Promise.all(events)
})
.then(() => {
// Check if we need to use a transaction
if (options.transacting) {
query = query.transacting(options.transacting)
}
return query
.update(attrs, this.idAttribute)
.where(this.format(this.attributes))
.where(`${result(this, 'tableName')}.${settings.field}`, settings.nullValue)
})
.then((resp) => {
// Check if the caller required a row to be deleted and if
// events weren't totally disabled
if (resp === 0 && options.require) {
throw new this.constructor.NoRowsDeletedError('No Rows Deleted')
} else if (!settings.events) {
return
}
// Add previous attr for reference and reset the model to pristine state
this.set(attrs)
options.previousAttributes = this._previousAttributes
this._reset()
let events = []
// Emulate all post update events
if (settings.events.destroyed) {
events.push(
this.triggerThen('destroyed', this, options).bind(this)
)
}
if (settings.events.saved) {
events.push(
this.triggerThen('saved', this, resp, options).bind(this)
)
}
if (settings.events.updated) {
events.push(
this.triggerThen('updated', this, resp, options).bind(this)
)
}
return Promise.all(events)
})
.then(() => this)
} else {
return modelPrototype.destroy.call(this, options)
}
}
})
}