From 305cc1c4cb659359e6b6fcd6faba7082331173cf Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Thu, 9 Jun 2022 19:34:21 -0500 Subject: [PATCH] manual rebase: add missing fields to items sent through observe/observequery --- packages/datastore/__tests__/DataStore.ts | 260 +++++++++++++++++- packages/datastore/src/datastore/datastore.ts | 44 +-- 2 files changed, 284 insertions(+), 20 deletions(-) diff --git a/packages/datastore/__tests__/DataStore.ts b/packages/datastore/__tests__/DataStore.ts index 88df077d90d..d3c3c6a971f 100644 --- a/packages/datastore/__tests__/DataStore.ts +++ b/packages/datastore/__tests__/DataStore.ts @@ -13,7 +13,16 @@ import { PersistentModel, PersistentModelConstructor, } from '../src/types'; -import { Comment, Model, Post, Metadata, testSchema, pause } from './helpers'; +import { + Comment, + Model, + Post, + Profile, + Metadata, + User, + testSchema, + pause, +} from './helpers'; let initSchema: typeof initSchemaType; let DataStore: typeof DataStoreType; @@ -265,13 +274,17 @@ describe('DataStore observeQuery, with fake-indexeddb and fake sync', () => { let Comment: PersistentModelConstructor; let Post: PersistentModelConstructor; + let User: PersistentModelConstructor; + let Profile: PersistentModelConstructor; beforeEach(async () => { ({ initSchema, DataStore } = require('../src/datastore/datastore')); const classes = initSchema(testSchema()); - ({ Comment, Post } = classes as { + ({ Comment, Post, User, Profile } = classes as { Comment: PersistentModelConstructor; Post: PersistentModelConstructor; + User: PersistentModelConstructor; + Profile: PersistentModelConstructor; }); // This prevents pollution between tests. DataStore may have processes in @@ -598,6 +611,249 @@ describe('DataStore observeQuery, with fake-indexeddb and fake sync', () => { } })(); }); + + test('attaches related belongsTo properties consistently with query() on INSERT', async done => { + try { + const expecteds = [5, 15]; + + for (let i = 0; i < 5; i++) { + await DataStore.save( + new Comment({ + content: `comment content ${i}`, + post: await DataStore.save( + new Post({ + title: `new post ${i}`, + }) + ), + }) + ); + } + + const sub = DataStore.observeQuery(Comment).subscribe( + ({ items, isSynced }) => { + const expected = expecteds.shift() || 0; + expect(items.length).toBe(expected); + + for (let i = 0; i < expected; i++) { + expect(items[i].content).toEqual(`comment content ${i}`); + expect(items[i].post.title).toEqual(`new post ${i}`); + } + + if (expecteds.length === 0) { + sub.unsubscribe(); + done(); + } + } + ); + + setTimeout(async () => { + for (let i = 5; i < 15; i++) { + await DataStore.save( + new Comment({ + content: `comment content ${i}`, + post: await DataStore.save( + new Post({ + title: `new post ${i}`, + }) + ), + }) + ); + } + }, 1); + } catch (error) { + done(error); + } + }); + + test('attaches related hasOne properties consistently with query() on INSERT', async done => { + try { + const expecteds = [5, 15]; + + for (let i = 0; i < 5; i++) { + await DataStore.save( + new User({ + name: `user ${i}`, + profile: await DataStore.save( + new Profile({ + firstName: `firstName ${i}`, + lastName: `lastName ${i}`, + }) + ), + }) + ); + } + + const sub = DataStore.observeQuery(User).subscribe( + ({ items, isSynced }) => { + const expected = expecteds.shift() || 0; + expect(items.length).toBe(expected); + + for (let i = 0; i < expected; i++) { + expect(items[i].name).toEqual(`user ${i}`); + expect(items[i].profile.firstName).toEqual(`firstName ${i}`); + expect(items[i].profile.lastName).toEqual(`lastName ${i}`); + } + + if (expecteds.length === 0) { + sub.unsubscribe(); + done(); + } + } + ); + + setTimeout(async () => { + for (let i = 5; i < 15; i++) { + await DataStore.save( + new User({ + name: `user ${i}`, + profile: await DataStore.save( + new Profile({ + firstName: `firstName ${i}`, + lastName: `lastName ${i}`, + }) + ), + }) + ); + } + }, 1); + } catch (error) { + done(error); + } + }); + + test('attaches related belongsTo properties consistently with query() on UPDATE', async done => { + try { + const expecteds = [ + ['old post 0', 'old post 1', 'old post 2', 'old post 3', 'old post 4'], + ['new post 0', 'new post 1', 'new post 2', 'new post 3', 'new post 4'], + ]; + + for (let i = 0; i < 5; i++) { + await DataStore.save( + new Comment({ + content: `comment content ${i}`, + post: await DataStore.save( + new Post({ + title: `old post ${i}`, + }) + ), + }) + ); + } + + const sub = DataStore.observeQuery(Comment).subscribe( + ({ items, isSynced }) => { + const expected = expecteds.shift() || []; + expect(items.length).toBe(expected.length); + + for (let i = 0; i < expected.length; i++) { + expect(items[i].content).toContain(`comment content ${i}`); + expect(items[i].post.title).toEqual(expected[i]); + } + + if (expecteds.length === 0) { + sub.unsubscribe(); + done(); + } + } + ); + + setTimeout(async () => { + let postIndex = 0; + const comments = await DataStore.query(Comment); + for (const comment of comments) { + const newPost = await DataStore.save( + new Post({ + title: `new post ${postIndex++}`, + }) + ); + + await DataStore.save( + Comment.copyOf(comment, draft => { + draft.content = `updated: ${comment.content}`; + draft.post = newPost; + }) + ); + } + }, 1); + } catch (error) { + done(error); + } + }); + + test('attaches related hasOne properties consistently with query() on UPDATE', async done => { + try { + const expecteds = [ + [ + 'first name 0', + 'first name 1', + 'first name 2', + 'first name 3', + 'first name 4', + ], + [ + 'new first name 0', + 'new first name 1', + 'new first name 2', + 'new first name 3', + 'new first name 4', + ], + ]; + + for (let i = 0; i < 5; i++) { + await DataStore.save( + new User({ + name: `user ${i}`, + profile: await DataStore.save( + new Profile({ + firstName: `first name ${i}`, + lastName: `last name ${i}`, + }) + ), + }) + ); + } + + const sub = DataStore.observeQuery(User).subscribe( + ({ items, isSynced }) => { + const expected = expecteds.shift() || []; + expect(items.length).toBe(expected.length); + + for (let i = 0; i < expected.length; i++) { + expect(items[i].name).toContain(`user ${i}`); + expect(items[i].profile.firstName).toEqual(expected[i]); + } + + if (expecteds.length === 0) { + sub.unsubscribe(); + done(); + } + } + ); + + setTimeout(async () => { + let userIndex = 0; + const users = await DataStore.query(User); + for (const user of users) { + const newProfile = await DataStore.save( + new Profile({ + firstName: `new first name ${userIndex++}`, + lastName: `new last name ${userIndex}`, + }) + ); + + await DataStore.save( + User.copyOf(user, draft => { + draft.name = `updated: ${user.name}`; + draft.profile = newProfile; + }) + ); + } + }, 1); + } catch (error) { + done(error); + } + }); }); describe('DataStore tests', () => { diff --git a/packages/datastore/src/datastore/datastore.ts b/packages/datastore/src/datastore/datastore.ts index 0353194f282..2c3ea5f25c9 100644 --- a/packages/datastore/src/datastore/datastore.ts +++ b/packages/datastore/src/datastore/datastore.ts @@ -1145,24 +1145,32 @@ class DataStore { handle = this.storage .observe(modelConstructor, predicate) .filter(({ model }) => namespaceResolver(model) === USER) - .map( - (event: InternalSubscriptionMessage): SubscriptionMessage => { - // The `element` returned by storage only contains updated fields. - // Intercept the event to send the `savedElement` so that the first - // snapshot returned to the consumer contains all fields. - // In the event of a delete we return `element`, as `savedElement` - // here is undefined. - const { opType, model, condition, element, savedElement } = event; - - return { - opType, - element: savedElement || element, - model, - condition, - }; - } - ) - .subscribe(observer); + .subscribe({ + next: async item => { + // the `element` doesn't necessarily contain all item details or + // have related records attached consistently with that of a query() + // result item. for consistency, we attach them here. + + let message = item; + + // as lnog as we're not dealing with a DELETE, we need to fetch a fresh + // item from storage to ensure it's fully populated. + if (item.opType !== 'DELETE') { + const freshElement = await this.query( + item.model, + item.element.id + ); + message = { + ...message, + element: freshElement as T, + }; + } + + observer.next(message as SubscriptionMessage); + }, + error: err => observer.error(err), + complete: () => observer.complete(), + }); })(); return () => {