From 37d04d38f23411d3004263f2693b00f3b2c09db6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Wed, 16 Mar 2022 12:03:46 +0100 Subject: [PATCH] Class models: Enforce `extends Realm.Object` (#4417) * Throw if constructors doesn't extend Realm.Object * Updating existing tests to the breaking change. * Adding a note to the changelog --- CHANGELOG.md | 3 + .../tests/src/tests/class-models.ts | 75 +++++++++++++++++++ integration-tests/tests/src/tests/index.ts | 3 +- .../tests/src/tests/serialization.ts | 4 +- src/js_realm.hpp | 22 +++++- tests/js/realm-tests.js | 33 ++------ 6 files changed, 108 insertions(+), 32 deletions(-) create mode 100644 integration-tests/tests/src/tests/class-models.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index caa3226af53..73c7fff2b73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ x.x.x Release notes (yyyy-MM-dd) ============================================================= +### Breaking change +* Model classes passed as schema to the `Realm` constructor must now extend `Realm.Object`. + ### Enhancements * None. diff --git a/integration-tests/tests/src/tests/class-models.ts b/integration-tests/tests/src/tests/class-models.ts new file mode 100644 index 00000000000..8bf6797fc6b --- /dev/null +++ b/integration-tests/tests/src/tests/class-models.ts @@ -0,0 +1,75 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2022 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 Realm from "realm"; + +describe("Class models", () => { + describe("as schema element", () => { + beforeEach(() => { + Realm.clearTestState(); + }); + + it("fails without a schema static", () => { + class Person extends Realm.Object {} + expect(() => { + new Realm({ schema: [Person as any] }); + }).throws("must have a 'schema' property"); + }); + + it("fails without a schema.properties static", () => { + class Person extends Realm.Object { + static schema = { name: "Person" }; + } + expect(() => { + new Realm({ schema: [Person as any] }); + }).throws("properties must be of type 'object'"); + }); + + it("fails if it doesn't extend Realm.Object", () => { + class Person { + name!: string; + static schema: Realm.ObjectSchema = { + name: "Person", + properties: { name: "string" }, + }; + } + expect(() => { + new Realm({ schema: [Person as any] }); + }).throws("Class 'Person' must extend Realm.Object"); + + // Mutate the name of the object schema to produce a more detailed error + Person.schema.name = "Foo"; + expect(() => { + new Realm({ schema: [Person as any] }); + }).throws("Class 'Person' (declaring 'Foo' schema) must extend Realm.Object"); + }); + + it("is allowed", () => { + class Person extends Realm.Object { + name!: string; + static schema: Realm.ObjectSchema = { + name: "Person", + properties: { name: "string" }, + }; + } + new Realm({ schema: [Person] }); + }); + }); +}); diff --git a/integration-tests/tests/src/tests/index.ts b/integration-tests/tests/src/tests/index.ts index e8dcdcddfcb..7b2e8fc2de5 100644 --- a/integration-tests/tests/src/tests/index.ts +++ b/integration-tests/tests/src/tests/index.ts @@ -22,8 +22,9 @@ import chai from "chai"; chai.use(chaiAsPromised); import "./realm-constructor"; -import "./serialization"; import "./objects"; +import "./class-models"; +import "./serialization"; import "./iterators"; import "./dynamic-schema-updates"; import "./bson"; diff --git a/integration-tests/tests/src/tests/serialization.ts b/integration-tests/tests/src/tests/serialization.ts index 2530d8b4889..f5bb171548b 100755 --- a/integration-tests/tests/src/tests/serialization.ts +++ b/integration-tests/tests/src/tests/serialization.ts @@ -143,7 +143,7 @@ const testSetups: TestSetup[] = [ }, }, { - name: "Class models (NO primaryKey)", + name: "Class model (NO primaryKey)", schema: [PlaylistNoId, SongNoId], testData(realm: Realm) { realm.write(() => { @@ -287,7 +287,7 @@ const testSetups: TestSetup[] = [ }, }, { - name: "Class models (Int primaryKey)", + name: "Class model (Int primaryKey)", schema: [PlaylistWithId, SongWithId], testData(realm: Realm) { realm.write(() => { diff --git a/src/js_realm.hpp b/src/js_realm.hpp index 79fa832005b..6f5bc582382 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -31,6 +31,7 @@ #include "platform.hpp" #include "realm/binary_data.hpp" #include +#include #if REALM_ENABLE_SYNC #include "js_sync.hpp" @@ -672,7 +673,26 @@ bool RealmClass::get_realm_config(ContextType ctx, size_t argc, const ValueTy ValueType schema_value = Object::get_property(ctx, object, schema_string); if (!Value::is_undefined(ctx, schema_value)) { ObjectType schema_array = Value::validated_to_array(ctx, schema_value, "schema"); - config.schema.emplace(Schema::parse_schema(ctx, schema_array, defaults, constructors)); + auto schema = Schema::parse_schema(ctx, schema_array, defaults, constructors); + // Check that all constructors provided by the user extend Realm.Object + const auto& realm_constructor = Value::validated_to_object(ctx, Object::get_global(ctx, "Realm")); + const auto& realm_object_constructor = Object::validated_get_object(ctx, realm_constructor, "Object"); + for (const auto& [name, constructor] : constructors) { + const auto& prototype = Object::get_prototype(ctx, constructor); + if (prototype != realm_object_constructor) { + const std::string& class_name = + Object::validated_get_string(ctx, constructor, "name", "Failed to read class name"); + if (class_name == name) { + throw std::invalid_argument( + util::format("Class '%1' must extend Realm.Object", class_name)); + } + else { + throw std::invalid_argument(util::format( + "Class '%1' (declaring '%2' schema) must extend Realm.Object", class_name, name)); + } + } + } + config.schema.emplace(std::move(schema)); schema_updated = true; } diff --git a/tests/js/realm-tests.js b/tests/js/realm-tests.js index 0a10fcf1a5e..36642f3d3d0 100644 --- a/tests/js/realm-tests.js +++ b/tests/js/realm-tests.js @@ -125,25 +125,7 @@ module.exports = { Object.setPrototypeOf(Car2.prototype, Realm.Object.prototype); Object.setPrototypeOf(Car2, Realm.Object); - //test class syntax support without extending Realm.Object - let car3ConstructorCalled = false; - class Car3 { - constructor() { - car3ConstructorCalled = true; - } - } - - Car3.schema = { - name: "Car3", - properties: { - make: "string", - model: "string", - otherType: { type: "string", mapTo: "type", optional: true }, - kilometers: { type: "int", default: 0 }, - }, - }; - - let realm = new Realm({ schema: [Car, Car2, Car3] }); + let realm = new Realm({ schema: [Car, Car2] }); realm.write(() => { let car = realm.create("Car", { make: "Audi", model: "A4", kilometers: 24 }); TestCase.assertTrue(constructorCalled); @@ -181,15 +163,6 @@ module.exports = { TestCase.assertEqual(car2_1.model, "Touareg"); TestCase.assertEqual(car2_1.kilometers, 13); TestCase.assertInstanceOf(car2_1, Realm.Object, "car2_1 not an instance of Realm.Object"); - - let car3 = realm.create("Car3", { make: "Audi", model: "A4", kilometers: 24 }); - TestCase.assertTrue(car3ConstructorCalled); - TestCase.assertEqual(car3.make, "Audi"); - TestCase.assertEqual(car3.model, "A4"); - TestCase.assertEqual(car3.kilometers, 24); - - //methods from Realm.Objects should be present - TestCase.assertDefined(car3.addListener); }); realm.close(); }, @@ -1176,6 +1149,7 @@ module.exports = { intCol: "int", }, }; + Object.setPrototypeOf(CustomObject, Realm.Object); function InvalidObject() { return {}; @@ -1191,6 +1165,7 @@ module.exports = { intCol: "int", }, }; + Object.setPrototypeOf(InvalidObject, Realm.Object); let realm = new Realm({ schema: [CustomObject, InvalidObject] }); realm.write(() => { @@ -1238,6 +1213,7 @@ module.exports = { intCol: "int", }, }; + Object.setPrototypeOf(CustomObject, Realm.Object); let realm = new Realm({ schema: [CustomObject] }); realm.write(() => { @@ -1247,6 +1223,7 @@ module.exports = { function NewCustomObject() {} NewCustomObject.schema = CustomObject.schema; + Object.setPrototypeOf(NewCustomObject, Realm.Object); realm = new Realm({ schema: [NewCustomObject] }); realm.write(() => {