diff --git a/documentation/LiveUpdating.md b/documentation/LiveUpdating.md index 6e184f5..374b1f6 100644 --- a/documentation/LiveUpdating.md +++ b/documentation/LiveUpdating.md @@ -150,7 +150,6 @@ We check if the object being deleted is currently in view, then we remove it fro const addedObject = convertObjects( [clone], state.currentSchema, - downloadData )[0]; newObjects.splice(index, 0, addedObject); const newLastObject = newObjects[newObjects.length - 1]; @@ -178,7 +177,6 @@ We check if the object being deleted is currently in view, then we remove it fro const addedObject = convertObjects( [clone], state.currentSchema, - downloadData )[0]; newObjects.splice(index, 1, addedObject); const newLastObject = newObjects[newObjects.length - 1]; diff --git a/flipper-plugin-realm/CHANGELOG.md b/flipper-plugin-realm/CHANGELOG.md new file mode 100644 index 0000000..aaa2993 --- /dev/null +++ b/flipper-plugin-realm/CHANGELOG.md @@ -0,0 +1,15 @@ +## v1.1.0 + +This version brings a number of major changes to plugin's functionality, improving the ability to display more complex types of Realm objects. Please make sure to update your `realm-flipper-plugin-device` to the new version as well to ensure compatibility. + +### Enhancements +* Referenced objects are now lazy-loaded. By default, they will display their object key and type (just for internal reference) and their actual values can be seen when they are inspected. This brings performance improvements when loading many objects with references as well as better support for circular and deeply nested references. +* Embedded object support. + +### Fixed +* Objects with circular references displaying as undefined. ([#96](https://github.com/realm/realm-flipper-plugin/issues/96)). +* Clicking "Cancel" when modifying objects still changing the objects' local display state in the desktop plugin. + +### Compatibility +* `realm` >= v11 +* `realm-flipper-plugin-device` >= v1.1.0 diff --git a/flipper-plugin-realm/package.json b/flipper-plugin-realm/package.json index 1b73a3e..ef15428 100644 --- a/flipper-plugin-realm/package.json +++ b/flipper-plugin-realm/package.json @@ -2,7 +2,7 @@ "$schema": "https://fbflipper.com/schemas/plugin-package/v2.json", "name": "realm-flipper-plugin", "id": "realm", - "version": "1.0.14", + "version": "1.1.0", "pluginType": "client", "description": "A Flipper Plugin to debug Realm applications.", "main": "dist/bundle.js", diff --git a/flipper-plugin-realm/src/CommonTypes.tsx b/flipper-plugin-realm/src/CommonTypes.tsx index 04d6bbd..67fd1b1 100644 --- a/flipper-plugin-realm/src/CommonTypes.tsx +++ b/flipper-plugin-realm/src/CommonTypes.tsx @@ -1,12 +1,26 @@ -// A helper interface which extends Realm.Object with -// a string index signature and object key field for reference. -export interface IndexableRealmObject extends Realm.Object { - [key: string]: unknown; - // Contains the object key that was sent by the device. - _pluginObjectKey: string; +import { AddLiveObjectRequest, AddObjectRequest, DownloadDataRequest, DeleteLiveObjectRequest, EditLiveObjectRequest, ModifyObjectRequest, GetObjectRequest, GetObjectsRequest, GetObjectsResponse, GetRealmsResponse, GetSchemasRequest, GetSchemasResponse, PlainRealmObject, ReceivedCurrentQueryRequest, RemoveObjectRequest, SerializedRealmObject, RealmObjectReference} from "./SharedTypes"; + + +/** + * A helper interface which wraps Realm.Object with + * information about its object type and key for reference. + * @see SerializedRealmObject +**/ +export interface DeserializedRealmObject extends RealmObjectReference { + // A plain representation of the Realm object + realmObject: PlainRealmObject; +} + +export interface DeserializedRealmData { + length: number; + info: [string, string, string]; } -// A Realm.CanonicalObjectSchema interface with a sorting order field. +export interface DeserializedRealmDecimal128 { + $numberDecimal: string +} + +/** A Realm.CanonicalObjectSchema interface with a sorting order field. */ export interface SortedObjectSchema extends Realm.CanonicalObjectSchema { order: string[]; } @@ -17,11 +31,13 @@ export interface CanonicalObjectSchemaPropertyRow primaryKey: boolean; } +export type DownloadDataFunction = (schema: string, objectKey: string, propertyName: string) => Promise; + export type RealmPluginState = { deviceSerial: string; realms: string[]; selectedRealm: string; - objects: IndexableRealmObject[]; + objects: DeserializedRealmObject[]; schemas: SortedObjectSchema[]; currentSchema: SortedObjectSchema | null; schemaHistory: SortedObjectSchema[]; @@ -37,113 +53,29 @@ export type RealmPluginState = { }; export type Events = { - getObjects: ObjectsMessage; - getSchemas: SchemaMessage; + getObjects: GetObjectsResponse; + getSchemas: GetSchemasResponse; liveObjectAdded: AddLiveObjectRequest; liveObjectDeleted: DeleteLiveObjectRequest; liveObjectEdited: EditLiveObjectRequest; getCurrentQuery: undefined; - getRealms: RealmsMessage; + getRealms: GetRealmsResponse; executeQuery: QueryResult; }; + export type Methods = { executeQuery: (query: QueryObject) => Promise; - getObjects: (data: getForwardsObjectsRequest) => Promise; - getSchemas: (data: RealmRequest) => Promise; - getRealms: () => Promise; - addObject: (object: AddObject) => Promise; - modifyObject: (newObject: EditObject) => Promise; - removeObject: (object: RemoveObject) => Promise; + getObjects: (data: GetObjectsRequest) => Promise; + getObject: (data: GetObjectRequest) => Promise; + getSchemas: (data: GetSchemasRequest) => Promise; + getRealms: () => Promise; + addObject: (object: AddObjectRequest) => Promise; + modifyObject: (newObject: ModifyObjectRequest) => Promise; + removeObject: (object: RemoveObjectRequest) => Promise; receivedCurrentQuery: (request: ReceivedCurrentQueryRequest) => Promise; - downloadData: (data: DataDownloadRequest) => Promise; -}; - -type ReceivedCurrentQueryRequest = { - schemaName: string | null; - realm: string; - sortingDirection: 'ascend' | 'descend' | null; - sortingColumn: string | null; -} - -type DataDownloadRequest = { - schemaName: string; - realm: string; - objectKey: string; - propertyName: string; -}; - -export type EditObject = { - schemaName?: string; - realm?: string; - object: Realm.Object; - propsChanged?: string[]; - objectKey: string; -}; - -export type RemoveObject = { - schemaName?: string; - realm?: string; - object: Realm.Object; - objectKey: string; -}; - -export type AddObject = { - schemaName?: string; - realm?: string; - object: Realm.Object; - propsChanged?: string[]; -}; -export type RealmsMessage = { - realms: string[]; - objects: Record[]; - total: number; -}; -export type ObjectsMessage = { - objects: IndexableRealmObject[]; - total: number; - nextCursor: string; - prev_cursor: { [sortingField: string]: number }; - hasMore: boolean; -}; -export type ObjectMessage = { - object: Realm.Object; -}; -export type SchemaMessage = { - schemas: Array; -}; -type RealmRequest = { - realm: string; -}; -type getForwardsObjectsRequest = { - schemaName: string; - realm: string; - cursor: string | null; - sortingColumn: string | null; - sortingDirection: 'ascend' | 'descend' | null; - query: string; + downloadData: (data: DownloadDataRequest) => Promise; }; -export type ObjectRequest = { - schemaName: string; - realm: string; - primaryKey: string; -}; -export type AddLiveObjectRequest = { - newObject: IndexableRealmObject; - index: number; - schemaName: string; - newObjectKey: string; -}; -export type DeleteLiveObjectRequest = { - index: number; - schemaName: string; -}; -export type EditLiveObjectRequest = { - newObject: IndexableRealmObject; - index: number; - schemaName: string; - newObjectKey: string; -}; type QueryObject = { schemaName: string; query: string; @@ -152,3 +84,30 @@ type QueryObject = { export type QueryResult = { result: Array | string; }; + + +export type MenuItem = { + key: number; + text: string; + onClick: () => void; +}; + +export type MenuItemGenerator = ( + row: DeserializedRealmObject, + schemaProperty: Realm.CanonicalObjectSchemaProperty, + schema: Realm.ObjectSchema, +) => Array; + +export type DropdownPropertyType = { + record: DeserializedRealmObject | null; + schemaProperty: Realm.CanonicalObjectSchemaProperty | null; + currentSchema: Realm.ObjectSchema; + visible: boolean; + pointerX: number; + pointerY: number; + scrollX: number; + scrollY: number; + generateMenuItems: MenuItemGenerator; +}; +export { AddLiveObjectRequest, AddObjectRequest, DownloadDataRequest, DeleteLiveObjectRequest, EditLiveObjectRequest, ModifyObjectRequest, GetObjectRequest, GetObjectsRequest, GetObjectsResponse, GetRealmsResponse, GetSchemasRequest, GetSchemasResponse, PlainRealmObject, ReceivedCurrentQueryRequest, RemoveObjectRequest, SerializedRealmObject, RealmObjectReference }; + diff --git a/flipper-plugin-realm/src/SharedTypes.tsx b/flipper-plugin-realm/src/SharedTypes.tsx new file mode 100644 index 0000000..4484a1b --- /dev/null +++ b/flipper-plugin-realm/src/SharedTypes.tsx @@ -0,0 +1,117 @@ +/** Types shared across the desktop plugin and the device library */ +export type PlainRealmObject = Record +/** + * An interface containing refereence information about a Realm object sent + * from the device plugin. + */ +export interface RealmObjectReference { + // The object key of the stored Realm object + objectKey: string; + objectType?: string; + } + +/** + * An interface for receiving and sending Realm Objects between + * the desktop plugin and the device. + * @see DeserializedRealmObject + **/ +export interface SerializedRealmObject extends RealmObjectReference { + // Result of serializaing a Realm object from flatted.toJSON(realmObject.toJSON()) + realmObject: any; +} + +export type ReceivedCurrentQueryRequest = { + schemaName: string | null; + realm: string; + sortingDirection: 'ascend' | 'descend' | null; + sortingColumn: string | null; +} + +export type DownloadDataRequest = { + schemaName: string; + realm: string; + objectKey: string; + propertyName: string; +}; + +export type ModifyObjectRequest = { + schemaName?: string; + realm?: string; + object: PlainRealmObject; + propsChanged?: string[]; + objectKey: string; +}; + +export type RemoveObjectRequest = { + schemaName?: string; + realm?: string; + object: PlainRealmObject; + objectKey: string; +}; + +export type AddObjectRequest = { + schemaName?: string; + realm?: string; + object: PlainRealmObject; + propsChanged?: string[]; +}; + +export type GetRealmsResponse = { + realms: string[]; + objects: Record[]; + total: number; +}; + +export type ObjectMessage = { + object: Realm.Object; +}; + +export type GetSchemasRequest = { + realm: string; +}; + +export type GetSchemasResponse = { + schemas: Array; +}; + +export type GetObjectRequest = { + schemaName: string; + realm: string; + objectKey: string; +}; + +export type GetObjectsRequest = { + schemaName: string; + realm: string; + cursor: string | null; + sortingColumn: string | null; + sortingDirection: 'ascend' | 'descend' | null; + query: string; +}; + +export type GetObjectsResponse = { + objects: SerializedRealmObject[]; + total: number; + nextCursor: string; + prev_cursor: { [sortingField: string]: number }; + hasMore: boolean; +}; + +export type AddLiveObjectRequest = { + newObject: SerializedRealmObject; + index: number; + schemaName: string; + newObjectKey: string; +}; + +export type DeleteLiveObjectRequest = { + index: number; + schemaName: string; +}; + +export type EditLiveObjectRequest = { + newObject: SerializedRealmObject; + index: number; + schemaName: string; + newObjectKey: string; +}; diff --git a/flipper-plugin-realm/src/components/CustomDropdown.tsx b/flipper-plugin-realm/src/components/CustomDropdown.tsx index 94c75c5..ee3935c 100644 --- a/flipper-plugin-realm/src/components/CustomDropdown.tsx +++ b/flipper-plugin-realm/src/components/CustomDropdown.tsx @@ -1,30 +1,6 @@ import React, { useState } from 'react'; import { theme } from 'flipper-plugin'; -import { IndexableRealmObject } from '../CommonTypes'; - -export type DropdownPropertyType = { - record: IndexableRealmObject | null; - schemaProperty: Realm.CanonicalObjectSchemaProperty | null; - currentSchema: Realm.ObjectSchema; - visible: boolean; - pointerX: number; - pointerY: number; - scrollX: number; - scrollY: number; - generateMenuItems: MenuItemGenerator; -}; - -type MenuItem = { - key: number; - text: string; - onClick: () => void; -}; - -export type MenuItemGenerator = ( - row: IndexableRealmObject, - schemaProperty: Realm.CanonicalObjectSchemaProperty, - schema: Realm.ObjectSchema, -) => Array; +import { DropdownPropertyType, MenuItem } from '../CommonTypes'; const listItem = (menuItem: MenuItem) => { const [hover, setHover] = useState(false); diff --git a/flipper-plugin-realm/src/components/DataTable.tsx b/flipper-plugin-realm/src/components/DataTable.tsx index feba9c9..6725a79 100644 --- a/flipper-plugin-realm/src/components/DataTable.tsx +++ b/flipper-plugin-realm/src/components/DataTable.tsx @@ -2,6 +2,7 @@ import { PlusOutlined } from '@ant-design/icons'; import { Button, Table } from 'antd'; import { ColumnsType, + ExpandableConfig, SorterResult, } from 'antd/lib/table/interface'; import { Layout, Spinner, usePlugin, useValue } from 'flipper-plugin'; @@ -11,8 +12,7 @@ import InfiniteScroll from 'react-infinite-scroller'; import { InspectionDataType } from './RealmDataInspector'; import { renderValue } from '../utils/Renderer'; import { ColumnTitle } from './ColumnTitle'; -import { MenuItemGenerator } from './CustomDropdown'; -import { IndexableRealmObject, SortedObjectSchema } from '../CommonTypes'; +import { DropdownPropertyType, MenuItemGenerator, PlainRealmObject, DeserializedRealmObject, SortedObjectSchema, RealmObjectReference } from '../CommonTypes'; export type ColumnType = { optional: boolean; @@ -22,19 +22,19 @@ export type ColumnType = { isPrimaryKey: boolean; }; + type DataTableProps = { - columns: ColumnType[]; - objects: IndexableRealmObject[]; + objects: DeserializedRealmObject[]; schemas: SortedObjectSchema[]; - currentSchema: Realm.CanonicalObjectSchema; + currentSchema: SortedObjectSchema; sortingDirection: 'ascend' | 'descend' | null; sortingColumn: string | null; generateMenuItems?: MenuItemGenerator; style?: Record; - setdropdownProp: Function; - dropdownProp: Object; - scrollX?: number; - scrollY?: number; + dropdownProp: DropdownPropertyType; + setdropdownProp: React.Dispatch>; + scrollX: number; + scrollY: number; enableSort: boolean; hasMore: boolean; totalObjects?: number; @@ -43,19 +43,21 @@ type DataTableProps = { inspectionData: InspectionDataType, wipeStacks?: boolean, ) => void; - clickAction?: (object: IndexableRealmObject) => void; + clickAction?: (object: DeserializedRealmObject) => void; + getObject: (object: RealmObjectReference, objectSchemaName: string) => Promise; }; -type ClickableTextType = { +type ClickableTextProps = { /** Content to be displayed for the given value. */ displayValue: string | number | JSX.Element; isLongString: boolean; - value: Record; + value: PlainRealmObject | RealmObjectReference; + isReference?: boolean; inspectorView: 'object' | 'property'; }; // Receives a schema and returns column objects for the table. -export const schemaObjToColumns = ( +const schemaObjectToColumns = ( schema: SortedObjectSchema, ): ColumnType[] => { return schema.order.map((propertyName) => { @@ -73,7 +75,6 @@ export const schemaObjToColumns = ( export const DataTable = (dataTableProps: DataTableProps) => { const { - columns, objects, schemas, currentSchema, @@ -88,6 +89,7 @@ export const DataTable = (dataTableProps: DataTableProps) => { totalObjects = 0, fetchMore = () => undefined, clickAction, + getObject, } = dataTableProps; const instance = usePlugin(plugin); const state = useValue(instance.state); @@ -107,7 +109,7 @@ export const DataTable = (dataTableProps: DataTableProps) => { return <>; }, showExpandColumn: false, - }); + } as ExpandableConfig); /** Hook to close the nested Table when clicked outside of it. */ useEffect(() => { @@ -119,7 +121,15 @@ export const DataTable = (dataTableProps: DataTableProps) => { }, []); if (!currentSchema) { - return Please select schema.; + return Please select schema.; + } + + if (currentSchema.embedded) { + return Embedded objects cannot be queried. Please view them from their parent schema or select a different schema.; + } + + if (!schemas || !schemas.length) { + return No schemas found. Check selected Realm.; } /** Functional component to render clickable text which opens the DataInspector.*/ @@ -128,7 +138,8 @@ export const DataTable = (dataTableProps: DataTableProps) => { isLongString, value, inspectorView, - }: ClickableTextType) => { + isReference = false, + }: ClickableTextProps) => { const [isHovering, setHovering] = useState(false); return (
@@ -139,7 +150,7 @@ export const DataTable = (dataTableProps: DataTableProps) => { textDecoration: isHovering ? 'underline' : undefined, }} onClick={() => { - setNewInspectionData({ data: value, view: inspectorView }, true); + setNewInspectionData({ data: value, view: inspectorView, isReference }, true); }} onMouseEnter={() => setHovering(true)} onMouseLeave={() => setHovering(false)} @@ -160,22 +171,22 @@ export const DataTable = (dataTableProps: DataTableProps) => { }; /** Definition of antd-specific columns. This constant is passed to the antd table as a property. */ - const antdColumns:ColumnsType = columns.map((column) => { + const antdColumns:ColumnsType = schemaObjectToColumns(currentSchema).map((column) => { const property: Realm.CanonicalObjectSchemaProperty = currentSchema.properties[column.name]; - + const linkedSchema = schemas.find( + (schema) => property && schema.name === property.objectType, + ); /* A function that is applied for every cell to specify what to render in each cell on top of the pure value specified in the 'dataSource' property of the antd table.*/ - const render = (value: IndexableRealmObject, row: IndexableRealmObject) => { + const render = (value: PlainRealmObject | RealmObjectReference, row: DeserializedRealmObject) => { /** Apply the renderValue function on the value in the cell to create a standard cell. */ - const cellValue = renderValue(value, property, schemas); - - const linkedSchema = schemas.find( - (schema) => schema.name === property.objectType, - ); + const cellValue = renderValue(value, property, schemas, instance.downloadData); - /** Render buttons to expand the row and a clickable text if the cell contains a linked Realm object. */ + /** Render buttons to expand the row and a clickable text if the cell contains a linked or embedded Realm object. */ if (value !== null && linkedSchema && property.type === 'object') { + const isEmbedded = linkedSchema.embedded; + return ( { gap: '5px', }} > - ) { } // Object already inserted - if (index < state.objects.length && state.objects[index]._pluginObjectKey == newObject._pluginObjectKey) { + if (index < state.objects.length && state.objects[index].objectKey == newObject.objectKey) { return; } const clone = structuredClone(newObject); const copyOfObjects = state.objects; - const addedObject = convertObjects( - [clone], + const addedObject = deserializeRealmObject( + clone, state.currentSchema, - downloadData, - )[0]; + ); copyOfObjects.splice(index, 0, addedObject); const newLastObject = copyOfObjects[copyOfObjects.length - 1]; pluginState.set({ ...state, objects: [...copyOfObjects], totalObjects: state.totalObjects + 1, - cursor: newLastObject._pluginObjectKey, + cursor: newLastObject.objectKey, }); }); @@ -126,7 +127,7 @@ export function plugin(client: PluginClient) { ...state, objects: state.objects, totalObjects: state.totalObjects - 1, - cursor: newLastObject ? newLastObject._pluginObjectKey : null, + cursor: newLastObject ? newLastObject.objectKey : null, }); }); @@ -140,22 +141,21 @@ export function plugin(client: PluginClient) { return; } // Edited object not at index. - if (state.objects[index]._pluginObjectKey != data.newObject._pluginObjectKey) { + if (state.objects[index].objectKey != data.newObject.objectKey) { return; } const clone = structuredClone(newObject); const copyOfObjects = state.objects; - const addedObject = convertObjects( - [clone], + const addedObject = deserializeRealmObject( + clone, state.currentSchema, - downloadData, - )[0]; + ); copyOfObjects.splice(index, 1, addedObject); const newLastObject = copyOfObjects[copyOfObjects.length - 1]; pluginState.set({ ...state, objects: [...copyOfObjects], - cursor: newLastObject._pluginObjectKey, + cursor: newLastObject.objectKey, }); }); @@ -167,7 +167,7 @@ export function plugin(client: PluginClient) { }); const getRealms = () => { - client.send('getRealms', undefined).then((realms: RealmsMessage) => { + client.send('getRealms', undefined).then((realms: GetRealmsResponse) => { const state = pluginState.get(); pluginState.set({ ...state, @@ -184,7 +184,7 @@ export function plugin(client: PluginClient) { toRestore?: Realm.Object[], cursor?: string | null, query?: string, - ): Promise => { + ): Promise => { const state = pluginState.get(); if (!state.currentSchema) { return Promise.reject(); @@ -199,6 +199,46 @@ export function plugin(client: PluginClient) { }); }; + const getObject = ( + realm: string, + schemaName: string, + objectKey: string, + ) => { + const state = pluginState.get(); + pluginState.set({ + ...state, + loading: true, + }); + return client.send('getObject', { + realm, + schemaName, + objectKey, + }).then( + (serializedObject: SerializedRealmObject) => { + const actualSchema = state.schemas.find((schema) => schema.name === schemaName); + if (!actualSchema) { + return null; + } + const deserializedObject = deserializeRealmObject(serializedObject, actualSchema); + return deserializedObject; + }, + (reason) => { + pluginState.set({ + ...state, + errorMessage: reason.message, + }); + return null; + }, + ) + .catch((error) => { + pluginState.set({ + ...state, + errorMessage: error.message, + }); + return null; + }); + }; + const getObjects = ( schemaName?: string | null, realm?: string | null, @@ -206,7 +246,7 @@ export function plugin(client: PluginClient) { cursor?: string | null, ) => { const state = pluginState.get(); - if (!state.currentSchema) { + if (!state.currentSchema || state.currentSchema.embedded) { return; } schemaName = schemaName ?? state.currentSchema.name; @@ -218,7 +258,7 @@ export function plugin(client: PluginClient) { }); requestObjects(schemaName, realm, toRestore, cursor) .then( - (response: ObjectsMessage) => { + (response: GetObjectsResponse) => { if (response.objects && !response.objects.length) { pluginState.set({ ...state, @@ -235,10 +275,9 @@ export function plugin(client: PluginClient) { if (!state.currentSchema || state.currentSchema?.name !== schemaName) { return; } - const objects = convertObjects( + const objects = deserializeRealmObjects( response.objects, state.currentSchema, - downloadData, ); pluginState.set({ ...state, @@ -285,7 +324,7 @@ export function plugin(client: PluginClient) { const getSchemas = (realm: string) => { client .send('getSchemas', { realm: realm }) - .then((schemaResult: SchemaMessage) => { + .then((schemaResult: GetSchemasResponse) => { const newSchemas = schemaResult.schemas.map((schema) => sortSchemaProperties(schema), ); @@ -315,7 +354,7 @@ export function plugin(client: PluginClient) { getObjects(); }; - const addObject = (object: Realm.Object) => { + const addObject = (object: PlainRealmObject) => { const state = pluginState.get(); if (!state.currentSchema) { return; @@ -404,16 +443,19 @@ export function plugin(client: PluginClient) { }); const modifyObject = ( - newObject: IndexableRealmObject, + newObject: DeserializedRealmObject, propsChanged: Set, ) => { + if(newObject.realmObject == undefined) { + return; + } const state = pluginState.get(); client .send('modifyObject', { realm: state.selectedRealm, schemaName: state.currentSchema?.name, - object: newObject, - objectKey: newObject._pluginObjectKey, + object: newObject.realmObject, + objectKey: newObject.objectKey, propsChanged: Array.from(propsChanged.values()), }) .catch((e: Error) => { @@ -424,7 +466,10 @@ export function plugin(client: PluginClient) { }); }; - const removeObject = (object: IndexableRealmObject) => { + const removeObject = (removedObject: DeserializedRealmObject) => { + if(removedObject.realmObject == undefined) { + return; + } const state = pluginState.get(); const schema = state.currentSchema; if (!schema) { @@ -433,8 +478,8 @@ export function plugin(client: PluginClient) { client.send('removeObject', { realm: state.selectedRealm, schemaName: schema.name, - object: object, - objectKey: object._pluginObjectKey, + object: removedObject.realmObject, + objectKey: removedObject.objectKey, }); }; @@ -501,6 +546,7 @@ export function plugin(client: PluginClient) { return { state: pluginState, + getObject, getObjects, getSchemas, executeQuery, @@ -517,6 +563,7 @@ export function plugin(client: PluginClient) { refreshState, clearError, requestObjects, + downloadData, }; } diff --git a/flipper-plugin-realm/src/pages/DataVisualizer.tsx b/flipper-plugin-realm/src/pages/DataVisualizer.tsx index 249c290..385f18e 100644 --- a/flipper-plugin-realm/src/pages/DataVisualizer.tsx +++ b/flipper-plugin-realm/src/pages/DataVisualizer.tsx @@ -3,13 +3,11 @@ import { usePlugin } from 'flipper-plugin'; import React, { useEffect, useRef, useState } from 'react'; import { CanonicalObjectSchemaProperty } from 'realm'; import { plugin } from '..'; -import { IndexableRealmObject, SortedObjectSchema } from '../CommonTypes'; +import { DropdownPropertyType, MenuItemGenerator, DeserializedRealmObject, SortedObjectSchema, PlainRealmObject, RealmObjectReference } from '../CommonTypes'; import { CustomDropdown, - DropdownPropertyType, - MenuItemGenerator, } from '../components/CustomDropdown'; -import { DataTable, schemaObjToColumns } from '../components/DataTable'; +import { DataTable } from '../components/DataTable'; import { FieldEdit } from '../components/objectManipulation/FieldEdit'; import { ObjectEdit } from '../components/objectManipulation/ObjectEdit'; import { @@ -17,8 +15,8 @@ import { RealmDataInspector, } from '../components/RealmDataInspector'; -type PropertyType = { - objects: Array; +type DataVisualizerProps = { + objects: Array; schemas: Array; currentSchema: SortedObjectSchema; sortingDirection: 'ascend' | 'descend' | null; @@ -26,7 +24,7 @@ type PropertyType = { hasMore: boolean; totalObjects?: number; enableSort: boolean; - clickAction?: (object: IndexableRealmObject) => void; + clickAction?: (object: DeserializedRealmObject) => void; fetchMore: () => void; handleDataInspector?: () => void; }; @@ -42,7 +40,7 @@ const DataVisualizer = ({ enableSort, clickAction, fetchMore, -}: PropertyType) => { +}: DataVisualizerProps) => { /** Hooks to manage the state of the DataInspector and open/close the sidebar. */ const [inspectionData, setInspectionData] = useState(); const [showSidebar, setShowSidebar] = useState(false); @@ -52,7 +50,7 @@ const DataVisualizer = ({ /** Hook to open/close the editing dialog and set its properties. */ const [editingObject, setEditingObject] = useState<{ editing: boolean; - object?: IndexableRealmObject; + object?: DeserializedRealmObject; // schemaProperty?: SchemaProperty; type?: 'field' | 'object'; fieldName?: string; @@ -60,18 +58,19 @@ const DataVisualizer = ({ editing: false, }); const pluginState = usePlugin(plugin); - const { removeObject } = pluginState; + const { removeObject, getObject } = pluginState; + const { selectedRealm } = pluginState.state.get(); /** refs to keep track of the current scrolling position for the context menu */ const scrollX = useRef(0); const scrollY = useRef(0); /** Functions for deleting and editing rows/objects */ - const deleteRow = (row: IndexableRealmObject) => { + const deleteRow = (row: DeserializedRealmObject) => { removeObject(row); }; const editField = ( - row: IndexableRealmObject, + row: DeserializedRealmObject, schemaProperty: CanonicalObjectSchemaProperty, ) => { setEditingObject({ @@ -81,7 +80,7 @@ const DataVisualizer = ({ fieldName: schemaProperty.name, }); }; - const editObject = (row: IndexableRealmObject) => { + const editObject = (row: DeserializedRealmObject) => { setEditingObject({ editing: true, object: row, @@ -91,7 +90,7 @@ const DataVisualizer = ({ /** Generate MenuItem objects for the context menu with all necessary data and functions.*/ const generateMenuItems: MenuItemGenerator = ( - row: IndexableRealmObject, + row: DeserializedRealmObject, schemaProperty: CanonicalObjectSchemaProperty, schema: Realm.ObjectSchema, ) => [ @@ -99,16 +98,13 @@ const DataVisualizer = ({ key: 1, text: 'Inspect Object', onClick: () => { - const object: IndexableRealmObject = Object(); - Object.keys(row).forEach((key) => { - object[key] = row[key]; - }); setNewInspectionData( { data: { - [schema.name]: object, + [schema.name]: row.realmObject, }, view: 'object', + isReference: false, }, true, ); @@ -118,22 +114,26 @@ const DataVisualizer = ({ key: 2, text: 'Inspect Property', onClick: () => { + const propertyValue = row.realmObject[schemaProperty.name] + //@ts-expect-error Property value should have objectKey if it has objectType. + const isReference = propertyValue && schemaProperty.objectType && propertyValue.objectKey setNewInspectionData( { - data: { + data: isReference ? propertyValue as RealmObjectReference : { [schema.name + '.' + schemaProperty.name]: - row[schemaProperty.name], + propertyValue, }, - view: 'property', + view: isReference ? 'object' : 'property', + isReference, }, true, ); + } }, - }, { key: 3, text: 'Edit Object', - onClick: () => editObject(row), + onClick: () => editObject(row) }, { key: 4, @@ -176,14 +176,6 @@ const DataVisualizer = ({ scrollY.current = scrollTop; }; - if (!currentSchema) { - return
Please select a schema.
; - } - - if (!schemas || !schemas.length) { - return
No schemas found. Check selected Realm.
; - } - /** Take the current dropdownProp and update it with the current x and y scroll values. This cannot be done with useState because it would cause too many rerenders.*/ const updatedDropdownProp = { @@ -237,7 +229,6 @@ const DataVisualizer = ({ /> ) : null} {return getObject(selectedRealm, schemaName, object.objectKey)}} /> {return getObject(selectedRealm, object.objectType!, object.objectKey)}} />
diff --git a/flipper-plugin-realm/src/pages/SchemaVisualizer.tsx b/flipper-plugin-realm/src/pages/SchemaVisualizer.tsx index 4323eee..e26cc4e 100644 --- a/flipper-plugin-realm/src/pages/SchemaVisualizer.tsx +++ b/flipper-plugin-realm/src/pages/SchemaVisualizer.tsx @@ -200,7 +200,6 @@ const SchemaVisualizer = ({ schemas, currentSchema }: InputType) => { 'optional', 'objectType', ]; - // eslint-disable-next-line @typescript-eslint/no-unused-vars const columnObjs = useMemoize((_) => createColumnConfig(), [columns]); const rows = createRows(currentSchema); diff --git a/flipper-plugin-realm/src/utils/ConvertFunctions.ts b/flipper-plugin-realm/src/utils/ConvertFunctions.ts index 8c59c8d..74acf4c 100644 --- a/flipper-plugin-realm/src/utils/ConvertFunctions.ts +++ b/flipper-plugin-realm/src/utils/ConvertFunctions.ts @@ -1,43 +1,38 @@ // type PropertyDescription -import { IndexableRealmObject } from '../CommonTypes'; +import { fromJSON } from 'flatted'; +import { DeserializedRealmObject, SerializedRealmObject } from '../CommonTypes'; -const convertObject = ( - object: IndexableRealmObject, +export const deserializeRealmObject = ( + receivedObject: SerializedRealmObject, schema: Realm.CanonicalObjectSchema, - downloadData: ( - schemaName: string, - objectKey: string, - propertyName: string, - ) => Promise, ) => { + if(receivedObject.realmObject == undefined) { + return receivedObject; + } const properties = schema.properties; - const newObj: IndexableRealmObject = Object(); - Object.keys(object).forEach((key) => { - const value = object[key]; - + const convertedObject: DeserializedRealmObject = { + objectKey: receivedObject.objectKey, + objectType: receivedObject.objectType, + realmObject: fromJSON(receivedObject.realmObject), + }; + Object.entries(convertedObject.realmObject).forEach(([key, value]) => { const property = properties[key]; if (property && property.type === 'data') { - newObj[key] = { + convertedObject.realmObject[key] = { length: (value as Record<'$binaryData', number>).$binaryData, - downloadData: () => - downloadData(schema.name, object._pluginObjectKey, property.name), + info: [schema.name, receivedObject.objectKey, property.name], }; } else { - newObj[key] = value; + convertedObject.realmObject[key] = value; } }); - return newObj; + return convertedObject; }; -export const convertObjects = ( - objects: IndexableRealmObject[], +export const deserializeRealmObjects = ( + serializedObjects: SerializedRealmObject[], schema: Realm.CanonicalObjectSchema, - downloadData: ( - schemaName: string, - objectKey: string, - propertyName: string, - ) => Promise, ) => { - return objects.map((v) => convertObject(v, schema, downloadData)); + return serializedObjects.map((object) => deserializeRealmObject(object, schema)); }; diff --git a/flipper-plugin-realm/src/utils/Renderer.tsx b/flipper-plugin-realm/src/utils/Renderer.tsx index aa4c2a2..3b0c8ab 100644 --- a/flipper-plugin-realm/src/utils/Renderer.tsx +++ b/flipper-plugin-realm/src/utils/Renderer.tsx @@ -3,7 +3,9 @@ import BooleanValue from '../components/BooleanValue'; import { Button, message, Typography } from 'antd'; import fileDownload from 'js-file-download'; import { CanonicalObjectSchema } from 'realm'; -import { IndexableRealmObject } from '../CommonTypes'; +import { DeserializedRealmData, DeserializedRealmDecimal128, DeserializedRealmObject, DownloadDataFunction } from '../CommonTypes'; +import { usePlugin } from 'flipper-plugin'; +import { plugin } from '../index'; type TypeDescription = { type: string; @@ -14,8 +16,9 @@ export const renderValue = ( value: unknown, property: TypeDescription, schemas: CanonicalObjectSchema[], + downloadData: DownloadDataFunction, inner?: boolean, -) => { +): JSX.Element | string | number => { if (value === null) { return inner ? 'null' : null; } @@ -27,49 +30,39 @@ export const renderValue = ( ); } let schema; - let returnValue: JSX.Element | string | number = ''; switch (property.type) { case 'string': - //@ts-expect-error These type errors are okay because the Realm data types guarantee type safety here. - returnValue = parseSimpleData(value); - break; case 'double': case 'int': case 'float': case 'objectId': case 'date': - case 'uuid': //@ts-expect-error - returnValue = parseSimpleData(value); - break; - case 'bool': //@ts-expect-error - returnValue = parseBoolean(value); - break; + case 'uuid': + return parseSimpleData(value as string | number); + case 'bool': + return parseBoolean(value as boolean); case 'list': - case 'set': //@ts-expect-error - returnValue = parseSetOrList(value, property, schemas); - break; - case 'data': //@ts-expect-error - returnValue = parseData(value); - break; - case 'dictionary': //@ts-expect-error - returnValue = parseDictionary(value); - break; - case 'decimal128': //@ts-expect-error - returnValue = parseDecimal128(value); - break; + case 'set': + return parseSetOrList(value as Realm.Set, property, schemas, downloadData); + case 'data': + return parseData(value as DeserializedRealmData, downloadData); + case 'dictionary': + return parseDictionary(value as Record); + case 'decimal128': + return parseDecimal128(value as DeserializedRealmDecimal128); case 'object': // eslint-disable-next-line @typescript-eslint/no-shadow schema = schemas.find((schema) => schema.name === property.objectType); - //@ts-expect-error - returnValue = parseLinkedObject(schema as Realm.ObjectSchema, value); - break; + if(schema?.embedded) { + return `[${schema.name}]` + } + return parseLinkedObject(schema as Realm.ObjectSchema, value as DeserializedRealmObject); case 'mixed': - returnValue = parseMixed(value); - break; + return parseMixed(value); + default: + return Unsupported type } - - return returnValue; }; function parseSimpleData(input: string | number): string | number { @@ -80,6 +73,7 @@ function parseSetOrList( input: Realm.Set | Realm.List, property: TypeDescription, schemas: Realm.CanonicalObjectSchema[], + downloadData: DownloadDataFunction, ): string { const output = input.map((value: unknown) => { // check if the container holds objects @@ -91,6 +85,7 @@ function parseSetOrList( objectType: property.objectType, }, schemas, + downloadData, true, ); } @@ -101,6 +96,7 @@ function parseSetOrList( type: property.objectType as string, }, schemas, + downloadData, true, ); }); @@ -112,23 +108,16 @@ function parseDictionary(input: Record): string { return JSON.stringify(input); } -function parseData(input: { - downloadData: () => Promise<{ data: Uint8Array }>; - length: number; -}) { - if (input.downloadData === undefined) { +function parseData(input: DeserializedRealmData, + downloadData: DownloadDataFunction, +) { + if (input.info === undefined) { return data; } - /* Structure of binary data: - input: { - downloadData: () => Promise<{ data: Uint8Array }>, - length, - } - */ const handleDownload = () => { - input.downloadData().then( + downloadData(input.info[0], input.info[1], input.info[2]).then( (res) => { - fileDownload(new Uint8Array(res.data).buffer, 'data'); + fileDownload(new Uint8Array(res).buffer, 'data'); }, (reason) => { message.error('downloading failed', reason.message); @@ -144,31 +133,27 @@ function parseBoolean(input: boolean): JSX.Element { return ; } -function parseDecimal128(input: { $numberDecimal: string }): string { +function parseDecimal128(input: DeserializedRealmDecimal128): string { return input.$numberDecimal ?? input; } function parseLinkedObject( schema: Realm.ObjectSchema, - linkedObj: IndexableRealmObject, + linkedObj: DeserializedRealmObject, ): string { - let returnValue = ''; const childSchema: Realm.ObjectSchema | undefined = schema; - if (childSchema.primaryKey !== undefined && childSchema !== undefined) { - returnValue = - '[' + + if (linkedObj.realmObject && childSchema.primaryKey !== undefined && childSchema !== undefined) { + return '[' + childSchema.name + ']' + '.' + childSchema.primaryKey + ': ' + - linkedObj[childSchema.primaryKey]; + linkedObj.realmObject[childSchema.primaryKey]; } else { - returnValue = - '[' + childSchema.name + ']._objectKey: ' + linkedObj._pluginObjectKey; + return '[' + childSchema.name + ']._objectKey: ' + linkedObj.objectKey; } - return returnValue; } function parseMixed(input: unknown): string | JSX.Element | number { diff --git a/realm-flipper-plugin-device/CHANGELOG.md b/realm-flipper-plugin-device/CHANGELOG.md new file mode 100644 index 0000000..506291d --- /dev/null +++ b/realm-flipper-plugin-device/CHANGELOG.md @@ -0,0 +1,14 @@ +## v1.1.0 + +This version brings a number of major changes to plugin's functionality, improving the ability to display more complex types of Realm objects. Please make sure to update your `realm-flipper-plugin` to the new version as well to ensure compatibility. + +### Enhancements +* Referenced objects are now lazy-loaded. By default, they will display their object key and type (just for internal reference) and their actual values can be seen when they are inspected. This brings performance improvements when loading many objects with references as well as better support for circular and deeply nested references. +* Embedded object support. + +### Fixed +* Plugin component crashing when trying to display embedded objects. + +### Compatibility +* `realm` >= v11 +* `realm-flipper-plugin` >= v1.1.0 diff --git a/realm-flipper-plugin-device/SharedTypes.tsx b/realm-flipper-plugin-device/SharedTypes.tsx new file mode 100644 index 0000000..714c424 --- /dev/null +++ b/realm-flipper-plugin-device/SharedTypes.tsx @@ -0,0 +1,121 @@ +/** Types shared across the desktop plugin and the device library */ +export type PlainRealmObject = Record +/** + * An interface containing refereence information about a Realm object sent + * from the device plugin. + */ +export interface RealmObjectReference { + // The object key of the stored Realm object + objectKey: string; + objectType?: string; + } + +/** + * An interface for receiving and sending Realm Objects between + * the desktop plugin and the device. + * @see DeserializedRealmObject + **/ +export interface SerializedRealmObject extends RealmObjectReference { + // Result of serializaing a Realm object from flatted.toJSON(realmObject.toJSON()) + realmObject: any; +} + +export type ReceivedCurrentQueryRequest = { + schemaName: string | null; + realm: string; + sortingDirection: 'ascend' | 'descend' | null; + sortingColumn: string | null; +} + +export type DownloadDataRequest = { + schemaName: string; + realm: string; + objectKey: string; + propertyName: string; +}; + +export type DownloadDataResponse = { + data: number[]; +}; + +export type ModifyObjectRequest = { + schemaName?: string; + realm?: string; + object: PlainRealmObject; + propsChanged?: string[]; + objectKey: string; +}; + +export type RemoveObjectRequest = { + schemaName?: string; + realm?: string; + object: PlainRealmObject; + objectKey: string; +}; + +export type AddObjectRequest = { + schemaName?: string; + realm?: string; + object: PlainRealmObject; + propsChanged?: string[]; +}; + +export type GetRealmsResponse = { + realms: string[]; + objects: Record[]; + total: number; +}; + +export type ObjectMessage = { + object: Realm.Object; +}; + +export type GetSchemasRequest = { + realm: string; +}; + +export type GetSchemasResponse = { + schemas: Array; +}; + +export type GetObjectRequest = { + schemaName: string; + realm: string; + objectKey: string; +}; + +export type GetObjectsRequest = { + schemaName: string; + realm: string; + cursor: string | null; + sortingColumn: string | null; + sortingDirection: 'ascend' | 'descend' | null; + query: string; +}; + +export type GetObjectsResponse = { + objects: SerializedRealmObject[]; + total: number; + nextCursor: string; + prev_cursor: { [sortingField: string]: number }; + hasMore: boolean; +}; + +export type AddLiveObjectRequest = { + newObject: SerializedRealmObject; + index: number; + schemaName: string; + newObjectKey: string; +}; + +export type DeleteLiveObjectRequest = { + index: number; + schemaName: string; +}; + +export type EditLiveObjectRequest = { + newObject: SerializedRealmObject; + index: number; + schemaName: string; + newObjectKey: string; +}; diff --git a/realm-flipper-plugin-device/package.json b/realm-flipper-plugin-device/package.json index 7d5f55a..3a2159e 100644 --- a/realm-flipper-plugin-device/package.json +++ b/realm-flipper-plugin-device/package.json @@ -1,6 +1,6 @@ { "name": "realm-flipper-plugin-device", - "version": "1.0.28", + "version": "1.1.0", "description": "Device code for interaction with the Realm Flipper Plugin", "main": "dist/index.js", "types": "src/RealmPlugin.tsx", @@ -39,7 +39,7 @@ "peerDependencies": { "react": ">=17", "react-native-flipper": ">=0.162.0", - "realm": ">=10.0.0" + "realm": ">=11.0.0" }, "dependencies": { "flatted": "^3.2.7" diff --git a/realm-flipper-plugin-device/src/ConvertFunctions.tsx b/realm-flipper-plugin-device/src/ConvertFunctions.tsx index c9729f7..17e2ea7 100644 --- a/realm-flipper-plugin-device/src/ConvertFunctions.tsx +++ b/realm-flipper-plugin-device/src/ConvertFunctions.tsx @@ -1,4 +1,3 @@ -// let JSObject = Object; import { BSON, CanonicalObjectSchema, @@ -6,69 +5,84 @@ import { Object as RealmObject, ObjectSchema, } from 'realm'; +import {toJSON} from 'flatted'; +import { PlainRealmObject, SerializedRealmObject } from '../SharedTypes'; -type PropertyDescription = { - type: string; - objectType?: string; -}; -// TODO: this function can probably be simplified as it largely just -// serializes object.toJSON() which supports circular relationships now. -const convertObjectToDesktop = ( - object: RealmObject, - properties: Realm.PropertiesTypes, -) => { - const obj = Object(); +/** Helper to recursively serialize Realm objects and embedded objects into plain JavaScript objects. */ +const serializeObject = (realmObject: RealmObject, objectSchema: Realm.ObjectSchema): Record => { + const properties = objectSchema.properties; + const jsonifiedObject = realmObject.toJSON(); + Object.keys(properties).forEach(key => { - const jsonifiedObject = object.toJSON(); - const property = properties[key] as PropertyDescription; - // make a copy of the object - obj[key] = jsonifiedObject[key]; + const property = properties[key]; + + //@ts-expect-error The field will exist on the Realm object + const propertyValue = realmObject[key]; + const propertyType = typeof property == "string" ? property : property.type; + const objectType = typeof property == "string" ? undefined : property.objectType; - if (property.type === 'object' && obj[key]) { - //@ts-expect-error We know the key exists - const objectKey = object[key]._objectKey(); - // Store object key as a seperate key for the plugin - obj[key]._pluginObjectKey = objectKey; + if (propertyValue) { + // Handle cases of property types where different information is needed than + // what is given from the default `toJSON` serialization. + switch(propertyType) { + case "set": + case "list": + // TODO: is there a better way to determine whether this is a list of objects? + if(objectType != "mixed" + && propertyValue && (propertyValue as any[]).length > 0 + && propertyValue[0].objectSchema) { + // let schema = propertyValue.objectSchema() as ObjectSchema; + jsonifiedObject[key] = (propertyValue as RealmObject[]).map( + (object) => {return {objectKey: object._objectKey(), objectType}}, + ) + } + break; + case "object": + const objectKey = propertyValue._objectKey(); + const isEmbedded = (propertyValue.objectSchema() as ObjectSchema).embedded + if (!isEmbedded) { + // If the object is linked (not embedded), store only the object key and type + // as a seperate key for later plugin lazy loading reference + jsonifiedObject[key] = {objectKey, objectType} as SerializedRealmObject; + } else { + jsonifiedObject[key] = serializeObject(propertyValue as Realm.Object, propertyValue.objectSchema()); + } + break; + case "data": + jsonifiedObject[key] = { + $binaryData: (propertyValue as Realm.Types.Data)?.byteLength, + } + break; + case "mixed": + // TODO: better mixed type support. This likely does not properly cover all scenarios. + if(propertyValue && propertyValue.objectSchema) { + jsonifiedObject[key] = serializeObject(propertyValue, propertyValue.objectSchema()); + } + break; + } } }); - const replacer = (key: keyof typeof properties, value: unknown) => { - if (!key) { - return value; - } - const property = properties[key] as PropertyDescription; - if (!property) { - return value; - } - if (property.type === 'data') { - return { - //@ts-expect-error Realm data type will have byteLength field. - $binaryData: value?.byteLength, - }; - } else if (property.type === 'mixed') { - return value; - } else { - return value; - } - }; + return jsonifiedObject; +} - let after; - try { - after = JSON.parse(JSON.stringify(obj, replacer)); - } catch (err) { - // a walkaround for #85 - return {}; - } - // save so that it's sent over -> serialization would remove a function - after._pluginObjectKey = object._objectKey(); - return after; +/** Serialized a given Realm Object into a SerializedRealmObject, providing circular dependency safe format. */ +export const serializeRealmObject = ( + realmObject: Realm.Object, + objectSchema: ObjectSchema, +): SerializedRealmObject => { + return { + objectKey: realmObject._objectKey(), + // flatted.toJSON is used to ensure circular objects can get stringified by flipper plugin. + realmObject: toJSON(serializeObject(realmObject, objectSchema)), + }; }; -export const convertObjectsToDesktop = ( +export const serializeRealmObjects = ( objects: RealmObject[], schema: ObjectSchema, -) => { - return objects.map(obj => convertObjectToDesktop(obj, schema.properties)); +):SerializedRealmObject[] => { + return objects.map(obj => serializeRealmObject(obj, schema)); }; /* @@ -77,7 +91,7 @@ the other way around complicated because we send entire inner objects if that's not the case, can be changed to shallow conversion of all the properties */ export const convertObjectsFromDesktop = ( - objects: RealmObject[], + objects: PlainRealmObject[], realm: Realm, schemaName?: string, ) => { @@ -97,7 +111,7 @@ const convertObjectFromDesktop = ( if (value === null) { return null; } - const objectKey = value._objectKey; + const objectKey = value.objectKey; if (objectKey !== undefined) { //@ts-expect-error _objectForObjectKey is not public. return realm._objectForObjectKey(objectType, objectKey); @@ -106,7 +120,9 @@ const convertObjectFromDesktop = ( const schema = realm.schema.find( schemaObj => schemaObj.name === objectType, ) as CanonicalObjectSchema; - + if(schema.embedded) { + return value; + } let primaryKey = object[schema.primaryKey as string]; if (schema.properties[schema.primaryKey as string].type === 'uuid') { primaryKey = new BSON.UUID(primaryKey); @@ -143,10 +159,9 @@ const convertObjectFromDesktop = ( switch (property.type) { case 'set': // due to a problem with serialization, Set is being passed over as a list - const realVal = (val as unknown[]).map(value => { + return (val as unknown[]).map(value => { return convertLeaf(value, property.objectType); }); - return realVal; case 'list': return val.map((obj: unknown) => { return convertLeaf(obj, property.objectType as string); diff --git a/realm-flipper-plugin-device/src/PluginConnectObjects.ts b/realm-flipper-plugin-device/src/PluginConnectObjects.ts index 41a5eb6..83aad4c 100644 --- a/realm-flipper-plugin-device/src/PluginConnectObjects.ts +++ b/realm-flipper-plugin-device/src/PluginConnectObjects.ts @@ -1,5 +1,5 @@ import {Flipper} from 'react-native-flipper'; -import {convertObjectsToDesktop} from './ConvertFunctions'; +import {serializeRealmObjects} from './ConvertFunctions'; // Attaches a listener to the collections which sends update // notifications to the Flipper plugin. @@ -62,13 +62,12 @@ export class PluginConnectedObjects { const schema = this.schemas.find( (schemaObj: Realm.ObjectSchema) => this.schemaName === schemaObj.name, ); - const converted = convertObjectsToDesktop([inserted], schema)[0]; + const converted = serializeRealmObjects([inserted], schema)[0]; if (this.connection) { this.connection.send('liveObjectAdded', { newObject: converted, index: index, schemaName: this.schemaName, - objects: objects, }); this.connection.send('getCurrentQuery', undefined); } @@ -79,7 +78,7 @@ export class PluginConnectedObjects { const schema = this.schemas.find( schemaObj => this.schemaName === schemaObj.name, ); - const converted = convertObjectsToDesktop([modified], schema)[0]; + const converted = serializeRealmObjects([modified], schema)[0]; if (this.connection) { this.connection.send('liveObjectEdited', { newObject: converted, diff --git a/realm-flipper-plugin-device/src/RealmPlugin.tsx b/realm-flipper-plugin-device/src/RealmPlugin.tsx index 1c1a248..0fdd49c 100644 --- a/realm-flipper-plugin-device/src/RealmPlugin.tsx +++ b/realm-flipper-plugin-device/src/RealmPlugin.tsx @@ -1,19 +1,20 @@ import React, {useEffect, useRef} from 'react'; import {addPlugin} from 'react-native-flipper'; import type Realm from 'realm'; +import { AddObjectRequest, DownloadDataRequest, DownloadDataResponse, GetObjectRequest, GetObjectsResponse, GetRealmsResponse, GetSchemasRequest, GetSchemasResponse, ModifyObjectRequest, ReceivedCurrentQueryRequest, RemoveObjectRequest, SerializedRealmObject } from '../SharedTypes'; import { convertObjectsFromDesktop, - convertObjectsToDesktop, + serializeRealmObject, + serializeRealmObjects, } from './ConvertFunctions'; import {PluginConnectedObjects} from './PluginConnectObjects'; -type getObjectsQuery = { +type GetObjectsRequest = { schemaName: string; realm: string; - cursor: string; - limit: number; - sortingDirection: 'ascend' | 'descend'; - sortingColumn: string; + cursor: string | null; + sortingColumn: string | null; + sortingDirection: 'ascend' | 'descend' | null; query: string; }; @@ -34,19 +35,19 @@ const RealmPlugin = React.memo((props: {realms: Realm[]}) => { onConnect(connection) { connection.send('getCurrentQuery', undefined); - connection.receive('receivedCurrentQuery', obj => { - const realm = realmsMap.get(obj.realm); - if (!realm || !obj.schemaName) { + connection.receive('receivedCurrentQuery', (req:ReceivedCurrentQueryRequest) => { + const realm = realmsMap.get(req.realm); + if (!realm || !req.schemaName) { return; } if (connectedObjects != null) { connectedObjects.removeListener(); } connectedObjects = new PluginConnectedObjects( - realm.objects(obj.schemaName), - obj.schemaName, - obj.sortingColumn, - obj.sortingDirection, + realm.objects(req.schemaName), + req.schemaName, + req.sortingColumn, + req.sortingDirection, connection, realm.schema, ); @@ -55,10 +56,37 @@ const RealmPlugin = React.memo((props: {realms: Realm[]}) => { connection.receive('getRealms', (_, responder) => { responder.success({ realms: Array.from(realmsMap.keys()), - }); + } as GetRealmsResponse); + }); + + connection.receive('getObject', (req: GetObjectRequest, responder) => { + const realm = realmsMap.get(req.realm); + if (!realm) { + responder.error({message: 'No realm found'}); + return; + } + const {schemaName, objectKey} = req; + let objects = realm.objects(schemaName); + const totalObjects = objects.length; + if (!totalObjects || objects.isEmpty()) { + responder.error({message: `No objects found in selected schema "${schemaName}".`}); + return; + } + let requestedObject = objects.find( + req => req._objectKey() === objectKey, + ); + if(requestedObject == undefined) { + responder.error({message: `Object with object key: "${objectKey}" not found.`}); + return; + } + const serializedObject = serializeRealmObject( + requestedObject, + requestedObject.objectSchema(), + ); + responder.success(serializedObject as SerializedRealmObject); }); - connection.receive('getObjects', (req: getObjectsQuery, responder) => { + connection.receive('getObjects', (req: GetObjectsRequest, responder) => { const realm = realmsMap.get(req.realm); if (!realm) { responder.error({message: 'No realm found'}); @@ -84,7 +112,7 @@ const RealmPlugin = React.memo((props: {realms: Realm[]}) => { total: totalObjects, hasMore: false, nextCursor: null, - }); + } as GetObjectsResponse); return; } let queryCursor = null; @@ -99,7 +127,7 @@ const RealmPlugin = React.memo((props: {realms: Realm[]}) => { //@ts-expect-error This is not a method which is exposed publically const firstObject = realm._objectForObjectKey(schemaName, queryCursor); //First object to send let indexOfFirstObject = objects.findIndex( - obj => obj._objectKey() === firstObject._objectKey(), + realmObject => realmObject._objectKey() === firstObject._objectKey(), ); if (query) { //Filtering if RQL query is provided @@ -119,7 +147,7 @@ const RealmPlugin = React.memo((props: {realms: Realm[]}) => { : indexOfFirstObject + 1, indexOfFirstObject + (LIMIT + 1), ); - const afterConversion = convertObjectsToDesktop( + const afterConversion = serializeRealmObjects( slicedObjects, realm.schema.find( convertedSchema => convertedSchema.name === schemaName, @@ -130,45 +158,45 @@ const RealmPlugin = React.memo((props: {realms: Realm[]}) => { total: totalObjects, hasMore: objects.length >= LIMIT, nextCursor: objects[objects.length - 1]?._objectKey(), - }); + } as GetObjectsResponse); }); - connection.receive('getSchemas', (obj, responder) => { - const realm = realmsMap.get(obj.realm); + connection.receive('getSchemas', (req:GetSchemasRequest, responder) => { + const realm = realmsMap.get(req.realm); if (!realm) { responder.error({message: 'No realm found,'}); return; } const schemas = realm.schema; - responder.success({schemas: schemas}); + responder.success({schemas: schemas} as GetSchemasResponse); }); - connection.receive('downloadData', (obj, responder) => { - const realm = realmsMap.get(obj.realm); + connection.receive('downloadData', (req:DownloadDataRequest, responder) => { + const realm = realmsMap.get(req.realm); if (!realm) { responder.error({message: 'Realm not found'}); return; } //@ts-expect-error This is not a method which is exposed publically - const object = realm._objectForObjectKey(obj.schemaName, obj.objectKey); + const object = realm._objectForObjectKey(req.schemaName, req.objectKey); responder.success({ - data: Array.from(new Uint8Array(object[obj.propertyName])), - }); + data: Array.from(new Uint8Array(object[req.propertyName])), + } as DownloadDataResponse); }); - connection.receive('addObject', (obj, responder) => { - const realm = realmsMap.get(obj.realm); + connection.receive('addObject', (req:AddObjectRequest, responder) => { + const realm = realmsMap.get(req.realm); if (!realm) { return; } const converted = convertObjectsFromDesktop( - [obj.object], + [req.object], realm, - obj.schemaName, + req.schemaName, )[0]; try { realm.write(() => { - realm.create(obj.schemaName, converted); + realm.create(req.schemaName, converted); }); } catch (err) { responder.error({ @@ -178,26 +206,26 @@ const RealmPlugin = React.memo((props: {realms: Realm[]}) => { } responder.success(undefined); }); - connection.receive('modifyObject', (obj, responder) => { - const realm = realmsMap.get(obj.realm); + connection.receive('modifyObject', (req:ModifyObjectRequest, responder) => { + const realm = realmsMap.get(req.realm); if (!realm) { return; } - const propsChanged = obj.propsChanged; + const propsChanged = req.propsChanged; const schema = realm.schema.find( - schemaObj => schemaObj.name === obj.schema, + schemaObj => schemaObj.name === req.schemaName, ) as Realm.CanonicalObjectSchema; const converted: Record = convertObjectsFromDesktop( - [obj.object], + [req.object], realm, - obj.schemaName, + req.schemaName, )[0]; //@ts-expect-error This is not a method which is exposed publically const realmObj = realm._objectForObjectKey( schema.name, - obj.objectKey, + req.objectKey, ); if (!realmObj) { responder.error({message: 'Realm Object removed while editing.'}); @@ -211,16 +239,16 @@ const RealmPlugin = React.memo((props: {realms: Realm[]}) => { }); }); - connection.receive('removeObject', obj => { - const realm = realmsMap.get(obj.realm); + connection.receive('removeObject', (req:RemoveObjectRequest) => { + const realm = realmsMap.get(req.realm); if (!realm) { return; } //@ts-expect-error This is not a method which is exposed publically const foundObject = realm._objectForObjectKey( - obj.schemaName, - obj.objectKey, + req.schemaName, + req.objectKey, ); realm.write(() => { realm.delete(foundObject); diff --git a/testApp/app/components/LoginScreen.tsx b/testApp/app/components/LoginScreen.tsx index 7eb1956..81fb0b5 100644 --- a/testApp/app/components/LoginScreen.tsx +++ b/testApp/app/components/LoginScreen.tsx @@ -58,7 +58,7 @@ export const LoginScreen = () => { style={styles.input} value={email} onChangeText={setEmail} - autoCompleteType="email" + autoComplete="email" textContentType="emailAddress" autoCapitalize="none" autoCorrect={false} @@ -71,7 +71,7 @@ export const LoginScreen = () => { value={password} onChangeText={setPassword} secureTextEntry - autoCompleteType="password" + autoComplete="password" textContentType="password" placeholder="Password" /> diff --git a/testApp/app/flipperTest/FlipperTestAppNonSync.tsx b/testApp/app/flipperTest/FlipperTestAppNonSync.tsx index 731ca16..bd69314 100644 --- a/testApp/app/flipperTest/FlipperTestAppNonSync.tsx +++ b/testApp/app/flipperTest/FlipperTestAppNonSync.tsx @@ -15,7 +15,7 @@ export default function FlipperTestAppNonSync() { const secondRealm = FlipperTestSecondRealmContext.useRealm(); const templateAppRealm = TaskRealmContext.useRealm(); - const [isLegacyTester, setIsLegacyTester] = useState(false); + const [isLegacyTester, setIsLegacyTester] = useState(true); const toggleSwitch = () => setIsLegacyTester(previousState => !previousState); const textStyle = { color: '#fff', diff --git a/testApp/app/flipperTest/LegacyTestView.tsx b/testApp/app/flipperTest/LegacyTestView.tsx index 211bd95..430a465 100644 --- a/testApp/app/flipperTest/LegacyTestView.tsx +++ b/testApp/app/flipperTest/LegacyTestView.tsx @@ -17,6 +17,7 @@ import { FlipperTestSecondRealmContext, } from './Schemas'; import {createParcelTestData} from './testData/createParcelTestData'; +import {createCornerCaseData} from './testData/createCornerCaseData'; function createBanana(realm: Realm) { let banana = realm.write(() => { @@ -79,6 +80,10 @@ const LegacyTestView = () => { title="Delete + create Parcel Testdata" onPress={() => createParcelTestData(realm2)} /> +