From e275f353c1b226d582db3d9bc67a6d7bd5132029 Mon Sep 17 00:00:00 2001 From: Evan Morikawa Date: Tue, 8 Dec 2015 16:11:22 -0500 Subject: [PATCH] fix(json): serialize blob data Summary: A fix to reserialize JSON blob data properly for complex object types. We should investigate overriding all of JSON.parse and JSON.stringify. This coming in a future diff. Also we were using old Electron APIs that were throwing backend errors Test Plan: todo Reviewers: juan, bengotow Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D2326 --- spec/n1-spec-reporter.coffee | 2 +- src/browser/application.coffee | 5 ++ src/flux/attributes.coffee | 3 + src/flux/attributes/attribute-object.coffee | 9 ++- .../attribute-serialized-objects.coffee | 21 +++++++ src/flux/models/json-blob-query.coffee | 8 +++ src/flux/models/json-blob.coffee | 2 +- src/flux/models/query.coffee | 18 +++--- src/flux/models/utils.coffee | 56 ++++++++++--------- src/flux/stores/database-store.coffee | 5 +- src/nylas-env.coffee | 8 +-- src/window-thin-bootstrap.coffee | 2 +- 12 files changed, 92 insertions(+), 47 deletions(-) create mode 100644 src/flux/attributes/attribute-serialized-objects.coffee create mode 100644 src/flux/models/json-blob-query.coffee diff --git a/spec/n1-spec-reporter.coffee b/spec/n1-spec-reporter.coffee index a0598c09e..d37a3e5a7 100644 --- a/spec/n1-spec-reporter.coffee +++ b/spec/n1-spec-reporter.coffee @@ -83,7 +83,7 @@ class N1SpecReporter extends View @on 'click', '.stack-trace', -> $(this).toggleClass('expanded') - @reloadButton.on 'click', -> require('electron').ipcRenderer.send('call-window-method', 'restart') + @reloadButton.on 'click', -> require('electron').ipcRenderer.send('call-webcontents-method', 'reload') reportRunnerResults: (runner) -> @updateSpecCounts() diff --git a/src/browser/application.coffee b/src/browser/application.coffee index a94ca2999..be91d63c7 100644 --- a/src/browser/application.coffee +++ b/src/browser/application.coffee @@ -364,6 +364,11 @@ class Application ipcMain.on 'call-webcontents-method', (event, method, args...) -> event.sender[method](args...) + ipcMain.on 'call-devtools-webcontents-method', (event, method, args...) -> + # If devtools aren't open the `webContents::devToolsWebContents` + # will be null + event.sender.devToolsWebContents?[method](args...) + ipcMain.on 'action-bridge-rebroadcast-to-all', (event, args...) => win = BrowserWindow.fromWebContents(event.sender) @windowManager.windows().forEach (nylasWindow) -> diff --git a/src/flux/attributes.coffee b/src/flux/attributes.coffee index f790962be..0b8492a8e 100644 --- a/src/flux/attributes.coffee +++ b/src/flux/attributes.coffee @@ -9,6 +9,7 @@ AttributeDateTime = require './attributes/attribute-datetime' AttributeCollection = require './attributes/attribute-collection' AttributeJoinedData = require './attributes/attribute-joined-data' AttributeServerId = require './attributes/attribute-serverid' +AttributeSerializedObjects = require './attributes/attribute-serialized-objects' module.exports = Matcher: Matcher @@ -22,6 +23,7 @@ module.exports = Collection: -> new AttributeCollection(arguments...) JoinedData: -> new AttributeJoinedData(arguments...) ServerId: -> new AttributeServerId(arguments...) + SerializedObjects: -> new AttributeSerializedObjects(arguments...) AttributeNumber: AttributeNumber AttributeString: AttributeString @@ -31,3 +33,4 @@ module.exports = AttributeCollection: AttributeCollection AttributeJoinedData: AttributeJoinedData AttributeServerId: AttributeServerId + AttributeSerializedObjects: AttributeSerializedObjects diff --git a/src/flux/attributes/attribute-object.coffee b/src/flux/attributes/attribute-object.coffee index 4a9bdb34e..e8b1b23b8 100644 --- a/src/flux/attributes/attribute-object.coffee +++ b/src/flux/attributes/attribute-object.coffee @@ -3,6 +3,10 @@ Matcher = require './matcher' ### Public: An object that can be cast to `itemClass` + +If you don't know the `itemClass` ahead of time and are storing complex, +typed, nested objects, use `AttributeSerializedObject` instead. + Section: Database ### class AttributeObject extends Attribute @@ -21,8 +25,9 @@ class AttributeObject extends Attribute if @itemClass obj = new @itemClass(val) # Important: if no ids are in the JSON, don't make them up randomly. - # This causes an object to be "different" each time it's de-serialized - # even if it's actually the same, makes React components re-render! + # This causes an object to be "different" each time it's + # de-serialized even if it's actually the same, makes React + # components re-render! obj.clientId = undefined # Warning: typeof(null) is object if obj.fromJSON and val and typeof(val) is 'object' diff --git a/src/flux/attributes/attribute-serialized-objects.coffee b/src/flux/attributes/attribute-serialized-objects.coffee new file mode 100644 index 000000000..e20cf4d50 --- /dev/null +++ b/src/flux/attributes/attribute-serialized-objects.coffee @@ -0,0 +1,21 @@ +Utils = require '../models/utils' +Attribute = require './attribute' + +### +Public: An object that is a composite of several types of objects. We +inflate and deflate them using `Utils.deserializeRegisteredObjects` and +`Utils.serializeRegisteredObjects`. + +If you're storing an object of a single type, use `AttributeObject` with +the `itemClass` option + +Section: Database +### +class AttributeSerializedObjects extends Attribute + toJSON: (val) -> + return Utils.serializeRegisteredObjects(val) + + fromJSON: (val) -> + return Utils.deserializeRegisteredObjects(val) + +module.exports = AttributeSerializedObjects diff --git a/src/flux/models/json-blob-query.coffee b/src/flux/models/json-blob-query.coffee new file mode 100644 index 000000000..b44689b8d --- /dev/null +++ b/src/flux/models/json-blob-query.coffee @@ -0,0 +1,8 @@ +Utils = require './utils' +ModelQuery = require './query' + +class JSONBlobQuery extends ModelQuery + formatResultObjects: (objects) => + return objects[0]?.json || null + +module.exports = JSONBlobQuery diff --git a/src/flux/models/json-blob.coffee b/src/flux/models/json-blob.coffee index 12d3eee62..e8313b5b7 100644 --- a/src/flux/models/json-blob.coffee +++ b/src/flux/models/json-blob.coffee @@ -17,7 +17,7 @@ class JSONBlob extends Model modelKey: 'serverId' jsonKey: 'server_id' - 'json': Attributes.Object + 'json': Attributes.SerializedObjects modelKey: 'json' jsonKey: 'json' diff --git a/src/flux/models/query.coffee b/src/flux/models/query.coffee index 9e87c2434..5479da97a 100644 --- a/src/flux/models/query.coffee +++ b/src/flux/models/query.coffee @@ -176,18 +176,20 @@ class ModelQuery return result[0]['count'] / 1 else try - objects = result.map (row) => - json = JSON.parse(row['data']) - object = (new @_klass).fromJSON(json) - for attr in @_includeJoinedData - value = row[attr.jsonKey] - value = null if value is AttributeJoinedData.NullPlaceholder - object[attr.modelKey] = value - object + objects = result.map(@inflateRawRow) catch jsonError throw new Error("Query could not parse the database result. Query: #{@sql()}, Error: #{jsonError.toString()}") return objects + inflateRawRow: (row) => + json = JSON.parse(row['data']) + object = (new @_klass).fromJSON(json) + for attr in @_includeJoinedData + value = row[attr.jsonKey] + value = null if value is AttributeJoinedData.NullPlaceholder + object[attr.modelKey] = value + object + formatResultObjects: (objects) -> return objects[0] if @_returnOne return objects diff --git a/src/flux/models/utils.coffee b/src/flux/models/utils.coffee index c6e12b2ff..b3cf4f8cc 100644 --- a/src/flux/models/utils.coffee +++ b/src/flux/models/utils.coffee @@ -10,35 +10,39 @@ DefaultResourcePath = null module.exports = Utils = - # To deserialize a string serialized with the `Utils.serializeRegisteredObjects` - # method. This will convert anything that has a known class to its - # appropriate object type. + JSONReviver: (k,v) -> + TaskRegistry ?= require '../../task-registry' + DatabaseObjectRegistry ?= require '../../database-object-registry' + type = v?.__constructorName + return v unless type + + if DatabaseObjectRegistry.isInRegistry(type) + return DatabaseObjectRegistry.deserialize(type, v) + + if TaskRegistry.isInRegistry(type) + return TaskRegistry.deserialize(type, v) + + return v + + JSONReplacer: (k,v) -> + TaskRegistry ?= require '../../task-registry' + DatabaseObjectRegistry ?= require '../../database-object-registry' + if _.isObject(v) + type = this[k].constructor.name + if DatabaseObjectRegistry.isInRegistry(type) or TaskRegistry.isInRegistry(type) + v.__constructorName = type + return v + + # To deserialize a string serialized with the + # `Utils.serializeRegisteredObjects` method. This will convert anything + # that has a known class to its appropriate object type. + ## TODO: Globally override JSON.parse??? deserializeRegisteredObjects: (json) -> - TaskRegistry ?= require '../../task-registry' - DatabaseObjectRegistry ?= require '../../database-object-registry' - - JSON.parse json, (k,v) -> - type = v?.__constructorName - return v unless type - - if DatabaseObjectRegistry.isInRegistry(type) - return DatabaseObjectRegistry.deserialize(type, v) - - if TaskRegistry.isInRegistry(type) - return TaskRegistry.deserialize(type, v) - - return v + return JSON.parse(json, Utils.JSONReviver) + ## TODO: Globally override JSON.stringify??? serializeRegisteredObjects: (object) -> - TaskRegistry ?= require '../../task-registry' - DatabaseObjectRegistry ?= require '../../database-object-registry' - - JSON.stringify object, (k, v) -> - if _.isObject(v) - type = this[k].constructor.name - if DatabaseObjectRegistry.isInRegistry(type) or TaskRegistry.isInRegistry(type) - v.__constructorName = type - return v + return JSON.stringify(object, Utils.JSONReplacer) timeZone: tz diff --git a/src/flux/stores/database-store.coffee b/src/flux/stores/database-store.coffee index 7b897aa07..bc5948928 100644 --- a/src/flux/stores/database-store.coffee +++ b/src/flux/stores/database-store.coffee @@ -6,6 +6,7 @@ Model = require '../models/model' Utils = require '../models/utils' Actions = require '../actions' ModelQuery = require '../models/query' +JSONBlobQuery = require '../models/json-blob-query' NylasStore = require '../../global/nylas-store' PromiseQueue = require 'promise-queue' DatabaseSetupQueryBuilder = require './database-setup-query-builder' @@ -34,10 +35,6 @@ DEBUG_MISSING_ACCOUNT_ID = false BEGIN_TRANSACTION = 'BEGIN TRANSACTION' COMMIT = 'COMMIT' -class JSONBlobQuery extends ModelQuery - formatResultObjects: (objects) => - return objects[0]?.json || null - ### Public: N1 is built on top of a custom database layer modeled after ActiveRecord. For many parts of the application, the database is the source diff --git a/src/nylas-env.coffee b/src/nylas-env.coffee index c2da73013..da84f0d33 100644 --- a/src/nylas-env.coffee +++ b/src/nylas-env.coffee @@ -521,7 +521,7 @@ class NylasEnvConstructor extends Model # Extended: Reload the current window. reload: -> - ipcRenderer.send('call-window-method', 'restart') + ipcRenderer.send('call-webcontents-method', 'reload') # Updates the window load settings - called when the app is ready to display # a hot-loaded window. Causes listeners registered with `onWindowPropsReceived` @@ -789,15 +789,15 @@ class NylasEnvConstructor extends Model # Extended: Open the dev tools for the current window. openDevTools: -> - ipcRenderer.send('call-window-method', 'openDevTools') + ipcRenderer.send('call-webcontents-method', 'openDevTools') # Extended: Toggle the visibility of the dev tools for the current window. toggleDevTools: -> - ipcRenderer.send('call-window-method', 'toggleDevTools') + ipcRenderer.send('call-webcontents-method', 'toggleDevTools') # Extended: Execute code in dev tools. executeJavaScriptInDevTools: (code) -> - ipcRenderer.send('call-webcontents-method', 'executeJavaScriptInDevTools', code) + ipcRenderer.send('call-devtools-webcontents-method', 'executeJavaScript', code) ### Section: Private diff --git a/src/window-thin-bootstrap.coffee b/src/window-thin-bootstrap.coffee index 9eb7a8faf..a3f28ce87 100644 --- a/src/window-thin-bootstrap.coffee +++ b/src/window-thin-bootstrap.coffee @@ -34,4 +34,4 @@ prefs.activate() ipc.on 'command', (command, args) -> if command is 'window:toggle-dev-tools' - ipc.send('call-window-method', 'toggleDevTools') + ipc.send('call-webcontents-method', 'toggleDevTools')