diff --git a/integration/test/ParseLocalDatastoreTest.js b/integration/test/ParseLocalDatastoreTest.js index 29c5b2905..ecfa9572a 100644 --- a/integration/test/ParseLocalDatastoreTest.js +++ b/integration/test/ParseLocalDatastoreTest.js @@ -139,15 +139,11 @@ function runTest(controller) { assert.equal(pinnedObject._localId, undefined); }); - it(`${controller.name} cannot pin unsaved pointer`, async () => { - try { - const object = new TestObject(); - const pointer = new Item(); - object.set('child', pointer); - await object.pin(); - } catch (e) { - assert.equal(e.message, 'Cannot create a pointer to an unsaved ParseObject'); - } + it(`${controller.name} can pin unsaved pointer`, async () => { + const object = new TestObject(); + const pointer = new Item(); + object.set('child', pointer); + await object.pin(); }); it(`${controller.name} can pin user (unsaved)`, async () => { @@ -2681,6 +2677,54 @@ function runTest(controller) { const results = await query.find(); assert.equal(results.length, 2); }); + + it(`${controller.name} can query from pin subclass`, async () => { + class ClassA extends Parse.Object { + constructor() { + super('ClassA'); + } + get name() { return this.get('name'); } + set name(value) { this.set('name', value); } + } + Parse.Object.registerSubclass('ClassA', ClassA); + + class ClassB extends Parse.Object { + constructor() { + super('ClassB'); + } + get name() { return this.get('name'); } + set name(value) { this.set('name', value); } + + get classA() { return this.get('classA'); } + set classA(value) { this.set('classA', value); } + } + Parse.Object.registerSubclass('ClassB', ClassB); + + const testClassA = new ClassA(); + testClassA.name = 'ABC'; + await testClassA.pin(); + + const query = new Parse.Query(ClassA); + query.fromLocalDatastore(); + query.equalTo('name', 'ABC'); + const result = await query.first(); + expect(result.get('name')).toBe('ABC'); + + const testClassB = new ClassB(); + testClassB.name = 'XYZ'; + testClassB.classA = testClassA; + await testClassB.pin(); + + let localDatastore = await Parse.LocalDatastore._getAllContents(); + expect(localDatastore[LDS_KEY(testClassB)][0].classA).toEqual(testClassA.toOfflinePointer()); + + await testClassB.save(); + expect(testClassB.classA).toBe(testClassA); + expect(testClassA.id).toBeDefined(); + + localDatastore = await Parse.LocalDatastore._getAllContents(); + expect(localDatastore[LDS_KEY(testClassB)][0].classA.objectId).toEqual(testClassA.id); + }); }); } diff --git a/src/LocalDatastore.js b/src/LocalDatastore.js index 6e6423708..aaa026270 100644 --- a/src/LocalDatastore.js +++ b/src/LocalDatastore.js @@ -79,7 +79,7 @@ const LocalDatastore = { for (const parent of objects) { const children = this._getChildren(parent); const parentKey = this.getKeyForObject(parent); - const json = parent._toFullJSON(); + const json = parent._toFullJSON(undefined, true); if (parent._localId) { json._localId = parent._localId; } @@ -139,7 +139,7 @@ const LocalDatastore = { // Retrieve all pointer fields from object recursively _getChildren(object: ParseObject) { const encountered = {}; - const json = object._toFullJSON(); + const json = object._toFullJSON(undefined, true); for (const key in json) { if (json[key] && json[key].__type && json[key].__type === 'Object') { this._traverse(json[key], encountered); diff --git a/src/ParseObject.js b/src/ParseObject.js index 3ae07cc78..758004051 100644 --- a/src/ParseObject.js +++ b/src/ParseObject.js @@ -273,8 +273,8 @@ class ParseObject { return dirty; } - _toFullJSON(seen?: Array): AttributeMap { - const json: { [key: string]: mixed } = this.toJSON(seen); + _toFullJSON(seen?: Array, offline?: boolean): AttributeMap { + const json: { [key: string]: mixed } = this.toJSON(seen, offline); json.__type = 'Object'; json.className = this.className; return json; @@ -434,7 +434,7 @@ class ParseObject { * Returns a JSON version of the object suitable for saving to Parse. * @return {Object} */ - toJSON(seen: Array | void): AttributeMap { + toJSON(seen: Array | void, offline?: boolean): AttributeMap { const seenEntry = this.id ? this.className + ':' + this.id : this; seen = seen || [seenEntry]; const json = {}; @@ -443,12 +443,12 @@ class ParseObject { if ((attr === 'createdAt' || attr === 'updatedAt') && attrs[attr].toJSON) { json[attr] = attrs[attr].toJSON(); } else { - json[attr] = encode(attrs[attr], false, false, seen); + json[attr] = encode(attrs[attr], false, false, seen, offline); } } const pending = this._getPendingOps(); for (const attr in pending[0]) { - json[attr] = pending[0][attr].toJSON(); + json[attr] = pending[0][attr].toJSON(offline); } if (this.id) { @@ -550,6 +550,21 @@ class ParseObject { }; } + /** + * Gets a Pointer referencing this Object. + * @return {Pointer} + */ + toOfflinePointer(): Pointer { + if (!this._localId) { + throw new Error('Cannot create a offline pointer to a saved ParseObject'); + } + return { + __type: 'Object', + className: this.className, + _localId: this._localId + }; + } + /** * Gets the value of an attribute. * @param {String} attr The string name of an attribute. diff --git a/src/ParseOp.js b/src/ParseOp.js index 45454a851..87ca74462 100644 --- a/src/ParseOp.js +++ b/src/ParseOp.js @@ -84,8 +84,8 @@ export class SetOp extends Op { return new SetOp(this._value); } - toJSON() { - return encode(this._value, false, true); + toJSON(offline?: boolean) { + return encode(this._value, false, true, undefined, offline); } } diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index d487e9148..47c250b5a 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -338,6 +338,20 @@ describe('ParseObject', () => { }); }); + it('can convert to a offline pointer', () => { + const o = new ParseObject('Item'); + o.id = 'AnObjectId'; + expect(function() {o.toOfflinePointer();}).toThrow( + 'Cannot create a offline pointer to a saved ParseObject' + ); + o._localId = 'local1234'; + expect(o.toOfflinePointer()).toEqual({ + __type: 'Object', + className: 'Item', + _localId: 'local1234' + }); + }); + it('can test equality against another ParseObject', () => { const a = new ParseObject('Item'); expect(a.equals(a)).toBe(true); diff --git a/src/__tests__/encode-test.js b/src/__tests__/encode-test.js index 79d37caab..5d369a177 100644 --- a/src/__tests__/encode-test.js +++ b/src/__tests__/encode-test.js @@ -23,17 +23,23 @@ mockObject.prototype = { toPointer() { return 'POINTER'; }, + toOfflinePointer() { + return 'OFFLINE_POINTER'; + }, + _getId() { + return 'local1234'; + }, dirty() {}, toJSON() { return this.attributes; }, - _toFullJSON(seen) { + _toFullJSON(seen, offline) { const json = { __type: 'Object', className: this.className }; for (const attr in this.attributes) { - json[attr] = encode(this.attributes[attr], false, false, seen.concat(this)); + json[attr] = encode(this.attributes[attr], false, false, seen.concat(this), offline); } return json; } @@ -139,6 +145,28 @@ describe('encode', () => { }); }); + it('encodes ParseObjects offline', () => { + const obj = new ParseObject('Item'); + obj._serverData = {}; + expect(encode(obj, false, false, undefined, true)).toEqual('OFFLINE_POINTER'); + obj._serverData = obj.attributes = { + str: 'string', + date: new Date(Date.UTC(2015, 1, 1)) + }; + obj.attributes.self = obj; + + expect(encode(obj, false, false, undefined, true)).toEqual({ + __type: 'Object', + className: 'Item', + str: 'string', + date: { + __type: 'Date', + iso: '2015-02-01T00:00:00.000Z' + }, + self: 'OFFLINE_POINTER' + }); + }); + it('does not encode ParseObjects when they are disallowed', () => { const obj = new ParseObject('Item'); expect(encode.bind(null, obj, true)).toThrow( diff --git a/src/encode.js b/src/encode.js index ea022e65f..20677894b 100644 --- a/src/encode.js +++ b/src/encode.js @@ -19,7 +19,7 @@ import ParseRelation from './ParseRelation'; const toString = Object.prototype.toString; -function encode(value: mixed, disallowObjects: boolean, forcePointers: boolean, seen: Array): any { +function encode(value: mixed, disallowObjects: boolean, forcePointers: boolean, seen: Array, offline: boolean): any { if (value instanceof ParseObject) { if (disallowObjects) { throw new Error('Parse Objects not allowed here'); @@ -31,10 +31,13 @@ function encode(value: mixed, disallowObjects: boolean, forcePointers: boolean, value.dirty() || Object.keys(value._getServerData()).length < 1 ) { + if (offline && value._getId().startsWith('local')) { + return value.toOfflinePointer(); + } return value.toPointer(); } seen = seen.concat(seenEntry); - return value._toFullJSON(seen); + return value._toFullJSON(seen, offline); } if (value instanceof Op || value instanceof ParseACL || @@ -62,14 +65,14 @@ function encode(value: mixed, disallowObjects: boolean, forcePointers: boolean, if (Array.isArray(value)) { return value.map((v) => { - return encode(v, disallowObjects, forcePointers, seen); + return encode(v, disallowObjects, forcePointers, seen, offline); }); } if (value && typeof value === 'object') { const output = {}; for (const k in value) { - output[k] = encode(value[k], disallowObjects, forcePointers, seen); + output[k] = encode(value[k], disallowObjects, forcePointers, seen, offline); } return output; } @@ -77,6 +80,6 @@ function encode(value: mixed, disallowObjects: boolean, forcePointers: boolean, return value; } -export default function(value: mixed, disallowObjects?: boolean, forcePointers?: boolean, seen?: Array): any { - return encode(value, !!disallowObjects, !!forcePointers, seen || []); +export default function(value: mixed, disallowObjects?: boolean, forcePointers?: boolean, seen?: Array, offline?: boolean): any { + return encode(value, !!disallowObjects, !!forcePointers, seen || [], offline); }