Skip to content

Add Endpoints to view and edit Cloud Functions in dashboard #6983

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

Closed
wants to merge 11 commits into from
10 changes: 10 additions & 0 deletions resources/buildConfigDefinitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ function getENVPrefix(iface) {
if (options[iface.id.name]) {
return options[iface.id.name]
}
if (iface.id.name === 'DashboardOptions') {
return 'PARSE_SERVER_DASHBOARD_OPTIONS_';
}
}

function processProperty(property, iface) {
Expand Down Expand Up @@ -172,6 +175,13 @@ function parseDefaultValue(elt, value, t) {
});
literalValue = t.objectExpression(props);
}
if (type == 'DashboardOptions') {
const object = parsers.objectParser(value);
const props = Object.keys(object).map((key) => {
return t.objectProperty(key, object[value]);
});
literalValue = t.objectExpression(props);
}
if (type == 'ProtectedFields') {
const prop = t.objectProperty(
t.stringLiteral('_User'), t.objectPattern([
Expand Down
228 changes: 228 additions & 0 deletions spec/CloudCode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,234 @@ describe('Cloud Code', () => {
});
});
});
const masterKeyHeaders = {
'X-Parse-Application-Id': 'test',
'X-Parse-Rest-API-Key': 'rest',
'X-Parse-Master-Key': 'test',
'Content-Type': 'application/json',
};
const masterKeyOptions = {
headers: masterKeyHeaders,
json: true,
};
it('can load cloud code file from dashboard', async done => {
const cloudDir = './spec/cloud/cloudCodeAbsoluteFile.js';
await reconfigureServer({
cloud: cloudDir,
dashboardOptions: {
cloudFileView: true,
},
});
const options = Object.assign({}, masterKeyOptions, {
method: 'GET',
url: Parse.serverURL + '/releases/latest',
});
request(options)
.then(res => {
expect(Array.isArray(res.data)).toBe(true);
const first = res.data[0];
expect(first.userFiles).toBeDefined();
expect(first.checksums).toBeDefined();
expect(first.userFiles).toContain(cloudDir);
expect(first.checksums).toContain(cloudDir);
options.url = Parse.serverURL + '/scripts/spec/cloud/cloudCodeAbsoluteFile.js';
return request(options);
})
.then(res => {
const response = res.data;
expect(response).toContain('It is possible to define cloud code in a file.');
done();
});
});

it('can load multiple cloud code files from dashboard', async done => {
const cloudDir = './spec/cloud/cloudCodeRequireFiles.js';
await reconfigureServer({
cloud: cloudDir,
dashboardOptions: {
cloudFileView: true,
},
});
const options = Object.assign({}, masterKeyOptions, {
method: 'GET',
url: Parse.serverURL + '/releases/latest',
});
request(options).then(res => {
expect(Array.isArray(res.data)).toBe(true);
const first = res.data[0];
expect(first.userFiles).toBeDefined();
expect(first.checksums).toBeDefined();
expect(first.userFiles).toContain(cloudDir);
expect(first.checksums).toContain(cloudDir);
expect(first.userFiles).toContain('spec/cloud/cloudCodeAbsoluteFile.js');
expect(first.checksums).toContain('spec/cloud/cloudCodeAbsoluteFile.js');
expect(first.userFiles).toContain('spec/cloud/cloudCodeRelativeFile.js');
expect(first.checksums).toContain('spec/cloud/cloudCodeRelativeFile.js');
done();
});
});

it('can server info for for file options', async () => {
const cloudDir = './spec/cloud/cloudCodeRequireFiles.js';
await reconfigureServer({
cloud: cloudDir,
});
const options = Object.assign({}, masterKeyOptions, {
method: 'GET',
url: Parse.serverURL + '/serverInfo',
});
let { data } = await request(options);
expect(data).not.toBe(null);
expect(data.features).not.toBe(null);
expect(data.features.cloudCode).not.toBe(null);
expect(data.features.cloudCode.viewCode).toBe(false);
expect(data.features.cloudCode.editCode).toBe(false);

await reconfigureServer({
cloud: cloudDir,
dashboardOptions: {
cloudFileView: true,
},
});
data = (await request(options)).data;
expect(data).not.toBe(null);
expect(data.features).not.toBe(null);
expect(data.features.cloudCode).not.toBe(null);
expect(data.features.cloudCode.viewCode).toBe(true);
expect(data.features.cloudCode.editCode).toBe(false);
await reconfigureServer({
cloud: cloudDir,
dashboardOptions: {
cloudFileView: true,
cloudFileEdit: true,
},
});
data = (await request(options)).data;
expect(data).not.toBe(null);
expect(data.features).not.toBe(null);
expect(data.features.cloudCode).not.toBe(null);
expect(data.features.cloudCode.viewCode).toBe(true);
expect(data.features.cloudCode.editCode).toBe(true);
});

