diff --git a/integration-tests/tests/src/schemas/playlist-with-songs-with-ids.ts b/integration-tests/tests/src/schemas/playlist-with-songs-with-ids.ts deleted file mode 100644 index 27a3a75d76..0000000000 --- a/integration-tests/tests/src/schemas/playlist-with-songs-with-ids.ts +++ /dev/null @@ -1,72 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2020 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 Realm from "realm"; - -/* tslint:disable max-classes-per-file */ - -export interface IPlaylist { - _id: number; - title: string; - songs: Realm.List; - related: Realm.List; -} - -export const PlaylistSchema: Realm.ObjectSchema = { - name: "Playlist", - primaryKey: "_id", - properties: { - _id: "int", - title: "string", - songs: "Song[]", - related: "Playlist[]", - }, -}; - -export class Playlist extends Realm.Object implements IPlaylist { - _id!: number; - title!: string; - songs!: Realm.List; - related!: Realm.List; - - static schema = PlaylistSchema; -} - -export interface ISong { - _id: number; - artist: string; - title: string; -} - -export const SongSchema: Realm.ObjectSchema = { - name: "Song", - primaryKey: "_id", - properties: { - _id: "int", - artist: "string", - title: "string", - }, -}; - -export class Song extends Realm.Object implements ISong { - _id!: number; - artist!: string; - title!: string; - - static schema = SongSchema; -} diff --git a/integration-tests/tests/src/schemas/playlist-with-songs.ts b/integration-tests/tests/src/schemas/playlist-with-songs.ts deleted file mode 100644 index 9aa940ca4d..0000000000 --- a/integration-tests/tests/src/schemas/playlist-with-songs.ts +++ /dev/null @@ -1,64 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// -// Copyright 2020 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 Realm from "realm"; - -/* tslint:disable max-classes-per-file */ - -export interface IPlaylist { - title: string; - songs: Realm.List; - related: Realm.List; -} - -export const PlaylistSchema: Realm.ObjectSchema = { - name: "Playlist", - properties: { - title: "string", - songs: "Song[]", - related: "Playlist[]", - }, -}; - -export class Playlist extends Realm.Object implements IPlaylist { - title!: string; - songs!: Realm.List; - related!: Realm.List; - - static schema = PlaylistSchema; -} - -export interface ISong { - artist: string; - title: string; -} - -export const SongSchema: Realm.ObjectSchema = { - name: "Song", - properties: { - artist: "string", - title: "string", - }, -}; - -export class Song extends Realm.Object implements ISong { - artist!: string; - title!: string; - - static schema = SongSchema; -} diff --git a/integration-tests/tests/src/tests/dictionary.ts b/integration-tests/tests/src/tests/dictionary.ts index 68e29cac15..a78dff8c3f 100644 --- a/integration-tests/tests/src/tests/dictionary.ts +++ b/integration-tests/tests/src/tests/dictionary.ts @@ -62,6 +62,7 @@ describe("Dictionary", () => { "addListener", "removeListener", "removeAllListeners", + "toJSON", ]; for (const name of methodNames) { it(`exposes a method named '${name}'`, function (this: RealmContext) { diff --git a/integration-tests/tests/src/tests/serialization.ts b/integration-tests/tests/src/tests/serialization.ts index b45467653a..2bee75491e 100755 --- a/integration-tests/tests/src/tests/serialization.ts +++ b/integration-tests/tests/src/tests/serialization.ts @@ -17,545 +17,191 @@ //////////////////////////////////////////////////////////////////////////// import { expect } from "chai"; -import Realm from "realm"; +import Realm, { DefaultObject } from "realm"; -import { - IPlaylist as IPlaylistNoId, - ISong as ISongNoId, - PlaylistSchema as PlaylistSchemaNoId, - SongSchema as SongSchemaNoId, - Playlist as PlaylistNoId, - Song as SongNoId, -} from "../schemas/playlist-with-songs"; -import { - IPlaylist as IPlaylistWithId, - ISong as ISongWithId, - PlaylistSchema as PlaylistSchemaWithId, - SongSchema as SongSchemaWithId, - Playlist as PlaylistWithId, - Song as SongWithId, -} from "../schemas/playlist-with-songs-with-ids"; -import circularCollectionResult from "../structures/circular-collection-result.json"; -import circularCollectionResultWithIds from "../structures/circular-collection-result-with-primary-ids.json"; -import { openRealmBeforeEach } from "../hooks"; +import { openRealmBefore } from "../hooks"; -describe("JSON serialization (exposed properties)", () => { - it("JsonSerializationReplacer is exposed on the Realm constructor", () => { - expect(typeof Realm.JsonSerializationReplacer).equals("function"); - expect(Realm.JsonSerializationReplacer.length).equals(2); - }); -}); - -type TestSetup = { - name: string; - schema: (Realm.ObjectSchema | Realm.ObjectClass)[]; - testData: (realm: Realm) => unknown; +const PlaylistSchema: Realm.ObjectSchema = { + name: "Playlist", + properties: { + title: "string", + songs: "Song[]", + related: "Playlist[]", + }, }; -interface ICacheIdTestSetup { - type: string; - schemaName: string; - testId: unknown; - expectedResult: string; -} - -/** - * Create test data (TestSetups) in 4 ways, with the same data structure: - * 1. Literals without primaryKeys - * 2. Class Models without primaryKeys - * 3. Literals with primaryKeys - * 4. Class Models with primaryKeys - */ -const testSetups: TestSetup[] = [ - { - name: "Object literal (NO primaryKey)", - schema: [PlaylistSchemaNoId, SongSchemaNoId], - testData(realm: Realm) { - realm.write(() => { - // Shared songs - const s1 = realm.create(SongSchemaNoId.name, { - artist: "Shared artist name 1", - title: "Shared title name 1", - }); - const s2 = realm.create(SongSchemaNoId.name, { - artist: "Shared artist name 2", - title: "Shared title name 2", - }); - const s3 = realm.create(SongSchemaNoId.name, { - artist: "Shared artist name 3", - title: "Shared title name 3", - }); - - // Playlists - const p1 = realm.create(PlaylistSchemaNoId.name, { - title: "Playlist 1", - songs: [ - s1, - s2, - s3, - { - artist: "Unique artist 1", - title: "Unique title 1", - }, - { - artist: "Unique artist 2", - title: "Unique title 2", - }, - ], - }); - const p2 = realm.create(PlaylistSchemaNoId.name, { - title: "Playlist 2", - songs: [ - { - artist: "Unique artist 3", - title: "Unique title 3", - }, - { - artist: "Unique artist 4", - title: "Unique title 4", - }, - s3, - ], - related: [p1], - }); - const p3 = realm.create(PlaylistSchemaNoId.name, { - title: "Playlist 3", - songs: [ - s1, - { - artist: "Unique artist 5", - title: "Unique title 5", - }, - { - artist: "Unique artist 6", - title: "Unique title 6", - }, - s2, - ], - related: [p1, p2], - }); - - // ensure circular references for p1 (ensure p1 reference self fist) - p1.related.push(p1, p2, p3); // test self reference - }); - - return circularCollectionResult; - }, +const SongSchema: Realm.ObjectSchema = { + name: "Song", + properties: { + artist: "string", + title: "string", }, - { - name: "Class model (NO primaryKey)", - schema: [PlaylistNoId, SongNoId], - testData(realm: Realm) { - realm.write(() => { - // Shared songs - const s1 = realm.create(SongNoId, { - artist: "Shared artist name 1", - title: "Shared title name 1", - }); - const s2 = realm.create(SongNoId, { - artist: "Shared artist name 2", - title: "Shared title name 2", - }); - const s3 = realm.create(SongNoId, { - artist: "Shared artist name 3", - title: "Shared title name 3", - }); +}; - // Playlists - const p1 = realm.create(PlaylistNoId, { - title: "Playlist 1", - songs: [ - s1, - s2, - s3, - { artist: "Unique artist 1", title: "Unique title 1" }, - { artist: "Unique artist 2", title: "Unique title 2" }, - ], - }); - const p2 = realm.create(PlaylistNoId, { - title: "Playlist 2", - songs: [ - { artist: "Unique artist 3", title: "Unique title 3" }, - { artist: "Unique artist 4", title: "Unique title 4" }, - s3, - ], - related: [p1], - }); - const p3 = realm.create(PlaylistNoId, { - title: "Playlist 3", - songs: [ - s1, - { artist: "Unique artist 5", title: "Unique title 5" }, - { artist: "Unique artist 6", title: "Unique title 6" }, - s2, - ], - related: [p1, p2], - }); +interface ISong { + artist: string; + title: string; +} - // ensure circular references for p1 (ensure p1 reference self fist) - p1.related.push(p1, p2, p3); // test self reference - }); +interface IPlaylist { + title: string; + related: Realm.List; + songs: Realm.List; +} - return circularCollectionResult; - }, +const BirthdaysSchema: Realm.ObjectSchema = { + name: "Birthdays", + properties: { + dict: "{}", }, - { - name: "Object literal (Int primaryKey)", - schema: [PlaylistSchemaWithId, SongSchemaWithId], - testData(realm: Realm) { - realm.write(() => { - // Shared songs - const s1 = realm.create(SongSchemaWithId.name, { - _id: 1, - artist: "Shared artist name 1", - title: "Shared title name 1", - }); - const s2 = realm.create(SongSchemaWithId.name, { - _id: 2, - artist: "Shared artist name 2", - title: "Shared title name 2", - }); - const s3 = realm.create(SongSchemaWithId.name, { - _id: 3, - artist: "Shared artist name 3", - title: "Shared title name 3", - }); +}; - // Playlists - const p1 = realm.create(PlaylistSchemaWithId.name, { - _id: 1, - title: "Playlist 1", - songs: [ - s1, - s2, - s3, - { - _id: 4, - artist: "Unique artist 1", - title: "Unique title 1", - }, - { - _id: 5, - artist: "Unique artist 2", - title: "Unique title 2", - }, - ], - }); - const p2 = realm.create(PlaylistSchemaWithId.name, { - _id: 2, - title: "Playlist 2", - songs: [ - { - _id: 6, - artist: "Unique artist 3", - title: "Unique title 3", - }, - { - _id: 7, - artist: "Unique artist 4", - title: "Unique title 4", - }, - s3, - ], - related: [p1], - }); - const p3 = realm.create(PlaylistSchemaWithId.name, { - _id: 3, - title: "Playlist 3", - songs: [ - s1, - { - _id: 8, - artist: "Unique artist 5", - title: "Unique title 5", - }, - { - _id: 9, - artist: "Unique artist 6", - title: "Unique title 6", - }, - s2, - ], - related: [p1, p2], - }); +interface IBirthdays { + dict: Record; +} - // ensure circular references for p1 (ensure p1 reference self fist) - p1.related.push(p1, p2, p3); // test self reference - }); +interface EdgeCaseSchema { + maybeNull: null; +} - return circularCollectionResultWithIds; - }, +const EdgeCaseSchema = { + name: "EdgeCase", + properties: { + maybeNull: "string?", }, - { - name: "Class model (Int primaryKey)", - schema: [PlaylistWithId, SongWithId], - testData(realm: Realm) { - realm.write(() => { - // Shared songs - const s1 = realm.create(SongWithId, { - _id: 1, - artist: "Shared artist name 1", - title: "Shared title name 1", - }); - const s2 = realm.create(SongWithId, { - _id: 2, - artist: "Shared artist name 2", - title: "Shared title name 2", - }); - const s3 = realm.create(SongWithId, { - _id: 3, - artist: "Shared artist name 3", - title: "Shared title name 3", - }); - - // Playlists - const p1 = realm.create(PlaylistWithId, { - _id: 1, - title: "Playlist 1", - songs: [ - s1, - s2, - s3, - { - _id: 4, - artist: "Unique artist 1", - title: "Unique title 1", - }, - { - _id: 5, - artist: "Unique artist 2", - title: "Unique title 2", - }, - ], - }); - const p2 = realm.create(PlaylistWithId, { - _id: 2, - title: "Playlist 2", - songs: [ - { - _id: 6, - artist: "Unique artist 3", - title: "Unique title 3", - }, - { - _id: 7, - artist: "Unique artist 4", - title: "Unique title 4", - }, - s3, - ], - related: [p1], - }); - const p3 = realm.create(PlaylistWithId, { - _id: 3, - title: "Playlist 3", - songs: [ - s1, - { - _id: 8, - artist: "Unique artist 5", - title: "Unique title 5", - }, - { - _id: 9, - artist: "Unique artist 6", - title: "Unique title 6", - }, - s2, - ], - related: [p1, p2], - }); - - // ensure circular references for p1 (ensure p1 reference self fist) - p1.related.push(p1, p2, p3); // test self reference - }); +}; - return circularCollectionResultWithIds; - }, - }, -]; +interface TestSetup { + // Realm instance being tested + subject: Realm.Object | Realm.Results; + // Type of the Realm instance + type: typeof Realm.Object | typeof Realm.Results | typeof Realm.Dictionary; + // Expected serialized plain object result + serialized: DefaultObject; +} -const cacheIdTestSetups: ICacheIdTestSetup[] = [ - { - type: "int", - schemaName: "IntIdTest", - testId: 1337, - expectedResult: "IntIdTest#1337", - }, - { - type: "string", - schemaName: "StringIdTest", - testId: "~!@#$%^&*()_+=-,./<>? 0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZÆØÅ abcdefghijklmnopqrstuvwxyzæøå", - expectedResult: - "StringIdTest#~!@#$%^&*()_+=-,./<>? 0123456789 ABCDEFGHIJKLMNOPQRSTUVWXYZÆØÅ abcdefghijklmnopqrstuvwxyzæøå", - }, - { - type: "objectId", - schemaName: "ObjectIdTest", - testId: new Realm.BSON.ObjectId("5f99418846da9c45005f50bf"), - expectedResult: "ObjectIdTest#5f99418846da9c45005f50bf", - }, -]; +// Describe common test types that will be run, +// must match this.commonTests that are defined in before(). +const commonTestsTypes = ["Object", "Results", "Dictionary"]; + +describe("toJSON functionality", () => { + type TestContext = { + commonTests: Record; + playlists: Realm.Results & IPlaylist[]; + birthdays: Realm.Object & IBirthdays; + p1Serialized: DefaultObject; + resultsSerialized: DefaultObject; + birthdaysSerialized: DefaultObject; + } & RealmContext; + openRealmBefore({ + inMemory: true, + schema: [PlaylistSchema, SongSchema, BirthdaysSchema, EdgeCaseSchema], + }); -describe("JSON serialization", () => { - for (const { type, schemaName, testId, expectedResult } of cacheIdTestSetups) { - describe(`Internal cache id check for type ${type}`, () => { - openRealmBeforeEach({ - inMemory: true, - schema: [ - { - name: schemaName, - primaryKey: "_id", - properties: { - _id: type, - title: "string", - }, - }, + before(function (this: RealmContext) { + this.realm.write(() => { + // Create expected serialized p1 and p2 objects. + const p1Serialized = { + title: "Playlist 1", + songs: [ + { title: "Song", artist: "First" }, + { title: "Another", artist: "Second" }, ], - }); - - it(`generates correct cache id for primaryKey type: ${type}`, function (this: RealmContext) { - this.realm.write(() => { - this.realm.create(schemaName, { - _id: testId, - title: `Cache id should be: ${expectedResult}`, - }); - }); - - const testSubject = this.realm.objectForPrimaryKey(schemaName, testId as string); - const json = JSON.stringify(testSubject, Realm.JsonSerializationReplacer); - const parsed = JSON.parse(json); - - expect(parsed.$refId).equals(expectedResult); - }); + related: [], + }; + + const p2Serialized = { + title: "Playlist 2", + songs: [{ title: "Title", artist: "Third" }], + related: [], + }; + // Playlists + const p1 = this.realm.create(PlaylistSchema.name, p1Serialized); + const p2 = this.realm.create(PlaylistSchema.name, p2Serialized); + // ensure circular references for p1 (ensure p1 references itself) + p1.related.push(p1, p2); + //@ts-expect-error Adding to related field to match + p1Serialized.related.push(p1Serialized, p2Serialized); + + p2.related.push(p1); + //@ts-expect-error Adding to related field to match + p2Serialized.related.push(p1Serialized); + + // Use playlist to test Result implementations + this.playlists = this.realm.objects(PlaylistSchema.name).sorted("title"); + this.playlistsSerialized = p1Serialized.related; + + this.birthdaysSerialized = { + dict: { + Bob: "August", + Tom: "January", + }, + }; + // Dictionary object test + this.birthdays = this.realm.create("Birthdays", this.birthdaysSerialized); + + this.birthdays.dict.grandparent = this.birthdays; + this.birthdaysSerialized.dict.grandparent = this.birthdaysSerialized; + + // Define the structures for the common test suite. + this.commonTests = { + Object: { + type: Realm.Object, + subject: p1, + serialized: p1Serialized, + }, + Results: { + type: Realm.Results, + subject: this.playlists, + serialized: this.playlistsSerialized, + }, + Dictionary: { + type: Realm.Dictionary, + subject: this.birthdays.dict, + serialized: this.birthdaysSerialized.dict, + }, + }; }); - } - - type TestContext = { predefinedStructure: any; playlists: Realm.Results } & RealmContext; - - for (const { name, schema, testData } of testSetups) { - describe(`Repeated test for "${name}":`, () => { - openRealmBeforeEach({ - inMemory: true, - schema, - }); - - beforeEach(function (this: RealmContext) { - this.predefinedStructure = testData(this.realm); - this.playlists = this.realm.objects(PlaylistSchemaNoId.name).sorted("title"); - }); - - describe("Realm.Object", () => { - it("extends Realm.Object", function (this: TestContext) { - // Check that entries in the result set extends Realm.Object. - expect(this.playlists[0]).instanceOf(Realm.Object); - }); - + }); + describe(`common tests`, () => { + for (const name of commonTestsTypes) { + describe(`with Realm.${name}`, () => { it("implements toJSON", function (this: TestContext) { - // Check that fist Playlist has toJSON implemented. - expect(typeof this.playlists[0].toJSON).equals("function"); - }); + const test = this.commonTests[name]; + expect(test.subject).instanceOf(test.type); - it("toJSON returns a circular structure", function (this: TestContext) { - const serializable = this.playlists[0].toJSON(); + expect(typeof test.subject.toJSON).equals("function"); + }); + it("toJSON returns a plain object or array", function (this: TestContext) { + const test = this.commonTests[name]; + const serializable = test.subject.toJSON(); + // Check that serializable object is not a Realm entity. + expect(serializable).not.instanceOf(test.type); // Check that no props are functions on the serializable object. expect(Object.values(serializable).some((val) => typeof val === "function")).equals(false); - // Check that linked list is not a Realm entity. - expect(serializable.related).not.instanceOf(Realm.Collection); - // But is a plain Array - expect(Array.isArray(serializable.related)).equals(true); - - // Check that the serializable object is the same as the first related object. - // (this check only makes sense because of our structure) - expect(serializable).equals(serializable.related[0]); - }); - - it("throws correct error on serialization", function (this: TestContext) { - // Check that we get a circular structure error. - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value - expect(() => JSON.stringify(this.playlists[0])).throws(TypeError, /circular|cyclic/i); - }); - - it("serializes to expected output using Realm.JsonSerializationReplacer", function (this: TestContext) { - const json = JSON.stringify(this.playlists[0], Realm.JsonSerializationReplacer); - const generated = JSON.parse(json); - - // Check that we get the expected structure. - // (parsing back to an object & using deep equals, as we can't rely on property order) - expect(generated).deep.equals(this.predefinedStructure[0]); + if (test.type == Realm.Results) expect(Array.isArray(serializable)).equals(true); + else expect(Object.getPrototypeOf(serializable)).equals(Object.prototype); }); - }); - - describe("Realm.Results", () => { - it("extends Realm.Collection", function (this: TestContext) { - // Check that the result set extends Realm.Collection. - expect(this.playlists).instanceOf(Realm.Collection); + it("toJSON matches expected structure", function (this: TestContext) { + const test = this.commonTests[name]; + const serializable = test.subject.toJSON(); + // Ensure the object is deeply equal to the expected serialized object. + expect(serializable).deep.equals(test.serialized); }); - - it("implements toJSON", function (this: TestContext) { - expect(typeof this.playlists.toJSON).equals("function"); - }); - - it("toJSON returns a circular structure", function (this: TestContext) { - const serializable = this.playlists.toJSON(); - - // Check that the serializable object is not a Realm entity. - expect(serializable).not.instanceOf(Realm.Collection); - // But is a plain Array - expect(Array.isArray(serializable)).equals(true); - - // Check that the serializable object is not a Realm entity. - expect(serializable).not.instanceOf(Realm.Collection); - // But is a plain Array - expect(Array.isArray(serializable)).equals(true); - - // Check that linked list is not a Realm entity. - expect(serializable[0].related).not.instanceOf(Realm.Collection); - // But is a plain Array - expect(Array.isArray(serializable[0].related)).equals(true); - - // Check that the serializable object is the same as the first related object. - // (this check only makes sense because of our structure) - expect(serializable[0]).equals(serializable[0].related[0]); - }); - it("throws correct error on serialization", function (this: TestContext) { + const test = this.commonTests[name]; + const serializable = test.subject.toJSON(); // Check that we get a circular structure error. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value - expect(() => JSON.stringify(this.playlists)).throws(TypeError, /circular|cyclic/i); - }); - - it("serializes to expected output using Realm.JsonSerializationReplacer", function (this: TestContext) { - const json = JSON.stringify(this.playlists, Realm.JsonSerializationReplacer); - const generated = JSON.parse(json); - - // Check that we get the expected structure. - // (parsing back to an object & using deep equals, as we can't rely on property order) - expect(generated).deep.equals(this.predefinedStructure); + expect(() => JSON.stringify(serializable)).throws(TypeError, /circular|cyclic/i); }); }); - }); - } - - describe("toJSON edge case handling", function () { - interface EdgeCaseSchema { - maybeNull: null; } + }); - const EdgeCaseSchema = { - name: "EdgeCase", - properties: { - maybeNull: "string?", - }, - }; - - openRealmBeforeEach({ - inMemory: true, - schema: [EdgeCaseSchema], - }); - + describe("edge cases", function () { it("handles null values", function (this: RealmContext) { const object = this.realm.write(() => { return this.realm.create(EdgeCaseSchema.name, { @@ -565,5 +211,13 @@ describe("JSON serialization", () => { expect(object.toJSON()).deep.equals({ maybeNull: null }); }); + it("handles a dictionary field referencing its parent", function (this: TestContext) { + const serializable = this.birthdays.toJSON(); + // Check that the serializable object is the same as the first related object. + // @ts-expect-error We know the field is a dict. + expect(serializable).equals(serializable.dict.grandparent); + // And matches expected serialized object. + expect(serializable).deep.equals(this.birthdaysSerialized); + }); }); }); diff --git a/packages/realm/src/Dictionary.ts b/packages/realm/src/Dictionary.ts index a6c4783d9c..9a299e1b5d 100644 --- a/packages/realm/src/Dictionary.ts +++ b/packages/realm/src/Dictionary.ts @@ -20,7 +20,10 @@ import * as binding from "./binding"; import { Collection } from "./Collection"; import { IllegalConstructorError } from "./errors"; import { INTERNAL } from "./internal"; +import { DefaultObject } from "./schema"; +import { Object as RealmObject } from "./Object"; import { TypeHelpers } from "./types"; +import { JSONCacheMap } from "./JSONCacheMap"; const HELPERS = Symbol("Dictionary#helpers"); @@ -217,4 +220,14 @@ export class Dictionary extends Collection [k, v instanceof RealmObject ? v.toJSON(k, cache) : v]), + ); + } } diff --git a/packages/realm/src/tests/serialization.test.ts b/packages/realm/src/JSONCacheMap.ts similarity index 51% rename from packages/realm/src/tests/serialization.test.ts rename to packages/realm/src/JSONCacheMap.ts index c336e0315f..fcc6920ad9 100644 --- a/packages/realm/src/tests/serialization.test.ts +++ b/packages/realm/src/JSONCacheMap.ts @@ -16,22 +16,22 @@ // //////////////////////////////////////////////////////////////////////////// -import { Realm } from "../index"; +import { INTERNAL } from "./internal"; +import { DefaultObject } from "./schema"; +import { Object as RealmObject } from "./Object"; -import { closeRealm, generateTempRealmPath, RealmContext } from "./utils"; - -describe("Serializing", () => { - describe("an Object", () => { - after(closeRealm); - it("returns a plain object", function (this: RealmContext) { - this.realm = new Realm({ - path: generateTempRealmPath(), - inMemory: true, - schema: [{ name: "Person", properties: { name: "string", age: "int", bestFriend: "Person" } }], - }); - const alice = this.realm.write(() => this.realm.create("Person", { name: "Alice", age: 32 })); - const serialized = alice.toJSON(); - console.log({ serialized }); - }); - }); -}); +//////////////////////////////////////////////////////////////////////////// +export class JSONCacheMap extends Map> { + add(object: RealmObject, value: DefaultObject) { + const tableKey = object[INTERNAL].table.key; + let cachedMap = this.get(tableKey); + if (!cachedMap) { + cachedMap = new Map(); + this.set(tableKey, cachedMap); + } + cachedMap.set(object._objectKey(), value); + } + find(object: RealmObject) { + return this.get(object[INTERNAL].table.key)?.get(object._objectKey()); + } +} diff --git a/packages/realm/src/Object.ts b/packages/realm/src/Object.ts index 3692f18516..01aa1548e7 100644 --- a/packages/realm/src/Object.ts +++ b/packages/realm/src/Object.ts @@ -21,12 +21,15 @@ import * as binding from "./binding"; import { INTERNAL } from "./internal"; import { Realm } from "./Realm"; import { Results } from "./Results"; +import { OrderedCollection } from "./OrderedCollection"; import { CanonicalObjectSchema, Constructor, DefaultObject, RealmObjectConstructor } from "./schema"; import { ObjectChangeCallback, ObjectListeners } from "./ObjectListeners"; import { INTERNAL_HELPERS, ClassHelpers } from "./ClassHelpers"; import { RealmInsertionModel } from "./InsertionModel"; import { assert } from "./assert"; import { TypeAssertionError } from "./errors"; +import { JSONCacheMap } from "./JSONCacheMap"; +import { Dictionary } from "./Dictionary"; export enum UpdateMode { Never = "never", @@ -214,14 +217,37 @@ class RealmObject { entries(): [string, unknown][] { throw new Error("Not yet implemented"); } - toJSON(): unknown { - // return { ...this }; + + /** + * @returns A plain object for JSON serialization. + **/ + toJSON(_?: string, cache = new JSONCacheMap()): DefaultObject { + // Construct a reference-id of table-name & primaryKey if it exists, or fall back to objectId. + + // Check if current objectId has already processed, to keep object references the same. + const existing = cache.find(this); + if (existing) { + return existing; + } + const result: DefaultObject = {}; + cache.add(this, result); + // Move all enumerable keys to result, triggering any specific toJSON implementation in the process. for (const key in this) { const value = this[key]; - console.log({ key, value }); + if (typeof value == "function") { + continue; + } + if (value instanceof RealmObject || value instanceof OrderedCollection || value instanceof Dictionary) { + // recursively trigger `toJSON` for Realm instances with the same cache. + result[key] = value.toJSON(key, cache); + } else { + // Other cases, including null and undefined. + result[key] = value; + } } - return { ...this }; + return result; } + isValid(): boolean { return this[INTERNAL] && this[INTERNAL].isValid; } diff --git a/packages/realm/src/OrderedCollection.ts b/packages/realm/src/OrderedCollection.ts index 20c052ae82..80c881b44d 100644 --- a/packages/realm/src/OrderedCollection.ts +++ b/packages/realm/src/OrderedCollection.ts @@ -21,13 +21,14 @@ import { Results } from "./Results"; import { Collection } from "./Collection"; import { unwind } from "./ranges"; import { TypeHelpers } from "./types"; -import { getBaseTypeName } from "./schema"; import { IllegalConstructorError, TypeAssertionError } from "./errors"; import { Realm } from "./Realm"; -import { Object as RealmObject } from "./Object"; import { getInternal } from "./internal"; import { assert } from "./assert"; import { ClassHelpers } from "./ClassHelpers"; +import { JSONCacheMap } from "./JSONCacheMap"; +import { Object as RealmObject } from "./Object"; +import { DefaultObject, getBaseTypeName } from "./schema"; const DEFAULT_COLUMN_KEY = 0n as unknown as binding.ColKey; @@ -177,6 +178,19 @@ export abstract class OrderedCollection throw new Error(`Assigning into a ${this.constructor.name} is not support`); } + /** + * @returns An array of plain objects for JSON serialization. + **/ + toJSON(_?: string, cache = new JSONCacheMap()): Array { + return this.map((item, index) => { + if (item instanceof RealmObject) { + return item.toJSON(index.toString(), cache); + } else { + return item as DefaultObject; + } + }); + } + *keys() { const size = this.results.size(); for (let i = 0; i < size; i++) { @@ -324,13 +338,6 @@ export abstract class OrderedCollection // Other methods - /** - * @returns An object for JSON serialization. - */ - toJSON(): Array { - throw new Error("Method not implemented."); - } - description(): string { throw new Error("Method not implemented."); }