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

Support Flexible Sync #5301

Merged
merged 39 commits into from
Jan 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
050bb2e
Refactored Realm constructor
kraenhansen Jan 18, 2023
8c91182
Implement Subscription.
elle-j Jan 11, 2023
26b26d0
Implement BaseSubscriptionSet.
elle-j Jan 11, 2023
1843c76
Update spec.yaml and expose helper functions.
elle-j Jan 11, 2023
e31fc4e
Implement MutableSubscriptionSet.
elle-j Jan 11, 2023
b0fed0d
Implement SubscriptionSet.
elle-j Jan 11, 2023
8ccfdb6
Handle configuration with flexible set to true.
elle-j Jan 11, 2023
94ecc6a
Increase timeout for Flexible Sync tests.
elle-j Jan 11, 2023
e911755
Add subscription classes to Sync namespace.
elle-j Jan 19, 2023
0bfc50e
Refactor SubscriptionSet.update() into separate async and sync methods.
elle-j Jan 19, 2023
3714923
Validate realm configuration.
elle-j Jan 19, 2023
5e2c864
Handle initial subscriptions.
elle-j Jan 19, 2023
c1f65ff
Update expected error messages for tests.
elle-j Jan 19, 2023
b1d27c9
Move schema validation from Configuration.ts to /schema directory.
elle-j Jan 19, 2023
eb72a08
Import types instead of accessing them through Realm namespace in Typ…
elle-j Jan 19, 2023
2a63ad8
Update variable names.
elle-j Jan 19, 2023
41e098a
Support Asymmetric Sync.
elle-j Jan 19, 2023
2d638a7
Enable previously skipped tests.
elle-j Jan 20, 2023
bf33629
Change test to not accept 'flexible: false'.
elle-j Jan 20, 2023
6c5ad82
Allow opening a synced realm locally.
elle-j Jan 20, 2023
ef248ea
Use BaseSubscriptionSet as a proxy to allow index access operator.
elle-j Jan 20, 2023
9ef8cc2
Rearrange if-conditions for increased readability.
elle-j Jan 23, 2023
20405a0
Refactor subscription classes to use parameter properties.
elle-j Jan 23, 2023
1858124
Make 'this.realm' available in asymmetric tests.
elle-j Jan 24, 2023
2cdd147
Make subscription sets iterable.
elle-j Jan 24, 2023
8f395b1
Fix bug when removing a subscription by object type.
elle-j Jan 24, 2023
1d32347
Add 'isAsymmetric()' and 'isEmbedded()' helpers.
elle-j Jan 25, 2023
aefa998
Add '@example' to TSDocs.
elle-j Jan 25, 2023
3cced95
Validate schema version is 0 or positive integer.
elle-j Jan 25, 2023
bb7024d
Validate sync config before getting a sync session.
elle-j Jan 25, 2023
8189f83
Remove unnecessary copy of variable.
elle-j Jan 27, 2023
3c7ffab
Update comments and minor refactor.
elle-j Jan 27, 2023
6faba8d
Set timeout in tests to same as CI, and small refactor.
elle-j Jan 27, 2023
4a0834f
Remove redundant docs.
elle-j Jan 27, 2023
822766e
Update assert error messages for consistency.
elle-j Jan 27, 2023
2d5c694
Remove redundant docs.
elle-j Jan 27, 2023
60a27c6
Replace string with char.
elle-j Jan 27, 2023
c7a3e2a
Add '@internal' to validation functions.
elle-j Jan 27, 2023
2eafc6f
Resolved merge conflict.
elle-j Jan 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions integration-tests/tests/src/tests/sync/asymmetric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,24 +45,41 @@ describe.skipIf(environment.missingServer, "Asymmetric sync", function () {
},
});

