Source: connector/sampleConnector.js

Source: connector/sampleConnector.js

/**
 * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
 * Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl.
 */
/* globals app, module, __dirname */
/* jshint esversion: 6 */
var config = require('./sampleConnectorConfig'),
	responses = require('./sampleConnectorResponses'),
	translationProvider = require('../provider/translationProvider').factory.create(),
	persistenceStore = require('../job-manager/persistenceStore').factory.create(),
	jobManager = require('../job-manager/sampleJobManager');



/**
 * @constructor
 * @alias SampleConnector
 */
var SampleConnector = function () {};

// Internal function to validate the LSP bearer token is provided along with any required parameters
SampleConnector.prototype.validateRequest = function (req, checks) {
	// Sample connector is protected by Basic Auth but needs a bearer token to communicate with the mock LSP server
	// Replace this with whatever is required by the real Language Service Provider
	var bearerToken = req.get('X-CEC-BearerToken') || req.get('BearerToken'),
		tokenType = bearerToken ? bearerToken.split(' ')[0] : '';

	if (bearerToken && (['Basic', 'Bearer'].indexOf(tokenType) === -1)) {
		// add 'Bearer' to token value if not already defined
		bearerToken = 'Bearer ' + bearerToken;
	}

	// get the arguments passed through the request
	var args = {
			BearerToken: bearerToken,
			params: req.params,
			headers: req.headers,
			data: req.body
		},
		functionName = checks && checks.functionName || 'validateRequest',
		requiresCommunity = checks && checks.hasOwnProperty('requiresCommunity') ? checks.requiresCommunity : true;

	return new Promise(function (resolve, reject) {
		// must have a BearerToken parameter
		if (!args.BearerToken) {
			reject({
				errorCode: 403, // Forbidden
				errorMessage: 'sampleConnector.' + functionName + '(): missing header - X-CEC-BearerToken'
			});
		}

		if (checks) {
			// handle required URL parameter checks
			(checks.requiredParameters || []).forEach(function (requiredParameter) {
				if (!args.params || !args.params[requiredParameter]) {
					reject({
						errorCode: 400, // Bad Request
						errorMessage: 'sampleConnector.' + functionName + '(): missing required parameter - ' + requiredParameter
					});
				}
			});

			// handle required headers parameter checks
			(checks.requiredHeaders || []).forEach(function (headerParameter) {
				if (!args.headers || !args.headers[headerParameter]) {
					reject({
						errorCode: 400, // Bad Request
						errorMessage: 'sampleConnector.' + functionName + '(): missing required header - ' + headerParameter
					});
				}
			});

			// handle required body (data) parameter checks
			(checks.requiredData || []).forEach(function (dataParameter) {
				if (!args.data || !args.data[dataParameter]) {
					reject({
						errorCode: 400, // Bad Request
						errorMessage: 'sampleConnector.' + functionName + '(): missing required form data - ' + dataParameter
					});
				}
			});

			// now get the community if required
			if (requiresCommunity) {
				translationProvider.getCommunity(args.BearerToken).then(function (communityId) {
					args.communityId = communityId;
					resolve(args);
				}).catch(function (error) {
					console.log('sampleConnector.' + functionName + '(): failed to get communityId');
					console.log('error', error);
					reject(error);
				});
			} else {
				resolve(args);
			}
		}
	});
};

