Skip to content

Commit db555e3

Browse files
authored
fix(LocalDatastore): Allow Pin of unsaved Parse.Object (#1225)
* fix(LocalDatastore): Allow Pin of unsaved Parse.Object Closes: #1118 Let me know if more tests are needed * Generate localId for subclasses * Improve coverage
1 parent 9ba5809 commit db555e3

File tree

7 files changed

+130
-26
lines changed

7 files changed

+130
-26
lines changed

integration/test/ParseLocalDatastoreTest.js

+53-9
Original file line numberDiff line numberDiff line change
@@ -139,15 +139,11 @@ function runTest(controller) {
139139
assert.equal(pinnedObject._localId, undefined);
140140
});
141141

142-
it(`${controller.name} cannot pin unsaved pointer`, async () => {
143-
try {
144-
const object = new TestObject();
145-
const pointer = new Item();
146-
object.set('child', pointer);
147-
await object.pin();
148-
} catch (e) {
149-
assert.equal(e.message, 'Cannot create a pointer to an unsaved ParseObject');
150-
}
142+
it(`${controller.name} can pin unsaved pointer`, async () => {
143+
const object = new TestObject();
144+
const pointer = new Item();
145+
object.set('child', pointer);
146+
await object.pin();
151147
});
152148

153149
it(`${controller.name} can pin user (unsaved)`, async () => {
@@ -2681,6 +2677,54 @@ function runTest(controller) {
26812677
const results = await query.find();
26822678
assert.equal(results.length, 2);
26832679
});
2680+
2681+
it(`${controller.name} can query from pin subclass`, async () => {
2682+
class ClassA extends Parse.Object {
2683+
constructor() {
2684+
super('ClassA');
2685+
}
2686+
get name() { return this.get('name'); }
2687+
set name(value) { this.set('name', value); }
2688+
}
2689+
Parse.Object.registerSubclass('ClassA', ClassA);
2690+
2691+
class ClassB extends Parse.Object {
2692+
constructor() {
2693+
super('ClassB');
2694+
}
2695+
get name() { return this.get('name'); }
2696+
set name(value) { this.set('name', value); }
2697+
2698+
get classA() { return this.get('classA'); }
2699+
set classA(value) { this.set('classA', value); }
2700+
}
2701+
Parse.Object.registerSubclass('ClassB', ClassB);
2702+
2703+
const testClassA = new ClassA();
2704+
testClassA.name = 'ABC';
2705+
await testClassA.pin();
2706+
2707+
const query = new Parse.Query(ClassA);
2708+
query.fromLocalDatastore();
2709+
query.equalTo('name', 'ABC');
2710+
const result = await query.first();
2711+
expect(result.get('name')).toBe('ABC');
2712+
2713+
const testClassB = new ClassB();
2714+
testClassB.name = 'XYZ';
2715+
testClassB.classA = testClassA;
2716+
await testClassB.pin();
2717+
2718+
let localDatastore = await Parse.LocalDatastore._getAllContents();
2719+
expect(localDatastore[LDS_KEY(testClassB)][0].classA).toEqual(testClassA.toOfflinePointer());
2720+
2721+
await testClassB.save();
2722+
expect(testClassB.classA).toBe(testClassA);
2723+
expect(testClassA.id).toBeDefined();
2724+
2725+
localDatastore = await Parse.LocalDatastore._getAllContents();
2726+
expect(localDatastore[LDS_KEY(testClassB)][0].classA.objectId).toEqual(testClassA.id);
2727+
});
26842728
});
26852729
}
26862730

