diff --git a/integration-tests/environments/react-native/package.json b/integration-tests/environments/react-native/package.json index 6370f9040e..b76d6db50c 100644 --- a/integration-tests/environments/react-native/package.json +++ b/integration-tests/environments/react-native/package.json @@ -96,7 +96,13 @@ ], "env": { "PLATFORM": "android", - "WATCH": "false" + "WATCH": "false", + "ORG_GRADLE_PROJECT_newArchEnabled": { + "external": true + }, + "ORG_GRADLE_PROJECT_hermesEnabled": { + "external": true + } } }, "test:ios": { diff --git a/integration-tests/tests/src/index.ts b/integration-tests/tests/src/index.ts index a2e0f51a7a..5d717c5b71 100644 --- a/integration-tests/tests/src/index.ts +++ b/integration-tests/tests/src/index.ts @@ -36,6 +36,7 @@ afterEach(() => { import "./utils/import-app.test.ts"; import "./utils/chai-plugin.test.ts"; +import "./utils/promise-handle.test.ts"; import "./mocha-internals.test.ts"; import "./tests"; diff --git a/integration-tests/tests/src/tests.ts b/integration-tests/tests/src/tests.ts index 912fc3c0f0..25eb3ad4b0 100644 --- a/integration-tests/tests/src/tests.ts +++ b/integration-tests/tests/src/tests.ts @@ -25,27 +25,45 @@ import "./tests/credentials/jwt"; import "./tests/sync/app"; import "./tests/sync/asymmetric"; import "./tests/sync/client-reset"; +import "./tests/sync/dictionary"; +import "./tests/sync/encryption"; import "./tests/sync/flexible"; import "./tests/sync/mixed"; -import "./tests/sync/sync-as-local"; -import "./tests/sync/upload-delete-download"; import "./tests/sync/mongo-db-client"; +import "./tests/sync/open-behavior"; import "./tests/sync/open"; +import "./tests/sync/partition-values"; +import "./tests/sync/realm-conversions"; +import "./tests/sync/realm"; +import "./tests/sync/set"; +import "./tests/sync/sync-as-local"; +import "./tests/sync/sync-session"; +import "./tests/sync/upload-delete-download"; +import "./tests/sync/user"; +import "./tests/sync/uuid"; +import "./tests/alias"; +import "./tests/array-buffer"; import "./tests/bson"; import "./tests/class-models"; import "./tests/dictionary"; import "./tests/dynamic-schema-updates"; import "./tests/enums"; import "./tests/iterators"; +import "./tests/linking-objects"; import "./tests/list"; import "./tests/listeners"; +import "./tests/migrations"; +import "./tests/mixed"; +import "./tests/notifications"; import "./tests/objects"; import "./tests/queries"; import "./tests/realm-constructor"; +import "./tests/results"; import "./tests/schema"; import "./tests/serialization"; import "./tests/set"; +import "./tests/sets"; import "./tests/shared-realms"; import "./tests/transaction"; import "./tests/types"; diff --git a/integration-tests/tests/src/tests/array-buffer.ts b/integration-tests/tests/src/tests/array-buffer.ts index 57b239a02a..253a975cd6 100644 --- a/integration-tests/tests/src/tests/array-buffer.ts +++ b/integration-tests/tests/src/tests/array-buffer.ts @@ -124,6 +124,6 @@ describe("ArrayBuffer type", () => { it("handles wrong input", function (this: RealmContext) { expect(() => { this.realm.write(() => this.realm.create(SingleSchema.name, { a: {} })); - }).throws(Error, "PrimitiveData.a must be of type 'binary?', got 'object' ([object Object])"); + }).throws("Expected value to be an instance of ArrayBuffer, got an object"); }); }); diff --git a/integration-tests/tests/src/tests/linking-objects.ts b/integration-tests/tests/src/tests/linking-objects.ts index b7e4610711..b34839d0ec 100644 --- a/integration-tests/tests/src/tests/linking-objects.ts +++ b/integration-tests/tests/src/tests/linking-objects.ts @@ -173,16 +173,13 @@ describe("Linking objects", () => { person = this.realm.create(PersonSchema.name, { name: "Person 1", age: 50 }); }); expect(() => person.linkingObjects("NoSuchSchema", "noSuchProperty")).throws( - Error, - "Could not find schema for type 'NoSuchSchema'", + "Object type 'NoSuchSchema' not found in schema.", ); expect(() => person.linkingObjects("PersonObject", "noSuchProperty")).throws( - Error, - "Type 'PersonObject' does not contain property 'noSuchProperty'", + "Property 'noSuchProperty' does not exist on 'PersonObject' objects", ); expect(() => person.linkingObjects("PersonObject", "name")).throws( - Error, - "'PersonObject.name' is not a relationship to 'PersonObject'", + "'PersonObject#name' is not a relationship to 'PersonObject'", ); let olivier: Person; let oliviersParents: Realm.Results; diff --git a/integration-tests/tests/src/tests/list.ts b/integration-tests/tests/src/tests/list.ts index 7439bcb45f..6e733386d2 100644 --- a/integration-tests/tests/src/tests/list.ts +++ b/integration-tests/tests/src/tests/list.ts @@ -1838,10 +1838,10 @@ describe("Lists", () => { }); // Test that we can push a created object on a list - const name = this.realm.create(NameObjectLocalSchema.name, { + const name = this.realm.create(NameObjectSchema.name, { _id: new BSON.ObjectId(), - family: "Petersen", - given: ["Gurli", "Margrete"], + family: "Larsen", + given: ["Lars"], prefix: [], }); const person = this.realm.create(ParentObjectSchema.name, { @@ -1853,7 +1853,7 @@ describe("Lists", () => { }); const objects = this.realm.objects(ParentObjectSchema.name).sorted([["id", false]]); - expect(objects.length).equals(2); + expect(objects.length).equals(3); expect(objects[0].name.length).equals(2); expect(objects[0].name[0].given.length).equals(2); expect(objects[0].name[0].prefix.length).equals(0); @@ -1868,6 +1868,12 @@ describe("Lists", () => { expect(objects[1].name[0].prefix.length).equals(0); expect(objects[1].name[0].given[0]).equals("Gurli"); expect(objects[1].name[0].given[1]).equals("Margrete"); + + expect(objects[2].name.length).equals(1); + expect(objects[2].name[0].given.length).equals(1); + expect(objects[2].name[0].prefix.length).equals(0); + expect(objects[2].name[0].given[0]).equals("Lars"); + expect(objects[2].name[0].family).equals("Larsen"); }); it("supports nested lists from parsed JSON", function (this: RealmContext) { const json = diff --git a/integration-tests/tests/src/tests/migrations.ts b/integration-tests/tests/src/tests/migrations.ts index 9e5197bbca..b7f4afc09b 100644 --- a/integration-tests/tests/src/tests/migrations.ts +++ b/integration-tests/tests/src/tests/migrations.ts @@ -89,7 +89,7 @@ describe("Migrations", () => { expect(function () { //@ts-expect-error This is an invalid function. new Realm({ schema: [], schemaVersion: 2, onMigration: "invalid", inMemory: true }); - }).throws("onMigration must be of type 'function'"); + }).throws("Expected 'onMigration' on realm configuration to be a function, got a string"); }); it("should propogate exceptions", () => { @@ -437,7 +437,7 @@ describe("Migrations", () => { expect(() => { // Deleting a model which is target of linkingObjects results in an exception newRealm.deleteModel("Person"); - }).throws("Table is target of cross-table link columns"); + }).throws("Cannot remove class_Person that is target of outside links"); }, }); diff --git a/integration-tests/tests/src/tests/objects.ts b/integration-tests/tests/src/tests/objects.ts index 6f6ef30199..074b07c086 100644 --- a/integration-tests/tests/src/tests/objects.ts +++ b/integration-tests/tests/src/tests/objects.ts @@ -1064,9 +1064,9 @@ describe("Realm.Object", () => { this.realm.delete(obj); expect(obj.isValid()).to.be.false; // Reading a column from deleted object should fail - expect(() => obj.doubleCol).to.throw("No object with key"); + expect(() => obj.doubleCol).to.throw("Accessing object which has been invalidated or deleted"); // Writing to a column from deleted object should fail - expect(() => (obj.doubleCol = 0)).to.throw("No object with key"); + expect(() => (obj.doubleCol = 0)).to.throw("Accessing object which has been invalidated or deleted"); return obj; }); @@ -1113,10 +1113,11 @@ describe("Realm.Object", () => { const freeKey = obj._objectKey(); //@ts-expect-error uses private method. const obj1 = this.realm._objectForObjectKey(AgeSchema.name, "1" + freeKey); - //@ts-expect-error uses private method. - const obj2 = this.realm._objectForObjectKey(AgeSchema.name, "invalid int64_t"); expect(obj1).to.be.undefined; - expect(obj2).to.be.undefined; + expect(() => { + //@ts-expect-error uses private method. + this.realm._objectForObjectKey(AgeSchema.name, "invalid int64_t"); + }).throws("Expected value to be a numeric string, got a string"); }); it("modifying object fetched from key propagates", async function (this: Mocha.Context & RealmContext) { diff --git a/integration-tests/tests/src/tests/results.ts b/integration-tests/tests/src/tests/results.ts index 195df585e3..815d3e2e1f 100644 --- a/integration-tests/tests/src/tests/results.ts +++ b/integration-tests/tests/src/tests/results.ts @@ -102,6 +102,10 @@ const NullableBasicTypesSchema = { }; describe("Results", () => { + afterEach(() => { + Realm.clearTestState(); + }); + describe("General functionality", () => { openRealmBeforeEach({ schema: [TestObject] }); @@ -169,7 +173,7 @@ describe("Results", () => { expect(() => { objects.indexOf(object4); - }).throws("Realm object is from another Realm"); + }).throws("Object of type 'TestObject' does not match Results type 'TestObject'"); }); it("should be read-only", function (this: RealmContext) { @@ -186,11 +190,11 @@ describe("Results", () => { expect(() => { //@ts-expect-error Should be an invalid write to read-only object. objects[0] = { doubleCol: 0 }; - }).throws(select({ reactNative: "Cannot assign to index", default: "Cannot assign to read only index 0" })); + }).throws("Assigning into a Results is not supported"); expect(() => { //@ts-expect-error Should be an invalid write to read-only object. objects[1] = { doubleCol: 0 }; - }).throws(select({ reactNative: "Cannot assign to index", default: "Cannot assign to read only index 1" })); + }).throws("Assigning into a Results is not supported"); expect(() => { //@ts-expect-error Should be an invalid write to read-only object. objects.length = 0; @@ -511,11 +515,11 @@ describe("Results", () => { expect(() => { //@ts-expect-error Expected to be an invalid sorted argument. objects.sorted(1); - }).throws("JS value must be of type 'string'"); + }).throws("Expected 'argument' to be property name and optional bool or an array of descriptors, got a number"); expect(() => { //@ts-expect-error Expected to be an invalid sorted argument. objects.sorted([1]); - }).throws("JS value must be of type 'string'"); + }).throws("Expected 'descriptor[0]' to be string or array with two elements [string, boolean], got a number"); expect(() => { objects.sorted("fish"); @@ -527,7 +531,7 @@ describe("Results", () => { expect(() => { //@ts-expect-error Expected to be an invalid sorted argument. objects.sorted(["valueCol", "primaryCol"], true); - }).throws("Second argument is not allowed if passed an array of sort descriptors"); + }).throws("Expected second 'argument' to be undefined, got a boolean"); realm.close(); }); @@ -752,28 +756,28 @@ describe("Results", () => { ["boolCol", "stringCol", "dataCol"].forEach((colName) => { expect(() => { results.min(colName); - }).throws(`Cannot min property '${colName}'`); + }).throws("Operation 'min' not supported for"); }); // bool, string & data columns don't support 'max' ["boolCol", "stringCol", "dataCol"].forEach((colName) => { expect(() => { results.max(colName); - }).throws(`Cannot max property '${colName}'`); + }).throws(`Operation 'max' not supported for`); }); // bool, string, date & data columns don't support 'avg' ["boolCol", "stringCol", "dateCol", "dataCol"].forEach((colName) => { expect(() => { results.avg(colName); - }).throws(`Cannot avg property '${colName}'`); + }).throws(`Operation 'average' not supported for`); }); // bool, string, date & data columns don't support 'sum' ["boolCol", "stringCol", "dateCol", "dataCol"].forEach((colName) => { expect(() => { results.sum(colName); - }).throws(`Cannot sum property '${colName}'`); + }).throws(`Operation 'sum' not supported for`); }); }); @@ -784,16 +788,16 @@ describe("Results", () => { const results = this.realm.objects("NullableBasicTypesObject"); expect(() => { results.min("foo"); - }).throws("Property 'foo' does not exist on object 'NullableBasicTypesObject'"); + }).throws("Property 'foo' does not exist on 'NullableBasicTypesObject' objects"); expect(() => { results.max("foo"); - }).throws("Property 'foo' does not exist on object 'NullableBasicTypesObject'"); + }).throws("Property 'foo' does not exist on 'NullableBasicTypesObject' objects"); expect(() => { results.sum("foo"); - }).throws("Property 'foo' does not exist on object 'NullableBasicTypesObject'"); + }).throws("Property 'foo' does not exist on 'NullableBasicTypesObject' objects"); expect(() => { results.avg("foo"); - }).throws("Property 'foo' does not exist on object 'NullableBasicTypesObject'"); + }).throws("Property 'foo' does not exist on 'NullableBasicTypesObject' objects"); }); }); @@ -979,11 +983,11 @@ describe("Results", () => { this.realm.write(() => { results.update("unknownCol", "world"); }); - }).throws("No such property: unknownCol"); + }).throws("Property 'unknownCol' does not exist on 'NullableBasicTypesObject' objects"); expect(() => { results.update("stringCol", "world"); - }).throws("Can only 'update' objects within a transaction."); + }).throws("Cannot modify managed objects outside of a write transaction."); }); }); }); diff --git a/integration-tests/tests/src/tests/sync/client-reset.ts b/integration-tests/tests/src/tests/sync/client-reset.ts index 3aaaf16a98..6403e899ab 100644 --- a/integration-tests/tests/src/tests/sync/client-reset.ts +++ b/integration-tests/tests/src/tests/sync/client-reset.ts @@ -91,7 +91,7 @@ async function waitServerSideClientResetDiscardUnsyncedChangesCallbacks( await realm.syncSession?.uploadAllLocalChanges(); await triggerClientReset(app, user); - await resetHandle.promise; + await resetHandle; } async function waitServerSideClientResetRecoveryCallbacks( @@ -139,7 +139,7 @@ async function waitServerSideClientResetRecoveryCallbacks( await realm.syncSession?.uploadAllLocalChanges(); await triggerClientReset(app, user); - await resetHandle.promise; + await resetHandle; } async function waitSimulatedClientResetDiscardUnsyncedChangesCallbacks( @@ -189,7 +189,7 @@ async function waitSimulatedClientResetDiscardUnsyncedChangesCallbacks( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore calling undocumented method _simulateError realm.syncSession?._simulateError(211, "Simulate Client Reset", "realm::sync::ProtocolError", false); // 211 -> diverging histories - await resetHandle.promise; + await resetHandle; } async function waitSimulatedClientResetRecoverCallbacks( @@ -243,7 +243,7 @@ async function waitSimulatedClientResetRecoverCallbacks( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore calling undocumented method _simulateError session._simulateError(211, "Simulate Client Reset", "realm::sync::ProtocolError", false); // 211 -> diverging histories; 217 -> synchronization no longer possible for client-side file - await resetHandle.promise; + await resetHandle; } /** diff --git a/integration-tests/tests/src/tests/sync/flexible.ts b/integration-tests/tests/src/tests/sync/flexible.ts index cf0fde0160..a57f160b61 100644 --- a/integration-tests/tests/src/tests/sync/flexible.ts +++ b/integration-tests/tests/src/tests/sync/flexible.ts @@ -135,12 +135,11 @@ describe.skipIf(environment.missingServer, "Flexible sync", function () { this.timeout(60_000); // TODO: Temporarily hardcoded until envs are set up. importAppBefore("with-db-flx"); authenticateUserBefore(); + afterEach(() => { + Realm.clearTestState(); + }); describe("Configuration", () => { - afterEach(() => { - Realm.clearTestState(); - }); - describe("flexible sync Realm config", function () { it.skip("respects cancelWaitOnNonFatalError", async function () { this.timeout(2_000); @@ -477,11 +476,11 @@ describe.skipIf(environment.missingServer, "Flexible sync", function () { }); await realm.syncSession?.uploadAllLocalChanges(); - await callbackHandle.promise; + await callbackHandle; }); }); - describe("With realm opened before", function () { + describe("with realm opened before", function () { openRealmBeforeEach({ schema: [FlexiblePersonSchema, DogSchema], sync: { @@ -1769,16 +1768,17 @@ describe.skipIf(environment.missingServer, "Flexible sync", function () { it("deletes the item if an item not matching the filter is created", async function (this: RealmContext) { await addSubscriptionAndSync(this.realm, this.realm.objects(FlexiblePersonSchema.name).filtered("age < 30")); - this.realm.write(() => { + const tom = this.realm.write(() => this.realm.create(FlexiblePersonSchema.name, { _id: new BSON.ObjectId(), name: "Tom Old", age: 99, - }); - }); + }), + ); + expect(tom.isValid()).equals(true); await this.realm.syncSession?.downloadAllServerChanges(); - expect(this.realm.objects(FlexiblePersonSchema.name)).to.have.length(0); + expect(tom.isValid()).equals(false); }); it("throw an exception if you remove a subscription without waiting for server acknowledgement, then modify objects that were only matched by the now removed subscription", async function (this: RealmContext) { diff --git a/integration-tests/tests/src/tests/sync/open-behavior.ts b/integration-tests/tests/src/tests/sync/open-behavior.ts index 622d71c6c9..dc5e86ee97 100644 --- a/integration-tests/tests/src/tests/sync/open-behavior.ts +++ b/integration-tests/tests/src/tests/sync/open-behavior.ts @@ -46,6 +46,7 @@ async function getRegisteredEmailPassCredentials(app: Realm.App) { } describe("OpenBehaviour", function () { + this.longTimeout(); importAppBefore("with-db"); afterEach(() => Realm.clearTestState()); @@ -536,14 +537,7 @@ describe("OpenBehaviour", function () { const openPromise2 = Realm.open(config); openPromise1.cancel(); // Will cancel both promise 1 and 2 at the native level. - - try { - await openPromise2; - throw new Error("openPromise2 should have been rejected.."); - } catch (err: any) { - //platforms either return "Operation canceled" or "Operation Canceled" - expect((err.message as string).toLowerCase()).equals("operation canceled"); - } + await expect(openPromise2).rejectedWith("Async open canceled"); }); it("progress-listener should not fire events on canceled realm.open", async function (this: AppContext) { diff --git a/integration-tests/tests/src/tests/sync/open.ts b/integration-tests/tests/src/tests/sync/open.ts index 61371319d7..7b6226fbde 100644 --- a/integration-tests/tests/src/tests/sync/open.ts +++ b/integration-tests/tests/src/tests/sync/open.ts @@ -17,6 +17,7 @@ //////////////////////////////////////////////////////////////////////////// import { expect } from "chai"; +import { Realm } from "realm"; import { authenticateUserBefore, importAppBefore } from "../../hooks"; diff --git a/integration-tests/tests/src/tests/sync/realm.ts b/integration-tests/tests/src/tests/sync/realm.ts index 57c61cbf39..2bc0c535e8 100644 --- a/integration-tests/tests/src/tests/sync/realm.ts +++ b/integration-tests/tests/src/tests/sync/realm.ts @@ -143,10 +143,10 @@ const IndexedTypesSchema = { intCol: { type: "int", indexed: true }, stringCol: { type: "string", indexed: true }, dateCol: { type: "date", indexed: true }, - optBoolCol: { type: "bool?", indexed: true }, - optIntCol: { type: "int?", indexed: true }, - optStringCol: { type: "string?", indexed: true }, - optDateCol: { type: "date?", indexed: true }, + optBoolCol: { type: "bool", optional: true, indexed: true }, + optIntCol: { type: "int", optional: true, indexed: true }, + optStringCol: { type: "string", optional: true, indexed: true }, + optDateCol: { type: "date", optional: true, indexed: true }, }, }; @@ -160,9 +160,9 @@ const DefaultValuesSchema = { stringCol: { type: "string", default: "defaultString" }, dateCol: { type: "date", default: new Date(1.111) }, dataCol: { type: "data", default: new ArrayBuffer(1) }, - objectCol: { type: "TestObject", default: { doubleCol: 1 } }, - nullObjectCol: { type: "TestObject", default: null }, - arrayCol: { type: "TestObject[]", default: [{ doubleCol: 2 }] }, + objectCol: { type: "object", objectType: "TestObject", default: { doubleCol: 1 } }, + nullObjectCol: { type: "object", objectType: "TestObject", default: null }, + arrayCol: { type: "list", objectType: "TestObject", default: [{ doubleCol: 2 }] }, }, }; @@ -1500,19 +1500,16 @@ describe("Realmtest", () => { openRealmBeforeEach({ schema: [] }); it("gives correct function and error message", function (this: RealmContext) { - function failingFunction() { - throw new Error("not implemented"); - } - try { - this.realm.write(() => { - failingFunction(); + this.realm.write(function failingFunction() { + throw new Error("boom"); }); - } catch (e: any) { - expect(e.stack).not.equals(undefined, "e.stack should not be undefined"); - expect(e.stack).not.equals(null, "e.stack should not be null"); - expect(e.stack.indexOf("at failingFunction (") !== -1).to.be.true; - expect(e.stack.indexOf("not implemented") !== -1).to.be.true; + } catch (e) { + expect(e).instanceOf(Error); + if (e instanceof Error) { + expect(e.message).contains("boom"); + expect(e.stack).contains("failingFunction"); + } } }); }); @@ -1764,7 +1761,7 @@ describe("Realmtest", () => { const normalizeProperty = (val: Realm.ObjectSchemaProperty | string) => { let prop: Realm.ObjectSchemaProperty | string; if (typeof val !== "string" && !(val instanceof String)) { - prop = val; + prop = { ...val }; prop.optional = val.optional || false; prop.indexed = val.indexed || false; } else { @@ -1937,7 +1934,7 @@ describe("Realmtest", () => { expect(() => { new Realm({ path: "bundled.realm", disableFormatUpgrade: true }); - }).throws("The Realm file format must be allowed to be upgraded in order to proceed."); + }).throws("Database upgrade required but prohibited."); }); }); @@ -2228,7 +2225,7 @@ describe("Realmtest", () => { // eslint-disable-next-line @typescript-eslint/no-empty-function onMigration: function () {}, }); - }).throws("Cannot include 'onMigration' when 'deleteRealmIfMigrationNeeded' is set."); + }).throws("Cannot set 'deleteRealmIfMigrationNeeded' when 'onMigration' is set."); }); }); }); diff --git a/integration-tests/tests/src/tests/sync/sync-session.ts b/integration-tests/tests/src/tests/sync/sync-session.ts index a437e65d03..31378994d4 100644 --- a/integration-tests/tests/src/tests/sync/sync-session.ts +++ b/integration-tests/tests/src/tests/sync/sync-session.ts @@ -17,7 +17,7 @@ //////////////////////////////////////////////////////////////////////////// import { expect } from "chai"; -import { ConnectionState, ObjectSchema, BSON } from "realm"; +import { Realm, ConnectionState, ObjectSchema, BSON, User, SyncConfiguration } from "realm"; import { importAppBefore } from "../../hooks"; import { DogSchema } from "../../schemas/person-and-dog-with-object-ids"; import { getRegisteredEmailPassCredentials } from "../../utils/credentials"; @@ -137,29 +137,21 @@ describe("SessionTest", () => { expect(realm.syncSession).to.be.null; }); - it("config with undefined sync property", () => { + it("config with undefined sync property", async () => { const config = { sync: undefined, }; - Realm.open(config).then((realm) => { - expect(realm.syncSession).to.be.null; - }); + const realm = await Realm.open(config); + expect(realm.syncSession).to.be.null; }); it("config with sync and inMemory set", async () => { - const config = { - sync: true, - inMemory: true, - }; - return new Promise((resolve, reject) => { - //@ts-expect-error try config with mutually exclusive properties - return Realm.open(config) - .then(() => reject("opened realm with invalid configuration")) - .catch((error) => { - expect(error.message).contains("Options 'inMemory' and 'sync' are mutual exclusive."); - resolve(); - }); - }); + await expect( + Realm.open({ + sync: {} as SyncConfiguration, + inMemory: true, + }), + ).rejectedWith("The realm configuration options 'inMemory' and 'sync' cannot both be defined."); }); it("config with onMigration and sync set", async function (this: AppContext) { @@ -168,14 +160,9 @@ describe("SessionTest", () => { config.onMigration = () => { /* empty function */ }; - return new Promise((resolve, reject) => { - return Realm.open(config) - .then(() => reject("opened realm with invalid configuration")) - .catch((error) => { - expect(error.message).contains("Options 'onMigration' and 'sync' are mutually exclusive"); - resolve(); - }); - }); + await expect(Realm.open(config)).rejectedWith( + "The realm configuration options 'onMigration' and 'sync' cannot both be defined.", + ); }); it("invalid sync user object", async function (this: AppContext) { @@ -184,11 +171,9 @@ describe("SessionTest", () => { const { config } = await getSyncConfWithUser(this.app, partition); //@ts-expect-error setting an invalid user object config.sync.user = { username: "John Doe" }; - try { - await Realm.open(config); - } catch (e: any) { - expect(e.message).contains("Expected 'user' to be an instance of User, got an object"); - } + await expect(Realm.open(config)).rejectedWith( + "Expected 'user' on realm sync configuration to be an instance of User, got an object", + ); }); it("propagates custom http headers", async function (this: AppContext) { @@ -586,41 +571,24 @@ describe("SessionTest", () => { it("timeout on download successfully throws", async function (this: AppContext) { const partition = generatePartition(); let realm!: Realm; - return this.app - .logIn(Realm.Credentials.anonymous()) - .then((user) => { + await expect( + this.app.logIn(Realm.Credentials.anonymous()).then((user) => { const config = getSyncConfiguration(user, partition); realm = new Realm(config); return realm.syncSession?.downloadAllServerChanges(1); - }) - .then( - () => { - throw new Error("Download did not time out"); - }, - (e) => { - expect(e).equals("Downloading changes did not complete in 1 ms."); - }, - ); + }), + ).is.rejectedWith("Downloading changes did not complete in 1 ms."); }); it("timeout on upload successfully throws", async function (this: AppContext) { - let realm!: Realm; const partition = generatePartition(); - return this.app - .logIn(Realm.Credentials.anonymous()) - .then((user) => { + expect( + this.app.logIn(Realm.Credentials.anonymous()).then((user) => { const config = getSyncConfiguration(user, partition); - realm = new Realm(config); + const realm = new Realm(config); return realm.syncSession?.uploadAllLocalChanges(1); - }) - .then( - () => { - throw new Error("Upload did not time out"); - }, - (e) => { - expect(e).equals("Uploading changes did not complete in 1 ms."); - }, - ); + }), + ).rejectedWith("Uploading changes did not complete in 1 ms."); }); }); @@ -951,6 +919,7 @@ describe("SessionTest", () => { age: i, firstName: "John", lastName: "Smith", + partition, }); } }); @@ -976,6 +945,7 @@ describe("SessionTest", () => { age: i, firstName: "John", lastName: "Smith", + partition, }); } }); @@ -997,7 +967,7 @@ describe("SessionTest", () => { // we haven't uploaded our recent changes -- we're not allowed to copy expect(() => { realm1.writeCopyTo(outputConfig2); - }).throws("Could not write file as not all client changes are integrated in server"); + }).throws("All client changes must be integrated in server before writing copy"); // log back in and upload the changes we made locally user1 = await this.app.logIn(credentials1); @@ -1041,6 +1011,7 @@ describe("SessionTest", () => { age: i, firstName: "John", lastName: "Smith", + partition, }); } }); @@ -1077,7 +1048,7 @@ describe("SessionTest", () => { clientReset: { //@ts-expect-error TYPEBUG: enum not exposed in realm namespace mode: "manual", - onManual: () => console.log("error"), + onManual: (...args) => console.log("error", args), }, }, schema: [PersonForSyncSchema, DogForSyncSchema], diff --git a/integration-tests/tests/src/tests/sync/user.ts b/integration-tests/tests/src/tests/sync/user.ts index 9dc339581e..b7b81844fd 100644 --- a/integration-tests/tests/src/tests/sync/user.ts +++ b/integration-tests/tests/src/tests/sync/user.ts @@ -317,7 +317,7 @@ describe.skipIf(environment.missingServer, "User", () => { .logIn(Realm.Credentials.emailPassword({ email: validEmail, password: validPassword })) .catch((err) => { expect(err.message).equals("invalid username/password"); - expect(err.code).equals(50); + expect(err.code).equals(4349); didFail = true; }); expect(user2).to.be.undefined; @@ -370,17 +370,20 @@ describe.skipIf(environment.missingServer, "User", () => { expect(user.apiKeys instanceof Realm.Auth.ApiKeyAuth).to.be.true; // TODO: Enable when fixed: Disabling this test since the CI stitch integration returns cryptic error. - const apikey = await user.apiKeys.create("mykey"); - const keys = await user.apiKeys.fetchAll(); - expect(Array.isArray(keys)).to.be.true; - - expect(keys.length).equals(1); + const key = await user.apiKeys.create("mykey"); + expect(key._id).is.string; + expect(key.name).equals("mykey"); + expect(key.disabled).equals(false); + expect(key.key).is.string; - //@ts-expect-error TYPEBUG: Realm.Auth.ApiKey expects an _id field while on the other hand this key only has a id field. - expect(keys[0].id).to.not.be.null; - //@ts-expect-error TYPEBUG: Realm.Auth.ApiKey expects an _id field while on the other hand this key only has a id field. - expect(keys[0].id).to.not.be.undefined; - expect(keys[0].name).equals("mykey"); + const keys = await user.apiKeys.fetchAll(); + expect(keys).deep.equals([ + { + _id: key._id, + name: key.name, + disabled: key.disabled, + }, + ]); await user.logOut(); }); diff --git a/integration-tests/tests/src/utils/promise-handle.test.ts b/integration-tests/tests/src/utils/promise-handle.test.ts new file mode 100644 index 0000000000..b80ab2553d --- /dev/null +++ b/integration-tests/tests/src/utils/promise-handle.test.ts @@ -0,0 +1,34 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +import { expect } from "chai"; +import { createPromiseHandle } from "./promise-handle"; + +describe("PromiseHandle", () => { + it("resolves", async () => { + const handle = createPromiseHandle(); + handle.resolve("value"); + expect(handle).eventually.equals("value"); + }); + + it("rejects", async () => { + const handle = createPromiseHandle(); + handle.reject(new Error("err")); + expect(handle).rejectedWith("err"); + }); +}); diff --git a/integration-tests/tests/src/utils/promise-handle.ts b/integration-tests/tests/src/utils/promise-handle.ts index 0bc7defa28..d93240504a 100644 --- a/integration-tests/tests/src/utils/promise-handle.ts +++ b/integration-tests/tests/src/utils/promise-handle.ts @@ -18,11 +18,10 @@ type ResolveType = (value: T | PromiseLike) => void; type RejectType = (reason?: any) => void; -type PromiseHandle = { - promise: Promise; +export type PromiseHandle = { resolve: ResolveType; reject: RejectType; -}; +} & Promise; export function createPromiseHandle(): PromiseHandle { let resolve: ResolveType | null = null; @@ -34,5 +33,18 @@ export function createPromiseHandle(): PromiseHandle { if (!resolve || !reject) { throw new Error("Expected promise executor to be called synchronously"); } - return { promise, resolve, reject }; + return { + [Symbol.toStringTag]: "PromiseHandle", + resolve, + reject, + then(onResolve, onReject) { + return promise.then(onResolve, onReject); + }, + catch(onReject) { + return promise.catch(onReject); + }, + finally(onFinally) { + return promise.finally(onFinally); + }, + }; } diff --git a/packages/bindgen/src/templates/jsi.ts b/packages/bindgen/src/templates/jsi.ts index 99d9d2fdae..a358eed509 100644 --- a/packages/bindgen/src/templates/jsi.ts +++ b/packages/bindgen/src/templates/jsi.ts @@ -955,9 +955,9 @@ class JsiCppDecls extends CppDecls { return FWD(val).asBigInt(_env).asInt64(_env); auto obj = FWD(val).asObject(_env); - auto high = uint32_t(obj.getProperty(_env, ${this.addon.getPropId("high")}).asNumber()); - auto low = uint32_t(obj.getProperty(_env, ${this.addon.getPropId("low")}).asNumber()); - return int64_t((uint64_t(high) << 32) | low); + auto high = uint32_t(int32_t(obj.getProperty(_env, ${this.addon.getPropId("high")}).asNumber())); + auto low = uint32_t(int32_t(obj.getProperty(_env, ${this.addon.getPropId("low")}).asNumber())); + return (int64_t(high) << 32) | low; `, }), new CppFunc("bigIntToU64", "uint64_t", [new CppVar("jsi::Runtime&", env), new CppVar("auto&&", "val")], { @@ -966,8 +966,8 @@ class JsiCppDecls extends CppDecls { return FWD(val).asBigInt(_env).asUint64(_env); auto obj = FWD(val).asObject(_env); - auto high = uint32_t(obj.getProperty(_env, ${this.addon.getPropId("high")}).asNumber()); - auto low = uint32_t(obj.getProperty(_env, ${this.addon.getPropId("low")}).asNumber()); + auto high = uint32_t(int32_t(obj.getProperty(_env, ${this.addon.getPropId("high")}).asNumber())); + auto low = uint32_t(int32_t(obj.getProperty(_env, ${this.addon.getPropId("low")}).asNumber())); return (uint64_t(high) << 32) | low; `, }), diff --git a/packages/realm/src/Configuration.ts b/packages/realm/src/Configuration.ts index ccf3582990..4551cf03e9 100644 --- a/packages/realm/src/Configuration.ts +++ b/packages/realm/src/Configuration.ts @@ -243,6 +243,7 @@ export function validateConfiguration(config: unknown): asserts config is Config } if (sync !== undefined) { assert(!onMigration, "The realm configuration options 'onMigration' and 'sync' cannot both be defined."); + assert(inMemory === undefined, "The realm configuration options 'inMemory' and 'sync' cannot both be defined."); assert( deleteRealmIfMigrationNeeded === undefined, "The realm configuration options 'deleteRealmIfMigrationNeeded' and 'sync' cannot both be defined.", diff --git a/packages/realm/src/Object.ts b/packages/realm/src/Object.ts index 2b18f1929f..1861323bf4 100644 --- a/packages/realm/src/Object.ts +++ b/packages/realm/src/Object.ts @@ -351,7 +351,7 @@ export class RealmObject { assert( linkedObjectSchema.name === property.objectType, - () => `'${linkedObjectSchema.name}#${propertyName}' is not a relationship to '${this.objectSchema.name}'`, + () => `'${linkedObjectSchema.name}#${propertyName}' is not a relationship to '${this.objectSchema().name}'`, ); // Create the Result for the backlink view diff --git a/packages/realm/src/OrderedCollection.ts b/packages/realm/src/OrderedCollection.ts index 41bc3cc117..e4fa89fa85 100644 --- a/packages/realm/src/OrderedCollection.ts +++ b/packages/realm/src/OrderedCollection.ts @@ -194,7 +194,7 @@ export abstract class OrderedCollection; sorted(arg0: boolean | SortDescriptor[] | string = "self", arg1?: boolean): Results { if (Array.isArray(arg0)) { - assert(typeof arg1 === "undefined", "Second argument is not allowed if passed an array of sort descriptors"); + assert.undefined(arg1, "second 'argument'"); const { results: parent, realm, helpers } = this; // Map optional "reversed" to "ascending" (expected by the binding) - const descriptors = arg0.map<[string, boolean]>((arg) => - typeof arg === "string" ? [arg, true] : [arg[0], !arg[1]], - ); + const descriptors = arg0.map<[string, boolean]>((arg, i) => { + if (typeof arg === "string") { + return [arg, true]; + } else if (Array.isArray(arg)) { + const [property, direction] = arg; + assert.string(property, "property"); + assert.boolean(direction, "direction"); + return [property, !direction]; + } else { + throw new TypeAssertionError("string or array with two elements [string, boolean]", arg, `descriptor[${i}]`); + } + }); // TODO: Call `parent.sort`, avoiding property name to column key conversion to speed up performance here. const results = parent.sortByNames(descriptors); return new Results(realm, results, helpers); @@ -606,7 +615,7 @@ export abstract class OrderedCollection { this.handle.resolve(realm); } else if (openBehavior === OpenRealmBehaviorType.DownloadBeforeOpen) { const { bindingConfig } = Realm.transformConfig(config); + // Construct an async open task this.task = binding.Realm.getSynchronizedRealm(bindingConfig); + // If the promise handle gets rejected, we should cancel the open task + // to avoid consuming a thread safe reference which is no longer registered + this.handle.promise.catch(() => this.task?.cancel()); + + this.createTimeoutPromise(config, { openBehavior, timeOut, timeOutBehavior }); + this.task .start() .then(async (tsr) => { @@ -114,35 +121,18 @@ export class ProgressRealmPromise implements Promise { } return realm; }) - .then(this.handle.resolve, this.handle.reject); + .then(this.handle.resolve, (err) => { + assert.undefined(err.code, "Update this to use the error code instead of matching on message"); + if (err instanceof Error && err.message === "Sync session became inactive") { + // This can happen when two async tasks are opened for the same Realm and one gets canceled + this.rejectAsCanceled(); + } else { + this.handle.reject(err); + } + }); // TODO: Consider storing the token returned here to unregister when the task gets cancelled, // if for some reason, that doesn't happen internally this.task.registerDownloadProgressNotifier(this.emitProgress); - if (typeof timeOut === "number") { - this.timeoutPromise = new TimeoutPromise( - this.handle.promise, // Ensures the timeout gets cancelled when the realm opens - timeOut, - `Realm could not be downloaded in the allocated time: ${timeOut} ms.`, - ); - if (timeOutBehavior === OpenRealmTimeOutBehavior.ThrowException) { - // Make failing the timeout, reject the promise - this.timeoutPromise.catch(this.handle.reject); - } else if (timeOutBehavior === OpenRealmTimeOutBehavior.OpenLocalRealm) { - // Make failing the timeout, resolve the promise - this.timeoutPromise.catch((err) => { - if (err instanceof TimeoutError) { - const realm = new Realm(config); - this.handle.resolve(realm); - } else { - this.handle.reject(err); - } - }); - } else { - throw new Error( - `Invalid 'timeOutBehavior': '${timeOutBehavior}'. Only 'throwException' and 'openLocalRealm' is allowed.`, - ); - } - } } else { throw new Error(`Unexpected open behavior '${openBehavior}'`); } @@ -152,17 +142,12 @@ export class ProgressRealmPromise implements Promise { } cancel(): void { - if (this.task) { - this.task.cancel(); - this.task.$resetSharedPtr(); - this.task = null; - } + this.cancelAndResetTask(); this.timeoutPromise?.cancel(); // Clearing all listeners to avoid accidental progress notifications this.listeners.clear(); // Tell anything awaiting the promise - const err = new Error("Async open canceled"); - this.handle.reject(err); + this.rejectAsCanceled(); } progress(callback: ProgressNotificationCallback): this { @@ -182,6 +167,48 @@ export class ProgressRealmPromise implements Promise { } }; + private createTimeoutPromise(config: Configuration, { timeOut, timeOutBehavior }: OpenBehavior) { + if (typeof timeOut === "number") { + this.timeoutPromise = new TimeoutPromise( + this.handle.promise, // Ensures the timeout gets cancelled when the realm opens + timeOut, + `Realm could not be downloaded in the allocated time: ${timeOut} ms.`, + ); + if (timeOutBehavior === OpenRealmTimeOutBehavior.ThrowException) { + // Make failing the timeout, reject the promise + this.timeoutPromise.catch(this.handle.reject); + } else if (timeOutBehavior === OpenRealmTimeOutBehavior.OpenLocalRealm) { + // Make failing the timeout, resolve the promise + this.timeoutPromise.catch((err) => { + if (err instanceof TimeoutError) { + this.cancelAndResetTask(); + const realm = new Realm(config); + this.handle.resolve(realm); + } else { + this.handle.reject(err); + } + }); + } else { + throw new Error( + `Invalid 'timeOutBehavior': '${timeOutBehavior}'. Only 'throwException' and 'openLocalRealm' is allowed.`, + ); + } + } + } + + private cancelAndResetTask() { + if (this.task) { + this.task.cancel(); + this.task.$resetSharedPtr(); + this.task = null; + } + } + + private rejectAsCanceled() { + const err = new Error("Async open canceled"); + this.handle.reject(err); + } + get [Symbol.toStringTag]() { return ProgressRealmPromise.name; } diff --git a/packages/realm/src/PropertyHelpers.ts b/packages/realm/src/PropertyHelpers.ts index bf4591b86b..a829ed91b2 100644 --- a/packages/realm/src/PropertyHelpers.ts +++ b/packages/realm/src/PropertyHelpers.ts @@ -79,14 +79,24 @@ export type PropertyHelpers = TypeHelpers & const defaultGet = ({ typeHelpers: { fromBinding }, columnKey }: PropertyOptions) => (obj: binding.Obj) => { - return fromBinding(obj.getAny(columnKey)); + try { + return fromBinding(obj.getAny(columnKey)); + } catch (err) { + assert.isValid(obj); + throw err; + } }; const defaultSet = ({ realm, typeHelpers: { toBinding }, columnKey }: PropertyOptions) => (obj: binding.Obj, value: unknown) => { assert.inTransaction(realm); - obj.setAny(columnKey, toBinding(value)); + try { + obj.setAny(columnKey, toBinding(value)); + } catch (err) { + assert.isValid(obj); + throw err; + } }; function embeddedSet({ typeHelpers: { toBinding }, columnKey }: PropertyOptions) { diff --git a/packages/realm/src/PropertyMap.ts b/packages/realm/src/PropertyMap.ts index dcca82d642..dd0af48cfb 100644 --- a/packages/realm/src/PropertyMap.ts +++ b/packages/realm/src/PropertyMap.ts @@ -31,7 +31,10 @@ export class PropertyMap { private objectSchemaName: string | null = null; private initialized = false; private mapping: Record = {}; - private nameByColumnKey: Map = new Map(); + /** + * Note: Cannot key by the binding.ColKey directly, as this is `Long` on JSC (which does not pass equality checks like `bigint` does) + */ + private nameByColumnKeyString: Map = new Map(); private _names: string[] = []; public initialize(objectSchema: BindingObjectSchema, defaults: Record, options: HelperOptions) { @@ -51,7 +54,7 @@ export class PropertyMap { return [propertyName, helpers]; }), ); - this.nameByColumnKey = new Map(properties.map((p) => [p.columnKey, p.publicName || p.name])); + this.nameByColumnKeyString = new Map(properties.map((p) => [p.columnKey.toString(), p.publicName || p.name])); this._names = properties.map((p) => p.publicName || p.name); this.initialized = true; } @@ -70,7 +73,7 @@ export class PropertyMap { public getName = (columnKey: binding.ColKey): keyof T => { if (this.initialized) { - return this.nameByColumnKey.get(columnKey) as keyof T; + return this.nameByColumnKeyString.get(columnKey.toString()) as keyof T; } else { throw new UninitializedPropertyMapError(); } diff --git a/packages/realm/src/Realm.ts b/packages/realm/src/Realm.ts index 0a1f164bfc..7073f57ed9 100644 --- a/packages/realm/src/Realm.ts +++ b/packages/realm/src/Realm.ts @@ -623,15 +623,15 @@ export class Realm { binding.Helpers.setBindingContext(this.internal, { didChange: (r) => { r.verifyOpen(); - this.changeListeners.callback(); + this.changeListeners.notify(); }, schemaDidChange: (r) => { r.verifyOpen(); - this.schemaListeners.callback(); + this.schemaListeners.notify(this.schema); }, beforeNotify: (r) => { r.verifyOpen(); - this.beforeNotifyListeners.callback(); + this.beforeNotifyListeners.notify(); }, }); } else { @@ -961,6 +961,8 @@ export class Realm { throw new Error("You cannot query an asymmetric object."); } + assert.numericString(objectKey); + const table = binding.Helpers.getTable(this.internal, objectSchema.tableKey); try { const objKey = binding.stringToObjKey(objectKey); diff --git a/packages/realm/src/RealmListeners.ts b/packages/realm/src/RealmListeners.ts index 99faef0da5..5ac74ee954 100644 --- a/packages/realm/src/RealmListeners.ts +++ b/packages/realm/src/RealmListeners.ts @@ -16,7 +16,7 @@ // //////////////////////////////////////////////////////////////////////////// -import { ObjectSchema, Realm } from "./internal"; +import { CanonicalRealmSchema, Realm } from "./internal"; export enum RealmEvent { Change = "change", @@ -24,7 +24,7 @@ export enum RealmEvent { BeforeNotify = "beforenotify", } -export type RealmListenerCallback = (r: Realm, name: RealmEvent, schema?: ObjectSchema[]) => void; +export type RealmListenerCallback = (realm: Realm, name: RealmEvent, schema?: CanonicalRealmSchema) => void; // Temporary functions to work between event names and corresponding enums // TODO: We should update the external API to take a `RealmEvent` instead of a string. @@ -41,12 +41,9 @@ class RealmListeners { private listeners = new Set(); // Combined callback which runs all listener callbacks in one call. - callback(): void { - let schema: ObjectSchema[] | undefined; - if (this.eventType === RealmEvent.Schema) { - schema = this.realm.schema; - } - for (const callback of this.listeners) { + notify(schema?: CanonicalRealmSchema): void { + // Spreading to an array to avoid firing listeners that gets added from another listener + for (const callback of [...this.listeners]) { callback(this.realm, this.eventType, schema); } } diff --git a/packages/realm/src/Results.ts b/packages/realm/src/Results.ts index 01c57f79e3..bda3a14799 100644 --- a/packages/realm/src/Results.ts +++ b/packages/realm/src/Results.ts @@ -73,6 +73,10 @@ export class Results extends OrderedCollection { return this.internal.size(); } + set length(value: number) { + throw new Error("Cannot assign to read only property 'length'"); + } + description(): string { return binding.Helpers.getResultsDescription(this.internal); } diff --git a/packages/realm/src/TypeHelpers.ts b/packages/realm/src/TypeHelpers.ts index ecc9824f04..93e1f150bb 100644 --- a/packages/realm/src/TypeHelpers.ts +++ b/packages/realm/src/TypeHelpers.ts @@ -83,7 +83,10 @@ export type TypeOptions = { // "Only Realm instances are supported." (which should probably have been "RealmObject") // instead of relying on the binding to throw. export function mixedToBinding(realm: binding.Realm, value: unknown): binding.MixedArg { - if (typeof value === "undefined") { + if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) { + // Fast track pass through for the most commonly used types + return value; + } else if (value === undefined) { return null; } else if (value instanceof Date) { return binding.Timestamp.fromDate(value); @@ -96,7 +99,14 @@ export function mixedToBinding(realm: binding.Realm, value: unknown): binding.Mi } else if (Array.isArray(value)) { throw new TypeError("A mixed property cannot contain an array of values."); } else { - return value as binding.Mixed; + // Convert typed arrays to an `ArrayBuffer` + for (const TypedArray of TYPED_ARRAY_CONSTRUCTORS) { + if (value instanceof TypedArray) { + return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength); + } + } + // Rely on the binding for any other value + return value as binding.MixedArg; } } diff --git a/packages/realm/src/assert.ts b/packages/realm/src/assert.ts index 7bf1dab2bc..495ac4f2b3 100644 --- a/packages/realm/src/assert.ts +++ b/packages/realm/src/assert.ts @@ -63,6 +63,11 @@ assert.number = (value: unknown, target?: string): asserts value is number => { assert(typeof value === "number", () => new TypeAssertionError("a number", value, target)); }; +assert.numericString = (value: unknown, target?: string) => { + assert.string(value); + assert(/^-?\d+$/.test(value), () => new TypeAssertionError("a numeric string", value, target)); +}; + assert.boolean = (value: unknown, target?: string): asserts value is boolean => { assert(typeof value === "boolean", () => new TypeAssertionError("a boolean", value, target)); };