Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented data exporting to JSON or local Realm #1134

Merged
merged 1 commit into from Apr 3, 2019
Merged
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -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))

This comment has been minimized.

Copy link
@bmunkholm

bmunkholm Apr 3, 2019

Contributor

Perhaps add that when the file is exported as a local Realm it will also be stored optimally without additional free space (aka compressed)

This comment has been minimized.

Copy link
@bmunkholm

bmunkholm Apr 3, 2019

Contributor

That should also close a related issue to exporting compressed version of a realm.

This comment has been minimized.

Copy link
@bmunkholm

This comment has been minimized.

Copy link
@kraenhansen

kraenhansen Apr 10, 2019

Author Contributor

I believe the term is "compacted" and as we are not actually zipping the file or anything.

I committed the following change ac0fec0.


### Fixed

@@ -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.
@@ -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,
};
@@ -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));
}
}
@@ -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);
}
}
@@ -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.
@@ -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';
@@ -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
? [
@@ -247,7 +262,7 @@ class RealmBrowserContainer
{
action: 'prepend',
id: 'close',
items: [exportSchemaMenu, { type: 'separator' }],
items: [exportSchemaMenu, exportDataMenu, { type: 'separator' }],
},
{
action: 'append',
@@ -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,
) => {
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.