src/LocalDatastore.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const LocalDatastore = {
7979
for (const parent of objects) {
8080
const children = this._getChildren(parent);
8181
const parentKey = this.getKeyForObject(parent);
82-
const json = parent._toFullJSON();
82+
const json = parent._toFullJSON(undefined, true);
8383
if (parent._localId) {
8484
json._localId = parent._localId;
8585
}
@@ -139,7 +139,7 @@ const LocalDatastore = {
139139
// Retrieve all pointer fields from object recursively
140140
_getChildren(object: ParseObject) {
141141
const encountered = {};
142-
const json = object._toFullJSON();
142+
const json = object._toFullJSON(undefined, true);
143143
for (const key in json) {
144144
if (json[key] && json[key].__type && json[key].__type === 'Object') {
145145
this._traverse(json[key], encountered);

src/ParseObject.js

+20-5
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,8 @@ class ParseObject {
273273
return dirty;
274274
}
275275

276-
_toFullJSON(seen?: Array<any>): AttributeMap {
277-
const json: { [key: string]: mixed } = this.toJSON(seen);
276+
_toFullJSON(seen?: Array<any>, offline?: boolean): AttributeMap {
277+
const json: { [key: string]: mixed } = this.toJSON(seen, offline);
278278
json.__type = 'Object';
279279
json.className = this.className;
280280
return json;
@@ -434,7 +434,7 @@ class ParseObject {
434434
* Returns a JSON version of the object suitable for saving to Parse.
435435
* @return {Object}
436436
*/
437-
toJSON(seen: Array<any> | void): AttributeMap {
437+
toJSON(seen: Array<any> | void, offline?: boolean): AttributeMap {
438438
const seenEntry = this.id ? this.className + ':' + this.id : this;
439439
seen = seen || [seenEntry];
440440
const json = {};
@@ -443,12 +443,12 @@ class ParseObject {
443443
if ((attr === 'createdAt' || attr === 'updatedAt') && attrs[attr].toJSON) {
444444
json[attr] = attrs[attr].toJSON();
445445
} else {
446-
json[attr] = encode(attrs[attr], false, false, seen);
446+
json[attr] = encode(attrs[attr], false, false, seen, offline);
447447
}
448448
}
449449
const pending = this._getPendingOps();
450450
for (const attr in pending[0]) {
451-
json[attr] = pending[0][attr].toJSON();
451+
json[attr] = pending[0][attr].toJSON(offline);
452452
}
453453

454454
if (this.id) {
@@ -550,6 +550,21 @@ class ParseObject {
550550
};
551551
}
552552

553+
/**
554+
* Gets a Pointer referencing this Object.
555+
* @return {Pointer}
556+
*/
557+
toOfflinePointer(): Pointer {
558+
if (!this._localId) {
559+
throw new Error('Cannot create a offline pointer to a saved ParseObject');
560+
}
561+
return {
562+
__type: 'Object',
563+
className: this.className,
564+
_localId: this._localId
565+
};
566+
}
567+
553568
/**
554569
* Gets the value of an attribute.
555570
* @param {String} attr The string name of an attribute.

src/ParseOp.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ export class SetOp extends Op {
8484
return new SetOp(this._value);
8585
}
8686

87-
toJSON() {
88-
return encode(this._value, false, true);
87+
toJSON(offline?: boolean) {
88+
return encode(this._value, false, true, undefined, offline);
8989
}
9090
}
9191

src/__tests__/ParseObject-test.js

+14
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,20 @@ describe('ParseObject', () => {
338338
});
339339
});
340340

341+
it('can convert to a offline pointer', () => {
342+
const o = new ParseObject('Item');
343+
o.id = 'AnObjectId';
344+
expect(function() {o.toOfflinePointer();}).toThrow(
345+
'Cannot create a offline pointer to a saved ParseObject'
346+
);
347+
o._localId = 'local1234';
348+
expect(o.toOfflinePointer()).toEqual({
349+
__type: 'Object',
350+
className: 'Item',
351+
_localId: 'local1234'
352+
});
353+
});
354+
341355
it('can test equality against another ParseObject', () => {
342356
const a = new ParseObject('Item');
343357
expect(a.equals(a)).toBe(true);

src/__tests__/encode-test.js

+30-2
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,23 @@ mockObject.prototype = {
2323
toPointer() {
2424
return 'POINTER';
2525
},
26+
toOfflinePointer() {
27+
return 'OFFLINE_POINTER';
28+
},
29+
_getId() {
30+
return 'local1234';
31+
},
2632
dirty() {},
2733
toJSON() {
2834
return this.attributes;
2935
},
30-
_toFullJSON(seen) {
36+
_toFullJSON(seen, offline) {
3137
const json = {
3238
__type: 'Object',
3339
className: this.className
3440
};
3541
for (const attr in this.attributes) {
36-
json[attr] = encode(this.attributes[attr], false, false, seen.concat(this));
42+
json[attr] = encode(this.attributes[attr], false, false, seen.concat(this), offline);
3743
}
3844
return json;
3945
}
@@ -139,6 +145,28 @@ describe('encode', () => {
139145
});
140146
});
141147

148+
it('encodes ParseObjects offline', () => {
149+
const obj = new ParseObject('Item');
150+
obj._serverData = {};
151+
expect(encode(obj, false, false, undefined, true)).toEqual('OFFLINE_POINTER');
152+
obj._serverData = obj.attributes = {
153+
str: 'string',
154+
date: new Date(Date.UTC(2015, 1, 1))
155+
};
156+
obj.attributes.self = obj;
157+
158+
expect(encode(obj, false, false, undefined, true)).toEqual({
159+
__type: 'Object',
160+
className: 'Item',
161+
str: 'string',
162+
date: {
163+
__type: 'Date',
164+
iso: '2015-02-01T00:00:00.000Z'
165+
},
166+
self: 'OFFLINE_POINTER'
167+
});
168+
});
169+
142170
it('does not encode ParseObjects when they are disallowed', () => {
143171
const obj = new ParseObject('Item');
144172
expect(encode.bind(null, obj, true)).toThrow(

src/encode.js

+9-6
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import ParseRelation from './ParseRelation';
1919

2020
const toString = Object.prototype.toString;
2121

22-
function encode(value: mixed, disallowObjects: boolean, forcePointers: boolean, seen: Array<mixed>): any {
22+
function encode(value: mixed, disallowObjects: boolean, forcePointers: boolean, seen: Array<mixed>, offline: boolean): any {
2323
if (value instanceof ParseObject) {
2424
if (disallowObjects) {
2525
throw new Error('Parse Objects not allowed here');
@@ -31,10 +31,13 @@ function encode(value: mixed, disallowObjects: boolean, forcePointers: boolean,
3131
value.dirty() ||
3232
Object.keys(value._getServerData()).length < 1
3333
) {
34+
if (offline && value._getId().startsWith('local')) {
35+
return value.toOfflinePointer();
36+
}
3437
return value.toPointer();
3538
}
3639
seen = seen.concat(seenEntry);
37-
return value._toFullJSON(seen);
40+
return value._toFullJSON(seen, offline);
3841
}
3942
if (value instanceof Op ||
4043
value instanceof ParseACL ||
@@ -62,21 +65,21 @@ function encode(value: mixed, disallowObjects: boolean, forcePointers: boolean,
6265

6366
if (Array.isArray(value)) {
6467
return value.map((v) => {
65-
return encode(v, disallowObjects, forcePointers, seen);
68+
return encode(v, disallowObjects, forcePointers, seen, offline);
6669
});
6770
}
6871

6972
if (value && typeof value === 'object') {
7073
const output = {};
7174
for (const k in value) {
72-
output[k] = encode(value[k], disallowObjects, forcePointers, seen);
75+
output[k] = encode(value[k], disallowObjects, forcePointers, seen, offline);
7376
}
7477
return output;
7578
}
7679

7780
return value;
7881
}
7982

80-
export default function(value: mixed, disallowObjects?: boolean, forcePointers?: boolean, seen?: Array<mixed>): any {
81-
return encode(value, !!disallowObjects, !!forcePointers, seen || []);
83+
export default function(value: mixed, disallowObjects?: boolean, forcePointers?: boolean, seen?: Array<mixed>, offline?: boolean): any {
84+
return encode(value, !!disallowObjects, !!forcePointers, seen || [], offline);
8285
}

0 commit comments

Comments
 (0)