it("Schema with asymmetric = true and embedded = false", function () {
it("Schema with asymmetric = true and embedded = false", function (this: RealmContext) {
Copy link
Contributor

Choose a reason for hiding this comment

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

(General comment, not specific to this line)

Should any of these changes be backported to the master branch or are they bindgen-specific? Ideally, we want the tests in bindgen and master to be as in-sync as possible since that is how we are ensuring that we don't change behavior (except where we intend to!)

const schema = this.realm.schema;
expect(schema.length).to.equal(1);
expect(schema[0].asymmetric).to.equal(true);
expect(schema[0].embedded).to.equal(false);
expect(schema[0].asymmetric).to.be.true;
expect(schema[0].embedded).to.be.false;
});

it("creating an object for an asymmetric schema returns undefined", function () {
it("creating an object for an asymmetric schema returns undefined", function (this: RealmContext) {
this.realm.write(() => {
const returnValue = this.realm.create(PersonSchema.name, { _id: new BSON.ObjectId(), name: "Joe", age: 12 });
expect(returnValue).to.equal(undefined);
const returnValue = this.realm.create(PersonSchema.name, {
_id: new BSON.ObjectId(),
name: "Joe",
age: 12,
});
expect(returnValue).to.be.undefined;
});
});

it("an asymmetric schema cannot be queried", function () {
it("an asymmetric schema cannot be queried through 'objects()'", function (this: RealmContext) {
expect(() => {
this.realm.objects(PersonSchema.name);
}).to.throw("You cannot query an asymmetric class.");
}).to.throw("You cannot query an asymmetric object.");
});

it("an asymmetric schema cannot be queried through 'objectForPrimaryKey()'", function (this: RealmContext) {
expect(() => {
this.realm.objectForPrimaryKey(PersonSchema.name, new BSON.ObjectId());
}).to.throw("You cannot query an asymmetric object.");
});

it("an asymmetric schema cannot be queried through '_objectForObjectKey()'", function (this: RealmContext) {
expect(() => {
// A valid objectKey is not needed for this test
this.realm._objectForObjectKey(PersonSchema.name, "12345");
}).to.throw("You cannot query an asymmetric object.");
});
});
});
119 changes: 78 additions & 41 deletions integration-tests/tests/src/tests/sync/flexible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
// fraction too long.

import { expect } from "chai";
import Realm, { BSON, ClientResetMode, FlexibleSyncConfiguration, SessionStopPolicy } from "realm";
import { BSON, ClientResetMode, FlexibleSyncConfiguration, Realm, SessionStopPolicy } from "realm";

import { authenticateUserBefore, importAppBefore, openRealmBeforeEach } from "../../hooks";
import { DogSchema, IPerson, PersonSchema } from "../../schemas/person-and-dog-with-object-ids";
Expand Down Expand Up @@ -121,6 +121,7 @@ async function addSubscriptionAndSync<T>(
}

