initial commit

This commit is contained in:
2026-08-11 15:14:02 +02:00
parent 29d6c0ce18
commit 15478068a0
26 changed files with 11657 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
.env
node_modules
uploads
.DS_Store
+28
View File
@@ -0,0 +1,28 @@
# Salesforce PDF Attachment Automation
A Node.js utility that extracts text and metadata (like *Codice Fiscale* and Name variations) from PDF documents and automatically attaches them to the corresponding Account records in a Salesforce Sandbox.
---
## Features
* **PDF Parsing:** Reads local PDF files and extracts metadata using regex matching.
* **Smart Account Matching:** Queries Salesforce Accounts by `Codice Fiscale` or Name permutations (first name / last name variations).
* **Automated Uploads:** Converts PDFs to Base64 strings and attaches them directly to Accounts in Salesforce via `ContentVersion`.
* **Duplicate Protection:** Filters and sorts matching accounts by creation date.
---
## Prerequisites
Before running this project, ensure you have:
* [Node.js](https://nodejs.org/)
* A **Salesforce Sandbox** account with API access
* Salesforce Security Token
---
## sfdc2.js
This projects uses the ```sfdc2.js``` library which facilitates interaction with the Salesforce API.
+16
View File
@@ -0,0 +1,16 @@
{
"access_token":"00D0Q0000008ipI!AQEAQMnYks5eNzLDp_OnKVFWoLjIRbH9.l5m1dWrXi9uincFgJLXfp2Xjsq3fUVLME_hWxZLUz6bi3MGn.kRRoS_dfH12yz1",
"refresh_token":"5Aep861kbRszIADsvg2SfeYjNbk56Irp0JDnM8BJPqvw6JZavvYBjsOg_Ha2KvaCiqyM6RKdfGpRnsZtRPSVdRB",
"signature":"oPM1SwaBGuPyYocnfh3h197bxQ5AzR0hK0+gdHhkJks=",
"scope":"refresh_token web api",
"instance_url":"https://almascuola--dev2021.sandbox.my.salesforce.com",
"id":{
"access_token":"00D0Q0000008ipI!AQEAQMnYks5eNzLDp_OnKVFWoLjIRbH9.l5m1dWrXi9uincFgJLXfp2Xjsq3fUVLME_hWxZLUz6bi3MGn.kRRoS_dfH12yz1",
"refresh_token":"5Aep861kbRszIADsvg2SfeYjNbk56Irp0JDnM8BJPqvw6JZavvYBjsOg_Ha2KvaCiqyM6RKdfGpRnsZtRPSVdRB",
"signature":"oPM1SwaBGuPyYocnfh3h197bxQ5AzR0hK0+gdHhkJks=",
"scope":"refresh_token web api",
"instance_url":"https://almascuola--dev2021.sandbox.my.salesforce.com",
"id":"https://test.salesforce.com/id/00D0Q0000008ipIUAQ/005MA00000AiipLYAR",
"token_type":"Bearer","issued_at":"1783859104265"
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"access_token":"00D1t000000ss9x!AQEAQE1CnJO6.Pj0Dp.sNM8GJiLqg78JObdGkoMe_8bSvKiAghEx96_U4UerGqmWNjC0HhRqT3WxS9SAJ2mV.Sx_zZEB9cKb",
"refresh_token":"5Aep8610F.RUa2F48BnJfSHjt4eCIOSyaqJl8Ge64SjsPyNpED5mcb5jPDcu_LKFFtZxiGH9VxtfA==",
"signature":"BKDhT/jYJpboLOxPNz5Wc4jOH6WA1zoPesOYzQbpLyg=",
"scope":"refresh_token chatter_api api full",
"id_token":"eyJraWQiOiIyNjAiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiNEs2VUplRmxJeXFaVVVURzE0dXRYUSIsInN1YiI6Imh0dHBzOi8vbG9naW4uc2FsZXNmb3JjZS5jb20vaWQvMDBEMXQwMDAwMDBzczl4RUFBLzAwNTF0MDAwMDAxcGRWckFBSSIsImF1ZCI6IjNNVkc5UmQzcUM2b01hbFVJQWhPMXlyYUpCU3NkT181WDB0VGgwTTNaVzJ3aWFfZEhaYXc0d2lrYlU5STVYVVpDakQ4RmI0dnZTMmd0djBranlLUjEiLCJhbXIiOlsiVG90cCIsIm1mYSIsIm90cCIsInB3ZCJdLCJpc3MiOiJodHRwczovL2xvZ2luLnNhbGVzZm9yY2UuY29tIiwiZXhwIjoxNzczNzMyMTY0LCJpYXQiOjE3NzM3MzIwNDR9.hNM0BuXYa40Bu8WAHfPJtA6AzgSRRaKgejUTu6XwD7-tdAUe8f_mSdU4OnSRrreW13ixGzqk8r0CEWGMvPPTNHYueePMoNYSRWZeERJlclbygijwLu-w6U2HVp6KTl7kl2-VSwT2xUZ2lOu-Cu0hAY1EoqVrq-oRHUWfSjKgjCanPeD4MIJ2yxo6NrOJ6EV6wQanInkMQeycVtMIJUGTlNqeQMuBJkk9HY_kjaMI_M4acIGnupUcl4i-h9-Kzxh4BCVCQBTdh45PQ69VbdfyJbhX1W3czaXxx0W0Tyr6J1EtX5ttlOB9Hyp16dkbE_ajpSilUSPil2-2_a_g1sLgWX3OkVO0OR8CLi1F4ZDLR8Li9NYVnZNABluM912siTHUJfhAJj1p5EsKN0TMpU4f-k7EL3h90C7oiwlFW1OmHs6r3XK9N3sVAqWz9ByRggyNw_l_hhyIxKNZNJisBSURRTr85thkhwqH9Gq8b8w0jnCxLN0pnz8rmDK6oxwbjcLLaOCGp-oqV8IamMGkJu45aCAqZVxSvr5oEz3zqb2PoeEyZDVEzzrRlw-CcTxH5tF14zxq4WJXqKV1kbRgIOGdgGAQzgP0fnm5HGO4cIDIHFDH2TYX9Km-AD3JkkHIKRvqcYOCVmW8ir5RJ10yNzNmn6C1IwjB8JnP4KNFmFQLQBc",
"instance_url":"https://almascuola.my.salesforce.com",
"id":"https://login.salesforce.com/id/00D1t000000ss9xEAA/0051t000001pdVrAAI",
"token_type":"Bearer",
"issued_at":"1773732044735"
}
+6795
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
{
"name": "backend",
"version": "1.0.0",
"description": "basic node project",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "denise",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"axios": "^1.19.0",
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"exceljs": "^4.4.0",
"express": "^5.2.1",
"jest": "^30.4.2",
"multer": "^2.2.0",
"nodemon": "^3.1.14",
"papaparse": "^5.5.4",
"pdf-lib": "^1.17.1",
"pdf-parse": "^2.4.5",
"supertest": "^7.2.2"
}
}
+70
View File
@@ -0,0 +1,70 @@
const express = require('express');
const multer = require('multer');
const cors = require('cors');
const {
initializeSalesforceConnection,
attachPdfToAccount,
splitExcelFile,
splitPDF,
cleanUp
} = require('./services');
const app = express();
app.use(cors({
origin: 'http://localhost:5173'
}));
const upload = multer({ dest: 'uploads/' });
app.post('/api/documents', upload.array('documents'), async (req, res) => {
const documentTitle = req.body.title;
let excelFile = null;
let pdfFile = null;
if (req.files && req.files.length > 0) {
req.files.forEach((file) => {
const fileName = file.originalname.toLowerCase();
if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) {
excelFile = file.path;
} else if (fileName.endsWith('.pdf')) {
pdfFile = file.path;
}
});
}
if (!documentTitle || !excelFile || !pdfFile) {
if (req.files) {
cleanUp(req.files.map((f) => f.path));
}
return res.status(400).json({
error: !documentTitle ? 'Document title is required' : 'Both excel and pdf files are required'
});
}
const tempFilePath = [];
try {
let resultExcel = await splitExcelFile(excelFile);
let resultPdf = await splitPDF(resultExcel, pdfFile, tempFilePath);
await initializeSalesforceConnection();
await attachPdfToAccount(resultPdf, documentTitle); // No need to pass sfdc
return res.status(200).json({
ok: true,
message: 'Files uploaded successfully!'
});
} catch (err) {
console.error('Error in processing the files: ', err);
return res.status(500).json({
ok: false,
message: 'Error in processing and uploading the files'
});
} finally {
cleanUp(tempFilePath);
cleanUp([excelFile, pdfFile]);
}
});
module.exports = { app };
+12
View File
@@ -0,0 +1,12 @@
// excel1 => /Users/ddatinguinoo/Downloads/Estratto IPP 5 Sic.xlsx
// excel2 => /Users/ddatinguinoo/Downloads/Estratto Haccp IPP 5 (1).xlsx
// excel3 di prova => /Users/ddatinguinoo/Downloads/File di prova.xlsx
// pdf1 => /Users/ddatinguinoo/Downloads/G-S IPP 5.pdf
// pdf2 => /Users/ddatinguinoo/Downloads/H IPP 5.pdf
const { app } = require('./app');
// this opens the network port
// this is separated from app.js so that it doesn't interfere with tests or serverless deployments
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
+226
View File
@@ -0,0 +1,226 @@
const ExcelJS = require('exceljs');
const { PDFParse } = require('pdf-parse');
const { PDFDocument } = require('pdf-lib');
const fs = require('fs');
const os = require('os');
const path = require('path');
const sfdc = require('./sfdc2');
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });
const authConfig = {
sfdcTokenFile: path.resolve(__dirname, '..', process.env.SFDC_TOKEN_FILE || 'almadev21.json'),
sfdcClientId: process.env.SFDC_CLIENT_ID,
sfdcClientSecret: process.env.SFDC_CLIENT_SECRET
};
async function initializeSalesforceConnection() {
// sfdc.setProductionBaseUrl(); // or sfdc.setSandboxBaseUrl();
sfdc.setSandboxBaseUrl();
sfdc.setTokenFile(authConfig.sfdcTokenFile);
sfdc.setClientId(authConfig.sfdcClientId);
sfdc.setClientSecret(authConfig.sfdcClientSecret);
// Try to load existing token or initiate OAuth flow
if (!await sfdc.getSalesforceToken()) {
console.log('No token found. Initiating OAuth2 flow...');
await sfdc.initToken((loginUrl) => {
console.log(`Please open this URL to authenticate: ${loginUrl}`);
});
} else {
console.log('Token loaded successfully.');
}
// Ensure the access token is valid and refresh if necessary
await sfdc.checkToken();
console.log('Salesforce connection is ready.');
}
// Escape single quotes for SOQL queries (e.g., D'Angelo -> D\'Angelo)
function escapeSoql(str) {
return str ? str.replace(/'/g, "\\'") : '';
}
// the pdf files contain the identifying ids of the users such as 'Codice Fiscale' (CF) and we use them to attach
// the page/pages to the specific account that has that id. If CFs are not available we search based on the
// name an lastname of the user
async function attachPdfToAccount(tempFiles, docTitle) {
let parser;
try {
for (let i = 0; i < tempFiles.length; i++) {
const pdfBuffer = fs.readFileSync(tempFiles[i]);
const base64Pdf = pdfBuffer.toString('base64');
parser = new PDFParse({ data: pdfBuffer });
try {
const result = await parser.getText();
let conditions = [];
let matchAccount = null;
const userID = result.text.match(/Codice fiscale:\s*([A-Za-z0-9]+)/i);
const codiceFiscale = userID ? userID[1].trim() : null;
if (codiceFiscale) {
const cfSoql = `SELECT Id, Name, CF__c FROM Account WHERE CF__c='${codiceFiscale}' ORDER BY CreatedDate ASC LIMIT 1`;
const cfResult = await sfdc.query(cfSoql);
if (cfResult && cfResult.records && cfResult.records.length > 0) {
matchAccount = cfResult.records[0];
}
}
if (!matchAccount) {
// since the name and lastname of the user are reversed in some files, we need to put it in order
// to match the name from the database
const nameMatch = result.text.match(/Il\s+Sig\.\/La\s+Sig\.ra[ \t]+([A-Za-zÀ-ÿ']+(?:[ \t]+[A-Za-zÀ-ÿ']+)+)/i);
if (nameMatch) {
const rawName = nameMatch[1].trim();
const parts = rawName.split(/\s+/);
if (parts.length >= 2) {
const fullName = parts.join(' ');
const reversedName = [...parts.slice(1), parts[0]].join(' ');
if (fullName !== reversedName) {
conditions.push(`Name='${escapeSoql(reversedName)}'`);
}
} else {
conditions.push(`Name='${escapeSoql(rawName)}'`);
}
const nameSoql = `SELECT Id, Name, CF__c FROM Account WHERE (${ conditions.join(' OR ') }) ORDER BY CF__c DESC NULLS LAST, CreatedDate ASC LIMIT 1`;
const nameResult = await sfdc.query(nameSoql);
if (nameResult && nameResult.records && nameResult.records.length > 0) {
matchAccount = nameResult.records[0];
}
}
}
if (matchAccount) {
console.log('--- Match found! ---');
const nowIsoString = new Date().toISOString();
const newDocument = await sfdc.create('Documento__c', {
Account__c: matchAccount.Id,
Name: docTitle,
Data_Caricamento__c: nowIsoString,
Tipo_Documento__c: docTitle,
Stato__c: 'Valido'
});
await sfdc.create('ContentVersion', {
Title: `${ docTitle } - ${ matchAccount.Name } -`,
PathOnClient: `${ docTitle }_${ matchAccount.Name }.pdf`,
VersionData: base64Pdf,
FirstPublishLocationId: newDocument.id
});
} else {
console.log('Account non trovato su Salesforce!');
}
} finally {
if (parser) {
await parser.destroy();
}
}
}
console.log(`PDFs successfully attached!`);
} catch (error) {
console.error('Error uploading PDF: ', error);
throw error; // Rethrow to let app.js catch the error
}
}
async function processPage(pdfDoc, pageIndex, tempFiles) {
try {
const newDoc = await PDFDocument.create();
const [copiedPage] = await newDoc.copyPages(pdfDoc, [pageIndex]);
newDoc.addPage(copiedPage);
const pdfBytes = await newDoc.save();
const filepath = path.join(os.tmpdir(), `fileTemp_page_${pageIndex}.pdf`);
fs.writeFileSync(filepath, pdfBytes);
tempFiles.push(filepath);
} catch (error) {
console.error('Error in processPage:', error);
throw error;
}
}
async function splitExcelFile(inputFile) {
try {
console.log(`Loading excel file: ${inputFile}...\n`);
const workbook = new ExcelJS.Workbook();
await workbook.xlsx.readFile(inputFile);
const worksheet = workbook.getWorksheet(1);
return worksheet.actualRowCount - 1;
} catch (error) {
console.error('Error processing the excel file: ', error);
throw error; // Rethrow error
}
}
const getCodiceFiscale = (pageText) => {
if (!pageText) return null;
const match = pageText.match(/Codice fiscale:\s*([A-Za-z0-9]+)/i);
return match ? match[1].toUpperCase() : null;
};
async function splitPDF(numRows, inputFile, tempFiles) {
console.log(`Loading pdf file: ${inputFile}...\n`);
let parser;
try {
const existingPdfBytes = fs.readFileSync(inputFile);
parser = new PDFParse({ data: existingPdfBytes });
const result = await parser.getText();
const pagesNumber = result.pages.length;
const srcPdf = await PDFDocument.load(existingPdfBytes);
if (pagesNumber > numRows) {
for (let i = 0; i < pagesNumber; i++) {
// we use optional chaining ?. so that when we encounter null or undefined values, the code doesn't crash
const prevCF = getCodiceFiscale(result.pages[i + 1]?.text);
const currentCF = getCodiceFiscale(result.pages[i]?.text);
if (prevCF && prevCF === currentCF) {
continue;
}
await processPage(srcPdf, i, tempFiles);
}
} else {
for (let i = 0; i < pagesNumber; i++) {
await processPage(srcPdf, i, tempFiles);
}
}
return tempFiles;
} catch (error) {
console.error(`Error processing the pdf file: `, error);
throw error;
} finally {
if (parser) {
await parser.destroy();
}
}
}
function cleanUp(tempFiles) {
if (!Array.isArray(tempFiles)) return;
tempFiles.forEach((file) => {
try {
if (file && fs.existsSync(file)) {
fs.unlinkSync(file);
}
} catch (err) {
console.error(`Failed in deleting the temporary file ${file}: `, err);
}
});
}
module.exports = {
initializeSalesforceConnection,
attachPdfToAccount,
splitExcelFile,
splitPDF,
cleanUp
};
+923
View File
@@ -0,0 +1,923 @@
var fs = require('fs');
var axios = require('axios');
var Papa = require('papaparse');
const sfdc = {
baseurl:'',
apiurl:'/services/data/v58.0',
callbackUrl:'http://localhost:3000/auth',
credentials: {},
afterLoginUrl:undefined,
app: {},
server: undefined,
logresult: function(resp) { console.log('internal--->'); console.log(JSON.stringify(resp,null,2)); },
}
/**
* Set token filename
* @param {string} filename
*/
sfdc.setTokenFile=function(filename) {
this.tokenfile=filename;
}
/**
* Set Custom URL
* @param {string} customUrl
*/
sfdc.setCustomBaseUrl=function(customUrl) {
this.baseurl=customUrl;
}
/**
* Set Sandbox URL
*/
sfdc.setSandboxBaseUrl=function() {
this.baseurl='https://test.salesforce.com';
}
/**
* Set Production URL
*/
sfdc.setProductionBaseUrl=function() {
this.baseurl='https://login.salesforce.com';
}
/**
* Set ClientId
* @param {string} clientId
*/
sfdc.setClientId=function(clientId) {
this.clientId=clientId;
}
/**
* Set ClientSecret
* @param {string} clientSecret
*/
sfdc.setClientSecret=function(clientSecret) {
this.clientSecret=clientSecret;
}
/**
* retrieve Token file
* @returns null if not found, token struct if found
*/
sfdc.getSalesforceToken=async function() {
try {
let data=fs.readFileSync(this.tokenfile);
this.credentials = JSON.parse(data);
return this.credentials;
} catch(err) {
console.log('cannot find files');
return false;
}
}
/**
* store Token file
*/
sfdc.storeSalesforceToken=async function() {
console.log('save token');
try {
fs.writeFileSync(this.tokenfile, JSON.stringify(this.credentials));
console.log('saved');
} catch (err) {
console.log(err);
console.log(this.credentials);
}
};
/**
* init oauth2 sequence
* listen for callback calls and store token when received
*/
sfdc.initToken=function(callback) {
return new Promise( (resolve, reject) => {
console.log('initToken');
var express = require('express');
this.app = express();
this.server=this.app.listen(3000);
this.app.get('/auth', async (req, res) => {
await this.manageSalesforceCallback(req, res);
if (this.oauth2phase==2) this.storeSalesforceToken();
resolve();
});
let loginurl=this.requestAuthCode();
if (callback) callback(loginurl);
})
}
/**
* initTokenExpress
* use this when already bind to 3000
* @param {express app} app
* @returns
*/
sfdc.initTokenExpress=function(app) {
return new Promise( (resolve, reject) => {
app.get('/auth', async (req, res) => {
await sfdc.manageSalesforceCallback(req, res);
if (this.oauth2phase==2) sfdc.storeSalesforceToken();
resolve();
});
});
}
/**
* manage callback
* internal function, receive express get method
*/
sfdc.manageSalesforceCallback=async function(req, res) {
var refresh_token = req.query.refresh_token;
var code = req.query.code;
console.log('refresh token: ' + refresh_token);
console.log('code: ' + code)
if (!sfdc.afterLoginUrl)
res.send('ok');
else
res.redirect(sfdc.afterLoginUrl);
if (code != undefined) {
this.oauth2phase=1;
console.log('code received, get token');
this.credentials=await this.getRefreshToken(code);
this.storeSalesforceToken();
if (this.server) this.server.close();
}
if (refresh_token) {
this.oauth2phase=2;
console.log('refresh_token received');
this.credentials=refresh_token;
}
}
/**
* create initial url for receiving auth code
* @returns false if errors, login url if ok
*/
sfdc.requestAuthCode=async function() {
console.log('requestAuthCode');
var url = this.baseurl+'/services/oauth2/authorize';
url += '?response_type=code';
url += '&client_id=' + encodeURIComponent(this.clientId);
url += '&redirect_uri=' + this.callbackUrl;
try {
console.log(url);
let response=await axios.get(url);
if (response.status == 200) {
console.log('Please authenticate via this url in the browser:');
console.log(response.request.res.responseUrl);
return response.request.res.responseUrl;
} else {
console.log('Error on authenticate:');
console.log(response.data.url);
console.log(response.data.explanation);
return false;
}
} catch(error) {
return false;
}
}
/**
* ask for new access token
* @returns non lo so
*/
sfdc.getAccessToken=async function() {
console.log('getAccessToken');
var url = this.baseurl+'/services/oauth2/token';
var body= 'grant_type=refresh_token';
body += '&client_id=' + this.clientId;
body += '&client_secret=' + this.clientSecret;
body += '&refresh_token=' + this.credentials.refresh_token;
try {
let response=await axios.post(url, body);
return response.data;
} catch (error) {
console.log('getAccessToken error:' + error.response.statusText);
console.log('error:', error.response.data);
return false;
}
}
/**
* ask for refresh token
* @param {string} code authcode for this scope
* @returns false if error, refresh token structure if ok
*/
sfdc.getRefreshToken=async function(code) {
console.log('getRefreshToken');
var url = this.baseurl+'/services/oauth2/token';
let body = 'grant_type=authorization_code';
body += '&code=' + code;
body += '&client_id=' + this.clientId;
body += '&client_secret=' + this.clientSecret;
body += '&redirect_uri=' + this.callbackUrl;
let opts = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
try {
console.log(url);
let response=await axios.post(url, body, opts);
console.log(response.data);
return response.data;
} catch (error) {
console.log('getRefreshToken error:' + error.response.statusText);
return false;
}
}
/**
* check autocode ttl
* @returns true if authcode is still valid, false if not
*/
sfdc.isTokenValid=function() {
if (!this.credentials.issued_at) {
console.log('no credentials available');
return false;
}
var h = 2; // token live in hours
var im = h * 60 * 60 * 1000; // token live in milliseconds
var i1 = parseInt(this.credentials.issued_at)
var i2 = new Date().valueOf();
// console.log('i2 vs i1 vs i1+im',i2,i1,i1+im);
if (i2 > (i1 + im)) {
console.log('authcode expired');
return false;
}
console.log('authcode is still valid');
return true;
}
/**
* fresh authcode if old is expired
* @returns authcode old or new
*/
sfdc.checkToken=async function() {
console.log('checkToken');
if (!this.isTokenValid()) {
try {
let res=await this.getAccessToken();
this.credentials.issued_at = res.issued_at;
this.credentials.access_token = res.access_token;
this.credentials.instance_url = res.instance_url;
console.log('new access token generated...');
return res;
} catch(err) {
console.log('checkToken: error on getting a new token!');
console.log('err',err);
return err;
}
} else {
return this.credentials;
}
}
// ======================
// Actions
// ======================
/*
var soql="selct id from account limit 10",
var nextBatch=false;
var irow=0;
do {
let data=await sfdc.query(soql, nextBatch);
console.log(`${irow} of ${data.totalSize}`);
// do what you need
for(var a of data.records) {
setData(a)
}
// do what you need
soql=data.nextRecordsUrl;
nextBatch=!data.done;
irow+=data.records.length;
} while (nextBatch);
*/
sfdc.search=async function(soql, nextRecord=false) {
var url = soql; // next batch
if(!nextRecord) url = this.credentials.instance_url + this.apiurl + '/search/?q=' + encodeURI(soql);
else url = this.credentials.instance_url + soql;
var config = { headers: { 'Authorization': 'Bearer ' + this.credentials.access_token } };
var t1=new Date();
try {
this.checkToken();
let response=await axios.get(url, config);
return response.data;
} catch(err) {
console.log('search error');
console.log(err);
console.log(err.response.data);
return err.response.data;
}
}
sfdc.query=async function(soql, nextRecord=false) {
var url = soql; // next batch
if(!nextRecord) url = this.credentials.instance_url + this.apiurl + '/query/?q=' + encodeURIComponent(soql);
else url = this.credentials.instance_url + soql;
var config = { headers: { 'Authorization': 'Bearer ' + this.credentials.access_token } };
var t1=new Date();
try {
this.checkToken();
let response=await axios.get(url, config);
// console.log(soql);
// console.log('query ok');
var t2=new Date();
// console.log('duration (ms): '+(t2.valueOf()-t1.valueOf() ));
// console.log('records:'+response.data.totalSize);
return response.data;
} catch(err) {
console.log('query error');
console.log(err);
console.log(err.response.data);
return err.response.data;
}
}
sfdc.retrieve=async function(obj, id, fields) {
var url = this.credentials.instance_url;
url += this.apiurl + '/sobjects/'+obj+'/'+id+'?fields='+fields.join(',');
var config = { headers: { 'Authorization': 'Bearer ' + this.credentials.access_token } };
try {
this.checkToken();
let response=await axios.get(url, config);
console.log('retrieve ok');
return response.data;
} catch(error) {
console.log('retrieve bad');
console.log(error.response.data);
return error;
}
}
sfdc.upsert=async function(obj, key, records) {
if (!records || !records.length) return {};
var body={ "allOrNone" : false, records: [] };
records.forEach(function(r) {
var r2=JSON.parse(JSON.stringify(r));
r2['attributes']={ type: obj };
body.records.push( r2 );
});
var url = this.credentials.instance_url;
url += this.apiurl + '/composite/sobjects/'+obj+'/'+key;
var config = { headers: { 'Authorization': 'Bearer ' + this.credentials.access_token } };
try {
this.checkToken();
let response=await axios.patch(url, body, config);
console.log('upsert ok');
return response.data;
} catch(error) {
console.log('upsert bad');
console.log(error.response.data);
return error;
}
}
sfdc.create=async function(obj, record) {
if (!record) return {};
var url = this.credentials.instance_url;
url += this.apiurl + '/sobjects/'+obj;
var config = { headers: { 'Authorization': 'Bearer ' + this.credentials.access_token } };
var body=record;
try {
this.checkToken();
let response=await axios.post(url, body, config);
console.log('create ok');
return response.data;
} catch(error) {
console.log('create bad');
console.log(error.response.data);
return error;
}
}
sfdc.update=async function(obj, key, record) {
if (!record) return {};
var url = this.credentials.instance_url;
url += this.apiurl + '/sobjects/'+obj+'/'+key;
var config = { headers: { 'Authorization': 'Bearer ' + this.credentials.access_token } };
var body=record;
try {
this.checkToken();
let response=await axios.patch(url, body, config);
console.log('create ok');
return response.data;
} catch(error) {
console.log('create bad');
console.log(error.response.data);
return error;
}
}
//
// bulk api 2.0
//
sfdc.bulkQuery=async function (query) {
await this.checkToken();
var config = {
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer ' + this.credentials.access_token
}
};
var body = {
"operation" : "query",
"query" : query
};
var url = this.credentials.instance_url;
url += this.apiurl + '/jobs/query/';
try {
let response=await axios.post(url, JSON.stringify(body), config)
console.log('create ok');
return response.data;
} catch(error) {
console.log('createJob bad');
console.log(error.response.data);
return error;
}
};
sfdc.bulkQueryStatus=async function (jobId) {
await this.checkToken();
var config = {
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'Accept': 'application/json',
'Authorization': 'Bearer ' + this.credentials.access_token
}
};
var url = this.credentials.instance_url;
url += this.apiurl + '/jobs/query/' + jobId + '/';
try {
let response=await axios.get(url, config)
console.log('jobStatus ok');
return response.data;
} catch(error) {
console.log('jobStatus bad');
console.log(error.response.data);
return error;
}
}
sfdc.bulkQueryStatusPolling=async function (jobId, waitTime, waitSteps) {
let bulkWait={state:''};
let count=0;
do {
await new Promise(resolve => setTimeout(resolve, waitTime?waitTime:1000));
bulkWait=await sfdc.bulkQueryStatus(jobId);
count++;
console.log('status:',bulkWait.state,'count:', count);
} while ( (bulkWait.state!='JobComplete' && bulkWait.state!='Failed') || (waitSteps && count<waitSteps) )
return bulkWait.state;
}
sfdc.bulkQueryResultsLoop=async function (jobId, maxRecords) {
let locator=undefined;
let data=[];
do {
let bulkId3=await sfdc.bulkQueryResults(jobId, locator, maxRecords?maxRecords:50000);
console.log('locator:', bulkId3.locator, bulkId3.maxRecords);
locator=bulkId3.locator;
let parsed=Papa.parse(bulkId3.data, {header:true, delimiter: ',', newline: "\n", skipEmptyLines: true});
console.log(parsed.data.length);
data=data.concat(parsed.data);
} while (locator!='null');
return data;
}
sfdc.bulkQueryResults=async function (jobId, locator, maxRecords) {
await this.checkToken();
var config = {
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'Accept': 'text/csv',
'Authorization': 'Bearer ' + this.credentials.access_token
}
};
var url = this.credentials.instance_url;
url += this.apiurl + '/jobs/query/' + jobId + '/results';
let params=[];
if (locator) params.push('locator='+locator);
if (maxRecords) params.push('maxRecords='+maxRecords);
if (params.length) {
url += '?';
url += params.join('&');
}
try {
let response=await axios.get(url, config)
console.log('jobStatus ok');
let res={
"locator": response.headers['sforce-locator'],
"maxRecords": response.headers['sforce-numberofrecords'],
data: response.data
}
return res;
} catch(error) {
console.log('jobStatus bad');
console.log(error.response.data);
return error;
}
}
sfdc.bulkCreate=async function (operation, obj, key) {
await this.checkToken();
var config = {
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'Accept': 'application/json',
'Authorization': 'Bearer ' + this.credentials.access_token
}
};
var body = {
operation: operation,
object: obj,
contentType: "CSV",
columnDelimiter: "COMMA",
lineEnding: "LF",
};
if (key) body.externalIdFieldName=key;
var url = this.credentials.instance_url;
url += this.apiurl + '/jobs/ingest/';
try {
let response=await axios.post(url, JSON.stringify(body), config)
console.log('create ok');
return response.data;
} catch(error) {
console.log('createJob bad');
console.log(error.response.data);
return error;
}
};
sfdc.bulkSendData=async function (jobId, data) {
await this.checkToken();
var config = {
headers: {
'Content-Type': 'text/csv',
'Accept': 'application/json',
'Authorization': 'Bearer ' + this.credentials.access_token
},
maxContentLength: Infinity,
maxBodyLength: Infinity,
};
var url = this.credentials.instance_url;
url += this.apiurl + '/jobs/ingest/' + jobId + '/batches/';
try {
let response=await axios.put(url, data, config)
console.log('sendBatch ok');
return response.data;
} catch(error) {
console.log('sendBatch bad');
console.log(error.response.data);
return error;
}
}
sfdc.bulkClose=async function (jobId) {
await this.checkToken();
var config = {
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'Accept': 'application/json',
'Authorization': 'Bearer ' + this.credentials.access_token
}
};
var body = {
state: "UploadComplete"
};
var url = this.credentials.instance_url;
url += this.apiurl + '/jobs/ingest/' + jobId + '/';
try {
let response=await axios.patch(url, JSON.stringify(body), config)
console.log('closeJob ok');
return response.data;
} catch(error) {
console.log('closeJob bad');
console.log(error.response.data);
return error;
}
}
sfdc.bulkStatus=async function (jobId) {
await this.checkToken();
var config = {
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'Accept': 'application/json',
'Authorization': 'Bearer ' + this.credentials.access_token
}
};
var url = this.credentials.instance_url;
url += this.apiurl + '/jobs/ingest/' + jobId + '/';
try {
let response=await axios.get(url, config)
console.log('jobStatus ok');
return response.data;
} catch(error) {
console.log('jobStatus bad');
console.log(error.response.data);
return error;
}
}
sfdc.bulkGetResults=async function (jobId) {
await this.checkToken();
var config = {
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'Accept': 'text/csv',
'Authorization': 'Bearer ' + this.credentials.access_token
}
};
var url = this.credentials.instance_url;
url += this.apiurl + '/jobs/ingest/' + jobId + '/successfulResults';
try {
let response=await axios.get(url, config)
console.log('jobStatus ok');
return response.data;
} catch(error) {
console.log('jobStatus bad');
console.log(error.response.data);
return error;
}
}
sfdc.bulkGetFailed=async function (jobId) {
await this.checkToken();
var config = {
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'Accept': 'text/csv',
'Authorization': 'Bearer ' + this.credentials.access_token
}
};
var url = this.credentials.instance_url;
url += this.apiurl + '/jobs/ingest/' + jobId + '/failedResults';
try {
let response=await axios.get(url, config)
console.log('jobStatus ok');
return response.data;
} catch(error) {
console.log('jobStatus bad');
console.log(error.response.data);
return error;
}
}
sfdc.createContentVersion=async function(Title, PathOnClient, VersionData, FirstPublishLocationId) {
var url = this.credentials.instance_url;
url += this.apiurl + '/sobjects/ContentVersion';
console.log(url);
var config = { headers: {
'Authorization': 'Bearer ' + this.credentials.access_token,
// 'Content-Type': 'multipart/form-data; boundary="boundary_string" '
'Content-Type': 'application/json'
} };
let body = {
Title: Title,
PathOnClient: PathOnClient,
VersionData: VersionData,
FirstPublishLocationId: FirstPublishLocationId
}
// let body='--boundary_string\nContent-Disposition: form-data; name="entity_content";\nContent-Type: application/json\n';
// body+=JSON.stringify(record);
// body+='\n--boundary_string\nContent-Disposition: form-data; name="VersionData"; filename="'+record.PathOnClient+'";\nContent-Type: application/octet-stream\n';
// body+=data;
// body+='\n--boundary_string\n';
console.log(body);
try {
this.checkToken();
let response=await axios.post(url, body, config);
console.log('createContentVersion ok');
return response.data;
} catch(error) {
console.log('createContentVersion bad');
if (error.response) console.log(error.response.data);
else console.log(error);
return error;
}
}
sfdc.getContentBody=async function(bodyUrl) {
var url = this.credentials.instance_url;
url += bodyUrl;
console.log(url);
var config = {
responseType: 'arraybuffer',
headers: {
'Authorization': 'Bearer ' + this.credentials.access_token,
} };
try {
this.checkToken();
let response=await axios.get(url, config);
console.log('getContentBody ok');
return response.data;
} catch(error) {
console.log('getContentBody bad');
if (error.response) console.log(error.response.data);
else console.log(error);
return error;
}
}
sfdc.objectFields = async function(obj) {
var url = this.credentials.instance_url;
url += this.apiurl + '/sobjects/'+obj+'/describe';
var config = { headers: { 'Authorization': 'Bearer ' + this.credentials.access_token } };
try {
this.checkToken();
let response=await axios.get(url, config);
console.log('objectFields ok');
return response.data;
} catch(error) {
console.log('objectFields bad');
console.log(error);
// console.log(error.response.data);
return error;
}
}
sfdc.object = async function(obj) {
var url = this.credentials.instance_url;
url += this.apiurl + '/sobjects/'+obj;
var config = { headers: { 'Authorization': 'Bearer ' + this.credentials.access_token } };
try {
this.checkToken();
let response=await axios.get(url, config);
console.log('object ok');
return response.data;
} catch(error) {
console.log('object bad');
console.log(error.response.data);
return error;
}
}
/**
* List all available objects in Salesforce
* @param {string} token - Salesforce access token
* @returns {Promise<Array>} List of objects with their metadata
*/
sfdc.listobjects = async function(token) {
var url = this.credentials.instance_url;
url += this.apiurl + '/sobjects';
var config = { headers: { 'Authorization': 'Bearer ' + this.credentials.access_token } };
try {
this.checkToken();
let response=await axios.get(url, config);
console.log('listobjects ok');
return response.data;
} catch(error) {
console.log('listobjects bad');
console.log(error.response.data);
return error;
}
};
// ======================
// Cache
// ======================
/**
* Example
*
var cache=sfdc.newCache(5);
for (var i=0; i<upd.length; i++) {
if (cache.add( upd[i] )) {
await sfdc.upsert("Account", "Id", cache.content() );
cache.reset();
}
}
await sfdc.upsert("Account", "Id", cache.content() );
*/
sfdc.newCache=function(maxrecords=100) {
return {
records:[],
maxrecords:maxrecords,
add: function(record) {
this.records.push(record);
if (this.maxrecords>= this.records.length) return false;
return true;
},
isEmpty: function() { return (this.records.length==0); },
content: function() { return this.records; },
reset: function() { this.records=[]; }
}
}
//export default sfdc;
module.exports = sfdc;
+52
View File
@@ -0,0 +1,52 @@
const request = require('supertest');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { app } = require('../src/app');
const services = require('../src/services');
jest.mock('../src/services'); // jest uses fake/dummy functions instead of the real ones
describe('POST /api/documents', () => {
let dummyFileExcel;
let dummyFilePdf;
beforeAll(() => {
dummyFileExcel = path.join(os.tmpdir(), 'test_file.xlsx');
dummyFilePdf = path.join(os.tmpdir(), 'test_file.pdf');
fs.writeFileSync(dummyFileExcel, 'dummy file excel content');
fs.writeFileSync(dummyFilePdf, 'dummy pdf file content');
});
afterAll(() => {
if (fs.existsSync(dummyFileExcel)) fs.unlinkSync(dummyFileExcel);
if (fs.existsSync(dummyFilePdf)) fs.unlinkSync(dummyFilePdf);
});
it('should process files and return status 200 when helpers succeed', async() => {
// this tells the fake functions what to return
services.splitExcelFile.mockResolvedValue(5); // Returns 5 rows
services.splitPDF.mockResolvedValue(['/tmp/page1.pdf']); // Returns 1 temp file
services.initializeSalesforceConnection.mockResolvedValue(); // Simulates success
services.attachPdfToAccount.mockResolvedValue(); // Simulates success
// this makes test request to Express
const response = await request(app)
.post('/api/documents')
.field('title', 'Quarterly Report')
.attach('documents', dummyFileExcel)
.attach('documents', dummyFilePdf);
// assertions => verify express responded properly and called our service
expect(response.statusCode).toBe(200);
expect(response.body).toEqual({
ok: true,
message: 'Files uploaded successfully!'
});
// Verify helpers were actually called
expect(services.splitExcelFile).toHaveBeenCalledTimes(1);
expect(services.splitPDF).toHaveBeenCalledTimes(1);
expect(services.initializeSalesforceConnection).toHaveBeenCalledTimes(1);
expect(services.cleanUp).toHaveBeenCalledTimes(2);
})
});