Repository: parse-community/Parse-SDK-JS
Pull request #780 (2019-04-02 20:44:34)

Parse.File getData
Closes: https://github.com/parse-community/Parse-SDK-JS/issues/678

Adds similar functionality for `.getData()` function in the [Parse SDK for Android](https://parseplatform.org/Parse-SDK-Android/api/com/parse/ParseFile.html#getData--) and [Parse SDK for PHP](http://parseplatform.org/parse-php-sdk/classes/Parse.ParseFile.html#method_getData)

Returns base64 (not full base64 with `data:...`)

codecov[bot]: # [Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/780?src=pr&el=h1) Report
> :exclamation: No coverage uploaded for pull request base (`master@ba6c448`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference#section-missing-base-commit).
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/780/graphs/tree.svg?width=650&token=8nCENM0QHj&height=150&src=pr)](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/780?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##             master     #780   +/-   ##
=========================================
  Coverage          ?   92.01%           
=========================================
  Files             ?       54           
  Lines             ?     4908           
  Branches          ?     1087           
=========================================
  Hits              ?     4516           
  Misses            ?      392           
  Partials          ?        0
```


| [Impacted Files](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/780?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/ParseFile.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/780/diff?src=pr&el=tree#diff-c3JjL1BhcnNlRmlsZS5qcw==) | `97.51% <100%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/780?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/780?src=pr&el=footer). Last update [ba6c448...23bc45d](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/780?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/package-lock.json b/package-lock.json
index 5bfa64b..3236a64 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2714,9 +2714,9 @@
       "dev": true
     },
     "codecov": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.2.0.tgz",