it('cannot view or edit cloud files by default', async () => {
const options = Object.assign({}, masterKeyOptions, {
method: 'GET',
url: Parse.serverURL + '/releases/latest',
});
try {
await request(options);
fail('should not have been able to get cloud files');
} catch (e) {
expect(e.text).toBe('{"code":101,"error":"Dashboard file viewing is not active."}');
}
try {
options.url = Parse.serverURL + '/scripts/spec/cloud/cloudCodeRequireFiles.js';
await request(options);
fail('should not have been able to get cloud files');
} catch (e) {
expect(e.text).toBe('{"code":101,"error":"Dashboard file viewing is not active."}');
}
try {
options.method = 'POST';
options.url = Parse.serverURL + '/scripts/spec/cloud/cloudCodeRequireFiles.js';
options.body = {
data: 'new file text',
};
await request(options);
fail('should not have been able to get cloud files');
} catch (e) {
expect(e.text).toBe('{"code":101,"error":"Dashboard file editing is not active."}');
}
});

it('can view cloud code file from dashboard', async () => {
const cloudDir = './spec/cloud/cloudCodeRequireFiles.js';
await reconfigureServer({
cloud: cloudDir,
dashboardOptions: {
cloudFileView: true,
},
});
const options = Object.assign({}, masterKeyOptions, {
method: 'GET',
url: Parse.serverURL + '/releases/latest',
});
let res = await request(options);
expect(Array.isArray(res.data)).toBe(true);
const first = res.data[0];
expect(first.userFiles).toBeDefined();
expect(first.checksums).toBeDefined();
expect(first.userFiles).toContain(cloudDir);
expect(first.checksums).toContain(cloudDir);
options.url = Parse.serverURL + '/scripts/spec/cloud/cloudCodeRequireFiles.js';
res = await request(options);
let response = res.data;
expect(response).toContain(`require('./cloudCodeAbsoluteFile.js`);
response = response + '\nconst additionalData;\n';
options.method = 'POST';
options.url = Parse.serverURL + '/scripts/spec/cloud/cloudCodeRequireFiles.js';
options.body = {
data: response,
};
try {
await request(options);
fail('should have failed to save');
} catch (e) {
expect(e.text).toBe('{"code":101,"error":"Dashboard file editing is not active."}');
}
});

it('can edit cloud code file from dashboard', async done => {
const cloudDir = './spec/cloud/cloudCodeRequireFiles.js';
await reconfigureServer({
cloud: cloudDir,
dashboardOptions: {
cloudFileView: true,
cloudFileEdit: true,
},
});
const options = Object.assign({}, masterKeyOptions, {
method: 'GET',
url: Parse.serverURL + '/releases/latest',
});
let originalFile = '';
request(options)
.then(res => {
expect(Array.isArray(res.data)).toBe(true);
const first = res.data[0];
expect(first.userFiles).toBeDefined();
expect(first.checksums).toBeDefined();
expect(first.userFiles).toContain(cloudDir);
expect(first.checksums).toContain(cloudDir);
options.url = Parse.serverURL + '/scripts/spec/cloud/cloudCodeRequireFiles.js';
return request(options);
})
.then(res => {
originalFile = res.data;
let response = res.data;
expect(response).toContain(`require('./cloudCodeAbsoluteFile.js`);
response = response + '\nconst additionalData;\n';
options.method = 'POST';
options.url = Parse.serverURL + '/scripts/spec/cloud/cloudCodeRequireFiles.js';
options.body = {
data: response,
};
return request(options);
})
.then(res => {
expect(res.data).toBe('This file has been saved.');
options.method = 'POST';
options.url = Parse.serverURL + '/scripts/spec/cloud/cloudCodeRequireFiles.js';
options.body = {
data: originalFile,
};
return request(options);
})
.then(() => {
done();
});
});

