diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 5688498318..bbd51d2a99 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -5,6 +5,19 @@ const request = require('../lib/request'); const InMemoryCacheAdapter = require('../lib/Adapters/Cache/InMemoryCacheAdapter') .InMemoryCacheAdapter; +const mockAdapter = { + createFile: async (filename) => ({ + name: filename, + location: `http://www.somewhere.com/${filename}`, + }), + deleteFile: () => {}, + getFileData: () => {}, + getFileLocation: (config, filename) => `http://www.somewhere.com/${filename}`, + validateFilename: () => { + return null; + }, +}; + describe('Cloud Code', () => { it('can load absolute cloud code file', done => { reconfigureServer({ @@ -2595,6 +2608,246 @@ describe('beforeLogin hook', () => { expect(beforeFinds).toEqual(1); expect(afterFinds).toEqual(1); }); + + it('beforeSaveFile should not change file if nothing is returned', async () => { + await reconfigureServer({ filesAdapter: mockAdapter }); + Parse.Cloud.beforeSaveFile(() => { + return; + }); + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + const result = await file.save({ useMasterKey: true }); + expect(result).toBe(file); + }); + + it('beforeSaveFile should return file that is already saved and not save anything to files adapter', async () => { + await reconfigureServer({ filesAdapter: mockAdapter }); + const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough(); + Parse.Cloud.beforeSaveFile(() => { + const newFile = new Parse.File('some-file.txt'); + newFile._url = 'http://www.somewhere.com/parse/files/some-app-id/some-file.txt'; + return newFile; + }); + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + const result = await file.save({ useMasterKey: true }); + expect(result).toBe(file); + expect(result._name).toBe('some-file.txt'); + expect(result._url).toBe('http://www.somewhere.com/parse/files/some-app-id/some-file.txt'); + expect(createFileSpy).not.toHaveBeenCalled(); + }); + + it('beforeSaveFile should throw error', async () => { + await reconfigureServer({ filesAdapter: mockAdapter }); + Parse.Cloud.beforeSaveFile(() => { + throw new Parse.Error(400, 'some-error-message'); + }); + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + try { + await file.save({ useMasterKey: true }); + } catch (error) { + expect(error.message).toBe('some-error-message'); + } + }); + + it('beforeSaveFile should change values of uploaded file by editing fileObject directly', async () => { + await reconfigureServer({ filesAdapter: mockAdapter }); + const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough(); + Parse.Cloud.beforeSaveFile(async (req) => { + expect(req.triggerName).toEqual('beforeSaveFile'); + expect(req.master).toBe(true); + req.file.addMetadata('foo', 'bar'); + req.file.addTag('tagA', 'some-tag'); + }); + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + const result = await file.save({ useMasterKey: true }); + expect(result).toBe(file); + const newData = new Buffer([1, 2, 3]); + const newOptions = { + tags: { + tagA: 'some-tag', + }, + metadata: { + foo: 'bar', + }, + }; + expect(createFileSpy).toHaveBeenCalledWith(jasmine.any(String), newData, 'text/plain', newOptions); + }); + + it('beforeSaveFile should change values by returning new fileObject', async () => { + await reconfigureServer({ filesAdapter: mockAdapter }); + const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough(); + Parse.Cloud.beforeSaveFile(async (req) => { + expect(req.triggerName).toEqual('beforeSaveFile'); + expect(req.fileSize).toBe(3); + const newFile = new Parse.File('donald_duck.pdf', [4, 5, 6], 'application/pdf'); + newFile.setMetadata({ foo: 'bar' }); + newFile.setTags({ tagA: 'some-tag' }); + return newFile; + }); + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + const result = await file.save({ useMasterKey: true }); + expect(result).toBeInstanceOf(Parse.File); + const newData = new Buffer([4, 5, 6]); + const newContentType = 'application/pdf'; + const newOptions = { + tags: { + tagA: 'some-tag', + }, + metadata: { + foo: 'bar', + }, + }; + expect(createFileSpy).toHaveBeenCalledWith(jasmine.any(String), newData, newContentType, newOptions); + const expectedFileName = 'donald_duck.pdf'; + expect(file._name.indexOf(expectedFileName)).toBe(file._name.length - expectedFileName.length); + }); + + it('beforeSaveFile should contain metadata and tags saved from client', async () => { + await reconfigureServer({ filesAdapter: mockAdapter }); + const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough(); + Parse.Cloud.beforeSaveFile(async (req) => { + expect(req.triggerName).toEqual('beforeSaveFile'); + expect(req.fileSize).toBe(3); + expect(req.file).toBeInstanceOf(Parse.File); + expect(req.file.name()).toBe('popeye.txt'); + expect(req.file.metadata()).toEqual({ foo: 'bar' }); + expect(req.file.tags()).toEqual({ bar: 'foo' }); + }); + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + file.setMetadata({ foo: 'bar' }); + file.setTags({ bar: 'foo' }); + const result = await file.save({ useMasterKey: true }); + expect(result).toBeInstanceOf(Parse.File); + const options = { + metadata: { foo: 'bar' }, + tags: { bar: 'foo' }, + }; + expect(createFileSpy).toHaveBeenCalledWith(jasmine.any(String), jasmine.any(Buffer), 'text/plain', options); + }); + + it('beforeSaveFile should return same file data with new file name', async () => { + await reconfigureServer({ filesAdapter: mockAdapter }); + const config = Config.get('test'); + config.filesController.options.preserveFileName = true; + Parse.Cloud.beforeSaveFile(async ({ file }) => { + expect(file.name()).toBe('popeye.txt'); + const fileData = await file.getData(); + const newFile = new Parse.File('2020-04-01.txt', { base64: fileData }); + return newFile; + }); + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + const result = await file.save({ useMasterKey: true }); + expect(result.name()).toBe('2020-04-01.txt'); + }); + + it('afterSaveFile should set fileSize to null if beforeSave returns an already saved file', async () => { + await reconfigureServer({ filesAdapter: mockAdapter }); + const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough(); + Parse.Cloud.beforeSaveFile((req) => { + expect(req.fileSize).toBe(3); + const newFile = new Parse.File('some-file.txt'); + newFile._url = 'http://www.somewhere.com/parse/files/some-app-id/some-file.txt'; + return newFile; + }); + Parse.Cloud.afterSaveFile((req) => { + expect(req.fileSize).toBe(null); + }); + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + const result = await file.save({ useMasterKey: true }); + expect(result).toBe(result); + expect(result._name).toBe('some-file.txt'); + expect(result._url).toBe('http://www.somewhere.com/parse/files/some-app-id/some-file.txt'); + expect(createFileSpy).not.toHaveBeenCalled(); + }); + + it('afterSaveFile should throw error', async () => { + await reconfigureServer({ filesAdapter: mockAdapter }); + Parse.Cloud.afterSaveFile(async () => { + throw new Parse.Error(400, 'some-error-message'); + }); + const filename = 'donald_duck.pdf'; + const file = new Parse.File(filename, [1, 2, 3], 'text/plain'); + try { + await file.save({ useMasterKey: true }); + } catch (error) { + expect(error.message).toBe('some-error-message'); + } + }); + + it('afterSaveFile should call with fileObject', async (done) => { + await reconfigureServer({ filesAdapter: mockAdapter }); + Parse.Cloud.beforeSaveFile(async (req) => { + req.file.setTags({ tagA: 'some-tag' }); + req.file.setMetadata({ foo: 'bar' }); + }); + Parse.Cloud.afterSaveFile(async (req) => { + expect(req.master).toBe(true); + expect(req.file._tags).toEqual({ tagA: 'some-tag' }); + expect(req.file._metadata).toEqual({ foo: 'bar' }); + done(); + }); + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save({ useMasterKey: true }); + }); + + it('afterSaveFile should change fileSize when file data changes', async (done) => { + await reconfigureServer({ filesAdapter: mockAdapter }); + Parse.Cloud.beforeSaveFile(async (req) => { + expect(req.fileSize).toBe(3); + expect(req.master).toBe(true); + const newFile = new Parse.File('donald_duck.pdf', [4, 5, 6, 7, 8, 9], 'application/pdf'); + return newFile; + }); + Parse.Cloud.afterSaveFile(async (req) => { + expect(req.fileSize).toBe(6); + expect(req.master).toBe(true); + done(); + }); + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save({ useMasterKey: true }); + }); + + it('beforeDeleteFile should call with fileObject', async () => { + await reconfigureServer({ filesAdapter: mockAdapter }); + Parse.Cloud.beforeDeleteFile((req) => { + expect(req.file).toBeInstanceOf(Parse.File); + expect(req.file._name).toEqual('popeye.txt'); + expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt'); + expect(req.fileSize).toBe(null); + }); + const file = new Parse.File('popeye.txt'); + await file.destroy({ useMasterKey: true }); + }); + + it('beforeDeleteFile should throw error', async (done) => { + await reconfigureServer({ filesAdapter: mockAdapter }); + Parse.Cloud.beforeDeleteFile(() => { + throw new Error('some error message'); + }); + const file = new Parse.File('popeye.txt'); + try { + await file.destroy({ useMasterKey: true }); + } catch (error) { + expect(error.message).toBe('some error message'); + done(); + } + }) + + it('afterDeleteFile should call with fileObject', async (done) => { + await reconfigureServer({ filesAdapter: mockAdapter }); + Parse.Cloud.beforeDeleteFile((req) => { + expect(req.file).toBeInstanceOf(Parse.File); + expect(req.file._name).toEqual('popeye.txt'); + expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt'); + }); + Parse.Cloud.afterDeleteFile((req) => { + expect(req.file).toBeInstanceOf(Parse.File); + expect(req.file._name).toEqual('popeye.txt'); + expect(req.file._url).toEqual('http://www.somewhere.com/popeye.txt'); + done(); + }); + const file = new Parse.File('popeye.txt'); + await file.destroy({ useMasterKey: true }); + }); }); describe('afterLogin hook', () => { diff --git a/spec/FilesController.spec.js b/spec/FilesController.spec.js index aef6448182..0ccc4ad074 100644 --- a/spec/FilesController.spec.js +++ b/spec/FilesController.spec.js @@ -70,7 +70,7 @@ describe('FilesController', () => { expect(log1.level).toBe('error'); const log2 = logs.find( - x => x.message === 'Could not store file: yolo.txt.' + x => x.message === 'it failed with xyz' ); expect(log2.level).toBe('error'); expect(log2.code).toBe(130); diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 075adcbd86..bb1c5c512f 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -626,7 +626,11 @@ describe('Parse.File testing', () => { }).then(fail, response => { expect(response.status).toBe(400); const body = response.text; - expect(body).toEqual('{"code":153,"error":"Could not delete file."}'); + expect(typeof body).toBe('string'); + const { code, error } = JSON.parse(body); + expect(code).toBe(153); + expect(typeof error).toBe('string'); + expect(error.length).toBeGreaterThan(0); done(); }); }); diff --git a/src/Adapters/Files/FilesAdapter.js b/src/Adapters/Files/FilesAdapter.js index 8c92454c2e..ba57fa326d 100644 --- a/src/Adapters/Files/FilesAdapter.js +++ b/src/Adapters/Files/FilesAdapter.js @@ -31,10 +31,19 @@ export class FilesAdapter { * @param {*} data - the buffer of data from the file * @param {string} contentType - the supposed contentType * @discussion the contentType can be undefined if the controller was not able to determine it + * @param {object} options - (Optional) options to be passed to file adapter (S3 File Adapter Only) + * - tags: object containing key value pairs that will be stored with file + * - metadata: object containing key value pairs that will be sotred with file (https://docs.aws.amazon.com/AmazonS3/latest/user-guide/add-object-metadata.html) + * @discussion options are not supported by all file adapters. Check the your adapter's documentation for compatibility * * @return {Promise} a promise that should fail if the storage didn't succeed */ - createFile(filename: string, data, contentType: string): Promise {} + createFile( + filename: string, + data, + contentType: string, + options: Object + ): Promise {} /** Responsible for deleting the specified file * diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index 461fa229a9..844330daa7 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -15,7 +15,7 @@ export class FilesController extends AdaptableController { return this.adapter.getFileData(filename); } - createFile(config, filename, data, contentType) { + createFile(config, filename, data, contentType, options) { const extname = path.extname(filename); const hasExtension = extname.length > 0; @@ -31,12 +31,14 @@ export class FilesController extends AdaptableController { } const location = this.adapter.getFileLocation(config, filename); - return this.adapter.createFile(filename, data, contentType).then(() => { - return Promise.resolve({ - url: location, - name: filename, + return this.adapter + .createFile(filename, data, contentType, options) + .then(() => { + return Promise.resolve({ + url: location, + name: filename, + }); }); - }); } deleteFile(config, filename) { diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index 56fb5a3cc8..ab46fca86c 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -5,6 +5,40 @@ import Parse from 'parse/node'; import Config from '../Config'; import mime from 'mime'; import logger from '../logger'; +const triggers = require('../triggers'); +const http = require('http'); + +const downloadFileFromURI = (uri) => { + return new Promise((res, rej) => { + http.get(uri, (response) => { + response.setDefaultEncoding('base64'); + let body = `data:${response.headers['content-type']};base64,`; + response.on('data', data => body += data); + response.on('end', () => res(body)); + }).on('error', (e) => { + rej(`Error downloading file from ${uri}: ${e.message}`); + }); + }); +}; + +const addFileDataIfNeeded = async (file) => { + if (file._source.format === 'uri') { + const base64 = await downloadFileFromURI(file._source.uri); + file._previousSave = file; + file._data = base64; + file._requestTask = null; + } + return file; +}; + +const errorMessageFromError = (e) => { + if (typeof e === 'string') { + return e; + } else if (e && e.message) { + return e.message; + } + return undefined; +} export class FilesRouter { expressRouter({ maxUploadSize = '20Mb' } = {}) { @@ -68,10 +102,10 @@ export class FilesRouter { } } - createHandler(req, res, next) { + async createHandler(req, res, next) { const config = req.config; const filesController = config.filesController; - const filename = req.params.filename; + const { filename } = req.params; const contentType = req.get('Content-type'); if (!req.body || !req.body.length) { @@ -87,41 +121,121 @@ export class FilesRouter { return; } - filesController - .createFile(config, filename, req.body, contentType) - .then(result => { - res.status(201); - res.set('Location', result.url); - res.json(result); - }) - .catch(e => { - logger.error('Error creating a file: ', e); - next( - new Parse.Error( - Parse.Error.FILE_SAVE_ERROR, - `Could not store file: ${filename}.` - ) + const base64 = req.body.toString('base64'); + const file = new Parse.File(filename, { base64 }, contentType); + const { metadata = {}, tags = {} } = req.fileData || {}; + file.setTags(tags); + file.setMetadata(metadata); + const fileSize = Buffer.byteLength(req.body); + const fileObject = { file, fileSize }; + try { + // run beforeSaveFile trigger + const triggerResult = await triggers.maybeRunFileTrigger( + triggers.Types.beforeSaveFile, + fileObject, + config, + req.auth + ) + let saveResult; + // if a new ParseFile is returned check if it's an already saved file + if (triggerResult instanceof Parse.File) { + fileObject.file = triggerResult; + if (triggerResult.url()) { + // set fileSize to null because we wont know how big it is here + fileObject.fileSize = null; + saveResult = { + url: triggerResult.url(), + name: triggerResult._name, + }; + } + } + // if the file returned by the trigger has already been saved skip saving anything + if (!saveResult) { + // if the ParseFile returned is type uri, download the file before saving it + await addFileDataIfNeeded(fileObject.file); + // update fileSize + const bufferData = Buffer.from(fileObject.file._data, 'base64'); + fileObject.fileSize = Buffer.byteLength(bufferData); + // save file + const createFileResult = await filesController.createFile( + config, + fileObject.file._name, + bufferData, + fileObject.file._source.type, + { + tags: fileObject.file._tags, + metadata: fileObject.file._metadata, + } ); - }); + // update file with new data + fileObject.file._name = createFileResult.name; + fileObject.file._url = createFileResult.url; + fileObject.file._requestTask = null; + fileObject.file._previousSave = Promise.resolve(fileObject.file); + saveResult = { + url: createFileResult.url, + name: createFileResult.name, + }; + } + // run afterSaveFile trigger + await triggers.maybeRunFileTrigger( + triggers.Types.afterSaveFile, + fileObject, + config, + req.auth + ); + res.status(201); + res.set('Location', saveResult.url); + res.json(saveResult); + + } catch (e) { + logger.error('Error creating a file: ', e); + const errorMessage = errorMessageFromError(e) || `Could not store file: ${fileObject.file._name}.`; + next( + new Parse.Error( + Parse.Error.FILE_SAVE_ERROR, + errorMessage + ) + ); + } } - deleteHandler(req, res, next) { - const filesController = req.config.filesController; - filesController - .deleteFile(req.config, req.params.filename) - .then(() => { - res.status(200); - // TODO: return useful JSON here? - res.end(); - }) - .catch(() => { - next( - new Parse.Error( - Parse.Error.FILE_DELETE_ERROR, - 'Could not delete file.' - ) - ); - }); + async deleteHandler(req, res, next) { + try { + const { filesController } = req.config; + const { filename } = req.params; + // run beforeDeleteFile trigger + const file = new Parse.File(filename); + file._url = filesController.adapter.getFileLocation(req.config, filename); + const fileObject = { file, fileSize: null } + await triggers.maybeRunFileTrigger( + triggers.Types.beforeDeleteFile, + fileObject, + req.config, + req.auth + ); + // delete file + await filesController.deleteFile(req.config, filename); + // run afterDeleteFile trigger + await triggers.maybeRunFileTrigger( + triggers.Types.afterDeleteFile, + fileObject, + req.config, + req.auth + ); + res.status(200); + // TODO: return useful JSON here? + res.end(); + } catch (e) { + logger.error('Error deleting a file: ', e); + const errorMessage = errorMessageFromError(e) || `Could not delete file.`; + next( + new Parse.Error( + Parse.Error.FILE_DELETE_ERROR, + errorMessage + ) + ); + } } } diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 8b5e0d1cfb..c4bd380b1e 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -361,6 +361,98 @@ ParseCloud.afterFind = function(parseClass, handler) { ); }; +/** + * Registers a before save file function. + * + * **Available in Cloud Code only.** + * + * ``` + * Parse.Cloud.beforeSaveFile(async (request) => { + * // code here + * }) + *``` + * + * @method beforeSaveFile + * @name Parse.Cloud.beforeSaveFile + * @param {Function} func The function to run before saving a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}. + */ +ParseCloud.beforeSaveFile = function(handler) { + triggers.addFileTrigger( + triggers.Types.beforeSaveFile, + handler, + Parse.applicationId + ); +}; + +/** + * Registers an after save file function. + * + * **Available in Cloud Code only.** + * + * ``` + * Parse.Cloud.afterSaveFile(async (request) => { + * // code here + * }) + *``` + * + * @method afterSaveFile + * @name Parse.Cloud.afterSaveFile + * @param {Function} func The function to run after saving a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}. + */ +ParseCloud.afterSaveFile = function(handler) { + triggers.addFileTrigger( + triggers.Types.afterSaveFile, + handler, + Parse.applicationId + ); +}; + +/** + * Registers a before delete file function. + * + * **Available in Cloud Code only.** + * + * ``` + * Parse.Cloud.beforeDeleteFile(async (request) => { + * // code here + * }) + *``` + * + * @method beforeDeleteFile + * @name Parse.Cloud.beforeDeleteFile + * @param {Function} func The function to run before deleting a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}. + */ +ParseCloud.beforeDeleteFile = function(handler) { + triggers.addFileTrigger( + triggers.Types.beforeDeleteFile, + handler, + Parse.applicationId, + ); +}; + +/** + * Registers an after delete file function. + * + * **Available in Cloud Code only.** + * + * ``` + * Parse.Cloud.afterDeleteFile(async (request) => { + * // code here + * }) + *``` + * + * @method afterDeleteFile + * @name Parse.Cloud.afterDeleteFile + * @param {Function} func The function to after before deleting a file. This function can be async and should take just one parameter, {@link Parse.Cloud.FileTriggerRequest}. + */ +ParseCloud.afterDeleteFile = function(handler) { + triggers.addFileTrigger( + triggers.Types.afterDeleteFile, + handler, + Parse.applicationId, + ); +}; + ParseCloud.onLiveQueryEvent = function(handler) { triggers.addLiveQueryEventHandler(handler, Parse.applicationId); }; @@ -393,6 +485,20 @@ module.exports = ParseCloud; * @property {Parse.Object} original If set, the object, as currently stored. */ +/** + * @interface Parse.Cloud.FileTriggerRequest + * @property {String} installationId If set, the installationId triggering the request. + * @property {Boolean} master If true, means the master key was used. + * @property {Parse.User} user If set, the user that made the request. + * @property {Parse.File} file The file that triggered the hook. + * @property {Integer} fileSize The size of the file in bytes. + * @property {Integer} contentLength The value from Content-Length header + * @property {String} ip The IP address of the client making the request. + * @property {Object} headers The original HTTP headers for the request. + * @property {String} triggerName The name of the trigger (`beforeSaveFile`, `afterSaveFile`) + * @property {Object} log The current logger inside Parse Server. + */ + /** * @interface Parse.Cloud.BeforeFindRequest * @property {String} installationId If set, the installationId triggering the request. diff --git a/src/middlewares.js b/src/middlewares.js index 9ff2654f23..9da196357c 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -114,6 +114,7 @@ export function handleParseHeaders(req, res, next) { } if (fileViaJSON) { + req.fileData = req.body.fileData; // We need to repopulate req.body with a buffer var base64 = req.body.base64; req.body = Buffer.from(base64, 'base64'); diff --git a/src/triggers.js b/src/triggers.js index 41c33ee94b..9ffa0fd7e6 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -12,8 +12,14 @@ export const Types = { afterDelete: 'afterDelete', beforeFind: 'beforeFind', afterFind: 'afterFind', + beforeSaveFile: 'beforeSaveFile', + afterSaveFile: 'afterSaveFile', + beforeDeleteFile: 'beforeDeleteFile', + afterDeleteFile: 'afterDeleteFile', }; +const FileClassName = '@File'; + const baseStore = function() { const Validators = {}; const Functions = {}; @@ -122,6 +128,10 @@ export function addTrigger(type, className, handler, applicationId) { add(Category.Triggers, `${type}.${className}`, handler, applicationId); } +export function addFileTrigger(type, handler, applicationId) { + add(Category.Triggers, `${type}.${FileClassName}`, handler, applicationId); +} + export function addLiveQueryEventHandler(handler, applicationId) { applicationId = applicationId || Parse.applicationId; _triggerStore[applicationId] = _triggerStore[applicationId] || baseStore(); @@ -147,6 +157,10 @@ export function getTrigger(className, triggerType, applicationId) { return get(Category.Triggers, `${triggerType}.${className}`, applicationId); } +export function getFileTrigger(type, applicationId) { + return getTrigger(FileClassName, type, applicationId); +} + export function triggerExists( className: string, type: string, @@ -672,3 +686,61 @@ export function runLiveQueryEventHandlers( } _triggerStore[applicationId].LiveQuery.forEach(handler => handler(data)); } + +export function getRequestFileObject(triggerType, auth, fileObject, config) { + const request = { + ...fileObject, + triggerName: triggerType, + master: false, + log: config.loggerController, + headers: config.headers, + ip: config.ip, + }; + + if (!auth) { + return request; + } + if (auth.isMaster) { + request['master'] = true; + } + if (auth.user) { + request['user'] = auth.user; + } + if (auth.installationId) { + request['installationId'] = auth.installationId; + } + return request; +} + +export async function maybeRunFileTrigger(triggerType, fileObject, config, auth) { + const fileTrigger = getFileTrigger(triggerType, config.applicationId); + if (typeof fileTrigger === 'function') { + try { + const request = getRequestFileObject( + triggerType, + auth, + fileObject, + config + ); + const result = await fileTrigger(request); + logTriggerSuccessBeforeHook( + triggerType, + 'Parse.File', + { ...fileObject.file.toJSON(), fileSize: fileObject.fileSize }, + result, + auth, + ) + return result || fileObject; + } catch (error) { + logTriggerErrorBeforeHook( + triggerType, + 'Parse.File', + { ...fileObject.file.toJSON(), fileSize: fileObject.fileSize }, + auth, + error, + ); + throw error; + } + } + return fileObject; +}