-      "integrity": "sha512-3NJvNARXxilqnqVfgzDHyVrF4oeVgaYW1c1O6Oi5mn93exE7HTSSFNiYdwojWW6IwrCZABJ8crpNbKoo9aUHQw==",
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.3.0.tgz",
+      "integrity": "sha512-S70c3Eg9SixumOvxaKE/yKUxb9ihu/uebD9iPO2IR73IdP4i6ZzjXEULj3d0HeyWPr0DqBfDkjNBWxURjVO5hw==",
       "dev": true,
       "requires": {
         "argv": "^0.0.2",
@@ -3826,9 +3826,9 @@
       }
     },
     "eslint": {
-      "version": "5.15.3",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.15.3.tgz",
-      "integrity": "sha512-vMGi0PjCHSokZxE0NLp2VneGw5sio7SSiDNgIUn2tC0XkWJRNOIoHIg3CliLVfXnJsiHxGAYrkw0PieAu8+KYQ==",
+      "version": "5.16.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz",
+      "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.0.0",
@@ -3851,7 +3851,7 @@
         "import-fresh": "^3.0.0",
         "imurmurhash": "^0.1.4",
         "inquirer": "^6.2.2",
-        "js-yaml": "^3.12.0",
+        "js-yaml": "^3.13.0",
         "json-stable-stringify-without-jsonify": "^1.0.1",
         "levn": "^0.3.0",
         "lodash": "^4.17.11",
@@ -3882,9 +3882,9 @@
       }
     },
     "eslint-plugin-flowtype": {
-      "version": "3.4.2",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-3.4.2.tgz",
-      "integrity": "sha512-sv6O6fiN3dIwhU4qRxfcyIpbKGVvsxwIQ6vgBLudpQKjH1rEyEFEOjGzGEUBTQP9J8LdTZm37OjiqZ0ZeFOa6g==",
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-3.5.1.tgz",
+      "integrity": "sha512-mIhq7jhBSWNMp3ECmlWK+b9EFmLE+3Jd9axk05LB+snbRPkc3/lYTgb4Bu9lgt1QLmrxFDWsASdKj9CAIehckg==",
       "dev": true,
       "requires": {
         "lodash": "^4.17.11"
diff --git a/package.json b/package.json
index 3681e7a..68111e6 100644
--- a/package.json
+++ b/package.json
@@ -49,9 +49,9 @@
     "babel-plugin-minify-dead-code-elimination": "0.5.0",
     "babel-plugin-transform-inline-environment-variables": "0.4.3",
     "browserify": "16.2.3",
-    "codecov": "3.2.0",
+    "codecov": "3.3.0",
     "eslint": "5.16.0",
-    "eslint-plugin-flowtype": "3.4.2",
+    "eslint-plugin-flowtype": "3.5.1",
     "express": "4.16.4",
     "gulp": "4.0.0",
     "gulp-babel": "8.0.0",
diff --git a/src/ParseFile.js b/src/ParseFile.js
index 528fda7..74a34da 100644
--- a/src/ParseFile.js
+++ b/src/ParseFile.js
@@ -65,6 +65,7 @@ class ParseFile {
   _url: ?string;
   _source: FileSource;
   _previousSave: ?Promise;
+  _data: string;
 
   /**
    * @param name {String} The file's name. This will be prefixed by a unique
@@ -101,9 +102,10 @@ class ParseFile {
 
     if (data !== undefined) {
       if (Array.isArray(data)) {
+        this._data = ParseFile.encodeBase64(data);
         this._source = {
           format: 'base64',
-          base64: ParseFile.encodeBase64(data),
+          base64: this._data,
           type: specifiedType
         };
       } else if (typeof File !== 'undefined' && data instanceof File) {
@@ -125,12 +127,14 @@ class ParseFile {
         if (commaIndex !== -1) {
           const matches = dataUriRegexp.exec(base64.slice(0, commaIndex + 1));
           // if data URI with type and charset, there will be 4 matches.
+          this._data = base64.slice(commaIndex + 1);
           this._source = {
             format: 'base64',
-            base64: base64.slice(commaIndex + 1),
+            base64: this._data,
             type: matches[1]
           };
         } else {
+          this._data = base64;
           this._source = {
             format: 'base64',
             base64: base64,
@@ -143,6 +147,25 @@ class ParseFile {
     }
   }
 
+  /**
+   * Return the data for the file, downloading it if not already present.
+   * Data is present if initialized with Byte Array, Base64 or Saved with Uri.
+   * Data is cleared if saved with File object selected with a file upload control
+   *
+   * @return {Promise} Promise that is resolve with base64 data
+   */
+  async getData(): Promise<String> {
+    if (this._data) {
+      return this._data;
+    }
+    if (!this._url) {
+      throw new Error('Cannot retrieve data for unsaved ParseFile.');
+    }
+    const controller = CoreManager.getFileController();
+    const result = await controller.download(this._url);
+    this._data = result.base64;
+    return this._data;
+  }
   /**
    * Gets the name of the file. Before save is called, this is the filename
    * given by the user. After save is called, that name gets prefixed with a
@@ -189,10 +212,19 @@ class ParseFile {
         this._previousSave = controller.saveFile(this._name, this._source, options).then((res) => {
           this._name = res.name;
           this._url = res.url;
+          this._data = null;
           return this;
         });
       } else if (this._source.format === 'uri') {
-        this._previousSave = controller.saveUri(this._name, this._source, options).then((res) => {
+        this._previousSave = controller.download(this._source.uri).then((result) => {
+          const newSource = {
+            format: 'base64',
+            base64: result.base64,
+            type: result.contentType,
+          };
+          this._data = result.base64;
+          return controller.saveBase64(this._name, newSource, options);
+        }).then((res) => {
           this._name = res.name;
           this._url = res.url;
           return this;
@@ -299,20 +331,6 @@ const DefaultController = {
     return CoreManager.getRESTController().request('POST', path, data, options);
   },
 
-  saveUri: function(name: string, source: FileSource, options?: FullOptions) {
-    if (source.format !== 'uri') {
-      throw new Error('saveUri can only be used with Uri-type sources.');
-    }
-    return this.download(source.uri).then((result) => {
-      const newSource = {
-        format: 'base64',
-        base64: result.base64,
-        type: result.contentType,
-      };
-      return this.saveBase64(name, newSource, options);
-    });
-  },
-
   download: function(uri) {
     if (XHR) {
       return this.downloadAjax(uri);


Test patch:
diff --git a/integration/test/ParseFileTest.js b/integration/test/ParseFileTest.js
index 980424b..933dde5 100644
--- a/integration/test/ParseFileTest.js
+++ b/integration/test/ParseFileTest.js
@@ -38,4 +38,47 @@ describe('Parse.File', () => {
     result = await query.get(object.id);
     assert.equal(file2.url(), result.get('file2').url());
   });
+
+  it('can not get data from unsaved file', async () => {
+    const file = new Parse.File('parse-server-logo', [61, 170, 236, 120]);
+    file._data = null;
+    try {
+      await file.getData();
+    } catch (e) {
+      assert.equal(e.message, 'Cannot retrieve data for unsaved ParseFile.');
+    }
+  });
+
+  it('can get file data from byte array', async () => {
+    const file = new Parse.File('parse-server-logo', [61, 170, 236, 120]);
+    let data = await file.getData();
+    assert.equal(data, 'ParseA==');
+    file._data = null;
+    await file.save();
+    assert.equal(file._data, null)
+    data = await file.getData();
+    assert.equal(data, 'ParseA==');
+  });
+
+  it('can get file data from base64', async () => {
+    const file = new Parse.File('parse-server-logo', { base64: 'ParseA==' });
+    let data = await file.getData();
+    assert.equal(data, 'ParseA==');
+    file._data = null;
+    await file.save();
+    assert.equal(file._data, null)
+    data = await file.getData();
+    assert.equal(data, 'ParseA==');
+  });
+
+  it('can get file data from full base64', async () => {
+    const file = new Parse.File('parse-server-logo', { base64: 'data:image/jpeg;base64,ParseA==' });
+    let data = await file.getData();
+    assert.equal(data, 'ParseA==');
+    file._data = null;
+    await file.save();
+    assert.equal(file._data, null)
+    data = await file.getData();
+    assert.equal(data, 'ParseA==');
+  });
 });
diff --git a/src/__tests__/ParseFile-test.js b/src/__tests__/ParseFile-test.js
index 75b5042..dff3f0a 100644
--- a/src/__tests__/ParseFile-test.js
+++ b/src/__tests__/ParseFile-test.js
@@ -37,7 +37,10 @@ describe('ParseFile', () => {
     CoreManager.setFileController({
       saveFile: generateSaveMock('http://files.parsetfss.com/a/'),
       saveBase64: generateSaveMock('http://files.parsetfss.com/a/'),
-      saveUri: generateSaveMock('http://files.parsetfss.com/a/'),
+      download: () => Promise.resolve({
+        base64: 'ParseA==',
+        contentType: 'image/png',
+      }),
     });
   });
 
@@ -274,17 +277,9 @@ describe('FileController', () => {
     });
   });
 
-  it('saveUri without uri type', () => {
-    try {
-      defaultController.saveUri('name', { format: 'unknown' });
-    } catch (error) {
-      expect(error.message).toBe('saveUri can only be used with Uri-type sources.');
-    }
-  });
-
   it('saveUri with uri type', async () => {
-    const source = { format: 'uri', uri: 'https://example.com/image.png' };
-    jest.spyOn(
+    const file = new ParseFile('parse.png', { uri: 'https://example.com/image.png' });
+    const spy = jest.spyOn(
       defaultController,
       'download'
     )
@@ -296,14 +291,15 @@ describe('FileController', () => {
       });
 
     jest.spyOn(defaultController, 'saveBase64');
-    await defaultController.saveUri('fileName', source, {});
+    await file.save();
     expect(defaultController.download).toHaveBeenCalledTimes(1);
     expect(defaultController.saveBase64).toHaveBeenCalledTimes(1);
     expect(defaultController.saveBase64.mock.calls[0]).toEqual([
-      'fileName',
+      'parse.png',
       { format: 'base64', base64: 'ParseA==', type: 'image/png' },
       {}
     ]);
+    spy.mockRestore();
   });
 
   it('download with base64 http', async () => {
@@ -408,4 +404,41 @@ describe('FileController', () => {
       expect(e).toBe('Cannot make a request: No definition of XMLHttpRequest was found.');
     }
   });
+
+  it('getData', async () => {
+    const file = new ParseFile('parse.png', [61, 170, 236, 120]);
+    const data = await file.getData();
+    expect(data).toBe('ParseA==');
+  });
+
+  it('getData unsaved file', async () => {
+    const file = new ParseFile('parse.png');
+    try {
+      await file.getData();
+    } catch (e) {
+      expect(e.message).toBe('Cannot retrieve data for unsaved ParseFile.');
+    }
+  });
+
+  it('getData via download', async () => {
+    const file = new ParseFile('parse.txt', { base64: 'ParseA==' });
+    file._data = null;
+    const result = await file.save();
+
+    const spy = jest.spyOn(
+      defaultController,
+      'download'
+    )
+      .mockImplementationOnce(() => {
+        return Promise.resolve({
+          base64: 'ParseA==',
+          contentType: 'image/png',
+        });
+      });
+
+    const data = await result.getData();
+    expect(defaultController.download).toHaveBeenCalledTimes(1);
+    expect(data).toBe('ParseA==');
+    spy.mockRestore();
+  });
 });