it('can create functions', done => {
Parse.Cloud.define('hello', () => {
Expand Down
2 changes: 2 additions & 0 deletions spec/cloud/cloudCodeRequireFiles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require('./cloudCodeAbsoluteFile.js');
require('./cloudCodeRelativeFile.js');
22 changes: 22 additions & 0 deletions src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ module.exports.ParseServerOptions = {
action: parsers.objectParser,
default: {},
},
dashboardOptions: {
env: 'PARSE_SERVER_DASHBOARD_OPTIONS',
help:
'Options for Parse dashboard. Caution, do not use cloudFileEdit on a multi-instance production server.',
action: parsers.objectParser,
default: {},
},
databaseAdapter: {
env: 'PARSE_SERVER_DATABASE_ADAPTER',
help: 'Adapter module for the database',
Expand Down Expand Up @@ -558,6 +565,21 @@ module.exports.IdempotencyOptions = {
default: 300,
},
};
module.exports.DashboardOptions = {
cloudFileEdit: {
env: 'PARSE_SERVER_DASHBOARD_OPTIONS_CLOUD_FILE_EDIT',
help:
'Whether the Parse Dashboard can edit cloud files. If set to true, dashboard can view and edit cloud code files. Do not use on multi-instance servers otherwise your cloud files will be inconsistent.',
action: parsers.booleanParser,
default: false,
},
cloudFileView: {
env: 'PARSE_SERVER_DASHBOARD_OPTIONS_CLOUD_FILE_VIEW',
help: 'Whether the Parse Dashboard can view cloud files.',
action: parsers.booleanParser,
default: false,
},
};
module.exports.AccountLockoutOptions = {
duration: {
env: 'PARSE_SERVER_ACCOUNT_LOCKOUT_DURATION',
Expand Down
7 changes: 7 additions & 0 deletions src/Options/docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* @property {Number|Boolean} cluster Run with cluster, optionally set the number of processes default to os.cpus().length
* @property {String} collectionPrefix A collection prefix for the classes
* @property {CustomPagesOptions} customPages custom pages for password validation and reset
* @property {DashboardOptions} dashboardOptions Options for Parse dashboard. Caution, do not use cloudFileEdit on a multi-instance production server.
* @property {Adapter<StorageAdapter>} databaseAdapter Adapter module for the database
* @property {Any} databaseOptions Options to pass to the mongodb client
* @property {String} databaseURI The full URI to your database. Supported databases are mongodb or postgres.
Expand Down Expand Up @@ -122,6 +123,12 @@
* @property {Number} ttl The duration in seconds after which a request record is discarded from the database, defaults to 300s.
*/

/**
* @interface DashboardOptions
* @property {Boolean} cloudFileEdit Whether the Parse Dashboard can edit cloud files. If set to true, dashboard can view and edit cloud code files. Do not use on multi-instance servers otherwise your cloud files will be inconsistent.
* @property {Boolean} cloudFileView Whether the Parse Dashboard can view cloud files.
*/

/**
* @interface AccountLockoutOptions
* @property {Number} duration number of minutes that a locked-out account remains locked out before automatically becoming unlocked.
Expand Down
13 changes: 13 additions & 0 deletions src/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,10 @@ export interface ParseServerOptions {
:ENV: PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS
:DEFAULT: false */
idempotencyOptions: ?IdempotencyOptions;
/* Options for Parse dashboard. Caution, do not use cloudFileEdit on a multi-instance production server.
:ENV: PARSE_SERVER_DASHBOARD_OPTIONS
:DEFAULT: false */
dashboardOptions: ?DashboardOptions;
/* Options for file uploads
:ENV: PARSE_SERVER_FILE_UPLOAD_OPTIONS
:DEFAULT: {} */
Expand Down Expand Up @@ -296,6 +300,15 @@ export interface IdempotencyOptions {
ttl: ?number;
}

export interface DashboardOptions {
/* Whether the Parse Dashboard can view cloud files.
:DEFAULT: false */
cloudFileView: ?boolean;
/* Whether the Parse Dashboard can edit cloud files. If set to true, dashboard can view and edit cloud code files. Do not use on multi-instance servers otherwise your cloud files will be inconsistent.
:DEFAULT: false */
cloudFileEdit: ?boolean;
}

export interface AccountLockoutOptions {
/* number of minutes that a locked-out account remains locked out before automatically becoming unlocked. */
duration: ?number;
Expand Down
Loading