realm-flipper-plugin-device
Version:
Device code for interaction with the Realm Flipper Plugin
418 lines (413 loc) • 20 kB
JavaScript
import React, { useRef, useEffect } from 'react';
import { addPlugin } from 'react-native-flipper';
import { BSON } from 'realm';
import { toJSON } from 'flatted';
/** Helper to recursively serialize Realm objects and embedded objects into plain JavaScript objects. */
var serializeObject = function (realmObject, objectSchema) {
var properties = objectSchema.properties;
var jsonifiedObject = realmObject.toJSON();
Object.keys(properties).forEach(function (key) {
var property = properties[key];
//@ts-expect-error The field will exist on the Realm object
var propertyValue = realmObject[key];
var propertyType = typeof property == "string" ? property : property.type;
var objectType = typeof property == "string" ? undefined : property.objectType;
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.length > 0
&& propertyValue[0].objectSchema) {
// let schema = propertyValue.objectSchema() as ObjectSchema;
jsonifiedObject[key] = propertyValue.map(function (object) { return { objectKey: object._objectKey(), objectType: objectType }; });
}
break;
case "object":
var objectKey = propertyValue._objectKey();
var isEmbedded = propertyValue.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: objectKey, objectType: objectType };
}
else {
jsonifiedObject[key] = serializeObject(propertyValue, propertyValue.objectSchema());
}
break;
case "data":
jsonifiedObject[key] = {
$binaryData: propertyValue === null || propertyValue === void 0 ? void 0 : propertyValue.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;
}
}
});
return jsonifiedObject;
};
/** Serialized a given Realm Object into a SerializedRealmObject, providing circular dependency safe format. */
var serializeRealmObject = function (realmObject, objectSchema) {
return {
objectKey: realmObject._objectKey(),
// flatted.toJSON is used to ensure circular objects can get stringified by flipper plugin.
realmObject: toJSON(serializeObject(realmObject, objectSchema)),
};
};
var serializeRealmObjects = function (objects, schema) {
return objects.map(function (obj) { return serializeRealmObject(obj, schema); });
};
/*
OBJECT FROM DESKTOP TO REALM TYPES
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
*/
var convertObjectsFromDesktop = function (objects, realm, schemaName) {
return objects.map(function (obj) { return convertObjectFromDesktop(obj, realm, schemaName); });
};
// convert object from a schema to realm one
var convertObjectFromDesktop = function (object, realm, schemaName) {
delete object._objectKey;
if (!schemaName) {
throw new Error('Converting with missing schema name');
}
var readObject = function (objectType, value) {
if (value === null) {
return null;
}
var objectKey = value.objectKey;
if (objectKey !== undefined) {
//@ts-expect-error _objectForObjectKey is not public.
return realm._objectForObjectKey(objectType, objectKey);
}
// have to use primary key, walkaround for #105
var schema = realm.schema.find(function (schemaObj) { return schemaObj.name === objectType; });
if (schema.embedded) {
return value;
}
var primaryKey = object[schema.primaryKey];
if (schema.properties[schema.primaryKey].type === 'uuid') {
primaryKey = new BSON.UUID(primaryKey);
}
return realm.objectForPrimaryKey(objectType, primaryKey);
};
var convertLeaf = function (value, type, objectType) {
if (realm.schema.some(function (schemaObj) { return schemaObj.name === type; })) {
return readObject(type, value);
}
switch (type) {
case 'object':
return readObject(objectType, value);
case 'uuid':
return new BSON.UUID(value);
case 'decimal128':
return new BSON.Decimal128(value);
case 'objectId':
return new BSON.ObjectId(value);
case 'data':
var arr = new Uint8Array(value);
return arr;
default:
return value;
}
};
var convertRoot = function (val, property) {
if (val === null || property === undefined) {
return null;
}
switch (property.type) {
case 'set':
// due to a problem with serialization, Set is being passed over as a list
return val.map(function (value) {
return convertLeaf(value, property.objectType);
});
case 'list':
return val.map(function (obj) {
return convertLeaf(obj, property.objectType);
});
case 'dictionary':
var res_1 = {};
Object.keys(val).forEach(function (key) {
res_1[key] = convertLeaf(val[key], property.objectType);
});
return res_1;
case 'object':
return readObject(property.objectType, val);
default:
return convertLeaf(val, property.type, property.objectType);
}
};
var schemaObj = realm.schema.find(function (schema) { return schema.name === schemaName; });
var obj = Object();
Object.entries(object).forEach(function (value) {
var type = schemaObj.properties[value[0]];
obj[value[0]] = convertRoot(value[1], type);
});
return obj;
};
// Attaches a listener to the collections which sends update
// notifications to the Flipper plugin.
var PluginConnectedObjects = /** @class */ (function () {
function PluginConnectedObjects(objects, schemaName, sortingColumn, sortingDirection, connection, schemas) {
var _this = this;
this.onObjectsChange = function (objects, changes) {
changes.deletions.forEach(function (index) {
if (_this.connection) {
_this.connection.send('liveObjectDeleted', {
index: index,
schemaName: _this.schemaName,
});
_this.connection.send('getCurrentQuery', undefined);
}
});
changes.insertions.forEach(function (index) {
var inserted = objects[index];
var schema = _this.schemas.find(function (schemaObj) { return _this.schemaName === schemaObj.name; });
var converted = serializeRealmObjects([inserted], schema)[0];
if (_this.connection) {
_this.connection.send('liveObjectAdded', {
newObject: converted,
index: index,
schemaName: _this.schemaName,
});
_this.connection.send('getCurrentQuery', undefined);
}
});
changes.newModifications.forEach(function (index) {
var modified = objects[index];
var schema = _this.schemas.find(function (schemaObj) { return _this.schemaName === schemaObj.name; });
var converted = serializeRealmObjects([modified], schema)[0];
if (_this.connection) {
_this.connection.send('liveObjectEdited', {
newObject: converted,
index: index,
schemaName: _this.schemaName,
});
_this.connection.send('getCurrentQuery', undefined);
}
});
};
this.schemaName = schemaName;
this.objects = objects;
var shouldSortDescending = sortingDirection === 'descend';
if (sortingColumn) {
this.connectedObjects = this.objects.sorted(sortingColumn, shouldSortDescending);
}
else {
this.connectedObjects = this.objects;
}
this.sortingColumn = sortingColumn;
this.sortingDirection = sortingDirection;
this.connection = connection;
this.schemas = schemas;
this.connectedObjects.addListener(this.onObjectsChange);
}
PluginConnectedObjects.prototype.removeListener = function () {
this.connectedObjects.removeListener(this.onObjectsChange);
};
return PluginConnectedObjects;
}());
var RealmPlugin = React.memo(function (props) {
var realms = useRef(props.realms);
useEffect(function () {
var connectedObjects = null;
var realmsMap = new Map();
realms.current.forEach(function (realm) {
realmsMap.set(realm.path, realm);
});
addPlugin({
getId: function () {
return 'realm';
},
onConnect: function (connection) {
connection.send('getCurrentQuery', undefined);
connection.receive('receivedCurrentQuery', function (req) {
var realm = realmsMap.get(req.realm);
if (!realm || !req.schemaName) {
return;
}
if (connectedObjects != null) {
connectedObjects.removeListener();
}
connectedObjects = new PluginConnectedObjects(realm.objects(req.schemaName), req.schemaName, req.sortingColumn, req.sortingDirection, connection, realm.schema);
});
connection.receive('getRealms', function (_, responder) {
responder.success({
realms: Array.from(realmsMap.keys()),
});
});
connection.receive('getObject', function (req, responder) {
var realm = realmsMap.get(req.realm);
if (!realm) {
responder.error({ message: 'No realm found' });
return;
}
var schemaName = req.schemaName, objectKey = req.objectKey;
var objects = realm.objects(schemaName);
var totalObjects = objects.length;
if (!totalObjects || objects.isEmpty()) {
responder.error({ message: "No objects found in selected schema \"".concat(schemaName, "\".") });
return;
}
var requestedObject = objects.find(function (req) { return req._objectKey() === objectKey; });
if (requestedObject == undefined) {
responder.error({ message: "Object with object key: \"".concat(objectKey, "\" not found.") });
return;
}
var serializedObject = serializeRealmObject(requestedObject, requestedObject.objectSchema());
responder.success(serializedObject);
});
connection.receive('getObjects', function (req, responder) {
var _a;
var realm = realmsMap.get(req.realm);
if (!realm) {
responder.error({ message: 'No realm found' });
return;
}
var schemaName = req.schemaName, sortingColumn = req.sortingColumn, sortingDirection = req.sortingDirection, query = req.query, cursor = req.cursor;
var objects = realm.objects(schemaName);
if (connectedObjects != null) {
connectedObjects.removeListener();
}
connectedObjects = new PluginConnectedObjects(objects, schemaName, sortingColumn, sortingDirection, connection, realm.schema);
var totalObjects = objects.length;
if (!totalObjects || objects.isEmpty()) {
responder.success({
objects: [],
total: totalObjects,
hasMore: false,
nextCursor: null,
});
return;
}
var queryCursor = null;
var LIMIT = 50;
var shouldSortDescending = sortingDirection === 'descend';
queryCursor = cursor !== null && cursor !== void 0 ? cursor : objects[0]._objectKey(); //If no cursor, choose first object in the collection
if (sortingColumn) {
objects = objects.sorted(sortingColumn, shouldSortDescending);
queryCursor = cursor !== null && cursor !== void 0 ? cursor : objects[0]._objectKey();
}
//@ts-expect-error This is not a method which is exposed publically
var firstObject = realm._objectForObjectKey(schemaName, queryCursor); //First object to send
var indexOfFirstObject = objects.findIndex(function (req) { return req._objectKey() === firstObject._objectKey(); });
if (query) {
//Filtering if RQL query is provided
try {
objects = objects.filtered(query);
}
catch (e) {
responder.error({
message: e.message,
});
return;
}
}
var slicedObjects = objects.slice(
//Send over list from index of first object to the limit
indexOfFirstObject === 0
? indexOfFirstObject
: indexOfFirstObject + 1, indexOfFirstObject + (LIMIT + 1));
var afterConversion = serializeRealmObjects(slicedObjects, realm.schema.find(function (convertedSchema) { return convertedSchema.name === schemaName; }));
responder.success({
objects: afterConversion,
total: totalObjects,
hasMore: objects.length >= LIMIT,
nextCursor: (_a = objects[objects.length - 1]) === null || _a === void 0 ? void 0 : _a._objectKey(),
});
});
connection.receive('getSchemas', function (req, responder) {
var realm = realmsMap.get(req.realm);
if (!realm) {
responder.error({ message: 'No realm found,' });
return;
}
var schemas = realm.schema;
responder.success({ schemas: schemas });
});
connection.receive('downloadData', function (req, responder) {
var 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
var object = realm._objectForObjectKey(req.schemaName, req.objectKey);
responder.success({
data: Array.from(new Uint8Array(object[req.propertyName])),
});
});
connection.receive('addObject', function (req, responder) {
var realm = realmsMap.get(req.realm);
if (!realm) {
return;
}
var converted = convertObjectsFromDesktop([req.object], realm, req.schemaName)[0];
try {
realm.write(function () {
realm.create(req.schemaName, converted);
});
}
catch (err) {
responder.error({
error: err.message,
});
return;
}
responder.success(undefined);
});
connection.receive('modifyObject', function (req, responder) {
var realm = realmsMap.get(req.realm);
if (!realm) {
return;
}
var propsChanged = req.propsChanged;
var schema = realm.schema.find(function (schemaObj) { return schemaObj.name === req.schemaName; });
var converted = convertObjectsFromDesktop([req.object], realm, req.schemaName)[0];
//@ts-expect-error This is not a method which is exposed publically
var realmObj = realm._objectForObjectKey(schema.name, req.objectKey);
if (!realmObj) {
responder.error({ message: 'Realm Object removed while editing.' });
return;
}
realm.write(function () {
propsChanged.forEach(function (propName) {
realmObj[propName] = converted[propName];
});
});
});
connection.receive('removeObject', function (req) {
var realm = realmsMap.get(req.realm);
if (!realm) {
return;
}
//@ts-expect-error This is not a method which is exposed publically
var foundObject = realm._objectForObjectKey(req.schemaName, req.objectKey);
realm.write(function () {
realm.delete(foundObject);
});
});
},
onDisconnect: function () {
if (connectedObjects) {
connectedObjects.removeListener();
}
},
});
return function () {
if (connectedObjects) {
connectedObjects.removeListener();
}
};
}, []);
return React.createElement(React.Fragment, null);
});
export { RealmPlugin, RealmPlugin as default };