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

RJS-2871: Add relaxed schema #6796

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions integration-tests/tests/src/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import "./tests/objects";
import "./tests/observable";
import "./tests/queries";
import "./tests/realm-constructor";
import "./tests/relaxed-schema";
import "./tests/results";
import "./tests/schema";
import "./tests/serialization";
Expand Down
213 changes: 213 additions & 0 deletions integration-tests/tests/src/tests/relaxed-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2024 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.
//
////////////////////////////////////////////////////////////////////////////

/* eslint-disable @typescript-eslint/no-non-null-assertion */

import { expect } from "chai";
import { PersonSchema, Person } from "../schemas/person-and-dogs";
import { openRealmBeforeEach } from "../hooks";
import { BSON } from "realm";

describe("Relaxed schema", () => {
openRealmBeforeEach({ relaxedSchema: true, schema: [PersonSchema] });

it("can open a Realm with a relaxed schema", function (this: Mocha.Context & RealmContext) {
expect(this.realm).not.null;
});

it("can add an object to a Realm with a relaxed schema", function (this: Mocha.Context & RealmContext) {
this.realm.write(() => {
this.realm.create(PersonSchema.name, {
name: "Joe",
age: 19,
});
});

expect(this.realm.objects(PersonSchema.name).length).equals(1);
});

it("can modify an existing property of an object in a Realm with a relaxed schema", function (this: Mocha.Context &
RealmContext) {
this.realm.write(() => {
this.realm.create(PersonSchema.name, {
name: "Joe",
age: 19,
});
});

this.realm.write(() => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const joe = this.realm.objectForPrimaryKey(PersonSchema.name, "Joe")!;
expect(joe).not.null;
joe.age = 25;
});

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const olderJoe = this.realm.objectForPrimaryKey(PersonSchema.name, "Joe")!;
expect(olderJoe.age).equals(25n); // TODO: why BigInt and not Number?
});

[
["primitive", 1234],
["data", new ArrayBuffer(10)],
["decimal128", 12n],
["objectId", new BSON.ObjectID()],
["uuid", new BSON.UUID()],
// ["linkingObjects", "linkingObjects"],
// ["list", ["123", "123", "12"]],
// [
// "dictionary",
// {
// dictionary: {
// windows: 3,
// apples: 3,
// },
// },
// ],
].forEach(([typeName, valueToSet]) => {
describe(`with ${typeName}`, () => {
let setValue: any;

beforeEach(function (this: Mocha.Context & RealmContext) {
if (valueToSet == "linkingObjects") {
this.realm.write(() => {
setValue = this.realm.create<Person>(PersonSchema.name, {
name: "Different Joe",
age: 81,
});
});
} else {
setValue = valueToSet;
}
});

it("can add a new property", function (this: Mocha.Context & RealmContext) {
this.realm.write(() => {
this.realm.create(PersonSchema.name, {
name: "Joe",
age: 19,
});
});

this.realm.write(() => {
const joe = this.realm.objectForPrimaryKey(PersonSchema.name, "Joe")!;
expect(joe).not.null;
joe.customProperty = setValue;
});
const joe = this.realm.objectForPrimaryKey(PersonSchema.name, "Joe")!;
expect(joe).not.null;
expect(joe.name).equals("Joe");
expect(joe.customProperty).deep.equals(setValue);
});

it("can add a new property", function (this: Mocha.Context & RealmContext) {
this.realm.write(() => {
this.realm.create(PersonSchema.name, {
name: "Joe",
age: 19,
});
});
let joe = this.realm.objectForPrimaryKey(PersonSchema.name, "Joe")!;
expect(() => joe.customProperty).throws("Property 'Person.customProperty' does not exist");

this.realm.write(() => {
joe = this.realm.objectForPrimaryKey(PersonSchema.name, "Joe")!;
expect(joe).not.null;
joe.customProperty = setValue;
});

joe = this.realm.objectForPrimaryKey(PersonSchema.name, "Joe")!;
expect(joe).not.null;
expect(joe.name).equals("Joe");

expect(joe.customProperty).deep.equals(setValue);
});

it("can delete a property", function () {
let joe: any;
this.realm.write(() => {
joe = this.realm.create(PersonSchema.name, {
name: "Joe",
age: 19,
});
});
this.realm.write(() => {
joe.customProperty = setValue;
});
expect(() => joe.customProperty).does.not.throw();

this.realm.write(() => {
delete joe.customProperty;
});
joe = this.realm.objectForPrimaryKey(PersonSchema.name, "Joe")!;
expect(() => joe.customProperty).throws("Property 'Person.customProperty' does not exist");
});
});
});

it("Object.keys(), Object.values(), and Object.entries()", function () {
this.realm.write(() => {
this.realm.create(PersonSchema.name, {
name: "Joe",
age: 19,
});
});

let joe = this.realm.objectForPrimaryKey(PersonSchema.name, "Joe");
let keys = Object.keys(joe);
expect(keys.length).equal(3); // 3 (from schema) + 0 (additional property)
expect(keys).to.have.deep.members(["age", "friends", "name"]);
expect(Object.entries(joe).length).equal(3);
expect(Object.values(joe).length).equal(3);

this.realm.write(() => {
const joe = this.realm.objectForPrimaryKey(PersonSchema.name, "Joe");
joe.nickname = "Johannes";
});

joe = this.realm.objectForPrimaryKey(PersonSchema.name, "Joe");
keys = Object.keys(joe);
expect(keys.length).equal(4); // 3 (from schema) + 1 (additional property)
expect(keys).to.have.deep.members(["age", "friends", "name", "nickname"]);
expect(Object.entries(joe).length).equal(4);
expect(Object.values(joe).length).equal(4);
});

it("spread operator", function () {
this.realm.write(() => {
this.realm.create(PersonSchema.name, {
name: "Joe",
age: 19,
});
});

const joe1 = this.realm.objectForPrimaryKey(PersonSchema.name, "Joe");
const plainJoe1 = {...joe1};
expect(plainJoe1 instanceof Object).to.be.true;
expect(Object.keys(plainJoe1)).to.have.deep.members(["age", "friends", "name"]);

this.realm.write(() => {
const joe = this.realm.objectForPrimaryKey(PersonSchema.name, "Joe");
joe.nickname = "Johannes";
});

const joe2 = this.realm.objectForPrimaryKey(PersonSchema.name, "Joe");
const plainJoe2 = {...joe2};
expect(Object.keys(plainJoe2)).to.have.deep.members(["age", "friends", "name", "nickname"]);
});
});
7 changes: 7 additions & 0 deletions packages/realm/bindgen/js_opt_in_spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ records:
- schema
- schema_version
- schema_mode
- flexible_schema
- disable_format_upgrade
- sync_config
- force_sync_history
Expand Down Expand Up @@ -298,20 +299,26 @@ classes:
- get_link_target
- clear
- get_primary_key_column
- get_column_key

