realm / realm-js

Realm is a mobile database: an alternative to SQLite & key-value stores
https://realm.io
Apache License 2.0
5.6k stars 558 forks source link

Combined: Support `Mixed` data type with collections #6613

Open elle-j opened 2 weeks ago

elle-j commented 2 weeks ago

What, How & Why?

[!NOTE] This feature has already been reviewed. New updates to review: CHANGELOG.md. The branch can be merged whenever we want to make a release with this new feature.

Adds support for storing lists and dictionaries (with nested collections) as the underlying value of a Mixed property.

Sets are not supported as a Mixed value.

Test overview

Click to expand ``` Mixed Collection types CRUD operations Create and access List ✓ has all primitive types (input: JS Array) ✓ has all primitive types (input: Realm List) ✓ has all primitive types (input: Default value) ✓ has nested lists of all primitive types ✓ has nested dictionaries of all primitive types ✓ has mix of nested collections of all types ✓ inserts all primitive types via `push()` ✓ inserts nested lists of all primitive types via `push()` ✓ inserts nested dictionaries of all primitive types via `push()` ✓ inserts mix of nested collections of all types via `push()` ✓ returns different reference for each access Dictionary ✓ has all primitive types (input: JS Object) ✓ has all primitive types (input: JS Object w/o proto) ✓ has all primitive types (input: Realm Dictionary) ✓ has all primitive types (input: Default value) ✓ can use the spread of embedded Realm object ✓ can use the spread of custom non-Realm object ✓ has nested lists of all primitive types ✓ has nested dictionaries of all primitive types ✓ has mix of nested collections of all types ✓ inserts all primitive types via setter ✓ inserts nested lists of all primitive types via setter ✓ inserts nested dictionaries of all primitive types via setter ✓ inserts mix of nested collections of all types via setter ✓ inserts mix of nested collections of all types via `set()` overloads ✓ returns different reference for each access Results from List snapshot() ✓ has all primitive types ✓ has mix of nested collections of all types objects().filtered() ✓ has all primitive types ✓ has mix of nested collections of all types from Dictionary objects().filtered() ✓ has all primitive types ✓ has mix of nested collections of all types Update List ✓ updates top-level item via setter ✓ updates nested item via setter ✓ updates itself to a new list ✓ updates nested list to a new list ✓ does not become invalidated when updated to a new list - self assigns - self assigns nested list Dictionary ✓ updates top-level entry via setter ✓ updates nested entry via setter ✓ updates itself to a new dictionary ✓ updates nested dictionary to a new dictionary ✓ does not become invalidated when updated to a new dictionary - self assigns - self assigns nested dictionary Remove List ✓ removes top-level item via `remove()` ✓ removes nested item via `remove()` Dictionary ✓ removes top-level entry via `remove()` ✓ removes nested entry via `remove()` JS collection methods List ✓ pop() ✓ shift() ✓ unshift() ✓ splice() ✓ indexOf() Iterators ✓ values() - list ✓ values() - dictionary ✓ entries() - list ✓ entries() - dictionary Filtering ✓ filters by query path on list of all primitive types ✓ filters by query path on nested list of all primitive types ✓ filters by query path on dictionary of all primitive types ✓ filters by query path on nested dictionary of all primitive types Invalid operations ✓ throws when creating a Mixed with a set ✓ throws when creating a set with a list ✓ throws when creating a set with a dictionary ✓ throws when updating a list item to a set ✓ throws when updating a dictionary entry to a set ✓ throws when creating a list or dictionary with an embedded object ✓ throws when setting a list or dictionary item to an embedded object ✓ throws when setting a list or dictionary outside a transaction ✓ throws when setting a list item out of bounds ✓ throws when setting a nested list item out of bounds ✓ throws when assigning to list snapshot (Results) ✓ invalidates the list when removed ✓ invalidates the dictionary when removed Observable Collections in Mixed Collection notifications List ✓ fires when inserting, updating, and deleting at top-level ✓ fires when inserting, updating, and deleting in nested list ✓ fires when inserting, updating, and deleting in nested dictionary ✓ does not fire when updating object at top-level Dictionary ✓ fires when inserting, updating, and deleting at top-level ✓ fires when inserting, updating, and deleting in nested list ✓ fires when inserting, updating, and deleting in nested dictionary ✓ does not fire when updating object at top-level Object notifications ✓ fires when inserting, updating, and deleting in top-level list ✓ fires when inserting, updating, and deleting in nested list ✓ fires when inserting, updating, and deleting in top-level dictionary ✓ fires when inserting, updating, and deleting in nested dictionary ✓ fires when inserting, updating, and deleting in nested dictionary (using key-path) ```

Brief overview of usage

class CustomObject extends Realm.Object {
  value!: Realm.Types.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: [
        {
          string: "world",
        },
      ],
    },
  });
});

// Accessing the collection value returns the managed collection.
const dictionary = realmObject.value;
expectDictionary(dictionary);
const list = dictionary.list;
expectList(list);
const leafDictionary = list[0];
expectDictionary(leafDictionary);
console.log(leafDictionary.string); // "world"

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

// Useful custom helper functions. (Will be provided in a future release.)
function expectList(value: unknown): asserts value is Realm.List {
  if (!(value instanceof Realm.List)) {
    throw new Error("Expected a 'Realm.List'.");
  }
}
function expectDictionary(value: unknown): asserts value is Realm.Dictionary {
  if (!(value instanceof Realm.Dictionary)) {
    throw new Error("Expected a 'Realm.Dictionary'.");
  }
}

☑️ ToDos