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 nested updates of objects through realm.create via UpdateMode.All #5944

Merged
merged 10 commits into from
Jul 14, 2023
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

### Fixed
* Fixed updating helpers (the `ClassMap`) used by `Realm` before notifying schema change listeners when the schema is changed during runtime. ([#5574](https://github.com/realm/realm-js/issues/5574))
* Fix crashes on refresh of the React Native application. ([#5904](https://github.com/realm/realm-js/issues/5904))
* Fixed crashes on refresh of the React Native application. ([#5904](https://github.com/realm/realm-js/issues/5904))
* Fixed applying `UpdateMode` recursively to all objects when passed to `Realm.create()`. ([#5933](https://github.com/realm/realm-js/issues/5933))

### Compatibility
* React Native >= v0.71.4
Expand Down
162 changes: 161 additions & 1 deletion integration-tests/tests/src/tests/objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,18 @@ const TestObjectSchema = {
},
};

const CollectionSchema: Realm.ObjectSchema = {
name: "CollectionObject",
primaryKey: "_id",
properties: {
_id: "objectId",
name: "string",
list: "CollectionObject[]",
dictionary: "CollectionObject{}",
set: "CollectionObject<>",
},
};

const AllTypesSchema = {
name: "AllTypesObject",
properties: {
Expand Down Expand Up @@ -285,6 +297,14 @@ interface INonPersistentTestObject extends ITestObject {
ignored: boolean;
}

interface ICollectionObject {
_id: Realm.BSON.ObjectId;
name: string;
list: Realm.List<ICollectionObject>;
dictionary: Realm.Dictionary<ICollectionObject>;
set: Realm.Set<ICollectionObject>;
}

interface IAllTypes {
boolCol: boolean;
intCol: number;
Expand Down Expand Up @@ -517,7 +537,7 @@ describe("Realm.Object", () => {
});

describe("with primary key", () => {
openRealmBeforeEach({ schema: [PersonWithId] });
openRealmBeforeEach({ schema: [PersonWithId, CollectionSchema] });
it("can be fetched with objectForPrimaryKey", function (this: Mocha.Context & RealmContext) {
const _id = new Realm.BSON.ObjectId();

Expand Down Expand Up @@ -586,6 +606,146 @@ describe("Realm.Object", () => {
const persons = this.realm.objects(PersonWithId);
expect(persons.length).equals(1);
});

describe("applying 'UpdateMode' recursively", () => {
let aliceId: Realm.BSON.ObjectId;
let bobId: Realm.BSON.ObjectId;
let maxId: Realm.BSON.ObjectId;

beforeEach(function (this: RealmContext & Mocha.Context) {
aliceId = new Realm.BSON.ObjectId();
bobId = new Realm.BSON.ObjectId();
maxId = new Realm.BSON.ObjectId();

// Create two mutual friends (Alice and Bob) and one
// that will be added as a friend later (Max).
this.realm.write(() => {
const alice = this.realm.create<ICollectionObject>(CollectionSchema.name, {
_id: aliceId,
name: "Alice",
});
const bob = this.realm.create<ICollectionObject>(CollectionSchema.name, {
_id: bobId,
name: "Bob",
});
const max = this.realm.create<ICollectionObject>(CollectionSchema.name, {
_id: maxId,
name: "Max",
});
// Make them mutual friends.
alice.list.push(bob);
bob.list.push(alice);
});
});

it("can be updated recursively in lists", function (this: Mocha.Context & RealmContext) {
const alice = this.realm.write(() => {
return this.realm.create(
CollectionSchema.name,
{
_id: aliceId,
name: "UpdatedAlice",
list: [
{
_id: bobId,
name: "UpdatedBob",
list: [
{
_id: maxId,
name: "UpdatedMax",
},
],
},
],
},
Realm.UpdateMode.All,
);
});

expect(alice._id.equals(aliceId)).to.be.true;
expect(alice.name).to.equal("UpdatedAlice");

const bob = alice.list[0];
expect(bob._id.equals(bobId)).to.be.true;
expect(bob.name).to.equal("UpdatedBob");

const max = bob.list[0];
expect(max._id.equals(maxId)).to.be.true;
expect(max.name).to.equal("UpdatedMax");
});

it("can be updated recursively in dictionaries", function (this: Mocha.Context & RealmContext) {
const alice = this.realm.write(() => {
return this.realm.create(
CollectionSchema.name,
{
_id: aliceId,
name: "UpdatedAlice",
dictionary: {
bob: {
_id: bobId,
name: "UpdatedBob",
dictionary: {
max: {
_id: maxId,
name: "UpdatedMax",
},
},
},
},
},
Realm.UpdateMode.All,
);
});

expect(alice._id.equals(aliceId)).to.be.true;
expect(alice.name).to.equal("UpdatedAlice");

const { bob } = alice.dictionary;
expect(bob._id.equals(bobId)).to.be.true;
expect(bob.name).to.equal("UpdatedBob");

const { max } = bob.dictionary;
expect(max._id.equals(maxId)).to.be.true;
expect(max.name).to.equal("UpdatedMax");
});

it("can be updated recursively in sets", function (this: Mocha.Context & RealmContext) {
takameyer marked this conversation as resolved.
Show resolved Hide resolved
const alice = this.realm.write(() => {
return this.realm.create(
CollectionSchema.name,
{
_id: aliceId,
name: "UpdatedAlice",
set: [
{
_id: bobId,
name: "UpdatedBob",
set: [
{
_id: maxId,
name: "UpdatedMax",
},
],
},
],
},
Realm.UpdateMode.All,
);
});

expect(alice._id.equals(aliceId)).to.be.true;
expect(alice.name).to.equal("UpdatedAlice");

const bob = alice.set[0];
expect(bob._id.equals(bobId)).to.be.true;
expect(bob.name).to.equal("UpdatedBob");

const max = bob.set[0];
expect(max._id.equals(maxId)).to.be.true;
expect(max.name).to.equal("UpdatedMax");
});
});
});
});

Expand Down
4 changes: 2 additions & 2 deletions packages/realm/src/Dictionary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const PROXY_HANDLER: ProxyHandler<Dictionary> = {
if (typeof prop === "string") {
const internal = target[INTERNAL];
const toBinding = target[HELPERS].toBinding;
internal.insertAny(prop, toBinding(value, undefined));
internal.insertAny(prop, toBinding(value));
return true;
} else {
assert(typeof prop !== "symbol", "Symbols cannot be used as keys of a dictionary");
Expand Down Expand Up @@ -280,7 +280,7 @@ export class Dictionary<T = unknown> extends Collection<string, T, [string, T],
const toBinding = this[HELPERS].toBinding;

for (const [key, val] of Object.entries(elements)) {
internal.insertAny(key, toBinding(val, undefined));
internal.insertAny(key, toBinding(val));
}
return this;
}
Expand Down
11 changes: 7 additions & 4 deletions packages/realm/src/List.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,10 @@
} = this;
assert.inTransaction(realm);
// TODO: Consider a more performant way to determine if the list is embedded
internal.setAny(index, toBinding(value, isEmbedded ? () => [internal.insertEmbedded(index), true] : undefined));
internal.setAny(
index,
toBinding(value, isEmbedded ? { createObj: () => [internal.insertEmbedded(index), true] } : undefined),
);
}

get length(): number {
Expand Down Expand Up @@ -142,7 +145,7 @@
const index = start + offset;
if (isEmbedded) {
// Simply transforming to binding will insert the embedded object
toBinding(item, () => [internal.insertEmbedded(index), true]);
toBinding(item, { createObj: () => [internal.insertEmbedded(index), true] });
} else {
internal.insertAny(index, toBinding(item));
}
Expand Down Expand Up @@ -175,7 +178,7 @@
* the list, or if an object being added to the list does not match the {@link ObjectSchema} for the list.
* @throws an {@link AssertionError} If not inside a write transaction.
* @returns The new {@link length} of the list after adding the values.
*/

Check warning on line 181 in packages/realm/src/List.ts

View workflow job for this annotation

GitHub Actions / Lint

The type 'length' is undefined
unshift(...items: T[]): number {
assert.inTransaction(this.realm);
const {
Expand All @@ -186,7 +189,7 @@
for (const [index, item] of items.entries()) {
if (isEmbedded) {
// Simply transforming to binding will insert the embedded object
toBinding(item, () => [internal.insertEmbedded(index), true]);
toBinding(item, { createObj: () => [internal.insertEmbedded(index), true] });
} else {
internal.insertAny(index, toBinding(item));
}
Expand Down Expand Up @@ -270,7 +273,7 @@
const index = start + offset;
if (isEmbedded) {
// Simply transforming to binding will insert the embedded object
toBinding(item, () => [internal.insertEmbedded(index), true]);
toBinding(item, { createObj: () => [internal.insertEmbedded(index), true] });
} else {
internal.insertAny(index, toBinding(item));
}
Expand Down
6 changes: 4 additions & 2 deletions packages/realm/src/Object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ export class RealmObject<T = DefaultObject, RequiredProperties extends keyof Omi
const propertyValue = values[propertyName];
if (typeof propertyValue !== "undefined") {
if (mode !== UpdateMode.Modified || result[propertyName] !== propertyValue) {
// This will call into the property setter in PropertyHelpers.ts.
// (E.g. the setter for [binding.PropertyType.Array] in the case of lists.)
result[propertyName] = propertyValue;
}
} else {
Expand All @@ -207,7 +209,7 @@ export class RealmObject<T = DefaultObject, RequiredProperties extends keyof Omi
* Create an object in the database and populate its primary key value, if required
* @internal
*/
public static createObj(
private static createObj(
realm: Realm,
values: DefaultObject,
mode: UpdateMode,
Expand Down Expand Up @@ -237,8 +239,8 @@ export class RealmObject<T = DefaultObject, RequiredProperties extends keyof Omi
typeof primaryKeyValue !== "undefined" && primaryKeyValue !== null
? primaryKeyValue
: primaryKeyHelpers.default,
undefined,
);

const result = binding.Helpers.getOrCreateObjectWithPrimaryKey(table, pk);
const [, created] = result;
if (mode === UpdateMode.Never && !created) {
Expand Down
2 changes: 1 addition & 1 deletion packages/realm/src/OrderedCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ export abstract class OrderedCollection<T = unknown, EntryType extends [unknown,
assert.instanceOf(searchElement, RealmObject);
return this.results.indexOfObj(searchElement[INTERNAL]);
} else {
return this.results.indexOf(this.helpers.toBinding(searchElement, undefined));
return this.results.indexOf(this.helpers.toBinding(searchElement));
}
}
/**
Expand Down
10 changes: 5 additions & 5 deletions packages/realm/src/PropertyHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ function embeddedSet({ typeHelpers: { toBinding }, columnKey }: PropertyOptions)
return (obj: binding.Obj, value: unknown) => {
// Asking for the toBinding will create the object and link it to the parent in one operation
// no need to actually set the value on the `obj`
toBinding(value, () => [obj.createAndSetLinkedObject(columnKey), true]);
toBinding(value, { createObj: () => [obj.createAndSetLinkedObject(columnKey), true] });
};
}

Expand Down Expand Up @@ -212,9 +212,9 @@ const ACCESSOR_FACTORIES: Partial<Record<binding.PropertyType, AccessorFactory>>
for (const value of values) {
try {
if (embedded) {
itemToBinding(value, () => [internal.insertEmbedded(index), true]);
itemToBinding(value, { createObj: () => [internal.insertEmbedded(index), true] });
} else {
bindingValues.push(itemToBinding(value, undefined));
bindingValues.push(itemToBinding(value));
}
} catch (err) {
if (err instanceof TypeAssertionError) {
Expand Down Expand Up @@ -260,9 +260,9 @@ const ACCESSOR_FACTORIES: Partial<Record<binding.PropertyType, AccessorFactory>>
for (const [k, v] of Object.entries(value)) {
try {
if (embedded) {
itemHelpers.toBinding(v, () => [internal.insertEmbedded(k), true]);
itemHelpers.toBinding(v, { createObj: () => [internal.insertEmbedded(k), true] });
} else {
internal.insertAny(k, itemHelpers.toBinding(v, undefined));
internal.insertAny(k, itemHelpers.toBinding(v));
}
} catch (err) {
if (err instanceof TypeAssertionError) {
Expand Down
17 changes: 14 additions & 3 deletions packages/realm/src/Realm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@

/**
* Asserts the event passed as string is a valid RealmEvent value.
* @throws {@link TypeAssertionError} If an unexpected name is passed via {@link name}.

Check warning on line 214 in packages/realm/src/Realm.ts

View workflow job for this annotation

GitHub Actions / Lint

Syntax error in type: @link TypeAssertionError
* @param name The name of the event.
* @internal
*/
Expand Down Expand Up @@ -605,6 +605,8 @@
private changeListeners = new RealmListeners(this, RealmEvent.Change);
private beforeNotifyListeners = new RealmListeners(this, RealmEvent.BeforeNotify);
private schemaListeners = new RealmListeners(this, RealmEvent.Schema);
/** @internal */
public currentUpdateMode: UpdateMode | undefined;

/**
* Create a new {@link Realm} instance, at the default path.
Expand Down Expand Up @@ -860,11 +862,20 @@
throw new Error("Cannot create an object from a detached RealmObject instance");
}
if (!Object.values(UpdateMode).includes(mode)) {
throw new Error("Unsupported 'updateMode'. Only 'never', 'modified' or 'all' is supported.");
throw new Error(
`Unsupported 'updateMode'. Only '${UpdateMode.Never}', '${UpdateMode.Modified}' or '${UpdateMode.All}' is supported.`,
);
}
this.internal.verifyOpen();
const helpers = this.classes.getHelpers(type);
const realmObject = RealmObject.create(this, values, mode, { helpers });

this.currentUpdateMode = mode;
let realmObject: RealmObject;
try {
realmObject = RealmObject.create(this, values, mode, { helpers });
} finally {
this.currentUpdateMode = undefined;
}

return isAsymmetric(helpers.objectSchema) ? undefined : realmObject;
}
Expand Down Expand Up @@ -904,7 +915,7 @@

/**
* Deletes a Realm model, including all of its objects.
* If called outside a migration function, {@link schema} and {@link schemaVersion} are updated.

Check warning on line 918 in packages/realm/src/Realm.ts

View workflow job for this annotation

GitHub Actions / Lint

The type 'schemaVersion' is undefined
* @param name The model name
*/
deleteModel(name: string): void {
Expand Down Expand Up @@ -954,7 +965,7 @@
throw new Error("You cannot query an asymmetric object.");
}
const table = binding.Helpers.getTable(this.internal, objectSchema.tableKey);
const value = properties.get(objectSchema.primaryKey).toBinding(primaryKey, undefined);
const value = properties.get(objectSchema.primaryKey).toBinding(primaryKey);
try {
const objKey = table.findPrimaryKey(value);
if (binding.isEmptyObjKey(objKey)) {
Expand Down Expand Up @@ -1064,7 +1075,7 @@
* Remove the listener {@link callback} for the specified event {@link eventName}.
* @param eventName The event name.
* @param callback Function that was previously added as a listener for this event through the {@link addListener} method.
* @throws an {@link Error} If an invalid event {@link eventName} is supplied, if Realm is closed or if {@link callback} is not a function.

Check warning on line 1078 in packages/realm/src/Realm.ts

View workflow job for this annotation

GitHub Actions / Lint

The type 'addListener' is undefined
*/
removeListener(eventName: RealmEventName, callback: RealmListenerCallback): void {
assert.open(this);
Expand Down Expand Up @@ -1108,7 +1119,7 @@
}

/**
* Synchronously call the provided {@link callback} inside a write transaction. If an exception happens inside a transaction,

Check warning on line 1122 in packages/realm/src/Realm.ts

View workflow job for this annotation

GitHub Actions / Lint

The type 'beginTransaction' is undefined

Check warning on line 1122 in packages/realm/src/Realm.ts

View workflow job for this annotation

GitHub Actions / Lint

The type 'commitTransaction' is undefined

Check warning on line 1122 in packages/realm/src/Realm.ts

View workflow job for this annotation

GitHub Actions / Lint

The type 'cancelTransaction' is undefined

Check warning on line 1122 in packages/realm/src/Realm.ts

View workflow job for this annotation

GitHub Actions / Lint

The type 'commitTransaction' is undefined

Check warning on line 1122 in packages/realm/src/Realm.ts

View workflow job for this annotation

GitHub Actions / Lint

The type 'write' is undefined
* you’ll lose the changes in that transaction, but the Realm itself won’t be affected (or corrupted).
* More precisely, {@link beginTransaction} and {@link commitTransaction} will be called
* automatically. If any exception is thrown during the transaction {@link cancelTransaction} will
Expand Down
4 changes: 2 additions & 2 deletions packages/realm/src/Set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export class RealmSet<T = unknown> extends OrderedCollection<T, [T, T]> {
*/
delete(value: T): boolean {
assert.inTransaction(this.realm);
const [, success] = this.internal.removeAny(this.helpers.toBinding(value, undefined));
const [, success] = this.internal.removeAny(this.helpers.toBinding(value));
return success;
}

Expand All @@ -92,7 +92,7 @@ export class RealmSet<T = unknown> extends OrderedCollection<T, [T, T]> {
*/
add(value: T): this {
assert.inTransaction(this.realm);
this.internal.insertAny(this.helpers.toBinding(value, undefined));
this.internal.insertAny(this.helpers.toBinding(value));
return this;
}

Expand Down
Loading
Loading