Skip to content

Commit

Permalink
Implemented data exporting to JSON or local Realm (#1134)
Browse files Browse the repository at this point in the history
  • Loading branch information
kraenhansen committed Apr 3, 2019
1 parent fce0385 commit c01599d
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 3 deletions.
1 change: 1 addition & 0 deletions RELEASENOTES.md
Expand Up @@ -3,6 +3,7 @@
### Enhancements

- Improved how log entries are displayed in the server adminstration window: The timestamp now displays the date when the entry is not from the current day and the context object gets expanded initially. ([#1131](https://github.com/realm/realm-studio/issues/1131), since v1.20.0)
- Added a menu item to export data to JSON or a local Realm from the Realm Browser window. ([#1134](https://github.com/realm/realm-studio/pull/1134))

### Fixed

Expand Down
2 changes: 1 addition & 1 deletion scripts/header.txt
@@ -1,6 +1,6 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2018 Realm Inc.
// Copyright 2019 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down
62 changes: 62 additions & 0 deletions src/services/data-exporter/index.ts
@@ -0,0 +1,62 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2019 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

import * as path from 'path';

export interface IExportEngine {
export(realm: Realm, destinationPath: string): void;
}

export enum DataExportFormat {
JSON = 'json',
LocalRealm = 'local-realm',
}

export class DataExporter {
private readonly format: DataExportFormat;

constructor(format: DataExportFormat) {
this.format = format;
}

public suggestFilename(realm: Realm) {
const basename = path.basename(realm.path, '.realm');
if (this.format === DataExportFormat.JSON) {
return `${basename}.json`;
} else if (this.format === DataExportFormat.LocalRealm) {
return `${basename}.realm`;
} else {
throw new Error(`Unexpected format ${this.format}`);
}
}

public export(realm: Realm, destinationPath: string) {
const ExportEngine = engines[this.format];
const engine = new ExportEngine();
return engine.export(realm, destinationPath);
}
}

// Doing this last to prevent circular reference

import { JSONExportEngine } from './json';
import { LocalRealmExportEngine } from './local-realm';
const engines = {
[DataExportFormat.JSON]: JSONExportEngine,
[DataExportFormat.LocalRealm]: LocalRealmExportEngine,
};
102 changes: 102 additions & 0 deletions src/services/data-exporter/json.ts
@@ -0,0 +1,102 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2019 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

import * as fs from 'fs';
import * as Realm from 'realm';

import { IExportEngine } from '.';

function serializeObject(object: { [key: string]: any } & Realm.Object) {
// This is an object reference
const objectSchema = object.objectSchema();
if (objectSchema.primaryKey) {
return object[objectSchema.primaryKey];
} else {
// Shallow copy the object
return RealmObjectToJSON.call(object);
}
}

function serializeValue(propertyName: string, value: any) {
if (
value === null ||
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean'
) {
return value;
} else if (value instanceof Date) {
return value.toISOString();
} else if (value instanceof ArrayBuffer) {
return Buffer.from(value).toString('base64');
} else if (
typeof value === 'object' &&
typeof value.objectSchema === 'function'
) {
return serializeObject(value);
} else if (typeof value === 'object' && typeof value.length === 'number') {
if (value.type === 'object') {
// A list of objects
return value.map((item: any) => {
if (typeof item === 'object') {
return serializeObject(item);
} else {
return item;
}
});
} else {
// A list of primitives
return [...value];
}
} else {
throw new Error(
`Failed to serialize '${propertyName}' field of type ${typeof value}`,
);
}
}

function RealmObjectToJSON(this: { [key: string]: any } & Realm.Object) {
const values: { [key: string]: any } = {};
for (const propertyName of Object.getOwnPropertyNames(this)) {
const value = this[propertyName];
if (propertyName === '_realm' || typeof value === 'function') {
continue; // Skip this property
} else {
values[propertyName] = serializeValue(propertyName, value);
}
}
return values;
}

export class JSONExportEngine implements IExportEngine {
public export(realm: Realm, destinationPath: string) {
const data: { [objectSchemaName: string]: any[] } = {};
for (const objectSchema of realm.schema) {
data[objectSchema.name] = [
...realm.objects(objectSchema.name).snapshot(),
].map((object: any) => {
return Object.defineProperty(object, 'toJSON', {
value: RealmObjectToJSON.bind(object),
enumerable: false,
});
});
}
// Write the stringified data to a file
fs.writeFileSync(destinationPath, JSON.stringify(data, null, 2));
}
}
25 changes: 25 additions & 0 deletions src/services/data-exporter/local-realm.ts
@@ -0,0 +1,25 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2019 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////

import { IExportEngine } from '.';

export class LocalRealmExportEngine implements IExportEngine {
public export(realm: Realm, destinationPath: string) {
realm.writeCopyTo(destinationPath);
}
}
36 changes: 34 additions & 2 deletions src/ui/RealmBrowser/index.tsx
@@ -1,6 +1,6 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2018 Realm Inc.
// Copyright 2019 Realm Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -21,6 +21,7 @@ import * as path from 'path';
import * as React from 'react';
import * as Realm from 'realm';

import { DataExporter, DataExportFormat } from '../../services/data-exporter';
import * as dataImporter from '../../services/data-importer';
import { Language, SchemaExporter } from '../../services/schema-export';
import { menu } from '../../utils';
Expand Down Expand Up @@ -188,6 +189,20 @@ class RealmBrowserContainer
],
};

const exportDataMenu: MenuItemConstructorOptions = {
label: 'Save data',
submenu: [
{
label: 'JSON',
click: () => this.onExportData(DataExportFormat.JSON),
},
{
label: 'Local Realm',
click: () => this.onExportData(DataExportFormat.LocalRealm),
},
],
};

const transactionMenuItems: MenuItemConstructorOptions[] =
this.realm && this.realm.isInTransaction
? [
Expand Down Expand Up @@ -247,7 +262,7 @@ class RealmBrowserContainer
{
action: 'prepend',
id: 'close',
items: [exportSchemaMenu, { type: 'separator' }],
items: [exportSchemaMenu, exportDataMenu, { type: 'separator' }],
},
{
action: 'append',
Expand Down Expand Up @@ -662,6 +677,23 @@ class RealmBrowserContainer
);
};

private onExportData = (format: DataExportFormat) => {
try {
const exporter = new DataExporter(format);
if (this.realm) {
const destinationPath = remote.dialog.showSaveDialog({
defaultPath: exporter.suggestFilename(this.realm),
message: 'Select a destination for the data',
});
exporter.export(this.realm, destinationPath);
} else {
throw new Error('Realm is not open');
}
} catch (err) {
showError('Failed to export data', err);
}
};

private onImportIntoExistingRealm = (
format: dataImporter.ImportFormat = dataImporter.ImportFormat.CSV,
) => {
Expand Down

0 comments on commit c01599d

Please sign in to comment.