// Internal function get Additional Data from the header.
SampleConnector.prototype.getAdditionalData = function(req) {

	var additionalDataJson = '',
		additionalData = req.get('X-CEC-AdditionalData') || req.get('AdditionalData');

	if (additionalData) {

		// Connector framework doesn't allow : and , characters. They are replaced by = and ;, respectively.
		// Reverse that in here.
		// Replace = by :
		// Replace ; by ,
		//
		// E.g. [{"parameter"="purchase_order";"name"="Purchase Order";"value"="1234567890";"readonly"=true};{"parameter"="business_unit";"name"="Business Unit";"value"="Marketing";"readonly"=false};{"parameter"="due_date";"name"="Due Date";"value"=1590811200000;"readonly"=false}]
		// Cases of the three replace statements:
		//
		// 1. Boolean value that doesn't start with "
		//       readonly"=false
		//    to readonly":false
		//
		// 2. Between two properties
		//       Marketing";"readonly
		//    to Marketing","readonly
		//    or value"=1590811200000;"
		//    to value"=1590811200000,"
		//
		// 3. Between two objects
		//       true};{"parameter
		//    to true},{"parameter

		var additionalDataAsJsonString = additionalData.replace(/\"=/g, '":');

		additionalDataAsJsonString = additionalDataAsJsonString.replace(/;\"/g, ',"');
		additionalDataAsJsonString = additionalDataAsJsonString.replace(/\};\{/g, '},{');

		additionalDataJson = JSON.parse(additionalDataAsJsonString);
	}

	return additionalDataJson;
};

/**
 * @typedef {Object} JobStatus
 * @memberof SampleConnector
 * @property {object} properties Properties of the job
 * @property {string} properties.id The connector job identifier for the requested job.
 * @property {string} properties.title The connector job label created for this job identifier.
 * @property {string} properties.workflowId The identifier for the workflow to use when translating files in this job. 
 * @property {('CREATED'|'IMPORTING'|'TRANSLATING'|'DOWNLOADING'|'TRANSLATED')} properties.status The status of the job.
 * @property {number} properties.progress Percentage progress through the translation of the job.
 */

/**
 * @typedef {Object} ZipFileStream
 * @memberof SampleConnector
 * @property {string} [Content-Type='application/zip'] Content Type Response Header - application type
 * @property {string} [Content-Length={size of zip file}] Content Length Response Header - size of the zip file
 * @property {stream} zipfile Contents of the zip file
 */

/**
 * Get the translation connector job details, status and translation progress that corresponds to the given job ID from a GET request.
 * The corresponding REST API endpoint is:  GET: /connector/rest/api/v1/job/:id
 * @param {object} req - HTTPS request object.
 * @param {object} req.params - Parameters passed on the URL.
 * @param {string} req.params.id - Identifier for the connector job to retrieve. 
 * @param {object} res.header - Parameters passed on via the header.
 * @param {string} res.header.BearerToken - OAuth Bearer token to connect to the Language Service Provider
 * @param {object} res - HTTPS response object.
 * @returns {HttpResponse.<SampleConnector.JobStatus>} 
 */
SampleConnector.prototype.getJob = function (req, res) {
	// validate the request parameters
	this.validateRequest(req, {
		functionName: 'getJob',
		requiredParameters: ['id']
	}).then(function (args) {
		// get the parameters
		var jobId = args.params.id;

		// get the meta-data for the job
		persistenceStore.getJob({
			jobId: jobId
		}).then(function (jobMetadata) {
			var response = responses.formatResponse('GET', '/v1/job/' + jobId, {
				jobId: jobMetadata.properties.id,
				jobTitle: jobMetadata.name,
				workflowId: jobMetadata.workflowId,
				jobStatus: jobMetadata.status,
				jobProgress: jobMetadata.progress,
				jobStatusMessage: jobMetadata.statusMessage
			});
			res.setHeader('Content-Type', 'application/json');
			res.end(JSON.stringify(response));
		}).catch(function (error) {
			// request to get the job
			res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
			res.end();
		});
	}).catch(function (error) {
		// request failed parameter validation, reject it
		res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
		res.end();
	});
};


/**
 * Create a translation job from a POST request.
 * The job is stored in the connector persistence store and will be used to correlate translation requests from OCE to the Language Service Provider.
 * The job "status" is set to "CREATED".
 * The corresponding REST API endpoint is: POST: /connector/rest/api/v1/job
 * @param {object} req - HTTPS request object.
 * @param {object} req.body - Body object extracted from the 'req' parameter.
 * @param {string} req.body.name - Name of the job to create.  
 * @param {object} req.header - Parameters passed on via the header.
 * @param {string} req.header.BearerToken - OAuth Bearer token to connect to the Language Service Provider
 * @param {string} req.header.workflowId - The identifier for the workflow to use when translating files in this job. 
 * @param {object} res - HTTPS response object.
 * @returns {HttpResponse.<SampleConnector.JobStatus>} 
 */
SampleConnector.prototype.createJob = function (req, res) {
	var self = this;

	// get the workflow ID
	var workflowId = req.get('X-CEC-WorkflowId') || req.get('WorkflowId');
	if (!workflowId) {
		var error = {
			errorCode: 400, // Bad Request
			errorMessage: 'sampleConnector.createJob(): missing required header - X-CEC-WorkflowId'
		};

		// failed to create the job
		res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
		res.end();
		return;
	}

	// validate the rest of the request parameters
	self.validateRequest(req, {
		functionName: 'createJob',
		requiredData: ['name']
	}).then(function (args) {
		// get the parameters
		var jobName = args.data.name,
			additionalData = self.getAdditionalData(req);

		// create a new job
		persistenceStore.createJob({
			jobName: jobName,
			workflowId: workflowId,
			authToken: args.BearerToken,
			additionalData: additionalData
		}).then(function (newJob) {
			// create a project in the LSP server for this job
			var projectName = args.data.name + Math.floor(100000 + Math.random() * 900000);

			translationProvider.addProject(args.BearerToken, args.communityId, workflowId, projectName).then(function (projectDetails) {
				// update the job details with the project details
				newJob.properties.projectId = projectDetails.properties.id;

				// save the updated job details
				persistenceStore.updateJob(newJob).then(function (jobJSON) {
					// format and return the response
					var response = responses.formatResponse('POST', '/v1/job', {
						jobName: jobJSON.name,
						jobId: jobJSON.properties.id,
						projectId: jobJSON.properties.projectId,
						workflowId: jobJSON.workflowId,
						jobStatus: jobJSON.status
					});
					res.setHeader('Content-Type', 'application/json');
					res.end(JSON.stringify(response));
				}).catch(function (error) {
					// failed to update the job
					res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
					res.end();
				});
			});
		}).catch(function (error) {
			// failed to create the job
			res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
			res.end();
		});
	}).catch(function (error) {
		// request failed parameter validation, reject it
		res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
		res.end();
	});
};


/**
 * Translate the POSTed zip from the request body using the connector job identifier passed on the URL. 
 * The corresponding REST API endpoint is:  POST: /connector/rest/api/v1/job/:id/translate
 * The job "status" is updated to "IMPORTING" until all files have been extracted from the zip and corresponding documents created in the Language Service Provider.
 * @param {object} req - HTTPS request object.
 * @param {object} req.params - Parameters passed on the URL.
 * @param {string} req.params.id - Identifier for the connector job to retrieve. 
 * @param {stream} req.body - Contents of the zip file passed as a stream
 * @param {object} req.header - Parameters passed on via the header.
 * @param {string} req.header.BearerToken - OAuth Bearer token to connect to the Language Service Provider
 * @param {object} res - HTTPS response object.
 * @returns {HttpResponse.<SampleConnector.JobStatus>} 
 */
SampleConnector.prototype.translateJob = function (req, res) {
	var self = this;

	// validate the request parameters
	this.validateRequest(req, {
		functionName: 'translateJob',
		requiredParameters: ['id']
	}).then(function (args) {
		// get the parameters
		var jobId = args.params.id;

		// get the metadata for this job
		persistenceStore.getJob({
			jobId: jobId
		}).then(function (origJobMetadata) {
			// get the zip content
			var body,
				data = [];

			req.on('data', function(chunk) {
				data.push(Buffer.from(chunk, 'binary'));
			});
			req.on('end', function() {
				body = Buffer.concat(data);

				// import the zip file
				persistenceStore.importSourceZip({
					jobId: jobId,
					zipFile: body
				}).then(function () {
					// zipfile saved

					// update status to "IMPORTING"
					origJobMetadata.status = "IMPORTING";
					persistenceStore.updateJob(origJobMetadata).then(function (jobMetadata) {
						// kickoff the job translation background task
						jobManager.translateJob(jobMetadata);

						// return the updated job details
						var response = responses.formatResponse('POST', '/v1/job/' + jobId + '/translate', {
							jobId: jobMetadata.properties.id,
							jobTitle: jobMetadata.name,
							workflowId: jobMetadata.workflowId,
							projectId: jobMetadata.projectId,
							jobStatus: jobMetadata.status
						});
						res.setHeader('Content-Type', 'application/json');
						res.end(JSON.stringify(response));
					}).catch(function (error) {
						console.log('sampleConnector.translateJob(): failed to update job status');
						res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
						res.end();
					});
				}).catch(function (error) {
					console.log('sampleConnector.translateJob(): failed to save zip file');
					res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
					res.end();
				});
			});
		}).catch(function (error) {
			res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
			res.end();
		});
	}).catch(function (error) {
		// request failed parameter validation, reject it
		res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
		res.end();
	});
};

/**
 * Refreshes (re-fetches) the translation files from the LSP server for the job ID passed on the URL.
 * The corresponding REST API endpoint is:  POST: /connector/rest/api/v1/job/:id/translate
 * The job "status" is updated to "IMPORTING" until all files have been extracted from the zip and corresponding documents created in the Language Service Provider.
 * @param {object} req - HTTPS request object.
 * @param {object} req.params - Parameters passed on the URL.
 * @param {string} req.params.id - Identifier for the connector job to retrieve. 
 * @param {stream} req.body - Contents of the zip file passed as a stream
 * @param {object} req.header - Parameters passed on via the header.
 * @param {string} req.header.BearerToken - OAuth Bearer token to connect to the Language Service Provider
 * @param {object} res - HTTPS response object.
 * @returns {HttpResponse.<SampleConnector.JobStatus>} 
 */
SampleConnector.prototype.refreshTranslation = function (req, res) {
	var self = this;

	// validate the request parameters
	this.validateRequest(req, {
		functionName: 'refreshTranslation',
		requiredParameters: ['id']
	}).then(function (args) {
		// get the parameters
		var jobId = args.params.id;

		// get the metadata for this job
		persistenceStore.getJob({
			jobId: jobId
		}).then(function (origJobMetadata) {
			// status must be "TRANSLATED" before we can switch to "DOWNLOADING"
			if (origJobMetadata.status === 'TRANSLATED') {
				// update status to "DOWNLOADING" and change the progress
				origJobMetadata.status = "DOWNLOADING";
				origJobMetadata.progress = '95';

				persistenceStore.updateJob(origJobMetadata).then(function (jobMetadata) {
					// kickoff the job translation background task
					jobManager.translateJob(jobMetadata);

					// return the updated job details
					var response = responses.formatResponse('POST', '/v1/job/' + jobId + '/refreshTranslation', {
						jobId: jobMetadata.properties.id,
						jobTitle: jobMetadata.name,
						workflowId: jobMetadata.workflowId,
						projectId: jobMetadata.projectId,
						jobStatus: jobMetadata.status,
						jobProgress: jobMetadata.progress
					});
					res.setHeader('Content-Type', 'application/json');
					res.end(JSON.stringify(response));
				}).catch(function (error) {
					console.log('sampleConnector.refreshTranslation(): failed to update job status');
					res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
					res.end();
				});
			} else {
				// can't switch to "DOWNLOADING" at this time
				res.writeHead(400, "Cannot refresh translations for job when job status is not 'TRANSLATED'");
				res.end();
			}
		}).catch(function (error) {
			res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
			res.end();
		});
	}).catch(function (error) {
		// request failed parameter validation, reject it
		res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
		res.end();
	});
};

/**
 * Get the translated zip file for the specified connector job identifier. 
 * The corresponding REST API endpoint is:  GET: /connector/rest/api/v1/job/:id/translation
 * @param {object} req - HTTPS request object.
 * @param {object} req.params - Parameters passed on the URL.
 * @param {string} req.params.id - Identifier for the connector job to retrieve. 
 * @param {object} req.header - Parameters passed on via the header.
 * @param {string} req.header.BearerToken - OAuth Bearer token to connect to the Language Service Provider
 * @param {object} res - HTTPS response object.
 * @returns {HttpResponse.<SampleConnector.ZipFileStream>} 
 */
SampleConnector.prototype.getTranslation = function (req, res) {
	// validate the request parameters
	this.validateRequest(req, {
		functionName: 'getTranslation',
		requiredParameters: ['id']
	}).then(function (args) {
		// get the parameters
		var jobId = args.params.id;

		// export the translation zip file for the job
		persistenceStore.exportTranslationZip({
			jobId: jobId
		}).then(function (zipInfo) {
			// update the response header
			res.writeHead(200, {
				'Content-Disposition': 'attachment;filename=translatedJob.zip',
				'Content-Type': 'application/zip',
				'Content-Length': zipInfo.size
			});

			// pipe the zip stream to the response
			zipInfo.stream.pipe(res);
		}).catch(function (error) {
			// request failed parameter validation, reject it
			res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
			res.end();
		});
	}).catch(function (error) {
		// request failed parameter validation, reject it
		res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
		res.end();
	});
};

/**
 * Remove the job and all associated data from the translation connector
 * The corresponding REST API endpoint is:  DELETE: /connector/rest/api/v1/job/:id
 * @param {object} req - HTTPS request object.
 * @param {object} req.params - Parameters passed on the URL.
 * @param {string} req.params.id - Identifier for the connector job to retrieve. 
 * @param {object} req.header - Parameters passed on via the header.
 * @param {string} req.header.BearerToken - OAuth Bearer token to connect to the Language Service Provider
 * @param {object} res - HTTPS response object.
 * @returns {HttpResponse.<string>} confirmation that deletion succeeded
 */
SampleConnector.prototype.deleteJob = function (req, res) {
	// validate the request parameters
	this.validateRequest(req, {
		functionName: 'deleteJob',
		requiredParameters: ['id']
	}).then(function (args) {
		// get the parameters
		var jobId = args.params.id;

		persistenceStore.deleteJob({
			jobId: jobId
		}).then(function () {
			res.writeHead(200);
			res.end(JSON.stringify({
				message: 'sampleConnector.deleteJob(): Successfully removed job: ' + jobId
			}));
		}).catch(function (error) {
			// request failed parameter validation, reject it
			res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
			res.end();
		});
	}).catch(function (error) {
		// request failed parameter validation, reject it
		res.writeHead(error && error.errorCode || 400, error && error.errorMessage || error);
		res.end();
	});
};

/**
 * Validate that the Basic Auth security is enabled and working
 * The corresponding REST API endpoint is:  GET: /connector/rest/api/v1/authorization/basicAuthorization
 * @param {object} req - HTTPS request object.
 * @param {object} req.header - Parameters passed on via the header.
 * @param {string} req.header.X-CEC-UserName - Username for basic auth validation.
 * @param {string} req.header.X-CEC-Pwd - Password to use for basic auth validation.
 * @param {object} res - HTTPS response object.
 * @returns {HttpResponse.<true>} confirmation that can connect using basic auth 
 */
SampleConnector.prototype.validateBasicAuthorization = function (req, res) {
	// endpoints are secured in sampleBasicAuth.js, if we got to here, it passed validation
	res.setHeader('Content-Type', 'application/json');
	res.end(JSON.stringify(true));
};

/**
 * Return the array of API versions supported by this server
 * The corresponding REST API endpoint is:  GET: /connector/rest/api
 * @param {object} req - HTTPS request object.
 * @param {object} res - HTTPS response object.
 * @returns {HttpResponse.<array>} ["v1"]
 */
SampleConnector.prototype.getApiVersions = function (req, res) {
	// only support 'v1'
	res.setHeader('Content-Type', 'application/json');
	res.end(JSON.stringify(['v1']));
};

/**
 * Return the connector configuration of this translation connector server
 * The corresponding REST API endpoint is:  GET: /connector/rest/api/v1/server
 * @param {object} req - HTTPS request object.
 * @param {object} res - HTTPS response object.
 * @returns {HttpResponse.<object>} JSON describing the server configuration
 */
SampleConnector.prototype.getServer = function (req, res) {
	// format and return the response
	var response = responses.formatResponse('GET', '/v1/server', {
		"localizedConnectorName": config.localizedConnectorName,
		"connectorAbout": config.connectorAbout,
		"locale": config.locale,
		"localizedConnectorAbout": config.localizedConnectorAbout,
		"authenticationType": config.authenticationType,
		"connectorServiceProviderName": config.connectorServiceProviderName,
		"connectorVersion": config.connectorVersion
	});
	res.setHeader('Content-Type', 'application/json');
	res.end(JSON.stringify(response));
};

// Export the sample connector
module.exports = new SampleConnector();