lib/document/index.js
'use strict';
const https = require('https');
const { readFileSync } = require('fs');
const { basename, extname } = require('path');
const {
responseHandler,
errorHandler,
buildRequestOptions,
stringifyQueryParams,
getMimeTypeByFileExtension,
isDocumentTypeAcceptable,
getFormDataBoundary,
getByDeclinedStatus,
getSignerEmails,
} = require('../common');
const { promisify } = require('../../utils');
/**
* Document methods
*/
class Document {
/**
* Create document payload
* @typedef {Object} DocumentCreateParams
* @property {string} filepath - path to file to be uploaded
* @property {string} token - your auth token
*/
/**
* Create document response data
* @typedef {Object} DocumentCreateResponse
* @property {string} id - an id of created document
*/
/**
* Uploads a file and creates a document.
* This endpoint accepts .pdf, .doc, .docx, .odt, .rtf, .png, .jpg, .jpeg, .gif, .bmp, .xml, .xls, .xlsx, .ppt, .pptx file types
* @param {DocumentCreateParams} data - create document payload
* @param {function(err: ApiErrorResponse, res: DocumentCreateResponse)} [callback] - error first node.js callback
*/
static create ({ filepath, token }, callback) {
const fileExt = extname(filepath).substr(1);
if (!isDocumentTypeAcceptable(fileExt)) {
callback('File type isn\'t supported.');
return;
}
let fileContent;
try {
fileContent = readFileSync(filepath);
} catch (err) {
callback(err);
return;
}
const mimeType = getMimeTypeByFileExtension(fileExt);
const formDataBoundary = getFormDataBoundary();
const formDataBeginning = `--${formDataBoundary}\r\n`
.concat(`Content-Disposition: form-data; name="file"; filename="${basename(filepath)}"\r\n`)
.concat(`Content-Type: ${mimeType}\r\n\r\n`);
const formDataEnding = `\r\n--${formDataBoundary}\r\n`;
const payload = Buffer.concat([
Buffer.from(formDataBeginning, 'utf8'),
fileContent,
Buffer.from(formDataEnding, 'utf8'),
]);
const req = https
.request(buildRequestOptions({
method: 'POST',
path: '/document',
authorization: {
type: 'Bearer',
token,
},
headers: {
'Content-Type': `multipart/form-data; boundary=${formDataBoundary}`,
'Content-Length': Buffer.byteLength(payload),
},
}), responseHandler(callback))
.on('error', errorHandler(callback));
req.write(payload);
req.end();
}
/**
* Upload document with field extract payload
* @typedef {Object} DocumentFieldExtractParams
* @property {string} filepath - path to file to be uploaded
* @property {string} token - your auth token
*/
/**
* Upload document with field extract response data
* @typedef {Object} DocumentFieldExtractResponse
* @property {string} id - an id of created document
*/
/**
* Uploads a file that contains SignNow Document Field Tags.
* This endpoint only accepts .pdf (you may convert the document from .doc or .docx to .pdf)
* @param {DocumentFieldExtractParams} data - upload document with field extract payload
* @param {function(err: ApiErrorResponse, res: DocumentFieldExtractResponse)} [callback] - error first node.js callback
*/
static fieldextract ({ filepath, token }, callback) {
const fileExt = extname(filepath).substr(1);
if (!isDocumentTypeAcceptable(fileExt)) {
callback('File type isn\'t supported.');
return;
}
let fileContent;
try {
fileContent = readFileSync(filepath);
} catch (err) {
callback(err);
return;
}
const mimeType = getMimeTypeByFileExtension(fileExt);
const formDataBoundary = getFormDataBoundary();
const formDataBeginning = `--${formDataBoundary}\r\n`
.concat(`Content-Disposition: form-data; name="file"; filename="${basename(filepath)}"\r\n`)
.concat(`Content-Type: ${mimeType}\r\n\r\n`);
const formDataEnding = `\r\n--${formDataBoundary}\r\n`;
const payload = Buffer.concat([
Buffer.from(formDataBeginning, 'utf8'),
Buffer.from(fileContent, 'binary'),
Buffer.from(formDataEnding, 'utf8'),
]);
const req = https
.request(buildRequestOptions({
method: 'POST',
path: '/document/fieldextract',
authorization: {
type: 'Bearer',
token,
},
headers: {
'Content-Type': `multipart/form-data; boundary=${formDataBoundary}`,
'Content-Length': Buffer.byteLength(payload),
},
}), responseHandler(callback))
.on('error', errorHandler(callback));
req.write(payload);
req.end();
}
/**
* Payload to receive user's document list
* @typedef {Object} DocumentListParams
* @property {string} token - your auth token
*/
/**
* Document list response item
* @typedef {Object} DocumentListItem
* @property {string} id - id of specific document
* @property {string} user_id - id of user that uploaded document
* @property {string} document_name - name of document
* @property {string} page_count - amount of pages in the document
* @property {string} created - timestamp document was created
* @property {string} updated - timestamp document was updated
* @property {string} original_filename - original filename with document format (.pdf, .doc, etc...)
* @property {?string} origin_document_id - an id of original document (if document is a copy)
* @property {string} owner - email of document owner
* @property {boolean} template - is document a template or not
* @property {?string} origin_user_id - id of user who created document
* @property {DocumentThumbnails} thumbnail - thumbnail urls with different sizes (small, medium, large)
* @property {Object[]} signatures - signature signed elements
* @property {Object[]} texts - text and enumeration filled elements
* @property {Object[]} checks - checkbox checked elements
* @property {Object[]} tags - field types document contains
* @property {Object[]} fields - all document fillable fields
* @property {Object[]} requests - requests for signing document
* @property {Object[]} roles - document signer roles
* @property {Object[]} field_invites - sent invites data
* @property {number} version_time - document updated timestamp
* @property {Object[]} enumeration_options - dropdown options
* @property {Object[]} attachments - attachments in document
* @property {Object[]} routing_details - signing steps routing details
* @property {Object[]} integrations - document integrations
* @property {Object[]} hyperlinks - document hyperlinks
* @property {Object[]} radiobuttons - checked radiobuttons after document was signed
* @property {Object[]} document_group_template_info - document group templates data
* @property {Object[]} document_group_info - document group template's data
* @property {Object[]} originator_organization_settings - specific organization settings for document
* @property {Object[]} field_validators - validation rules for fields (date fields, payment fields etc.)
* @property {Object} settings - specific document settings
*/
/**
* User's Document list response
* @typedef {DocumentListItem[]} DocumentListResponse
*/
/**
* Retrieve user's document list
* @param {DocumentListParams} data - payload to receive user's document list
* @param {function(err: ApiErrorResponse, res: DocumentListItem)} [callback] - error first node.js callback
*/
static list ({ token }, callback) {
https
.request(buildRequestOptions({
method: 'GET',
path: '/user/documentsv2',
authorization: {
type: 'Bearer',
token,
},
}), responseHandler(callback))
.on('error', errorHandler(callback))
.end();
}
/**
* Document view payload
* @typedef {Object} DocumentViewParams
* @property {string} id - id of specific document
* @property {string} token - your auth token
*/
/**
* Document thumbnail urls with different sizes (small, medium, large)
* @typedef {Object} DocumentThumbnails
* @property {string} large
* @property {string} medium
* @property {string} small
*/
/**
* Document view response of specific document
* @typedef {Object} DocumentViewResponse
* @property {string} id - id of specific document
* @property {string} user_id - id of user that uploaded document
* @property {string} document_name - name of document
* @property {string} page_count - amount of pages in the document
* @property {string} created - timestamp document was created
* @property {string} updated - timestamp document was updated
* @property {string} original_filename - original filename with document format (.pdf, .doc, etc...)
* @property {?string} origin_document_id - an id of original document (if document is a copy)
* @property {string} owner - email of document owner
* @property {boolean} template - is document a template or not
* @property {DocumentThumbnails} thumbnail - thumbnail urls with different sizes (small, medium, large)
* @property {Object[]} signatures - signature signed elements
* @property {Object[]} texts - text and enumeration filled elements
* @property {Object[]} checks - checkbox checked elements
* @property {Object[]} tags - field types document contains
* @property {Object[]} fields - all document fillable fields
* @property {Object[]} requests - requests for signing document
* @property {Object[]} roles - document signer roles
* @property {Object[]} field_invites - sent invites data
* @property {number} version_time - document updated timestamp
* @property {Object[]} enumeration_options - dropdown options
* @property {Object[]} attachments - attachments in document
* @property {Object[]} routing_details - signing steps routing details
* @property {Object[]} integrations - document integrations
* @property {Object[]} hyperlinks - document hyperlinks
* @property {Object[]} radiobuttons - checked radiobuttons after document was signed
* @property {Object[]} document_group_template_info - document group template's data
* @property {Object[]} document_group_info - document group's data
* @property {Object[]} originator_organization_settings - specific organization settings for document
* @property {Object[]} field_validators - validation rules for fields (date fields, payment fields etc.)
* @property {Object} settings - specific document settings
* @property {string} parent_id - id of document folder
* @property {string} originator_logo - logo url
* @property {Object[]} pages - list of document pages with page sources and page size.
*/
/**
* Retrieves a document detailed data
* @param {DocumentViewParams} data - view document details payload
* @param {function(err: ApiErrorResponse, res: DocumentViewResponse)} [callback] - error first node.js callback
*/
static view ({ id, token }, callback) {
https
.request(buildRequestOptions({
method: 'GET',
path: `/document/${id}`,
authorization: {
type: 'Bearer',
token,
},
}), responseHandler(callback))
.on('error', errorHandler(callback))
.end();
}
/**
* @typedef {Object} SignatureField
* @property {number} page_number - page number of document
* @property {string} type - type of field (signature)
* @property {string} name - unique name of the field
* @property {string} role - role name
* @property {boolean} required - required field
* @property {number} height - height of the field
* @property {number} width - width of the field
* @property {number} x - X coordinate of the field
* @property {number} y - Y coordinate of the field
* @property {string} [label] - field label
*/
/**
* @typedef {Object} InitialsField
* @property {number} page_number - page number of document
* @property {string} type - type of field (initials)
* @property {string} name - unique name of the field
* @property {string} role - role name
* @property {boolean} required - required field
* @property {number} height - height of the field
* @property {number} width - width of the field
* @property {number} x - X coordinate of the field
* @property {number} y - Y coordinate of the field
* @property {string} [label] - field label
*/
/**
* @typedef {Object} TextField
* @property {number} page_number - page number of document
* @property {string} type - type of field (text)
* @property {string} name - unique name of the field
* @property {string} role - role name
* @property {boolean} required - required field
* @property {number} height - height of the field
* @property {number} width - width of the field
* @property {number} x - X coordinate of the field
* @property {number} y - Y coordinate of the field
* @property {string|number} [prefilled_text] - prefilled field value
* @property {string} [validator_id] - field value validator ID
* @property {string} [label] - field label
*/
/**
* @typedef {Object} CheckField
* @property {number} page_number - page number of document
* @property {string} type - type of field (checkbox)
* @property {string} name - unique name of the field
* @property {string} role - role name
* @property {boolean} required - required field
* @property {number} height - height of the field
* @property {number} width - width of the field
* @property {number} x - X coordinate of the field
* @property {number} y - Y coordinate of the field
* @property {boolean} [prefilled_text] - prefilled field value
* @property {string} [label] - field label
*/
/**
* @typedef {Object} EnumerationField
* @property {number} page_number - page number of document
* @property {string} type - type of field (enumeration)
* @property {string} name - unique name of the field
* @property {string} role - role name
* @property {boolean} required - required field
* @property {number} height - height of the field
* @property {number} width - width of the field
* @property {number} x - X coordinate of the field
* @property {number} y - Y coordinate of the field
* @property {Array<number|string>} enumeration_options - choice options of enumerable field
* @property {string|number} [prefilled_text] - prefilled field value
* @property {string} [label] - field label
* @property {boolean} [custom_defined_option=false] - if 'true' user can change values, if 'false' user can only read
*/
/**
* @typedef {Object} RadioButton
* @property {number} page_number - page number of document
* @property {number} height - height of the field
* @property {number} width - width of the field
* @property {number} x - X coordinate of the field
* @property {number} y - Y coordinate of the field
* @property {string} value - value of radio button
* @property {string} checked - radio button check status
* @property {number} created - redio button creation timestamp
*/
/**
* @typedef {Object} RadioButtonField
* @property {number} page_number - page number of document
* @property {string} type - type of field (radiobutton)
* @property {string} name - unique name of the field
* @property {string} role - role name
* @property {boolean} required - required field
* @property {number} height - height of the field
* @property {number} width - width of the field
* @property {number} x - X coordinate of the field
* @property {number} y - Y coordinate of the field
* @property {RadioButton[]} radio - array of radio buttons
* @property {string|number} [prefilled_text] - prefilled field value
* @property {string} [label] - field label
*/
/**
* @typedef {Object} FormulaTreeBranchTerminator
* @property {string} term - name of field which will participate in calculation formula
*/
/**
* @typedef {Object} FormulaTreeBranch
* @property {FormulaTree} term - a part of formula parsed into tree
*/
/**
* @typedef {Object} FormulaTree
* @property {string} operator - arithmetic operator's token
* @property {FormulaTreeBranch|FormulaTreeBranchTerminator} left - left branch of formula tree
* @property {FormulaTreeBranch|FormulaTreeBranchTerminator} right - right branch of formula tree
*/
/**
* @typedef {Object} CalculatedField
* @property {number} page_number - page number of document
* @property {string} type - type of field (text)
* @property {string} name - unique name of the field
* @property {string} role - role name
* @property {boolean} required - required field
* @property {number} height - height of the field
* @property {number} width - width of the field
* @property {number} x - X coordinate of the field
* @property {number} y - Y coordinate of the field
* @property {string} formula - arithmetic formula, e.g. 'TextName+EnumerationName'
* @property {FormulaTree} calculation_formula - formula parsed into tree
* @property {number} calculation_precision - calculation precision
* @property {string} [label] - field label
* @property {boolean} [custom_defined_option=false] - if 'true' user can change values, if 'false' user can only read
*/
/**
* @typedef {Object} AttachmentField
* @property {number} page_number - page number of document
* @property {string} type - type of field (attachment)
* @property {string} name - unique name of the field
* @property {string} role - role name
* @property {boolean} required - required field
* @property {number} height - height of the field
* @property {number} width - width of the field
* @property {number} x - X coordinate of the field
* @property {number} y - Y coordinate of the field
* @property {string} [label] - field label
*/
/**
* @typedef {Object} DocumentFields
* @property {number} client_timestamp - local time of user
* @property {Array<SignatureField|InitialsField|TextField|CheckField|EnumerationField|RadioButtonField|CalculatedField|AttachmentField>} fields - all types of fields
*/
/**
* Update document payload
* @typedef {Object} DocumentUpdateParams
* @property {string} id - id of specific document
* @property {DocumentFields} fields - set of fields
* @property {string} token - your auth token
*/
/**
* Update document response data
* @typedef {Object} DocumentUpdateResponse
* @property {string} id - an id of document
* @property {Object[]} signatures - signature and initial elements
* @property {Object[]} texts - text and enumeration elements
* @property {Object[]} checks - checkbox elements
* @property {Object[]} attachments - attachment elements
* @property {Object[]} radiobuttons - radiobutton elements
*/
/**
* Updates an existing document.
* Adds fields (signature, text, initials, checkbox, radiobutton group, calculated) and elements (signature, text, check).
* Every call of this method will add only specified fields into document.
* Old fields will be removed
* @param {DocumentUpdateParams} data - update document request payload
* @param {function(err: ApiErrorResponse, res: DocumentUpdateResponse)} [callback] - error first node.js callback
*/
static update ({
id,
fields,
token,
}, callback) {
const JSONData = JSON.stringify(fields);
const req = https
.request(buildRequestOptions({
method: 'PUT',
path: `/document/${id}`,
authorization: {
type: 'Bearer',
token,
},
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(JSONData),
},
}), responseHandler(callback))
.on('error', errorHandler(callback));
req.write(JSONData);
req.end();
}
/**
* Download document optional settings
* @typedef {Object} DocumentDownloadOptions
* @property {boolean} [withAttachments=false] - if true document will be downloaded as zip package with its attachments
* @property {boolean} [withHistory=false] - if true document will be downloaded with history
*/
/**
* Document download payload
* @typedef {Object} DocumentDownloadParams
* @property {string} id - id of specific document
* @property {string} token - your auth token
* @property {DocumentDownloadOptions} options - download document optional settings
*/
/**
* Document binary data
* @typedef {Buffer} DocumentDownloadResponse
*/
/**
* Downloads document with embedded fields and elements.
* By default document is downloaded without history. To download document with history set `withHistory` option to `true`.
* @param {DocumentDownloadParams} data - download document request payload
* @param {function(err: ApiErrorResponse, res: DocumentDownloadResponse)} [callback] - error first node.js callback
*/
static download ({
id,
token,
options: {
withAttachments = false,
withHistory = false,
} = {},
}, callback) {
const queryParams = {};
const clientTimestamp = Math.floor(Date.now() / 1000);
queryParams.type = withAttachments ? 'zip' : 'collapsed';
withHistory && (queryParams.with_history = '1');
https
.request(buildRequestOptions({
method: 'GET',
path: `/document/${id}/download?client_timestamp=${clientTimestamp}&${stringifyQueryParams(queryParams)}`,
authorization: {
type: 'Bearer',
token,
},
}), responseHandler(callback))
.on('error', errorHandler(callback))
.end();
}
/**
* Document share payload
* @typedef {Object} DocumentShareParams
* @property {string} id - id of specific document
* @property {string} token - your auth token
*/
/**
* Document share response data
* @typedef {Object} DocumentShareResponse
* @property {string} link - link to download specified document in PDF format
*/
/**
* Requests a link to download document with embedded fields and elements
* Link can be used once and then will expire
* @param {DocumentShareParams} data - share document request payload
* @param {function(err: ApiErrorResponse, res: DocumentShareResponse)} [callback] - error first node.js callback
*/
static share ({ id, token }, callback) {
https
.request(buildRequestOptions({
method: 'POST',
path: `/document/${id}/download/link`,
authorization: {
type: 'Bearer',
token,
},
}), responseHandler(callback))
.on('error', errorHandler(callback))
.end();
}
/**
* Document invite signer settings
* @typedef {Object} SignerSettings
* @property {string} email - signer's email
* @property {string} role - role name
* @property {string} [role_id] - role unique id (can be discovered in document details)
* @property {number} order - role signing order
* @property {string} reassign - allow reassign signer
* @property {string} decline_by_signature - signer can decline invite
* @property {number} reminder - remind via email in days
* @property {number} expiration_days - expiration of invite in days
* @property {string} [subject] - specify subject of email
* @property {string} [message] - specify body of email
*/
/**
* Document invite settings
* @typedef {Object} DocumentInviteSettings
* @property {string} from - email of sender
* @property {SignerSettings[]|string} to - array of signers or email of single signer
* @property {string} [document_id] - an id of document
* @property {string[]} [cc] - array with emails of copy receivers
* @property {string} [subject] - specify subject of email
* @property {string} [message] - specify body of email
* @property {string} [on_complete] - on signing complete action
*/
/**
* Document invite optional settings
* @typedef {Object} DocumentInviteOptions
* @property {string} [email] - ability to disable invitation by email. to disable email assign value 'disable'
*/
/**
* Create document invite payload
* @typedef {Object} DocumentInviteParams
* @property {string} id - an id of document
* @property {DocumentInviteSettings} data - document invite settings
* @property {DocumentInviteOptions} [options] - document invite optional settings
* @property {string} token - your auth token
*/
/**
* Create document field invite response data
* @typedef {Object} DocumentFieldInviteResponse
* @property {string} status - status of invitation, e.g. 'success'
*/
/**
* Create document free form invite response data
* @typedef {Object} DocumentFreeformInviteResponse
* @property {string} result - free form invitation result, e.g. 'success'
* @property {string} id - unique id of invite
* @property {string} callback_url - url for front-end redirect or 'none'
*/
/**
* Create document invite response data
* @typedef {DocumentFieldInviteResponse|DocumentFreeformInviteResponse} DocumentInviteResponse
*/
/**
* Creates an invite to sign a document.
* You can create a simple free form invite(s) or a role-based invite(s).
* @param {DocumentInviteParams} data - create document invite payload
* @param {function(err: ApiErrorResponse, res: DocumentInviteResponse)} [callback] - error first node.js callback
*/
static invite ({
id,
data,
options: { email } = {},
token,
}, callback) {
const inviteData = Object.assign({}, data, { document_id: id });
const JSONData = JSON.stringify(inviteData);
let path = `/document/${id}/invite`;
if (email === 'disable') {
path = `${path}?email=disable`;
}
const req = https
.request(buildRequestOptions({
method: 'POST',
authorization: {
type: 'Bearer',
token,
},
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(JSONData),
},
path,
}), responseHandler(callback))
.on('error', errorHandler(callback));
req.write(JSONData);
req.end();
}
/**
* Cancel freeform invite payload
* @typedef {Object} CancelFreeformInviteParams
* @property {string} id - unique id of invite
* @property {string} token - your auth token
*/
/**
* Cancel freeform invite response data
* @typedef {Object} CancelFreeformInviteResponse
* @property {string} id - unique id of invite
*/
/**
* Cancels sent freeform invite
* @param {CancelFreeformInviteParams} data - cancel freeform invite payload
* @param {function(err: ApiErrorResponse, res: CancelFreeformInviteResponse)} [callback] - error first node.js callback
*/
static cancelFreeFormInvite ({ id, token }, callback) {
https
.request(buildRequestOptions({
method: 'PUT',
path: `/invite/${id}/cancel`,
authorization: {
type: 'Bearer',
token,
},
}), responseHandler(callback))
.on('error', errorHandler(callback))
.end();
}
/**
* Cancel document field invite payload
* @typedef {Object} DocumentFieldInviteCancelParams
* @property {string} id - an id of document
* @property {string} token - your auth token
*/
/**
* Cancel document field invite response data
* @typedef {Object} DocumentFieldInviteCancelResponse
* @property {string} status - status of field invite cancellation, e.g. 'success'
*/
/**
* Cancels an invite to a document
* @param {DocumentFieldInviteCancelParams} data - cancel field invite payload
* @param {function(err: ApiErrorResponse, res: DocumentFieldInviteCancelResponse)} [callback] - error first node.js callback
*/
static cancelFieldInvite ({ id, token }, callback) {
https
.request(buildRequestOptions({
method: 'PUT',
path: `/document/${id}/fieldinvitecancel`,
authorization: {
type: 'Bearer',
token,
},
}), responseHandler(callback))
.on('error', errorHandler(callback))
.end();
}
/**
* Cancels an invite to a document
* @deprecated use Document.cancelFieldInvite instead of this
* @param {DocumentFieldInviteCancelParams} data - cancel field invite payload
* @param {function(err: ApiErrorResponse, res: DocumentFieldInviteCancelResponse)} [callback] - error first node.js callback
*/
static cancelInvite ({ id, token }, callback) {
Document.cancelFieldInvite({
id,
token,
}, callback);
}
/**
* Merge documents optional settings
* @typedef {Object} DocumentMergeOptions
* @property {boolean} [removeOriginalDocuments=false] - if true original documents will be removed
*/
/**
* Merge documents payload
* @typedef {Object} DocumentMergeParams
* @property {string} token - your auth token
* @property {string} name - new name for merged document
* @property {string[]} document_ids - an array of document IDs
* @property {DocumentMergeOptions} options - merge documents optional settings
*/
/**
* Merge documents response data
* @typedef {Object} DocumentMergeResponse
* @property {string} document_id - an ID of merged document
*/
/**
* Merges existing documents into one.
* By default original documents are not removed after merging. To remove original documents set `removeOriginalDocuments` option to `true`.
* @param {DocumentMergeParams} data - merge documents payload
* @param {function(err: ApiErrorResponse, res: DocumentMergeResponse)} [originalCallback] - error first node.js callback
*/
static merge ({
name,
document_ids,
options: { removeOriginalDocuments = false } = {},
token,
}, originalCallback) {
const JSONData = JSON.stringify({
upload_document: true,
document_ids,
name,
});
const callbackWithDocumentDeletion = (mergeErr, mergeRes) => {
if (mergeErr) {
originalCallback(mergeErr);
return;
} else {
const removeDocument = ({ id }) => new Promise((resolve, reject) => {
Document.remove({
id,
token,
}, (err, res) => {
if (err) {
reject(err);
} else {
resolve(res.status === 'success');
}
});
});
const documentsDeleted = document_ids
.map(id => removeDocument({ id }));
Promise.all(documentsDeleted)
.then(deletionStatuses => deletionStatuses.reduce((generalStatus, currentStatus) => (generalStatus && currentStatus), true))
.then(allDocumentsDeleted => {
if (allDocumentsDeleted) {
originalCallback(null, mergeRes);
return;
} else {
throw new Error('Some of the original documents were not removed');
}
})
.catch(removeErr => {
originalCallback(removeErr);
return;
});
}
};
const callback = removeOriginalDocuments
? callbackWithDocumentDeletion
: originalCallback;
const req = https
.request(buildRequestOptions({
method: 'POST',
path: '/document/merge',
authorization: {
type: 'Bearer',
token,
},
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(JSONData),
},
}), responseHandler(callback))
.on('error', errorHandler(originalCallback));
req.write(JSONData);
req.end();
}
/**
* Document history payload
* @typedef {Object} DocumentHistoryParams
* @property {string} id - id of specific document
* @property {string} token - your auth token
*/
/**
* Document action log item
* @typedef {Object} DocumentEvent
* @property {string} client_app_name - name of a client application
* @property {?string} client_timestamp - client application timestamp
* @property {string} created - timestamp of action creation
* @property {string} email - email of an actor
* @property {string} event - name of event
* @property {string} ip_address - actor's IP address
* @property {?string} element_id - an ID of specific element
* @property {?string} field_id - an ID of specific field
* @property {?string} json_attributes - field attributes (e.g. font, style, size etc.)
* @property {string} [document_id] - an ID of specific document
* @property {string} [origin] - an origin of document
* @property {string} [unique_id] - an ID of event
* @property {string} [user_agent] - user agent
* @property {string} [user_id] - id of an actor
* @property {string} [version] - version of signed document (incremented after each signature addition)
*/
/**
* Document history response data
* @typedef {DocumentEvent[]} DocumentHistoryResponse
*/
/**
* Retrieves a document action log (history) data
* @param {DocumentHistoryParams} data - payload
* @param {function(err: ApiErrorResponse, res: DocumentHistoryResponse)} [callback] - error first node.js callback
*/
static history ({ id, token }, callback) {
https
.request(buildRequestOptions({
method: 'GET',
path: `/document/${id}/history`,
authorization: {
type: 'Bearer',
token,
},
}), responseHandler(callback))
.on('error', errorHandler(callback))
.end();
}
/**
* Document remove optional settings
* @typedef {Object} DocumentRemoveOptions
* @property {boolean} [cancelInvites=false] - ability to cancel all document invites during deletion
*/
/**
* Remove document payload
* @typedef {Object} DocumentRemoveParams
* @property {string} id - id of specific document
* @property {string} token - your auth token
* @property {DocumentRemoveOptions} [options] - document remove optional settings
*/
/**
* Remove document response data
* @typedef {Object} DocumentRemoveResponse
* @property {string} status - status of deletion, e.g. 'success'
*/
/**
* Removes a specified document from folder
* @param {DocumentRemoveParams} data - remove document payload
* @param {function(err: ApiErrorResponse, res: DocumentRemoveResponse)} [originalCallback] - error first node.js callback
*/
static remove ({
id: documentId,
token,
options: { cancelInvites = false } = {},
}, originalCallback) {
const callbackWithInviteCancellation = (removeErr, removeRes) => {
if (removeErr) {
originalCallback(removeErr);
return;
} else {
promisify(Document.view)({
id: documentId,
token,
})
.then(document => {
const { field_invites: fieldInvites, requests: freeformInvites } = document;
return {
fieldInvites,
freeformInvites,
};
})
.then(({ fieldInvites, freeformInvites }) => {
const fieldInvitesCancelled = new Promise(resolve => {
if (Array.isArray(fieldInvites) && fieldInvites.length) {
resolve(promisify(Document.cancelFieldInvite)({
id: documentId,
token,
}));
} else {
resolve({ status: 'success' });
}
})
.then(({ status }) => status === 'success');
const freeformInvitesCancelled = freeformInvites
.map(({ id: inviteId }) => {
return promisify(Document.cancelFreeFormInvite)({
id: inviteId,
token,
})
.then(({ id }) => !!id);
});
Promise.all([
fieldInvitesCancelled,
...freeformInvitesCancelled,
])
.then(allInvitesCancelled => {
if (allInvitesCancelled) {
originalCallback(null, removeRes);
return;
} else {
throw new Error('Some of invites have not been cancelled');
}
})
.catch(err => {
originalCallback(err.message);
return;
});
});
}
};
const callback = cancelInvites
? callbackWithInviteCancellation
: originalCallback;
https
.request(buildRequestOptions({
method: 'DELETE',
path: `/document/${documentId}`,
authorization: {
type: 'Bearer',
token,
},
}), responseHandler(callback))
.on('error', errorHandler(originalCallback))
.end();
}
/**
* Get Document Signers optional settings
* @typedef {Object} DocumentSignersOptions
* @property {boolean} [allSignatures=true] - if true receive all document signers email
* @property {boolean} [freeFormInvites=false] - if `true` return free from invite signer emails
* @property {?string[]} [fieldInvitesStatus=null] - return signers according to specified field invite status(es)
* Accepts list of statuses or a single status.
* Acceptable statuses: all, pending, declined, fulfilled, created, skipped.
* @property {?string[]} [paymentRequestsStatus=null] - return signers according to specified to payment request status(es)
* Accepts list of statuses or a single status.
* Available statuses: all, pending, fulfilled, created, skipped.
*/
/**
* Get Document Signers payload
* @typedef {Object} DocumentSignersParams
* @property {string} id - id of specific document
* @property {string} token - your auth token
* @property {DocumentSignersOptions} [options] - document signers optional settings,
*/
/**
* Get Document Signers response data
* @typedef {Object} DocumentSignersResponse
* @property {string[]} - emails of document signers
*/
/**
* Retrieves all signers of specific document
* @experimental this class includes breaking change.
* @param {DocumentSignersParams} data - get document signers payload
* @param {function(err: ApiErrorResponse, res: DocumentSignersResponse)} [callback] - error first node.js callback
*/
static signers ({
id,
token,
options = {},
}, callback) {
promisify(Document.view)({
id,
token,
})
.then(document => {
const {
requests,
field_invites,
} = document;
const {
freeFormInvites = false,
fieldInviteStatus = null,
paymentRequestStatus = null,
} = options;
const emails = [];
if (Object.keys(options).length === 0) {
const fieldInvitesEmails = getSignerEmails(field_invites, 'email') || [];
const freeFormInvitesEmails = getSignerEmails(requests, 'signer_email') || [];
emails.push(...fieldInvitesEmails);
emails.push(...freeFormInvitesEmails);
}
if (freeFormInvites) {
const freeFomInvites = getSignerEmails(requests, 'signer_email') || [];
emails.push(...freeFomInvites);
}
if (fieldInviteStatus) {
const fieldInvites = getSignerEmails(field_invites, 'email', fieldInviteStatus) || [];
const declined = getByDeclinedStatus(field_invites, fieldInviteStatus) || [];
if (fieldInvites.length > 0) {
emails.push(...fieldInvites);
} else {
emails.push(...fieldInvites);
emails.push(...declined);
}
}
if (paymentRequestStatus) {
const paymentRequests = getSignerEmails(field_invites, 'email', paymentRequestStatus) || [];
emails.push(...paymentRequests);
}
callback(null, [...new Set(emails)]);
})
.catch(err => callback(err));
}
}
module.exports = Document;