From 0a350456b1f69deb925c50a1da7a5e4c2aa94569 Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Wed, 12 Oct 2022 16:01:47 +0400 Subject: [PATCH] Improve toJSON functionality and remove JSONSerializerReplacer (#4997) --- CHANGELOG.md | 2 + .../tests/src/tests/dictionary.ts | 2 + .../tests/src/tests/serialization.ts | 666 +++++------------- lib/dictionary.js | 9 - lib/extensions.js | 97 +-- src/js_realm_object.hpp | 18 + types/index.d.ts | 20 +- 7 files changed, 225 insertions(+), 589 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f385883ad0..ee0226c2a99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,9 +82,11 @@ Based on Realm JS v10.21.1: See changelog below for details on enhancements and ``` * A typo was fixed in the `SubscriptionsState` enum, in which `SubscriptionsState.Superseded` now returns `superseded` in place of `Superseded` * `"discardLocal"` is now the default client reset mode. ([#4382](https://github.com/realm/realm-js/issues/4382)) +* Removed `Realm.JsonSerializationReplacer`. Use circular JSON serialization libraries such as [@ungap/structured-clone](https://www.npmjs.com/package/@ungap/structured-clone) and [flatted](https://www.npmjs.com/package/flatted) for stringifying Realm entities that have circular structures. The Realm entities' `toJSON` method returns plain objects and arrays (with circular references if applicable) which makes them compatible with any serialization library that supports stringifying plain JavaScript types. ([#4997](https://github.com/realm/realm-js/pull/4997)) ### Enhancements * Small improvement to performance by caching JSI property String object [#4863](https://github.com/realm/realm-js/pull/4863) +* Small improvement to performance for `toJSON` which should make it useful for cases where a plain representations of Realm entities are needed, e.g. when inspecting them for debugging purposes through `console.log(realmObj.toJSON())`. ([#4997](https://github.com/realm/realm-js/pull/4997)) ### Fixed * None diff --git a/integration-tests/tests/src/tests/dictionary.ts b/integration-tests/tests/src/tests/dictionary.ts index 44e9007383a..0f1b0a0cf62 100644 --- a/integration-tests/tests/src/tests/dictionary.ts +++ b/integration-tests/tests/src/tests/dictionary.ts @@ -64,6 +64,7 @@ describe("Dictionary", () => { "addListener", "removeListener", "removeAllListeners", + "toJSON", ]; for (const name of methodNames) { it(`exposes a method named '${name}'`, function (this: RealmContext) { @@ -225,6 +226,7 @@ describe("Dictionary", () => { // Previously this would throw on JSC, because the Dictionary was still a Proxy, // so modifying it tried to write to the Realm outside of a write transaction expect(() => { + // @ts-expect-error We know the field is a dict. jsonObject.dict.something = "test2"; }).to.not.throw(); }); diff --git a/integration-tests/tests/src/tests/serialization.ts b/integration-tests/tests/src/tests/serialization.ts index b45467653a4..6b242d12ba6 100755 --- a/integration-tests/tests/src/tests/serialization.ts +++ b/integration-tests/tests/src/tests/serialization.ts @@ -19,543 +19,189 @@ import { expect } from "chai"; import Realm 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"; - -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; +import { openRealmBefore } from "../hooks"; + +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: Record; +} -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: Record; + resultsSerialized: Record; + birthdaysSerialized: Record; + } & 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/lib/dictionary.js b/lib/dictionary.js index 4934c42b88e..cfc35721949 100644 --- a/lib/dictionary.js +++ b/lib/dictionary.js @@ -25,15 +25,6 @@ const dictionaryHandler = { return true; } - if (key === "toJSON") { - return function () { - const keys = target._keys(); - let obj = {}; - keys.forEach((key) => (obj[key] = target.getter(key))); - return obj; - }; - } - if (typeof target[key] === "function") { return function () { return target[key].apply(target, arguments); diff --git a/lib/extensions.js b/lib/extensions.js index a1d718095ac..69fb30e753f 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -61,6 +61,25 @@ module.exports = function (realmConstructor) { realmConstructor._UUID = realmConstructor.BSON.UUID; const { DefaultNetworkTransport } = require("@realm/network-transport"); realmConstructor._networkTransport = new DefaultNetworkTransport(); + + // Adds to cache when serializing an object for toJSON + const addToCache = (cache, realmObj, value) => { + const tableKey = realmObj._tableKey(); + let cachedMap = cache.get(tableKey); + if (!cachedMap) { + cachedMap = new Map(); + cache.set(tableKey, cachedMap); + } + cachedMap.set(realmObj._objectKey(), value); + }; + + // Adds to cache when serializing an object for toJSON + const getFromCache = (cache, realmObj) => { + const tableKey = realmObj._tableKey(); + let cachedMap = cache.get(tableKey); + return cachedMap ? cachedMap.get(realmObj._objectKey()) : undefined; + }; + Object.defineProperty(realmConstructor.Collection.prototype, "toJSON", { value: function toJSON(_, cache = new Map()) { return this.map((item, index) => @@ -73,29 +92,31 @@ module.exports = function (realmConstructor) { enumerable: false, }); - const getInternalCacheId = (realmObj) => { - const { name, primaryKey } = realmObj.objectSchema(); - const id = primaryKey ? realmObj[primaryKey] : realmObj._objectKey(); - return `${name}#${id}`; - }; + Object.defineProperty(realmConstructor.Dictionary.prototype, "toJSON", { + value: function toJSON(_, cache = new Map()) { + const result = {}; + for (const k of this._keys()) { + const v = this.getter(k); + result[k] = v instanceof realmConstructor.Object ? v.toJSON(k, cache) : v; + } + return result; + }, + writable: true, + configurable: true, + enumerable: false, + }); Object.defineProperty(realmConstructor.Object.prototype, "toJSON", { value: function (_, cache = new Map()) { - // Construct a reference-id of table-name & primaryKey if it exists, or fall back to objectId. - const id = getInternalCacheId(this); - // Check if current objectId has already processed, to keep object references the same. - const existing = cache.get(id); + const existing = getFromCache(cache, this); if (existing) { return existing; } // Create new result, and store in cache. const result = {}; - cache.set(id, result); - - // Add the generated reference-id, as a non-enumerable prop '$refId', for later exposure though e.g. Realm.JsonSerializationReplacer. - Object.defineProperty(result, "$refId", { value: id, configurable: true }); + addToCache(cache, this, result); // Move all enumerable keys to result, triggering any specific toJSON implementation in the process. Object.keys(this) @@ -545,54 +566,4 @@ module.exports = function (realmConstructor) { }, configurable: true, }); - - if (!realmConstructor.JsonSerializationReplacer) { - Object.defineProperty(realmConstructor, "JsonSerializationReplacer", { - get: function () { - const seen = []; - - return function (_, value) { - // Only check for circular references when dealing with objects & arrays. - if (value === null || typeof value !== "object") { - return value; - } - - // 'this' refers to the object or array containing the the current key/value. - const parent = this; - - if (value.$refId) { - // Expose the non-enumerable prop $refId for circular serialization, if it exists. - Object.defineProperty(value, "$refId", { enumerable: true }); - } - - if (!seen.length) { - // If we haven't seen anything yet, we only push the current value (root element/array). - seen.push(value); - return value; - } - - const pos = seen.indexOf(parent); - if (pos !== -1) { - // If we have seen the parent before, we have already traversed a sibling in the array. - // We then discard information gathered for the sibling (zero back to the current array). - seen.splice(pos + 1); - } else { - // If we haven't seen the parent before, we add it to the seen-path. - // Note that this is done both for objects & arrays, to detect when we go to the next item in an array (see above). - seen.push(parent); - } - - if (seen.includes(value)) { - // If we have seen the current value before, return a reference-structure if possible. - if (value.$refId) { - return { $ref: value.$refId }; - } - return "[Circular reference]"; - } - - return value; - }; - }, - }); - } }; diff --git a/src/js_realm_object.hpp b/src/js_realm_object.hpp index a51fa2ebce1..9c1fa15fdd9 100644 --- a/src/js_realm_object.hpp +++ b/src/js_realm_object.hpp @@ -85,6 +85,7 @@ struct RealmObjectClass : ClassDefinition> { static void linking_objects(ContextType, ObjectType, Arguments&, ReturnValue&); static void linking_objects_count(ContextType, ObjectType, Arguments&, ReturnValue&); static void get_object_key(ContextType, ObjectType, Arguments&, ReturnValue&); + static void get_table_key(ContextType, ObjectType, Arguments&, ReturnValue&); static void get_object_id(ContextType, ObjectType, Arguments&, ReturnValue&); static void is_same_object(ContextType, ObjectType, Arguments&, ReturnValue&); static void set_link(ContextType, ObjectType, Arguments&, ReturnValue&); @@ -110,6 +111,7 @@ struct RealmObjectClass : ClassDefinition> { {"linkingObjectsCount", wrap}, {"_isSameObject", wrap}, {"_objectKey", wrap}, + {"_tableKey", wrap}, {"_setLink", wrap}, {"addListener", wrap}, {"removeListener", wrap}, @@ -352,6 +354,22 @@ void RealmObjectClass::get_object_key(ContextType ctx, ObjectType object, Arg return_value.set(std::to_string(obj_key.value)); } +template +void RealmObjectClass::get_table_key(ContextType ctx, ObjectType object, Arguments& args, + ReturnValue& return_value) +{ + args.validate_maximum(0); + + auto realm_object = get_internal>(ctx, object); + if (!realm_object) { + throw std::runtime_error("Invalid 'this' object"); + } + + const Obj& obj = realm_object->obj(); + auto table_key = obj.get_table()->get_key(); + return_value.set(table_key.value); +} + template void RealmObjectClass::is_same_object(ContextType ctx, ObjectType object, Arguments& args, ReturnValue& return_value) diff --git a/types/index.d.ts b/types/index.d.ts index 8fd080b4f34..d2c96841d2f 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -296,9 +296,9 @@ declare namespace Realm { entries(): [string, any][]; /** - * @returns An object for JSON serialization. + * @returns A plain object for JSON serialization. */ - toJSON(): any; + toJSON(): Record; /** * @returns boolean @@ -336,13 +336,6 @@ declare namespace Realm { */ getPropertyType(propertyName: string): string; } - - /** - * JsonSerializationReplacer solves circular structures when serializing Realm entities - * @example JSON.stringify(realm.objects("Person"), Realm.JsonSerializationReplacer) - */ - const JsonSerializationReplacer: (key: string, val: any) => any; - /** * SortDescriptor * @see { @link https://realm.io/docs/javascript/latest/api/Realm.Collection.html#~SortDescriptor } @@ -390,6 +383,11 @@ declare namespace Realm { addListener(callback: DictionaryChangeCallback): void; removeListener(callback: DictionaryChangeCallback): void; removeAllListeners(): void; + + /** + * @returns A plain object for JSON serialization. + */ + toJSON(): Record; } /** @@ -401,9 +399,9 @@ declare namespace Realm { readonly optional: boolean; /** - * @returns An object for JSON serialization. + * @returns An array of plain objects for JSON serialization. */ - toJSON(): Array; + toJSON(): Array>; description(): string;