Skip to content

Commit

Permalink
RJS-2680: Implement support for Mixed data type with nested collect…
Browse files Browse the repository at this point in the history
…ions (#6513)

* Move geospatial helper functions to related file.

* Implement setting nested lists in Mixed.

* Implement setting nested dictionaries in Mixed.

* Implement getting nested lists in Mixed.

* Implement getting nested dictionaries in Mixed.

* Test creating and accessing nested lists and dicts.

* Make previous flat collections tests use the new 'expect' function.

* Test that max nesting level throws.

* Delegate throwing when using a Set to 'mixedToBinding()'.

* Implement setting nested collections on a dictionary via setter.

* Test nested collections on dictionary via setter.

* Minor update to names of tests.

* Combine nested and flat collections tests into same suite.

* Implement setting nested collections on a list via setter.

* Test nested collections on list via setter.

* Refactor common test logic to helper functions.

* Optimize property setter for hot-path and use friendlier err msg.

* Refactor test helper function to build collections of any depth.

* Implement inserting nested collections on a list via 'push()'.

* Test nested collections on a list via 'push()'.

* Test updating dictionary entry to nested collections via setter.

* Test updating nested list/dictionary item via setter.

* Test removing items from collections via 'remove()'.

* Test object notifications when modifying nested collections.

* Group previous notification tests into one test.

* Group collection notifications tests into 'List' and 'Dictionary'.

* Test collection notifications when modifying nested collections.

* Remove collections from test context.

* Test filtering by query path on nested collections.

* Align object schema property names in tests.

* Test filtering with int at_type.

* Implement setting nested collections on a dictionary via 'set()' overloads.

* Test JS Array method 'values()'.

* Test JS Array method 'entries()'.

* Implement getting nested collections on dictionary 'values()' and 'entries()'.

* Test 'values()' and 'entries()' on dictionary with nested collections.

* Remove unnecessary 'fromBinding()' calls.

* Refactor collection helpers from 'PropertyHelpers' into the respective collection file.

* Introduce list/dict sentinels to circumvent extra Core access.

* Rename getter to default.

* Remove redundant 'snapshotGet'.

* Add abstract 'get' and 'set' to 'OrderedCollection'.

* Rename the collection helpers to 'accessor'.

* Move tests into subsuites.

* Fix 'Results.update()'.

* Support nested collections in 'pop()', 'shift()', 'unshift()', 'splice()'.

* Test list 'pop()'.

* Test list 'shift()'.

* Test list 'unshift()'.

* Test list 'splice()'.

* Return 'not found' for collections searched for in 'indexOf()'.

* Test ordered collection 'indexOf()'.

* Support list/dict sentinels in JSI.

* Test references per access.

* Enable skipped tests after Core bug fix.

* Point to updated Core.

* Fix accessor for non-Mixed top-level collection with Mixed items.

* Enable and fix previously skipped test.

* Update 'mixed{}'.

* Update 'mixed<>'.

* Remove now-invalidated test.

* Remove unused injectable from Node bindgen template.

* Replace if-statements with switch.

* Add explicit Results and Set accessors for Mixed.

* Adapt to change in Core treating missing keys as null in queries.

* Rename insertion function.

* Include tests of Dictionary property type with Mixed.

* Test reassigning to new collection and self-assignment.

* Test mixed

* Update 'mixed[]'.

* Test results accessor.

* Update error messages.

* Make accessor helpers an object field rather than spread.

* Suggestions for "nested collections in mixed" (#6566)

* Fix type bundling issue

* Inline functions into "create*Accessor*" functions

* Refactored typeHelpers out of accessors

* Remove leftover 'Symbol_for' in node-wrapper template.

* Test not invalidating new collection.

* Remove test for max nesting level.

The max nesting level in debug in Core has been updated to be the same as for release.

* Remove reliance on issue-fix in certain tests.

* Add key path test for object listener on mixed field.

* Use '.values()' and '.entries()' in iteration.

* Update comments.

* Add CHANGELOG entry.

---------

Co-authored-by: Kræn Hansen <kraen.hansen@mongodb.com>
  • Loading branch information
elle-j and kraenhansen committed Apr 11, 2024
1 parent f8863dd commit 333c19d
Show file tree
Hide file tree
Showing 22 changed files with 3,883 additions and 1,027 deletions.
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
> This version communicates with Atlas Device Services through a different URL (https://services.cloud.mongodb.com). While we consider this an internal detail of the SDK, you might need to update rules in firewalls or other configuration that you've used to limit connections made by your app.
### Enhancements

* Updated bundled OpenSSL version to 3.2.0. ([realm/realm-core#7303](https://github.com/realm/realm-core/pull/7303))
* Improved performance of object notifiers with complex schemas by ~20%. ([realm/realm-core#7424](https://github.com/realm/realm-core/pull/7424))
* Improved performance with very large number of notifiers by ~75%. ([realm/realm-core#7424](https://github.com/realm/realm-core/pull/7424))
Expand All @@ -22,6 +23,53 @@
* Improved file compaction performance on platforms with page sizes greater than 4k (for example arm64 Apple platforms) for files less than 256 pages in size. ([realm/realm-core#7492](https://github.com/realm/realm-core/pull/7492))
* Added the ability to set the log level for one or more categories via `Realm.setLogLevel`. ([#6560](https://github.com/realm/realm-js/issues/6560))
* Added detection and better instructions when imported from the Expo Go app. ([#6523](https://github.com/realm/realm-js/pull/6523))
* A `mixed` value can now hold a `Realm.List` and `Realm.Dictionary` with nested collections. Note that `Realm.Set` is not supported as a `mixed` value. ([#6513](https://github.com/realm/realm-js/pull/6513))

```typescript
class CustomObject extends Realm.Object {
value!: Realm.Mixed;

static schema: ObjectSchema = {
name: "CustomObject",
properties: {
value: "mixed",
},
};
}

const realm = await Realm.open({ schema: [CustomObject] });

// Create an object with a dictionary value as the Mixed property,
// containing primitives and a list.
const realmObject = realm.write(() => {
return realm.create(CustomObject, {
value: {
num: 1,
string: "hello",
bool: true,
list: [
{
dict: {
string: "world",
},
},
],
},
});
});

// Accessing the collection value returns the managed collection.
// The default generic type argument is `unknown` (mixed).
const dictionary = realmObject.value as Realm.Dictionary;
const list = dictionary.list as Realm.List;
const leafDictionary = (list[0] as Realm.Dictionary).dict as Realm.Dictionary;
console.log(leafDictionary.string); // "world"

// Update the Mixed property to a list.
realm.write(() => {
realmObject.value = [1, "hello", { newKey: "new value" }];
});
```

### Fixed
* Aligned Dictionaries to Lists and Sets when they get cleared. ([#6205](https://github.com/realm/realm-core/issues/6205), since v10.3.0-rc.1)
Expand Down
37 changes: 7 additions & 30 deletions integration-tests/tests/src/tests/dictionary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { expect } from "chai";
import Realm, { PropertySchema } from "realm";

import { openRealmBefore, openRealmBeforeEach } from "../hooks";
import { sleep } from "../utils/sleep";

type Item<ValueType = Realm.Mixed> = {
dict: Realm.Dictionary<ValueType>;
Expand Down Expand Up @@ -60,17 +59,6 @@ const DictTypedSchema: Realm.ObjectSchema = {
},
};

const DictMixedSchema = {
name: "MixedDictionary",
properties: {
dict1: "mixed{}",
dict2: "mixed{}",
},
};

type IDictSchema = {
fields: Record<any, any>;
};
type ITwoDictSchema = {
dict1: Record<any, any>;
dict2: Record<any, any>;
Expand Down Expand Up @@ -307,17 +295,17 @@ describe("Dictionary", () => {
});
});

// This is currently not supported
it.skip("can store dictionary values using string keys", function (this: RealmContext) {
it("can store dictionary values using string keys", function (this: RealmContext) {
const item = this.realm.write(() => {
const item = this.realm.create<Item>("Item", {});
const item2 = this.realm.create<Item>("Item", {});
item2.dict.key1 = "Hello";
item.dict.key1 = item2.dict;
item2.dict.key1 = "hello";
item.dict.key1 = item2;
return item;
});
// @ts-expect-error We expect a dictionary inside dictionary
expect(item.dict.key1.dict.key1).equals("hello");
const innerObject = item.dict.key1 as Realm.Object<Item> & Item;
expect(innerObject).instanceOf(Realm.Object);
expect(innerObject.dict).deep.equals({ key1: "hello" });
});

it("can store a reference to itself using string keys", function (this: RealmContext) {
Expand Down Expand Up @@ -599,7 +587,7 @@ describe("Dictionary", () => {
});

describe("embedded models", () => {
openRealmBeforeEach({ schema: [DictTypedSchema, DictMixedSchema, EmbeddedChild] });
openRealmBeforeEach({ schema: [DictTypedSchema, EmbeddedChild] });
it("inserts correctly", function (this: RealmContext) {
this.realm.write(() => {
this.realm.create(DictTypedSchema.name, {
Expand All @@ -615,16 +603,5 @@ describe("Dictionary", () => {
expect(dict_2.children1?.num).equal(4, "We expect children1#4");
expect(dict_2.children2?.num).equal(5, "We expect children2#5");
});

it("throws on invalid input", function (this: RealmContext) {
this.realm.write(() => {
expect(() => {
this.realm.create(DictMixedSchema.name, {
dict1: { children1: { num: 2 }, children2: { num: 3 } },
dict2: { children1: { num: 4 }, children2: { num: 5 } },
});
}).throws("Unable to convert an object with ctor 'Object' to a Mixed");
});
});
});
});
50 changes: 32 additions & 18 deletions integration-tests/tests/src/tests/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,7 @@ describe("Lists", () => {
Error,
"Requested index 2 calling set() on list 'LinkTypesObject.arrayCol' when max is 1",
);
expect(() => (array[-1] = { doubleCol: 1 })).throws(Error, "Index -1 cannot be less than zero.");
expect(() => (array[-1] = { doubleCol: 1 })).throws(Error, "Cannot set item at negative index -1");

//@ts-expect-error TYPEBUG: our List type-definition expects index accesses to be done with a number , should probably be extended.
array["foo"] = "bar";
Expand Down Expand Up @@ -772,6 +772,7 @@ describe("Lists", () => {
openRealmBeforeEach({
schema: [LinkTypeSchema, TestObjectSchema, PersonListSchema, PersonSchema, PrimitiveArraysSchema],
});

it("are typesafe", function (this: RealmContext) {
let obj: ILinkTypeSchema;
let prim: IPrimitiveArraysSchema;
Expand All @@ -792,8 +793,10 @@ describe("Lists", () => {
//@ts-expect-error TYPEBUG: type missmatch, forcecasting shouldn't be done
obj.arrayCol = [this.realm.create<ITestObjectSchema>(TestObjectSchema.name, { doubleCol: 1.0 })];
expect(obj.arrayCol[0].doubleCol).equals(1.0);
obj.arrayCol = obj.arrayCol; // eslint-disable-line no-self-assign
expect(obj.arrayCol[0].doubleCol).equals(1.0);

// TODO: Solve the "removeAll()" case for self-assignment.
// obj.arrayCol = obj.arrayCol; // eslint-disable-line no-self-assign
// expect(obj.arrayCol[0].doubleCol).equals(1.0);

//@ts-expect-error Person is not assignable to boolean.
expect(() => (prim.bool = [person])).throws(
Expand Down Expand Up @@ -868,21 +871,6 @@ describe("Lists", () => {
testAssign("data", DATA1);
testAssign("date", DATE1);

function testAssignNull(name: string, expected: string) {
//@ts-expect-error TYPEBUG: our List type-definition expects index accesses to be done with a number , should probably be extended.
expect(() => (prim[name] = [null])).throws(Error, `Expected '${name}[0]' to be ${expected}, got null`);
//@ts-expect-error TYPEBUG: our List type-definition expects index accesses to be done with a number , should probably be extended.
expect(prim[name].length).equals(1);
}

testAssignNull("bool", "a boolean");
testAssignNull("int", "a number or bigint");
testAssignNull("float", "a number");
testAssignNull("double", "a number");
testAssignNull("string", "a string");
testAssignNull("data", "an instance of ArrayBuffer");
testAssignNull("date", "an instance of Date");

testAssign("optBool", true);
testAssign("optInt", 1);
testAssign("optFloat", 1.1);
Expand All @@ -905,7 +893,33 @@ describe("Lists", () => {
//@ts-expect-error throws on modification outside of transaction.
expect(() => (prim.bool = [])).throws("Cannot modify managed objects outside of a write transaction.");
});

it("throws when assigning null to non-nullable", function (this: RealmContext) {
const realm = this.realm;
const prim = realm.write(() => realm.create<IPrimitiveArraysSchema>(PrimitiveArraysSchema.name, {}));

function testAssignNull(name: string, expected: string) {
expect(() => {
realm.write(() => {
// @ts-expect-error TYPEBUG: our List type-definition expects index accesses to be done with a number , should probably be extended.
prim[name] = [null];
});
}).throws(Error, `Expected '${name}[0]' to be ${expected}, got null`);

// @ts-expect-error TYPEBUG: our List type-definition expects index accesses to be done with a number , should probably be extended.
expect(prim[name].length).equals(0);
}

testAssignNull("bool", "a boolean");
testAssignNull("int", "a number or bigint");
testAssignNull("float", "a number");
testAssignNull("double", "a number");
testAssignNull("string", "a string");
testAssignNull("data", "an instance of ArrayBuffer");
testAssignNull("date", "an instance of Date");
});
});

describe("operations", () => {
openRealmBeforeEach({ schema: [LinkTypeSchema, TestObjectSchema, PersonSchema, PersonListSchema] });
it("supports enumeration", function (this: RealmContext) {
Expand Down
Loading

0 comments on commit 333c19d

Please sign in to comment.