Skip to content

Commit 67ce4ed

Browse files
authored
fix: purify AV._decode/_encode (#417)
and prevent Object.set from muting value
1 parent 7f5c497 commit 67ce4ed

File tree

10 files changed

+110
-63
lines changed

10 files changed

+110
-63
lines changed

src/av.js

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,13 @@ AV.setProduction = (production) => {
117117
*/
118118
AV._getAVPath = function(path) {
119119
if (!AV.applicationId) {
120-
throw "You need to call AV.initialize before using AV.";
120+
throw new Error("You need to call AV.initialize before using AV.");
121121
}
122122
if (!path) {
123123
path = "";
124124
}
125125
if (!_.isString(path)) {
126-
throw "Tried to get a localStorage path that wasn't a String.";
126+
throw new Error("Tried to get a localStorage path that wasn't a String.");
127127
}
128128
if (path[0] === "/") {
129129
path = path.substring(1);
@@ -216,7 +216,7 @@ AV._getValue = function(object, prop) {
216216
AV._encode = function(value, seenObjects, disallowObjects) {
217217
if (value instanceof AV.Object) {
218218
if (disallowObjects) {
219-
throw "AV.Objects not allowed here";
219+
throw new Error("AV.Objects not allowed here");
220220
}
221221
if (!seenObjects || _.include(seenObjects, value) || !value._hasData) {
222222
return value._toPointer();
@@ -227,7 +227,7 @@ AV._encode = function(value, seenObjects, disallowObjects) {
227227
seenObjects,
228228
disallowObjects);
229229
}
230-
throw "Tried to save an object with a pointer to a new, unsaved object.";
230+
throw new Error("Tried to save an object with a pointer to a new, unsaved object.");
231231
}
232232
if (value instanceof AV.ACL) {
233233
return value.toJSON();
@@ -254,7 +254,7 @@ AV._encode = function(value, seenObjects, disallowObjects) {
254254
}
255255
if (value instanceof AV.File) {
256256
if (!value.url() && !value.id) {
257-
throw "Tried to save an object containing an unsaved file.";
257+
throw new Error("Tried to save an object containing an unsaved file.");
258258
}
259259
return {
260260
__type: "File",
@@ -264,29 +264,21 @@ AV._encode = function(value, seenObjects, disallowObjects) {
264264
};
265265
}
266266
if (_.isObject(value)) {
267-
var output = {};
268-
AV._objectEach(value, function(v, k) {
269-
output[k] = AV._encode(v, seenObjects, disallowObjects);
270-
});
271-
return output;
267+
return _.mapObject(value, (v, k) => AV._encode(v, seenObjects, disallowObjects));
272268
}
273269
return value;
274270
};
275271

276272
/**
277273
* The inverse function of AV._encode.
278-
* TODO: make decode not mutate value.
279274
* @private
280275
*/
281-
AV._decode = function(key, value) {
276+
AV._decode = function(value, key) {
282277
if (!_.isObject(value)) {
283278
return value;
284279
}
285280
if (_.isArray(value)) {
286-
AV._arrayEach(value, function(v, k) {
287-
value[k] = AV._decode(k, v);
288-
});
289-
return value;
281+
return _.map(value, v => AV._decode(v));
290282
}
291283
if (value instanceof AV.Object) {
292284
return value;
@@ -297,6 +289,12 @@ AV._decode = function(key, value) {
297289
if (value instanceof AV.Op) {
298290
return value;
299291
}
292+
if (value instanceof AV.GeoPoint) {
293+
return value;
294+
}
295+
if (value instanceof AV.ACL) {
296+
return value;
297+
}
300298
if (value.__op) {
301299
return AV.Op._decode(value);
302300
}
@@ -305,9 +303,10 @@ AV._decode = function(key, value) {
305303
className = value.className;
306304
var pointer = AV.Object._create(className);
307305
if(Object.keys(value).length > 3) {
308-
delete value.__type;
309-
delete value.className;
310-
pointer._finishFetch(value, true);
306+
const v = _.clone(value);
307+
delete v.__type;
308+
delete v.className;
309+
pointer._finishFetch(v, true);
311310
}else{
312311
pointer._finishFetch({ objectId: value.objectId }, false);
313312
}
@@ -316,10 +315,11 @@ AV._decode = function(key, value) {
316315
if (value.__type === "Object") {
317316
// It's an Object included in a query result.
318317
className = value.className;
319-
delete value.__type;
320-
delete value.className;
318+
const v = _.clone(value);
319+
delete v.__type;
320+
delete v.className;
321321
var object = AV.Object._create(className);
322-
object._finishFetch(value, true);
322+
object._finishFetch(v, true);
323323
return object;
324324
}
325325
if (value.__type === "Date") {
@@ -331,13 +331,8 @@ AV._decode = function(key, value) {
331331
longitude: value.longitude
332332
});
333333
}
334-
if (key === "ACL") {
335-
if (value instanceof AV.ACL) {
336-
return value;
337-
}
338-
return new AV.ACL(value);
339-
}
340334
if (value.__type === "Relation") {
335+
if (!key) throw new Error('key missing decoding a Relation');
341336
var relation = new AV.Relation(null, key);
342337
relation.targetClassName = value.className;
343338
return relation;
@@ -349,10 +344,12 @@ AV._decode = function(key, value) {
349344
file.id = value.objectId;
350345
return file;
351346
}
352-
AV._objectEach(value, function(v, k) {
353-
value[k] = AV._decode(k, v);
347+
return _.mapObject(value, function(v, k) {
348+
if (k === "ACL") {
349+
return new AV.ACL(v);
350+
}
351+
return AV._decode(v, k);
354352
});
355-
return value;
356353
};
357354

358355
AV._encodeObjectOrArray = function(value) {

src/cloudfunction.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ module.exports = function(AV) {
2626
AV._encode(data, null, true), options);
2727

2828
return request.then(function(resp) {
29-
return AV._decode(null, resp).result;
29+
return AV._decode(resp).result;
3030
});
3131
},
3232

@@ -44,7 +44,7 @@ module.exports = function(AV) {
4444
}
4545

4646
return AVRequest('call', name, null, 'POST', AV._encodeObjectOrArray(data), options).then(function(resp) {
47-
return AV._decode('', resp).result;
47+
return AV._decode(resp).result;
4848
});
4949
},
5050

@@ -58,7 +58,7 @@ module.exports = function(AV) {
5858
var request = AVRequest("date", null, null, 'GET');
5959

6060
return request.then(function(resp) {
61-
return AV._decode(null, resp);
61+
return AV._decode(resp);
6262
});
6363
},
6464

src/insight.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ module.exports = function(AV) {
4343
AV._encode(data, null, true), options);
4444

4545
return request.then(function(resp) {
46-
return AV._decode(null, resp).id;
46+
return AV._decode(resp).id;
4747
});
4848
},
4949

src/object.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ module.exports = function(AV) {
315315
var value = this.get(attr);
316316
if (value) {
317317
if (!(value instanceof AV.Relation)) {
318-
throw "Called relation() on non-relation field " + attr;
318+
throw new Error("Called relation() on non-relation field " + attr);
319319
}
320320
value._ensureParentAndKey(this, attr);
321321
return value;
@@ -441,7 +441,7 @@ module.exports = function(AV) {
441441
this._mergeMagicFields(serverData);
442442
var self = this;
443443
AV._objectEach(serverData, function(value, key) {
444-
self._serverData[key] = AV._decode(key, value);
444+
self._serverData[key] = AV._decode(value, key);
445445

446446
// Look for any objects that might have become unfetched and fix them
447447
// by replacing their values with the previously observed values.
@@ -471,7 +471,7 @@ module.exports = function(AV) {
471471
this._mergeMagicFields(serverData);
472472
var self = this;
473473
AV._objectEach(serverData, function(value, key) {
474-
self._serverData[key] = AV._decode(key, value);
474+
self._serverData[key] = AV._decode(value, key);
475475
});
476476

477477
// Refresh the attributes.
@@ -608,18 +608,17 @@ module.exports = function(AV) {
608608
* @see AV.Object#validate
609609
*/
610610
set: function(key, value, options) {
611-
var attrs, attr;
611+
var attrs;
612612
if (_.isObject(key) || utils.isNullOrUndefined(key)) {
613-
attrs = key;
614-
AV._objectEach(attrs, function(v, k) {
613+
attrs = _.mapObject(key, function(v, k) {
615614
checkReservedKey(k);
616-
attrs[k] = AV._decode(k, v);
615+
return AV._decode(v, k);
617616
});
618617
options = value;
619618
} else {
620619
attrs = {};
621620
checkReservedKey(key);
622-
attrs[key] = AV._decode(key, value);
621+
attrs[key] = AV._decode(value, key);
623622
}
624623

625624
// Extract attributes and options.

src/op.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ module.exports = function(AV) {
245245
});
246246

247247
AV.Op._registerDecoder("Add", function(json) {
248-
return new AV.Op.Add(AV._decode(undefined, json.objects));
248+
return new AV.Op.Add(AV._decode(json.objects));
249249
});
250250

251251
/**
@@ -321,7 +321,7 @@ module.exports = function(AV) {
321321
});
322322

323323
AV.Op._registerDecoder("AddUnique", function(json) {
324-
return new AV.Op.AddUnique(AV._decode(undefined, json.objects));
324+
return new AV.Op.AddUnique(AV._decode(json.objects));
325325
});
326326

327327
/**
@@ -384,7 +384,7 @@ module.exports = function(AV) {
384384
});
385385

386386
AV.Op._registerDecoder("Remove", function(json) {
387-
return new AV.Op.Remove(AV._decode(undefined, json.objects));
387+
return new AV.Op.Remove(AV._decode(json.objects));
388388
});
389389

390390
/**
@@ -531,10 +531,10 @@ module.exports = function(AV) {
531531
});
532532

533533
AV.Op._registerDecoder("AddRelation", function(json) {
534-
return new AV.Op.Relation(AV._decode(undefined, json.objects), []);
534+
return new AV.Op.Relation(AV._decode(json.objects), []);
535535
});
536536
AV.Op._registerDecoder("RemoveRelation", function(json) {
537-
return new AV.Op.Relation([], AV._decode(undefined, json.objects));
537+
return new AV.Op.Relation([], AV._decode(json.objects));
538538
});
539539

540540
};

src/status.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ module.exports = function(AV) {
125125
delete serverData.objectId;
126126
delete serverData.createdAt;
127127
delete serverData.updatedAt;
128-
this.data = AV._decode(undefined, serverData);
128+
this.data = AV._decode(serverData);
129129
}
130130
};
131131

test/av.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
describe('AV utils', () => {
2+
describe('_encode', () => {
3+
it('should be pure', () => {
4+
const date = new Date();
5+
const object = { date };
6+
const array = [date];
7+
AV._encode(object);
8+
AV._encode(array);
9+
object.date.should.be.a.Date();
10+
object.date.should.be.exactly(date);
11+
array[0].should.be.a.Date();
12+
array[0].should.be.exactly(date);
13+
});
14+
});
15+
16+
describe('_decode', () => {
17+
it('should be pure', () => {
18+
const value = '1970-01-01 08:00:00.000 +0800';
19+
const object = { date: value };
20+
const array = [value];
21+
AV._decode(object);
22+
AV._decode(array);
23+
object.date.should.be.a.String();
24+
object.date.should.be.exactly(value);
25+
array[0].should.be.a.String();
26+
array[0].should.be.exactly(value);
27+
});
28+
});
29+
});

test/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require('should');
22
require('./test.js');
3+
require('./av.js');
34
require('./file.js');
45
require('./error.js');
56
require('./object.js');

test/object.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,27 @@ describe('Objects', function(){
131131
});
132132
});
133133

134+
describe('set', () => {
135+
it('should not mute value', () => {
136+
const originalValue = {
137+
name: 'LC',
138+
objectId: '11111111111',
139+
className: '_User',
140+
__type: 'Object',
141+
avatar: {
142+
__type: 'File',
143+
id: '11111111111',
144+
name: 'avatar',
145+
url: 'url'
146+
}
147+
};
148+
const originalString = JSON.stringify(originalValue);
149+
new GameScore().set('user', originalValue);
150+
JSON.stringify(originalValue).should.be.exactly(originalString);
151+
originalValue.should.not.be.instanceof(AV.Object);
152+
});
153+
});
154+
134155
describe("Retrieving Objects",function(){
135156
it("should be the just save Object",function(){
136157
var GameScore = AV.Object.extend("GameScore");

test/test.html

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,20 @@
2323
console.log(arguments);
2424
}
2525
</script>
26-
<script src="storage.js"></script>
27-
<script src="cache.js"></script>
28-
<script src="file.js"></script>
29-
<script src="error.js"></script>
30-
<script src="object.js"></script>
31-
<script src="user.js"></script>
32-
<script src="query.js"></script>
33-
<script src="geopoints.js"></script>
34-
<script src="acl.js"></script>
35-
<script src="role.js"></script>
36-
<script src="status.js"></script>
37-
<script src="sms.js"></script>
38-
<script src="search.js"></script>
39-
<script src="cloud.js"></script>
26+
<script src="av.js"></script>
27+
<script src="cache.js"></script>
28+
<script src="file.js"></script>
29+
<script src="error.js"></script>
30+
<script src="object.js"></script>
31+
<script src="user.js"></script>
32+
<script src="query.js"></script>
33+
<script src="geopoints.js"></script>
34+
<script src="acl.js"></script>
35+
<script src="role.js"></script>
36+
<script src="status.js"></script>
37+
<script src="sms.js"></script>
38+
<script src="search.js"></script>
39+
<script src="cloud.js"></script>
4040
<script>
4141
onload = function(){
4242
mocha.checkLeaks();

0 commit comments

Comments
 (0)