describe.skipIf(environment.missingServer, "Flexible sync", function () {
this.timeout(60_000); // TODO: Temporarily hardcoded until envs are set up.
importAppBefore("with-db-flx");
authenticateUserBefore();
openRealmBeforeEach({
Expand Down Expand Up @@ -164,7 +165,7 @@ describe.skipIf(environment.missingServer, "Flexible sync", function () {
}).to.throw("'partitionValue' cannot be specified when flexible sync is enabled");
});

it("accepts { flexible: false } and a partition value", function () {
it("does not accept { flexible: false } and a partition value", function () {
expect(() => {
// @ts-expect-error This is not a compatible configuration anymore and will cause a typescript error
new Realm({
Expand All @@ -175,7 +176,9 @@ describe.skipIf(environment.missingServer, "Flexible sync", function () {
partitionValue: "test",
},
});
}).to.not.throw();
}).to.throw(
"'flexible' can only be specified to enable flexible sync. To enable flexible sync, remove 'partitionValue' and set 'flexible' to true",
);
});

it("accepts { flexible: undefined } and a partition value", function () {
Expand Down Expand Up @@ -208,16 +211,12 @@ describe.skipIf(environment.missingServer, "Flexible sync", function () {
}

describe("error", function () {
afterEach(function () {
Realm.deleteFile(this.config);
});

elle-j marked this conversation as resolved.
Show resolved Hide resolved
it("throws an error if no update function is provided", async function (this: RealmContext) {
// @ts-expect-error Intentionally testing the wrong type
this.config = getConfig(this.user, {});

await expect(Realm.open(this.config)).to.be.rejectedWith(
"update must be of type 'function', got (undefined)",
"Expected 'initialSubscriptions.update' on realm sync configuration to be a function, got undefined",
);
});

Expand All @@ -228,7 +227,7 @@ describe.skipIf(environment.missingServer, "Flexible sync", function () {
});

await expect(Realm.open(this.config)).to.be.rejectedWith(
"update must be of type 'function', got (undefined)",
"Expected 'initialSubscriptions.update' on realm sync configuration to be a function, got undefined",
);
});

Expand All @@ -238,7 +237,9 @@ describe.skipIf(environment.missingServer, "Flexible sync", function () {
update: "Person",
});

await expect(Realm.open(this.config)).to.be.rejectedWith("update must be of type 'function', got (Person)");
await expect(Realm.open(this.config)).to.be.rejectedWith(
"Expected 'initialSubscriptions.update' on realm sync configuration to be a function, got a string",
);
});

it("throws an error if `rerunOnOpen` is not a boolean", async function (this: RealmContext) {
Expand All @@ -250,7 +251,9 @@ describe.skipIf(environment.missingServer, "Flexible sync", function () {
rerunOnOpen: "yes please",
});

await expect(Realm.open(this.config)).to.be.rejectedWith(/rerunOnOpen must be of type 'boolean', got.*/);
await expect(Realm.open(this.config)).to.be.rejectedWith(
"Expected 'initialSubscriptions.rerunOnOpen' on realm sync configuration to be a boolean, got a string",
);
});
});

Expand Down Expand Up @@ -587,20 +590,53 @@ describe.skipIf(environment.missingServer, "Flexible sync", function () {
});

describe("array-like access", function () {
async function addThreeSubscriptions(this: RealmContext) {
addSubscriptionForPerson(this.realm);
await addSubscriptionAndSync(this.realm, this.realm.objects(FlexiblePersonSchema.name).filtered("age > 10"));
return await addSubscriptionAndSync(
this.realm,
this.realm.objects(FlexiblePersonSchema.name).filtered("age < 50"),
);
}

it("returns an empty array if there are no subscriptions", function (this: RealmContext) {
const subs = this.realm.subscriptions;
expect(subs).to.have.length(0);
});

it("returns an array of Subscription objects", async function (this: RealmContext) {
addSubscriptionForPerson(this.realm);
const { subs } = await addSubscriptionAndSync(
this.realm,
this.realm.objects(FlexiblePersonSchema.name).filtered("age > 10"),
);
it("accesses a SubscriptionSet using index operator", async function (this: RealmContext) {
const { subs } = await addThreeSubscriptions.call(this);

expect(subs).to.have.length(2);
expect(subs.every((s) => s instanceof Realm.App.Sync.Subscription)).to.be.true;
expect(subs).to.have.length(3);
expect(subs[0]).to.be.instanceOf(Realm.App.Sync.Subscription);
expect(subs[1]).to.be.instanceOf(Realm.App.Sync.Subscription);
expect(subs[2]).to.be.instanceOf(Realm.App.Sync.Subscription);
});

it("spreads a SubscriptionSet using spread operator", async function (this: RealmContext) {
const { subs } = await addThreeSubscriptions.call(this);

const spreadSubs = [...subs];
expect(spreadSubs).to.have.length(3);
expect(spreadSubs.every((s) => s instanceof Realm.App.Sync.Subscription)).to.be.true;
});

it("iterates over a SubscriptionSet using for-of loop", async function (this: RealmContext) {
const { subs } = await addThreeSubscriptions.call(this);

let numSubs = 0;
for (const sub of subs) {
expect(sub).to.be.an.instanceOf(Realm.App.Sync.Subscription);
numSubs++;
}
expect(numSubs).to.equal(3);
});

it("iterates over a SubscriptionSet using 'Object.keys()'", async function (this: RealmContext) {
const { subs } = await addThreeSubscriptions.call(this);

// Object.keys() always returns an array of strings.
expect(Object.keys(subs)).deep.equals(["0", "1", "2"]);
});

it("is an immutable snapshot of the subscriptions from when it was called", async function (this: RealmContext) {
Expand Down Expand Up @@ -862,7 +898,7 @@ describe.skipIf(environment.missingServer, "Flexible sync", function () {
it("passes a MutableSubscriptionSet instance as an argument", async function (this: RealmContext) {
const subs = this.realm.subscriptions;
await subs.update((mutableSubs) => {
expect(mutableSubs).to.be.instanceOf(Realm.App.Sync.MutableSubscriptionSet);
expect(mutableSubs).to.be.an.instanceOf(Realm.App.Sync.MutableSubscriptionSet);
});
});

Expand Down Expand Up @@ -1124,7 +1160,7 @@ describe.skipIf(environment.missingServer, "Flexible sync", function () {
mutableSubs.add(query, { name: "test", throwOnUpdate: true });
}),
).to.be.rejectedWith(
"A subscription with the name 'test' already exists but has a different query. If you meant to update it, remove `throwOnUpdate: true` from the subscription options.",
"A subscription with the name 'test' already exists but has a different query. If you meant to update it, remove 'throwOnUpdate: true' from the subscription options.",
);

expect(subs.findByQuery(query)).to.be.null;
Expand Down Expand Up @@ -1411,14 +1447,14 @@ describe.skipIf(environment.missingServer, "Flexible sync", function () {
const { id } = await addPersonAndWaitForUpload(realm);

const newRealm = await closeAndReopenRealm(realm, config);
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.be.undefined;
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.be.null;

await newRealm.subscriptions.update((mutableSubs) => subsUpdateFn(mutableSubs, newRealm));

return { id, newRealm };
}

it.skip("syncs added items to a subscribed collection", async function (this: RealmContext) {
it("syncs added items to a subscribed collection", async function (this: RealmContext) {
elle-j marked this conversation as resolved.
Show resolved Hide resolved
const { id, newRealm } = await addPersonAndResyncWithSubscription(
this.realm,
this.config,
Expand All @@ -1427,10 +1463,11 @@ describe.skipIf(environment.missingServer, "Flexible sync", function () {
},
);

expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.not.be.undefined;
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.not.be.null;
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.not.be.null;
});

it.skip("syncs added items to a subscribed collection with a filter", async function (this: RealmContext) {
it("syncs added items to a subscribed collection with a filter", async function (this: RealmContext) {
const { id, newRealm } = await addPersonAndResyncWithSubscription(
this.realm,
this.config,
Expand All @@ -1439,10 +1476,10 @@ describe.skipIf(environment.missingServer, "Flexible sync", function () {
},
);

expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.not.be.undefined;
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.not.be.null;
});

it.skip("does not sync added items not matching the filter", async function (this: RealmContext) {
it("does not sync added items not matching the filter", async function (this: RealmContext) {
const { id, newRealm } = await addPersonAndResyncWithSubscription(
this.realm,
this.config,
Expand All @@ -1451,7 +1488,7 @@ describe.skipIf(environment.missingServer, "Flexible sync", function () {
},
);

expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.be.undefined;
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.be.null;
});

// TODO: Probably remove this as it is testing old functionality
Expand All @@ -1463,15 +1500,15 @@ describe.skipIf(environment.missingServer, "Flexible sync", function () {
mutableSubs.add(realm.objects(FlexiblePersonSchema.name).filtered("age < 30"));
},
);
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.be.undefined;
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.be.null;

const subs = newRealm.subscriptions;
await subs.update((mutableSubs) => {
mutableSubs.add(newRealm.objects(FlexiblePersonSchema.name).filtered("age > 30"));
});

newRealm.addListener("change", () => {
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.not.be.undefined;
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.not.be.null;
});
});

Expand All @@ -1484,7 +1521,7 @@ describe.skipIf(environment.missingServer, "Flexible sync", function () {
mutableSubs.add(realm.objects(FlexiblePersonSchema.name).filtered("age < 30"));
},
);
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.be.undefined;
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.be.null;

const subs = newRealm.subscriptions;
await subs.update((mutableSubs) => {
Expand All @@ -1493,11 +1530,11 @@ describe.skipIf(environment.missingServer, "Flexible sync", function () {
});

newRealm.addListener("change", () => {
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.not.be.undefined;
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.not.be.null;
});
});

it.skip("stops syncing items when a subscription is removed (but other subscriptions still exist)", async function (this: RealmContext) {
it("stops syncing items when a subscription is removed (but other subscriptions still exist)", async function (this: RealmContext) {
const { id, newRealm } = await addPersonAndResyncWithSubscription(
this.realm,
this.config,
Expand All @@ -1506,55 +1543,55 @@ describe.skipIf(environment.missingServer, "Flexible sync", function () {
mutableSubs.add(realm.objects(FlexiblePersonSchema.name).filtered("age > 50"));
},
);
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.not.be.undefined;
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.not.be.null;

const subs = newRealm.subscriptions;
await subs.update((mutableSubs) => {
mutableSubs.removeByName("test");
});

newRealm.addListener("change", () => {
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.be.undefined;
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.be.null;
});
});

it.skip("stops syncing items when all subscriptions are removed", async function (this: RealmContext) {
it("stops syncing items when all subscriptions are removed", async function (this: RealmContext) {
const { id, newRealm } = await addPersonAndResyncWithSubscription(
this.realm,
this.config,
(mutableSubs, realm) => {
mutableSubs.add(realm.objects(FlexiblePersonSchema.name));
},
);
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.not.be.undefined;
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.not.be.null;

const subs = newRealm.subscriptions;
await subs.update((mutableSubs) => {
mutableSubs.removeAll();
});

newRealm.addListener("change", () => {
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.be.undefined;
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.be.null;
});
});

it.skip("stops syncing items if the filter changes to not match some items", async function (this: RealmContext) {
it("stops syncing items if the filter changes to not match some items", async function (this: RealmContext) {
const { id, newRealm } = await addPersonAndResyncWithSubscription(
this.realm,
this.config,
(mutableSubs, realm) => {
mutableSubs.add(realm.objects(FlexiblePersonSchema.name).filtered("age > 30"));
},
);
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.not.be.undefined;
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.not.be.null;

const subs = newRealm.subscriptions;
await subs.update((mutableSubs) => {
mutableSubs.removeAll();
mutableSubs.add(newRealm.objects(FlexiblePersonSchema.name).filtered("age < 30"));
});

expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.be.undefined;
expect(newRealm.objectForPrimaryKey(FlexiblePersonSchema.name, id)).to.be.null;
});

// TODO test more complex integration scenarios, e.g. links, embedded objects, collections, complex queries
Expand Down
Loading