Skip to content

Commit 938333b

Browse files
authored
fix(import-export): Use query document count in export COMPASS-4537, COMPASS-4906 (#2307)
* temp commit * Make an import and an export store to avoid double listening, show correct count, fix showing undefined for filter * Code cleanup, better comments * fix standalone running import/export
1 parent 6118aef commit 938333b

File tree

11 files changed

+261
-124
lines changed

11 files changed

+261
-124
lines changed

packages/compass-crud/src/stores/crud-store.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -652,11 +652,12 @@ const configureStore = (options = {}) => {
652652
},
653653

654654
/**
655-
* Open an import file dialog from compass-import-export-plugin.
655+
* Open an export file dialog from compass-import-export-plugin.
656656
* Emits a global app registry event the plugin listens to.
657657
*/
658658
openExportFileDialog() {
659-
this.localAppRegistry.emit('open-export');
659+
// Pass the doc count to the export modal so we can avoid re-counting.
660+
this.localAppRegistry.emit('open-export', this.state.count);
660661
},
661662

662663
/**

packages/compass-import-export/electron/renderer/components/import-export/import-export.jsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ class ImportExport extends Component {
1111

1212
static propTypes = {
1313
appRegistry: PropTypes.object.isRequired,
14-
store: PropTypes.object.isRequired
14+
exportStore: PropTypes.object.isRequired,
15+
importStore: PropTypes.object.isRequired
1516
};
1617

1718
handleExportModalOpen = () => {
@@ -33,12 +34,17 @@ class ImportExport extends Component {
3334
<TextButton
3435
className="btn btn-default btn-sm"
3536
clickHandler={this.handleImportModalOpen}
36-
text="Import" />
37+
text="Import"
38+
/>
3739
<TextButton
3840
className="btn btn-default btn-sm"
3941
clickHandler={this.handleExportModalOpen}
40-
text="Export" />
41-
<Plugin store={this.props.store} />
42+
text="Export"
43+
/>
44+
<Plugin
45+
exportStore={this.props.exportStore}
46+
importStore={this.props.importStore}
47+
/>
4248
</div>
4349
);
4450
}

packages/compass-import-export/electron/renderer/index.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import AppRegistry from 'hadron-app-registry';
1010
import { AppContainer } from 'react-hot-loader';
1111
import { activate } from '../../src/index.js';
1212
import ImportExportPlugin from './components/import-export';
13-
import configureStore, { setDataProvider } from '../../src/stores';
13+
import configureExportStore, {
14+
setDataProvider as setExportDataProvider
15+
} from '../../src/stores/export-store';
16+
import configureImportStore, {
17+
setDataProvider as setImportDataProvider
18+
} from '../../src/stores/import-store';
1419
import { activate as activateStats } from '@mongodb-js/compass-collection-stats';
1520

1621
// Import global less file. Note: these styles WILL NOT be used in compass, as compass provides its own set
@@ -79,7 +84,11 @@ root.id = 'root';
7984
document.body.appendChild(root);
8085

8186
const localAppRegistry = new AppRegistry();
82-
const store = configureStore({
87+
const exportStore = configureExportStore({
88+
namespace: NS,
89+
localAppRegistry: localAppRegistry
90+
});
91+
const importStore = configureImportStore({
8392
namespace: NS,
8493
localAppRegistry: localAppRegistry
8594
});
@@ -88,7 +97,11 @@ const store = configureStore({
8897
const render = (Component) => {
8998
ReactDOM.render(
9099
<AppContainer>
91-
<Component store={store} appRegistry={localAppRegistry} />
100+
<Component
101+
exportStore={exportStore}
102+
importStore={importStore}
103+
appRegistry={localAppRegistry}
104+
/>
92105
</AppContainer>,
93106
document.getElementById('root')
94107
);
@@ -108,7 +121,8 @@ import DataService from 'mongodb-data-service';
108121
const dataService = new DataService(connection);
109122

110123
dataService.connect((error, ds) => {
111-
setDataProvider(store, error, ds);
124+
setImportDataProvider(importStore, error, ds);
125+
setExportDataProvider(exportStore, error, ds);
112126
onDataServiceConnected(localAppRegistry);
113127
});
114128

packages/compass-import-export/src/index.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import Plugin from './plugin';
22
import ImportPlugin from './import-plugin';
33
import ExportPlugin from './export-plugin';
4-
import configureStore from './stores';
4+
import configureExportStore from './stores/export-store';
5+
import configureImportStore from './stores/import-store';
56

67
/**
78
* The import plugin.
89
*/
910
const IMPORT_ROLE = {
1011
name: 'Import',
1112
component: ImportPlugin,
12-
configureStore: configureStore,
13+
configureStore: configureImportStore,
1314
configureActions: () => {},
1415
storeName: 'Import.Store'
1516
};
@@ -20,7 +21,7 @@ const IMPORT_ROLE = {
2021
const EXPORT_ROLE = {
2122
name: 'Export',
2223
component: ExportPlugin,
23-
configureStore: configureStore,
24+
configureStore: configureExportStore,
2425
configureActions: () => {},
2526
storeName: 'Export.Store'
2627
};
@@ -44,4 +45,11 @@ function deactivate(appRegistry) {
4445
}
4546

4647
export default Plugin;
47-
export { activate, deactivate, ImportPlugin, ExportPlugin, configureStore };
48+
export {
49+
activate,
50+
deactivate,
51+
ImportPlugin,
52+
ExportPlugin,
53+
configureExportStore,
54+
configureImportStore
55+
};

packages/compass-import-export/src/modules/export.js

Lines changed: 116 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable valid-jsdoc */
22
import fs from 'fs';
33
import stream from 'stream';
4+
import { promisify } from 'util';
45

56
import PROCESS_STATUS from '../constants/process-status';
67
import EXPORT_STEP from '../constants/export-step';
@@ -322,16 +323,51 @@ export const changeExportStep = (status) => ({
322323
status: status
323324
});
324325

326+
const fetchDocumentCount = async(dataService, ns, query) => {
327+
// When there is no filter/limit/skip try to use the estimated count.
328+
if (
329+
(!query.filter || Object.keys(query.filter).length < 1)
330+
&& !query.limit
331+
&& !query.skip
332+
) {
333+
try {
334+
const runEstimatedDocumentCount = promisify(dataService.estimatedCount.bind(dataService));
335+
const count = await runEstimatedDocumentCount(ns, {});
336+
337+
return count;
338+
} catch (estimatedCountErr) {
339+
// `estimatedDocumentCount` is currently unsupported for
340+
// views and time-series collections, so we can fallback to a full
341+
// count in these cases and ignore this error.
342+
}
343+
}
344+
345+
const runCount = promisify(dataService.count.bind(dataService));
346+
347+
const count = await runCount(
348+
ns,
349+
query.filter || {},
350+
{
351+
...(query.limit ? { limit: query.limit } : {} ),
352+
...(query.skip ? { skip: query.skip } : {} )
353+
}
354+
);
355+
return count;
356+
};
357+
325358
/**
326359
* Open the export modal.
327360
*
361+
* @param {number} [count] - optional pre supplied count to shortcut and
362+
* avoid a possibly expensive re-count.
363+
*
328364
* Counts the documents to be exported given the current query on modal open to
329365
* provide user with accurate export data
330366
*
331367
* @api public
332368
*/
333-
export const openExport = () => {
334-
return (dispatch, getState) => {
369+
export const openExport = (count) => {
370+
return async(dispatch, getState) => {
335371
const {
336372
ns,
337373
exportData,
@@ -340,12 +376,16 @@ export const openExport = () => {
340376

341377
const spec = exportData.query;
342378

343-
dataService.estimatedCount(ns, {query: spec.filter}, function(countErr, count) {
344-
if (countErr) {
345-
return onError(countErr);
346-
}
347-
dispatch(onModalOpen(count, spec));
348-
});
379+
if (count) {
380+
return dispatch(onModalOpen(count, spec));
381+
}
382+
383+
try {
384+
const docCount = await fetchDocumentCount(dataService, ns, spec);
385+
dispatch(onModalOpen(docCount, spec));
386+
} catch (e) {
387+
dispatch(onError(e));
388+
}
349389
};
350390
};
351391

@@ -389,7 +429,7 @@ export const sampleFields = () => {
389429
* @api public
390430
*/
391431
export const startExport = () => {
392-
return (dispatch, getState) => {
432+
return async(dispatch, getState) => {
393433
const {
394434
ns,
395435
exportData,
@@ -400,87 +440,85 @@ export const startExport = () => {
400440
? { filter: {} }
401441
: exportData.query;
402442

443+
const numDocsToExport = exportData.isFullCollection
444+
? await fetchDocumentCount(dataService, ns, spec)
445+
: exportData.count;
446+
403447
// filter out only the fields we want to include in our export data
404448
const projection = Object.fromEntries(
405449
Object.entries(exportData.fields)
406450
.filter((keyAndValue) => keyAndValue[1] === 1));
407451

408-
dataService.estimatedCount(ns, {query: spec.filter}, function(countErr, numDocsToExport) {
409-
if (countErr) {
410-
return onError(countErr);
411-
}
452+
debug('count says to expect %d docs in export', numDocsToExport);
453+
const source = createReadableCollectionStream(dataService, ns, spec, projection);
412454

413-
debug('count says to expect %d docs in export', numDocsToExport);
414-
const source = createReadableCollectionStream(dataService, ns, spec, projection);
455+
const progress = createProgressStream({
456+
objectMode: true,
457+
length: numDocsToExport,
458+
time: 250 /* ms */
459+
});
415460

416-
const progress = createProgressStream({
417-
objectMode: true,
418-
length: numDocsToExport,
419-
time: 250 /* ms */
420-
});
461+
progress.on('progress', function(info) {
462+
dispatch(onProgress(info.percentage, info.transferred));
463+
});
421464

422-
progress.on('progress', function(info) {
423-
dispatch(onProgress(info.percentage, info.transferred));
424-
});
465+
// Pick the columns that are going to be matched by the projection,
466+
// where some prefix the field (e.g. ['a', 'a.b', 'a.b.c'] for 'a.b.c')
467+
// has an entry in the projection object.
468+
const columns = Object.keys(exportData.allFields)
469+
.filter(field => field.split('.').some(
470+
(_part, index, parts) => projection[parts.slice(0, index + 1).join('.')]));
471+
let formatter;
472+
if (exportData.fileType === 'csv') {
473+
formatter = createCSVFormatter({ columns });
474+
} else {
475+
formatter = createJSONFormatter();
476+
}
425477

426-
// Pick the columns that are going to be matched by the projection,
427-
// where some prefix the field (e.g. ['a', 'a.b', 'a.b.c'] for 'a.b.c')
428-
// has an entry in the projection object.
429-
const columns = Object.keys(exportData.allFields)
430-
.filter(field => field.split('.').some(
431-
(_part, index, parts) => projection[parts.slice(0, index + 1).join('.')]));
432-
let formatter;
433-
if (exportData.fileType === 'csv') {
434-
formatter = createCSVFormatter({ columns });
435-
} else {
436-
formatter = createJSONFormatter();
437-
}
478+
const dest = fs.createWriteStream(exportData.fileName);
438479

439-
const dest = fs.createWriteStream(exportData.fileName);
480+
debug('executing pipeline');
481+
dispatch(onStarted(source, dest, numDocsToExport));
482+
stream.pipeline(source, progress, formatter, dest, function(err) {
483+
if (err) {
484+
debug('error running export pipeline', err);
485+
return dispatch(onError(err));
486+
}
487+
debug(
488+
'done. %d docs exported to %s',
489+
numDocsToExport,
490+
exportData.fileName
491+
);
492+
dispatch(onFinished(numDocsToExport));
493+
dispatch(
494+
appRegistryEmit(
495+
'export-finished',
496+
numDocsToExport,
497+
exportData.fileType
498+
)
499+
);
440500

441-
debug('executing pipeline');
442-
dispatch(onStarted(source, dest, numDocsToExport));
443-
stream.pipeline(source, progress, formatter, dest, function(err) {
444-
if (err) {
445-
debug('error running export pipeline', err);
446-
return dispatch(onError(err));
447-
}
448-
debug(
449-
'done. %d docs exported to %s',
501+
/**
502+
* TODO: lucas: For metrics:
503+
*
504+
* "resource": "Export",
505+
* "action": "completed",
506+
* "file_type": "<csv|json_array>",
507+
* "num_docs": "<how many docs exported>",
508+
* "full_collection": true|false
509+
* "filter": true|false,
510+
* "projection": true|false,
511+
* "skip": true|false,
512+
* "limit": true|false,
513+
* "fields_selected": true|false
514+
*/
515+
dispatch(
516+
globalAppRegistryEmit(
517+
'export-finished',
450518
numDocsToExport,
451-
exportData.fileName
452-
);
453-
dispatch(onFinished(numDocsToExport));
454-
dispatch(
455-
appRegistryEmit(
456-
'export-finished',
457-
numDocsToExport,
458-
exportData.fileType
459-
)
460-
);
461-
462-
/**
463-
* TODO: lucas: For metrics:
464-
*
465-
* "resource": "Export",
466-
* "action": "completed",
467-
* "file_type": "<csv|json_array>",
468-
* "num_docs": "<how many docs exported>",
469-
* "full_collection": true|false
470-
* "filter": true|false,
471-
* "projection": true|false,
472-
* "skip": true|false,
473-
* "limit": true|false,
474-
* "fields_selected": true|false
475-
*/
476-
dispatch(
477-
globalAppRegistryEmit(
478-
'export-finished',
479-
numDocsToExport,
480-
exportData.fileType
481-
)
482-
);
483-
});
519+
exportData.fileType
520+
)
521+
);
484522
});
485523
};
486524
};

0 commit comments

Comments
 (0)