diff --git a/lib/objects.js b/lib/objects.js index 135005b6..e22a0a45 100644 --- a/lib/objects.js +++ b/lib/objects.js @@ -32,6 +32,13 @@ function create(realmId, info) { }); }); + if (constructor) { + let result = constructor.call(object); + if (result != null && result != object) { + throw new Error('Realm object constructor must not return another value'); + } + } + return object; } diff --git a/lib/realm.js b/lib/realm.js index 608e0b08..ae4f82a1 100644 --- a/lib/realm.js +++ b/lib/realm.js @@ -20,16 +20,27 @@ rpc.registerTypeConverter(objectTypes.RESULTS, results.create); class Realm { constructor(config) { - let schema = typeof config == 'object' && config.schema; + let schemas = typeof config == 'object' && config.schema; let constructors = {}; - for (let i = 0, len = schema ? schema.length : 0; i < len; i++) { - let item = schema[i]; - let proto = item.prototype; + for (let i = 0, len = schemas ? schemas.length : 0; i < len; i++) { + let item = schemas[i]; - if (proto && proto.schema) { - schema.splice(i, 1, proto.schema); - constructors[proto.schema.name] = item; + if (typeof item == 'function') { + let schema = item.schema; + if (!schema || typeof schema != 'object') { + throw new Error("Realm object constructor must have 'schema' property"); + } + + let {name, properties} = schema; + if (!name || typeof name != 'string') { + throw new Error("Realm object schema must have 'name' property"); + } else if (!properties || typeof properties != 'object') { + throw new Error("Realm object schema must have 'properties' property"); + } + + schemas.splice(i, 1, schema); + constructors[name] = item; } } diff --git a/src/js_object.cpp b/src/js_object.cpp index 19e1ce7b..2dc8f0f8 100644 --- a/src/js_object.cpp +++ b/src/js_object.cpp @@ -58,8 +58,24 @@ JSClassRef RJSObjectClass() { } JSObjectRef RJSObjectCreate(JSContextRef ctx, Object object) { - JSValueRef prototype = RJSPrototypes(object.realm().get())[object.get_object_schema().name]; + static JSStringRef prototypeString = JSStringCreateWithUTF8CString("prototype"); + + JSObjectRef constructor = RJSConstructors(object.realm().get())[object.get_object_schema().name]; + JSObjectRef prototype = constructor ? RJSValidatedObjectProperty(ctx, constructor, prototypeString) : NULL; JSObjectRef jsObject = RJSWrapObject(ctx, RJSObjectClass(), new Object(object), prototype); + + if (constructor) { + JSValueRef exception = NULL; + JSValueRef result = JSObjectCallAsFunction(ctx, constructor, jsObject, 0, NULL, &exception); + + if (exception) { + throw RJSException(ctx, exception); + } + else if (result != jsObject && !JSValueIsNull(ctx, result) && !JSValueIsUndefined(ctx, result)) { + throw std::runtime_error("Realm object constructor must not return another value"); + } + } + return jsObject; } diff --git a/src/js_realm.cpp b/src/js_realm.cpp index c2196ce5..8fc6ce5c 100644 --- a/src/js_realm.cpp +++ b/src/js_realm.cpp @@ -39,8 +39,8 @@ public: ~RJSRealmDelegate() { remove_all_notifications(); - for (auto prototype : m_prototypes) { - JSValueUnprotect(m_context, prototype.second); + for (auto constructor : m_constructors) { + JSValueUnprotect(m_context, constructor.second); } for (auto objectDefaults : m_defaults) { for (auto value : objectDefaults.second) { @@ -70,7 +70,7 @@ public: } std::map m_defaults; - std::map m_prototypes; + std::map m_constructors; private: std::set m_notifications; @@ -101,8 +101,8 @@ std::map &RJSDefaults(Realm *realm) { return static_cast(realm->m_binding_context.get())->m_defaults; } -std::map &RJSPrototypes(Realm *realm) { - return static_cast(realm->m_binding_context.get())->m_prototypes; +std::map &RJSConstructors(Realm *realm) { + return static_cast(realm->m_binding_context.get())->m_constructors; } // static std::string s_defaultPath = realm::default_realm_file_directory() + "/default.realm"; @@ -137,7 +137,7 @@ JSObjectRef RealmConstructor(JSContextRef ctx, JSObjectRef constructor, size_t a try { Realm::Config config; std::map defaults; - std::map prototypes; + std::map constructors; switch (argumentCount) { case 0: config.path = RJSDefaultPath(); @@ -163,7 +163,7 @@ JSObjectRef RealmConstructor(JSContextRef ctx, JSObjectRef constructor, size_t a static JSStringRef schemaString = JSStringCreateWithUTF8CString("schema"); JSValueRef schemaValue = RJSValidatedPropertyValue(ctx, object, schemaString); if (!JSValueIsUndefined(ctx, schemaValue)) { - config.schema.reset(new Schema(RJSParseSchema(ctx, RJSValidatedValueToObject(ctx, schemaValue), defaults, prototypes))); + config.schema.reset(new Schema(RJSParseSchema(ctx, RJSValidatedValueToObject(ctx, schemaValue), defaults, constructors))); } static JSStringRef schemaVersionString = JSStringCreateWithUTF8CString("schemaVersion"); @@ -187,7 +187,7 @@ JSObjectRef RealmConstructor(JSContextRef ctx, JSObjectRef constructor, size_t a realm->m_binding_context.reset(new RJSRealmDelegate(realm, JSContextGetGlobalContext(ctx))); } RJSDefaults(realm.get()) = defaults; - RJSPrototypes(realm.get()) = prototypes; + RJSConstructors(realm.get()) = constructors; return RJSWrapObject(ctx, RJSRealmClass(), new SharedRealm(realm)); } catch (std::exception &ex) { diff --git a/src/js_realm.hpp b/src/js_realm.hpp index fa2b4fdd..d7e248b5 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -19,4 +19,4 @@ std::string RJSDefaultPath(); void RJSSetDefaultPath(std::string path); std::map &RJSDefaults(realm::Realm *realm); -std::map &RJSPrototypes(realm::Realm *realm); +std::map &RJSConstructors(realm::Realm *realm); diff --git a/src/js_schema.cpp b/src/js_schema.cpp index 20e02f32..6cc67226 100644 --- a/src/js_schema.cpp +++ b/src/js_schema.cpp @@ -114,19 +114,17 @@ static inline Property RJSParseProperty(JSContextRef ctx, JSValueRef propertyAtt return prop; } -static inline ObjectSchema RJSParseObjectSchema(JSContextRef ctx, JSObjectRef objectSchemaObject, std::map &defaults, std::map &prototypes) { +static inline ObjectSchema RJSParseObjectSchema(JSContextRef ctx, JSObjectRef objectSchemaObject, std::map &defaults, std::map &constructors) { static JSStringRef nameString = JSStringCreateWithUTF8CString("name"); static JSStringRef primaryString = JSStringCreateWithUTF8CString("primaryKey"); - static JSStringRef prototypeString = JSStringCreateWithUTF8CString("prototype"); static JSStringRef propertiesString = JSStringCreateWithUTF8CString("properties"); static JSStringRef schemaString = JSStringCreateWithUTF8CString("schema"); - JSObjectRef prototypeObject = NULL; - JSValueRef prototypeValue = RJSValidatedPropertyValue(ctx, objectSchemaObject, prototypeString); + JSObjectRef objectConstructor = NULL; - if (!JSValueIsUndefined(ctx, prototypeValue)) { - prototypeObject = RJSValidatedValueToObject(ctx, prototypeValue); - objectSchemaObject = RJSValidatedObjectProperty(ctx, prototypeObject, schemaString, "Realm object prototype must have a 'schema' property."); + if (JSObjectIsFunction(ctx, objectSchemaObject) || JSObjectIsConstructor(ctx, objectSchemaObject)) { + objectConstructor = objectSchemaObject; + objectSchemaObject = RJSValidatedObjectProperty(ctx, objectConstructor, schemaString, "Realm object constructor must have a 'schema' property."); } else { JSValueRef subSchemaValue = RJSValidatedPropertyValue(ctx, objectSchemaObject, schemaString); @@ -170,9 +168,9 @@ static inline ObjectSchema RJSParseObjectSchema(JSContextRef ctx, JSObjectRef ob } // Store prototype so that objects of this type will have their prototype set to this prototype object. - if (prototypeObject) { - JSValueProtect(ctx, prototypeObject); - prototypes[objectSchema.name] = std::move(prototypeObject); + if (objectConstructor) { + JSValueProtect(ctx, objectConstructor); + constructors[objectSchema.name] = std::move(objectConstructor); } defaults.emplace(objectSchema.name, std::move(objectDefaults)); @@ -180,12 +178,12 @@ static inline ObjectSchema RJSParseObjectSchema(JSContextRef ctx, JSObjectRef ob return objectSchema; } -realm::Schema RJSParseSchema(JSContextRef ctx, JSObjectRef jsonObject, std::map &defaults, std::map &prototypes) { +realm::Schema RJSParseSchema(JSContextRef ctx, JSObjectRef jsonObject, std::map &defaults, std::map &constructors) { std::vector schema; size_t length = RJSValidatedListLength(ctx, jsonObject); for (unsigned int i = 0; i < length; i++) { JSObjectRef jsonObjectSchema = RJSValidatedObjectAtIndex(ctx, jsonObject, i); - ObjectSchema objectSchema = RJSParseObjectSchema(ctx, jsonObjectSchema, defaults, prototypes); + ObjectSchema objectSchema = RJSParseObjectSchema(ctx, jsonObjectSchema, defaults, constructors); schema.emplace_back(std::move(objectSchema)); } diff --git a/src/js_schema.hpp b/src/js_schema.hpp index 4564fcf4..6de643f6 100644 --- a/src/js_schema.hpp +++ b/src/js_schema.hpp @@ -15,4 +15,4 @@ namespace realm { JSClassRef RJSSchemaClass(); JSObjectRef RJSSchemaCreate(JSContextRef ctx, realm::Schema *schema); -realm::Schema RJSParseSchema(JSContextRef ctx, JSObjectRef jsonObject, std::map &defaults, std::map &prototypes); +realm::Schema RJSParseSchema(JSContextRef ctx, JSObjectRef jsonObject, std::map &defaults, std::map &constructors); diff --git a/tests/lib/realm-tests.js b/tests/lib/realm-tests.js index c19736e2..4c42e969 100644 --- a/tests/lib/realm-tests.js +++ b/tests/lib/realm-tests.js @@ -261,6 +261,53 @@ module.exports = BaseTest.extend({ }); }, + testRealmCreateWithConstructor: function() { + var customCreated = 0; + + function CustomObject() { + customCreated++; + this.intCol *= 100; + } + CustomObject.schema = { + name: 'CustomObject', + properties: { + intCol: 'int' + } + } + + function InvalidObject() { + return {}; + } + TestCase.assertThrows(function() { + new Realm({schema: [InvalidObject]}); + }); + + InvalidObject.schema = { + name: 'InvalidObject', + properties: { + intCol: 'int' + } + } + + var realm = new Realm({schema: [CustomObject, InvalidObject]}); + + realm.write(function() { + var object = realm.create('CustomObject', {intCol: 1}); + TestCase.assertTrue(object instanceof CustomObject); + TestCase.assertTrue(Object.getPrototypeOf(object) == CustomObject.prototype); + TestCase.assertEqual(customCreated, 1); + + // Should have been multiplied by 100 in the constructor. + TestCase.assertEqual(object.intCol, 100); + }); + + TestCase.assertThrows(function() { + realm.write(function() { + realm.create('InvalidObject', {intCol: 1}); + }); + }); + }, + testRealmDelete: function() { var realm = new Realm({schema: [schemas.TestObject]}); diff --git a/tests/lib/schemas.js b/tests/lib/schemas.js index c3b414af..a7c81940 100644 --- a/tests/lib/schemas.js +++ b/tests/lib/schemas.js @@ -14,7 +14,7 @@ exports.TestObject = { }; function PersonObject() {} -PersonObject.prototype.schema = { +PersonObject.schema = { name: 'PersonObject', properties: { name: Realm.Types.STRING,