Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(@aws-amplify/datastore): fixes observeQuery not removing newly-filtered items from snapshot #9879

Merged
merged 13 commits into from
Jun 9, 2022
197 changes: 172 additions & 25 deletions packages/datastore/__tests__/DataStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
PersistentModel,
PersistentModelConstructor,
} from '../src/types';
import { Comment, Model, Post, Metadata, testSchema } from './helpers';
import { Comment, Model, Post, Metadata, testSchema, pause } from './helpers';

let initSchema: typeof initSchemaType;
let DataStore: typeof DataStoreType;
Expand Down Expand Up @@ -273,6 +273,8 @@ describe('DataStore observeQuery, with fake-indexeddb and fake sync', () => {
Comment: PersistentModelConstructor<Comment>;
Post: PersistentModelConstructor<Post>;
});

await DataStore.start();
await DataStore.clear();

// Fully faking or mocking the sync engine would be pretty significant.
Expand All @@ -294,6 +296,27 @@ describe('DataStore observeQuery, with fake-indexeddb and fake sync', () => {
(DataStore as any).syncPageSize = 1000;
});

afterEach(async () => {
//
// ~~~~ NAUGHTINESS WARNING! ~~~~
//
// ( cover your eyes )
//
// this prevents pollution between tests that may include observe() calls.
// This is a cheap solution let DataStore "settle" before clearing it and
// starting the next test. If we don't do this, we get "spooky"
// contamination between tests.
//
// NOTE: If you know of a better way to isolate these tests, please
// replace this pause!
//
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid this naughtiness by using different models for each test?

It may make the test more verbose but will make them more isolated from each other

svidgen marked this conversation as resolved.
Show resolved Hide resolved
await pause(10);

// an abundance of caution
await DataStore.start();
await DataStore.clear();
});

test('publishes preexisting local data immediately', async done => {
try {
for (let i = 0; i < 5; i++) {
Expand Down Expand Up @@ -349,39 +372,31 @@ describe('DataStore observeQuery, with fake-indexeddb and fake sync', () => {
}
});

test('publishes preexisting local data AND follows up with subsequent saves', async done => {
test('can filter items', async done => {
try {
const expecteds = [5, 15];

for (let i = 0; i < 5; i++) {
await DataStore.save(
new Post({
title: `the post ${i}`,
})
);
}
const expecteds = [0, 5];

const sub = DataStore.observeQuery(Post).subscribe(
({ items, isSynced }) => {
const expected = expecteds.shift() || 0;
expect(items.length).toBe(expected);
const sub = DataStore.observeQuery(Post, p =>
p.title('contains', 'include')
).subscribe(({ items }) => {
const expected = expecteds.shift() || 0;
expect(items.length).toBe(expected);

for (let i = 0; i < expected; i++) {
expect(items[i].title).toEqual(`the post ${i}`);
}
for (const item of items) {
expect(item.title).toMatch('include');
}

if (expecteds.length === 0) {
sub.unsubscribe();
done();
}
if (expecteds.length === 0) {
sub.unsubscribe();
done();
}
);
});

setTimeout(async () => {
for (let i = 5; i < 15; i++) {
for (let i = 0; i < 10; i++) {
await DataStore.save(
new Post({
title: `the post ${i}`,
title: `the post ${i} - ${Boolean(i % 2) ? 'include' : 'omit'}`,
})
);
}
Expand All @@ -390,6 +405,138 @@ describe('DataStore observeQuery, with fake-indexeddb and fake sync', () => {
done(error);
}
});

// Fix for: https://github.com/aws-amplify/amplify-js/issues/9325
test('can remove newly-unmatched items out of the snapshot on subsequent saves', async done => {
try {
// watch for post snapshots.
// the first "real" snapshot should include all five posts with "include"
// in the title. after the update to change ONE of those posts to "omit" instead,
// we should see a snapshot of 4 posts with the updated post removed.
const expecteds = [0, 4, 3];
const sub = DataStore.observeQuery(Post, p =>
p.title('contains', 'include')
).subscribe(async ({ items }) => {
const expected = expecteds.shift() || 0;
expect(items.length).toBe(expected);

for (const item of items) {
expect(item.title).toMatch('include');
}

if (expecteds.length === 1) {
// After the second snapshot arrives, changes a single post from
// "the post # - include"
// to
// "edited post - omit"

// This is intended to trigger a new, after-sync'd snapshot.
// This sanity-checks helps confirms we're testing what we think
// we're testing:
expect(
((DataStore as any).sync as any).getModelSyncedStatus({})
).toBe(true);

await pause(1);
const itemToEdit = (
await DataStore.query(Post, p => p.title('contains', 'include'))
).pop();
await DataStore.save(
Post.copyOf(itemToEdit, draft => {
draft.title = 'second edited post - omit';
})
);
} else if (expecteds.length === 0) {
sub.unsubscribe();
done();
}
});

setTimeout(async () => {
// Creates posts like:
//
// "the post 0 - include"
// "the post 1 - omit"
// "the post 2 - include"
// "the post 3 - omit"
//
// etc.
//
for (let i = 0; i < 10; i++) {
await DataStore.save(
new Post({
title: `the post ${i} - ${Boolean(i % 2) ? 'include' : 'omit'}`,
})
);
}

// Changes a single post from
// "the post # - include"
// to
// "edited post - omit"
await pause(1);
((DataStore as any).sync as any).getModelSyncedStatus = (model: any) =>
true;

// the first edit simulates a quick-turnaround update that gets
// applied while the first snapshot is still being generated
const itemToEdit = (
await DataStore.query(Post, p => p.title('contains', 'include'))
).pop();
await DataStore.save(
Post.copyOf(itemToEdit, draft => {
draft.title = 'first edited post - omit';
})
);
}, 1);
} catch (error) {
done(error);
}
});

test('publishes preexisting local data AND follows up with subsequent saves', done => {
(async () => {
try {
const expecteds = [5, 15];

for (let i = 0; i < 5; i++) {
await DataStore.save(
new Post({
title: `the post ${i}`,
})
);
}

const sub = DataStore.observeQuery(Post).subscribe(
({ items, isSynced }) => {
const expected = expecteds.shift() || 0;
expect(items.length).toBe(expected);

for (let i = 0; i < expected; i++) {
expect(items[i].title).toEqual(`the post ${i}`);
}

if (expecteds.length === 0) {
sub.unsubscribe();
done();
}
}
);

setTimeout(async () => {
for (let i = 5; i < 15; i++) {
await DataStore.save(
new Post({
title: `the post ${i}`,
})
);
}
}, 100);
} catch (error) {
done(error);
}
})();
});
});

describe('DataStore tests', () => {
Expand Down
Loading