Skip to content

Commit

Permalink
Support Flexible Sync (#5301)
Browse files Browse the repository at this point in the history
Co-authored-by: Kræn Hansen <[email protected]>
  • Loading branch information
elle-j and kraenhansen authored Jan 27, 2023
1 parent 4fda530 commit 75c35d8
Show file tree
Hide file tree
Showing 22 changed files with 1,331 additions and 293 deletions.
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) {
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);
});

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) {
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

0 comments on commit 75c35d8

Please sign in to comment.