-
Notifications
You must be signed in to change notification settings - Fork 601
/
save.js
220 lines (175 loc) · 6.23 KB
/
save.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
214
215
216
217
218
219
220
var _ = require('@sailshq/lodash');
var async = require('async');
var deep = require('deep-diff');
var updateInstance = require('../associationMethods/update');
var addAssociation = require('../associationMethods/add');
var removeAssociation = require('../associationMethods/remove');
var hop = require('../../../utils/helpers').object.hasOwnProperty;
var defer = require('../../../utils/defer');
var WLError = require('../../../error/WLError');
var noop = function() {};
/**
* Model.save()
*
* Takes the currently set attributes and updates the database.
* Shorthand for Model.update({ attributes }, cb)
*
* @param {Object} context
* @param {Object} proto
* @param {Function} callback
* @param {Object} options
* @return {Promise}
* @api public
*/
module.exports = function(context, proto, options, cb) {
var deferred;
if (typeof options === 'function') {
cb = options;
options = {};
}
if (typeof cb !== 'function') {
deferred = defer();
}
cb = cb || noop;
/**
* TO-DO:
* This should all be wrapped in a transaction. That's coming next but for the meantime
* just hope we don't get in a nasty state where the operation fails!
*/
var mutatedModels = [];
async.auto({
// Compare any populated model values to their current state.
// If they have been mutated then the values will need to be synced.
compareModelValues: function(next) {
var modelKeys = Object.keys(proto.associationsCache);
async.each(modelKeys, function(key, nextKey) {
if (!hop(proto, key) || proto[key] === undefined) {
return async.setImmediate(function() {
nextKey();
});
}
var currentVal = proto[key];
var previousVal = proto.associationsCache[key];
// Normalize previousVal to an object
if (Array.isArray(previousVal)) {
previousVal = previousVal[0];
}
if (deep(currentVal, previousVal)) {
mutatedModels.push(key);
}
return async.setImmediate(function() {
nextKey();
});
}, next);
},
// Update The Current Record
updateRecord: ['compareModelValues', function(unused, next) {
// Shallow clone proto.toObject() to remove all the functions
var data = _.clone(proto.toObject());
new updateInstance(context, data, mutatedModels, function(err, data) {
next(err, data);
});
}],
// Build a set of associations to add and remove.
// These are populated from using model[associationKey].add() and
// model[associationKey].remove().
buildAssociationOperations: ['compareModelValues', function(unused, next) {
// Build a dictionary to hold operations based on association key
var operations = {
addKeys: {},
removeKeys: {}
};
Object.keys(proto.associations).forEach(function(key) {
// Ignore belongsTo associations
if (proto.associations[key].hasOwnProperty('model')) return;
// Grab what records need adding
if (proto.associations[key].addModels.length > 0) {
operations.addKeys[key] = proto.associations[key].addModels;
}
// Grab what records need removing
if (proto.associations[key].removeModels.length > 0) {
operations.removeKeys[key] = proto.associations[key].removeModels;
}
});
return async.setImmediate(function() {
return next(null, operations);
});
}],
// Create new associations for each association key
addAssociations: ['buildAssociationOperations', 'updateRecord', function(results, next) {
var keys = results.buildAssociationOperations.addKeys;
return new addAssociation(context, proto, keys, function(err, failedTransactions) {
if (err) return next(err);
// reset addKeys
for (var key in results.buildAssociationOperations.addKeys) {
proto.associations[key].addModels = [];
}
next(null, failedTransactions);
});
}],
// Remove associations for each association key
// Run after the addAssociations so that the connection pools don't get exhausted.
// Once transactions are ready we can remove this restriction as they will be run on the same
// connection.
removeAssociations: ['buildAssociationOperations', 'addAssociations', function(results, next) {
var keys = results.buildAssociationOperations.removeKeys;
return new removeAssociation(context, proto, keys, function(err, failedTransactions) {
if (err) return next(err);
// reset removeKeys
for (var key in results.buildAssociationOperations.removeKeys) {
proto.associations[key].removeModels = [];
}
next(null, failedTransactions);
});
}]
},
function(err, results) {
if (err) {
if (deferred) {
deferred.reject(err);
}
return cb(err);
}
// Collect all failed transactions if any
var failedTransactions = [];
var error;
if (results.addAssociations) {
failedTransactions = failedTransactions.concat(results.addAssociations);
}
if (results.removeAssociations) {
failedTransactions = failedTransactions.concat(results.removeAssociations);
}
if (failedTransactions.length > 0) {
error = new Error('Some associations could not be added or destroyed during save().');
error.failedTransactions = failedTransactions;
if (deferred) {
deferred.reject(new WLError(error));
}
return cb(new WLError(error));
}
if (!results.updateRecord.length) {
error = new Error('Error updating a record.');
if (deferred) {
deferred.reject(new WLError(error));
}
return cb(new WLError(error));
}
// Reset the model attribute values with the new values.
// This is needed because you could have a lifecycle callback that has
// changed the data since last time you accessed it.
// Attach attributes to the model instance
var newData = results.updateRecord[0];
_.each(newData, function(val, key) {
proto[key] = val;
});
// If a promise, resolve it
if (deferred) {
deferred.resolve();
}
// Return the callback
return cb();
});
if (deferred) {
return deferred.promise;
}
};