Skip to content

Commit 996ce91

Browse files
authored
feat: Add Cloud Function execution on Parse Object in data browser (#2409)
1 parent 5f4696c commit 996ce91

File tree

5 files changed

+108
-4
lines changed

5 files changed

+108
-4
lines changed

README.md

+66
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Parse Dashboard is a standalone dashboard for managing your [Parse Server](https
4242
- [Other Configuration Options](#other-configuration-options)
4343
- [Prevent columns sorting](#prevent-columns-sorting)
4444
- [Custom order in the filter popup](#custom-order-in-the-filter-popup)
45+
- [Scripts](#scripts)
4546
- [Running as Express Middleware](#running-as-express-middleware)
4647
- [Deploying Parse Dashboard](#deploying-parse-dashboard)
4748
- [Preparing for Deployment](#preparing-for-deployment)
@@ -362,6 +363,71 @@ For example:
362363

363364
You can conveniently create a filter definition without having to write it by hand by first saving a filter in the data browser, then exporting the filter definition under *App Settings > Export Class Preferences*.
364365

366+
### Scripts
367+
368+
You can specify scripts to execute Cloud Functions with the `scripts` option:
369+
370+
371+
```json
372+
"apps": [
373+
{
374+
"scripts": [
375+
{
376+
"title": "Delete Account",
377+
"classes": ["_User"],
378+
"cloudCodeFunction": "deleteAccount"
379+
}
380+
]
381+
}
382+
]
383+
```
384+
385+
Next, define the Cloud Function in Parse Server that will be called. The object that has been selected in the data browser will be made available as a request parameter:
386+
387+
```js
388+
Parse.Cloud.define('deleteAccount', async (req) => {
389+
req.params.object.set('deleted', true);
390+
await req.params.object.save(null, {useMasterKey: true});
391+
}, {
392+
requireMaster: true
393+
});
394+
```
395+
396+
⚠️ Depending on your Parse Server version you may need to set the Parse Server option `encodeParseObjectInCloudFunction` to `true` so that the selected object in the data browser is made available in the Cloud Function as an instance of `Parse.Object`. If the option is not set, is set to `false`, or you are using an older version of Parse Server, the object is made available as a plain JavaScript object and needs to be converted from a JSON object to a `Parse.Object` instance with `req.params.object = Parse.Object.fromJSON(req.params.object);`, before you can call any `Parse.Object` properties and methods on it.
397+
398+
For older versions of Parse Server:
399+
400+
<details>
401+
<summary>Parse Server &gt;=4.4.0 &lt;6.2.0</summary>
402+
403+
```js
404+
Parse.Cloud.define('deleteAccount', async (req) => {
405+
req.params.object = Parse.Object.fromJSON(req.params.object);
406+
req.params.object.set('deleted', true);
407+
await req.params.object.save(null, {useMasterKey: true});
408+
}, {
409+
requireMaster: true
410+
});
411+
```
412+
413+
</details>
414+
415+
<details>
416+
<summary>Parse Server &gt;=2.1.4 &lt;4.4.0</summary>
417+
418+
```js
419+
Parse.Cloud.define('deleteAccount', async (req) => {
420+
if (!req.master || !req.params.object) {
421+
throw 'Unauthorized';
422+
}
423+
req.params.object = Parse.Object.fromJSON(req.params.object);
424+
req.params.object.set('deleted', true);
425+
await req.params.object.save(null, {useMasterKey: true});
426+
});
427+
```
428+
429+
</details>
430+
365431
# Running as Express Middleware
366432

367433
Instead of starting Parse Dashboard with the CLI, you can also run it as an [express](https://github.com/expressjs/express) middleware.

src/components/BrowserCell/BrowserCell.react.js

+23-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import React, { Component } from 'react';
1515
import styles from 'components/BrowserCell/BrowserCell.scss';
1616
import baseStyles from 'stylesheets/base.scss';
1717
import * as ColumnPreferences from 'lib/ColumnPreferences';
18-
1918
export default class BrowserCell extends Component {
2019
constructor() {
2120
super();
@@ -279,6 +278,29 @@ export default class BrowserCell extends Component {
279278
});
280279
}
281280

281+
const validScripts = (this.props.scripts || []).filter(script => script.classes?.includes(this.props.className));
282+
if (validScripts.length) {
283+
onEditSelectedRow && contextMenuOptions.push({
284+
text: 'Scripts',
285+
items: validScripts.map(script => {
286+
return {
287+
text: script.title,
288+
callback: async () => {
289+
try {
290+
const object = Parse.Object.extend(this.props.className).createWithoutData(this.props.objectId);
291+
const response = await Parse.Cloud.run(script.cloudCodeFunction, {object: object.toPointer()}, {useMasterKey: true});
292+
this.props.showNote(response || `${script.title} ran with object ${object.id}}`);
293+
this.props.onRefresh();
294+
} catch (e) {
295+
this.props.showNote(e.message, true);
296+
console.log(`Could not run ${script.title}: ${e}`);
297+
}
298+
}
299+
}
300+
})
301+
});
302+
}
303+
282304
return contextMenuOptions;
283305
}
284306

src/components/BrowserRow/BrowserRow.react.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,11 @@ export default class BrowserRow extends Component {
100100
markRequiredFieldRow={markRequiredFieldRow}
101101
setCopyableValue={setCopyableValue}
102102
setContextMenu={setContextMenu}
103-
onEditSelectedRow={onEditSelectedRow} />
103+
onEditSelectedRow={onEditSelectedRow}
104+
showNote={this.props.showNote}
105+
onRefresh={this.props.onRefresh}
106+
scripts={this.props.scripts}
107+
/>
104108
);
105109
})}
106110
</div>

src/dashboard/Data/Browser/BrowserTable.react.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ export default class BrowserTable extends React.Component {
159159
setContextMenu={this.props.setContextMenu}
160160
onEditSelectedRow={this.props.onEditSelectedRow}
161161
markRequiredFieldRow={this.props.markRequiredFieldRow}
162+
showNote={this.props.showNote}
163+
onRefresh={this.props.onRefresh}
164+
scripts={this.context.scripts}
162165
/>
163166
<Button
164167
value="Clone"
@@ -211,6 +214,9 @@ export default class BrowserTable extends React.Component {
211214
setContextMenu={this.props.setContextMenu}
212215
onEditSelectedRow={this.props.onEditSelectedRow}
213216
markRequiredFieldRow={this.props.markRequiredFieldRow}
217+
showNote={this.props.showNote}
218+
onRefresh={this.props.onRefresh}
219+
scripts={this.context.scripts}
214220
/>
215221
<Button
216222
value="Add"
@@ -267,7 +273,11 @@ export default class BrowserTable extends React.Component {
267273
setRelation={this.props.setRelation}
268274
setCopyableValue={this.props.setCopyableValue}
269275
setContextMenu={this.props.setContextMenu}
270-
onEditSelectedRow={this.props.onEditSelectedRow} />
276+
onEditSelectedRow={this.props.onEditSelectedRow}
277+
showNote={this.props.showNote}
278+
onRefresh={this.props.onRefresh}
279+
scripts={this.context.scripts}
280+
/>
271281
}
272282

273283
if (this.props.editing) {

src/lib/ParseApp.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ export default class ParseApp {
4646
preventSchemaEdits,
4747
graphQLServerURL,
4848
columnPreference,
49-
classPreference
49+
scripts,
50+
classPreference,
5051
}) {
5152
this.name = appName;
5253
this.createdAt = created_at ? new Date(created_at) : new Date();
@@ -73,6 +74,7 @@ export default class ParseApp {
7374
this.preventSchemaEdits = preventSchemaEdits || false;
7475
this.graphQLServerURL = graphQLServerURL;
7576
this.columnPreference = columnPreference;
77+
this.scripts = scripts;
7678

7779
if(!supportedPushLocales) {
7880
console.warn('Missing push locales for \'' + appName + '\', see this link for details on setting localizations up. https://github.com/parse-community/parse-dashboard#configuring-localized-push-notifications');

0 commit comments

Comments
 (0)