Obj:
methods:
- is_valid
- get_table
- get_key
- get_any
- get_any_by_name
- set_any
- set_any_by_name
- set_collection
- add_int
- get_linked_object
- get_backlink_count
- get_backlink_view
- create_and_set_linked_object
- has_schema_property
- erase_additional_prop
- get_additional_properties

Timestamp:
methods:
Expand Down
2 changes: 1 addition & 1 deletion packages/realm/bindgen/vendor/realm-core
Submodule realm-core updated 172 files
2 changes: 1 addition & 1 deletion packages/realm/src/ClassMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export class ClassMap {
properties,
wrapObject(obj) {
if (obj.isValid) {
return RealmObject.createWrapper(obj, constructor);
return RealmObject.createWrapper(obj, constructor, realm.internal.config.flexibleSchema);
} else {
return null;
}
Expand Down
10 changes: 10 additions & 0 deletions packages/realm/src/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ export type BaseConfiguration = {
* @since 2.23.0
*/
fifoFilesFallbackPath?: string;
/**
* Opening a Realm with relaxed schema means that you can dynamically add, update, and remove properties
* at runtime. These additional properties will always have the type Mixed.
* @default false
*/
relaxedSchema?: boolean;
sync?: SyncConfiguration;
/** @internal */
openSyncedRealmLocally?: true;
Expand Down Expand Up @@ -184,6 +190,7 @@ export function validateConfiguration(config: unknown): asserts config is Config
path,
schema,
schemaVersion,
relaxedSchema,
inMemory,
readOnly,
fifoFilesFallbackPath,
Expand Down Expand Up @@ -211,6 +218,9 @@ export function validateConfiguration(config: unknown): asserts config is Config
"'schemaVersion' on realm configuration must be 0 or a positive integer.",
);
}
if (relaxedSchema !== undefined) {
assert.boolean(relaxedSchema, "'relaxedSchema' on realm configuration");
}
if (inMemory !== undefined) {
assert.boolean(inMemory, "'inMemory' on realm configuration");
}
Expand Down
Loading