mindee 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,39 +1,42 @@
1
1
  # CHANGELOG
2
2
 
3
- ## v1.0.6 (2021-06-14)
3
+ ## v1.0.7 (2021-11-25)
4
4
 
5
5
  ### New
6
+ - ✨ Added pdf page number parameter for multi-pages pdfs with file
6
7
 
7
- - ✨ Invoices return API document type
8
+ ### Changes
9
+ - :arrow_up: upgrade path-parse dependency
10
+ - :arrow_up: upgrade browserslist dependency
11
+ - :arrow_up: upgrade lodash dependency
12
+ - :arrow_up: upgrade y18n dependency
8
13
 
9
14
  ## v1.0.4 (2021-02-18)
10
15
 
11
16
  ### New
12
-
13
- - :sparkles: :zap: Add a parameter `filename` and a default filename for streams
17
+ * :sparkles: :zap: Add a parameter `filename` and a default filename for streams
14
18
 
15
19
  ### Changes
20
+ * :zap: Change parse function to use an object instead of multiples parameters
16
21
 
17
- - :zap: Change parse function to use an object instead of multiples parameters
18
22
 
19
23
  ## v1.0.3 (2021-02-01)
20
24
 
21
25
  ### Fixes
26
+ * :bug: _request parameters
27
+ * :bug: `pageNumber` default value
28
+ * :bug: reconstruction method set fields to probability
22
29
 
23
- - :bug: \_request parameters
24
- - :bug: `pageNumber` default value
25
- - :bug: reconstruction method set fields to probability
26
30
 
27
31
  ## v1.0.2 (2021-02-01)
28
32
 
29
33
  ### Changes
30
-
31
- - :zap: Better coverage for total tax
34
+ * :zap: Better coverage for total tax
32
35
 
33
36
  ### Fixes
37
+ * :bug: `includeWords` is now working
34
38
 
35
- - :bug: `includeWords` is now working
36
39
 
37
40
  ## v1.0.1 (2021-01-11)
38
41
 
39
- - 🎉 First release
42
+ * 🎉 First release
package/README.md CHANGED
@@ -17,7 +17,7 @@ You can use the SDK easily by creating a Client like this :
17
17
  ```js
18
18
  const { Client } = require("mindee");
19
19
 
20
- const mindeeClient = Client({
20
+ const mindeeClient = new Client({
21
21
  invoiceToken: "invoiceApiToken",
22
22
  receiptToken: "receiptExpenseApiToken",
23
23
  });
package/lib/inputs.js CHANGED
@@ -16,6 +16,16 @@ var {
16
16
  PDFDocument
17
17
  } = require("pdf-lib");
18
18
 
19
+ var magic = require("stream-mmmagic");
20
+
21
+ var concat = require("concat-stream");
22
+
23
+ var {
24
+ Base64Encode
25
+ } = require("base64-stream");
26
+
27
+ var ReadableStreamClone = require("readable-stream-clone");
28
+
19
29
  class Input {
20
30
  /**
21
31
  * @param {(String | Buffer)} file - the file that will be read. Either path or base64 string, or a steam
@@ -99,7 +109,15 @@ class Input {
99
109
  return _asyncToGenerator(function* () {
100
110
  _this3.fileObject = _this3.file;
101
111
  _this3.filename = _this3.filename || "stream";
102
- _this3.filepath = undefined;
112
+ _this3.filepath = undefined; //Copy the ReadableStream
113
+
114
+ var stream = new ReadableStreamClone(_this3.fileObject);
115
+ _this3.fileObject = new ReadableStreamClone(_this3.fileObject);
116
+ var [mime, output] = yield magic.promise(stream);
117
+
118
+ if (mime.type === "application/pdf" && _this3.allowCutPdf == true) {
119
+ yield _this3.cutPdf();
120
+ }
103
121
  })();
104
122
  }
105
123
 
@@ -109,6 +127,29 @@ class Input {
109
127
  this.filepath = "";
110
128
  this.fileExtension = "";
111
129
  }
130
+ /**
131
+ * Convert ReadableStream to Base64 encoded String
132
+ *
133
+ * @param {*} stream ReadableStream to encode
134
+ * @returns Base64 encoded String
135
+ */
136
+
137
+
138
+ streamToBase64(stream) {
139
+ return _asyncToGenerator(function* () {
140
+ return yield new Promise((resolve, reject) => {
141
+ var base64 = new Base64Encode();
142
+
143
+ var cbConcat = base64 => {
144
+ resolve(base64);
145
+ };
146
+
147
+ stream.pipe(base64).pipe(concat(cbConcat)).on("error", error => {
148
+ reject(error);
149
+ });
150
+ });
151
+ })();
152
+ }
112
153
  /** Cut PDF if pages > 5 */
113
154
 
114
155
 
@@ -117,7 +158,14 @@ class Input {
117
158
 
118
159
  return _asyncToGenerator(function* () {
119
160
  // convert document to PDFDocument & cut CUT_PDF_SIZE - 1 first pages and last page
120
- var pdfDocument = yield PDFDocument.load(_this4.fileObject);
161
+ var pdfDocument;
162
+
163
+ if (_this4.filename == "stream") {
164
+ pdfDocument = yield PDFDocument.load(yield _this4.streamToBase64(_this4.fileObject));
165
+ } else {
166
+ pdfDocument = yield PDFDocument.load(_this4.fileObject);
167
+ }
168
+
121
169
  var splitedPdfDocument = yield PDFDocument.create();
122
170
  var pdfLength = pdfDocument.getPageCount();
123
171
  if (pdfLength <= _this4.CUT_PDF_SIZE) return;
@@ -0,0 +1,40 @@
1
+ const APIObject = require("./object");
2
+ const Input = require("../inputs");
3
+
4
+ class APIFinancialDocument extends APIObject {
5
+ constructor(invoiceToken, receiptToken) {
6
+ super(undefined, "financialDocument");
7
+ this.invoiceToken = invoiceToken;
8
+ this.receiptToken = receiptToken;
9
+ }
10
+
11
+ /**
12
+ * @param {String} file: Receipt filepath (allowed jpg, png, tiff, pdf)
13
+ * @param {String} inputType: String in {'path', 'stream', 'base64'}
14
+ * @param {Boolean} includeWords: extract all words into http_response
15
+ * @param {String} version: expense_receipt api version
16
+ * @param {Boolean} cutPdf: Automatically reconstruct pdf with more than 4 pages
17
+ * @returns {Response} Wrapped response with Receipts objects parsed
18
+ */
19
+ async parse({
20
+ input,
21
+ inputType = "path",
22
+ filename = undefined,
23
+ version = "3",
24
+ cutPdf = true,
25
+ includeWords = false,
26
+ }) {
27
+ const inputFile = new Input({ file: input, inputType, filename, cutPdf });
28
+ this.apiToken =
29
+ inputFile.fileExtension === "pdf" ? this.invoiceToken : this.receiptToken;
30
+ await inputFile.init();
31
+ const url =
32
+ inputFile.fileExtension === "pdf"
33
+ ? "/invoices/v1/predict"
34
+ : "/expense_receipts/v3/predict";
35
+ super.parse();
36
+ return await super._request(url, inputFile, version, includeWords);
37
+ }
38
+ }
39
+
40
+ module.exports = APIFinancialDocument;
@@ -0,0 +1,5 @@
1
+ exports.APIObject = require("./object");
2
+ exports.APIReceipt = require("./receipt");
3
+ exports.Response = require("./response");
4
+ exports.Request = require("./response");
5
+ exports.Invoice = require("./invoice");
@@ -0,0 +1,48 @@
1
+ const APIObject = require("./object");
2
+ const Input = require("../inputs");
3
+
4
+ class APIInvoice extends APIObject {
5
+ constructor(apiToken = undefined) {
6
+ super(apiToken, "invoice");
7
+ this.baseUrl = `${this.baseUrl}/invoices/`;
8
+ }
9
+
10
+ /**
11
+ * @param {Boolean} includeWords: , extract all words into http_response
12
+ * @param {Boolean} cutPdf: Automatically reconstruct pdf with more than 4 pages
13
+ * @param {String} inputType: String in {'path', 'stream', 'base64'}
14
+ * @param {String} file: Receipt filepath (allowed jpg, png, tiff, pdf)
15
+ * @param {String} version: expense_receipt api version
16
+ * @returns {Response} Wrapped response with Receipts objects parsed
17
+ */
18
+ async parse({
19
+ input,
20
+ inputType = "path",
21
+ filename = undefined,
22
+ version = "2",
23
+ cutPdf = true,
24
+ includeWords = false,
25
+ }) {
26
+ super.parse();
27
+ const inputFile = new Input({ file: input, inputType, filename, cutPdf });
28
+ await inputFile.init();
29
+ const url = `v${version}/predict`;
30
+ return await super._request(url, inputFile, includeWords);
31
+ }
32
+
33
+ /**
34
+ @param {String} inputFile - Input object
35
+ @param {} response - HTTP response
36
+ @param {Document} documentType - Document class in {"Receipt", "Invoice", "Financial_document"}
37
+ @returns {Response}
38
+ */
39
+ wrapResponse(inputFile, response, documentType) {
40
+ let result = super.wrapResponse(inputFile, response, documentType);
41
+ result.documentType =
42
+ result.httpResponse.data?.predictions?.[0]?.document_type?.value?.toLowerCase() ||
43
+ result.documentType;
44
+ return result;
45
+ }
46
+ }
47
+
48
+ module.exports = APIInvoice;
@@ -0,0 +1,82 @@
1
+ const Response = require("./response");
2
+ const errorHandler = require("../errors/handler");
3
+ const request = require("./request");
4
+
5
+ /**
6
+ * Base class for APIs (APIReceipt & APIInvoice)
7
+ * @param {String} apiToken - Token of the API used for parsing document
8
+ * @param {String} apiName - Name of the API used for parsing document
9
+ */
10
+ class APIObject {
11
+ constructor(apiToken = undefined, apiName = "") {
12
+ this.apiToken = apiToken;
13
+ this.baseUrl = "https://api.mindee.net/products";
14
+ this.apiName = apiName;
15
+ }
16
+
17
+ /**
18
+ */
19
+ parse() {
20
+ if (!this.apiToken) {
21
+ errorHandler.throw({
22
+ error: new Error(
23
+ `Missing API token for ${this.apiName}. \
24
+ Have you create a mindee Client with a ${this.apiName}Token in parameters ?`
25
+ ),
26
+ });
27
+ }
28
+ }
29
+
30
+ /**
31
+ @param {String} url - API url for request
32
+ @param {Input} inputFile - input file for API
33
+ @param {Boolean} includeWords - Include Mindee vision words in Response
34
+ @returns {Response}
35
+ */
36
+ async _request(url, inputFile, includeWords = false) {
37
+ const headers = {
38
+ "X-Inferuser-Token": this.apiToken,
39
+ };
40
+ const response = await request(
41
+ `${this.baseUrl}${url}`,
42
+ "POST",
43
+ headers,
44
+ inputFile,
45
+ includeWords
46
+ );
47
+ return this.wrapResponse(inputFile, response, this.apiName);
48
+ }
49
+
50
+ /**
51
+ @param {String} inputFile - Input object
52
+ @param {} response - HTTP response
53
+ @param {Document} documentType - Document class in {"Receipt", "Invoice", "Financial_document"}
54
+ @returns {Response}
55
+ */
56
+ wrapResponse(inputFile, response, documentType) {
57
+ if (response.statusCode != 200) {
58
+ const errorMessage = JSON.stringify(response.data, null, 4);
59
+ errorHandler.throw(
60
+ new Error(
61
+ `${this.apiName} API ${response.statusCode} HTTP error: ${errorMessage}`
62
+ ),
63
+ false
64
+ );
65
+ return new Response({
66
+ httpResponse: response,
67
+ documentType: documentType,
68
+ document: undefined,
69
+ input: inputFile,
70
+ error: true,
71
+ });
72
+ }
73
+ return new Response({
74
+ httpResponse: response,
75
+ documentType: documentType,
76
+ document: inputFile,
77
+ input: inputFile,
78
+ });
79
+ }
80
+ }
81
+
82
+ module.exports = APIObject;
@@ -0,0 +1,34 @@
1
+ const APIObject = require("./object");
2
+ const Input = require("../inputs");
3
+
4
+ class APIReceipt extends APIObject {
5
+ constructor(apiToken = undefined) {
6
+ super(apiToken, "receipt");
7
+ this.baseUrl = `${this.baseUrl}/expense_receipts/`;
8
+ }
9
+
10
+ /**
11
+ * @param {String} file: Receipt filepath (allowed jpg, png, tiff, pdf)
12
+ * @param {String} inputType: String in {'path', 'stream', 'base64'}
13
+ * @param {Boolean} includeWords: extract all words into http_response
14
+ * @param {String} version: expense_receipt api version
15
+ * @param {Boolean} cutPdf: Automatically reconstruct pdf with more than 4 pages
16
+ * @returns {Response} Wrapped response with Receipts objects parsed
17
+ */
18
+ async parse({
19
+ input,
20
+ inputType = "path",
21
+ filename = undefined,
22
+ version = "3",
23
+ cutPdf = true,
24
+ includeWords = false,
25
+ }) {
26
+ super.parse();
27
+ const inputFile = new Input({ file: input, inputType, filename, cutPdf });
28
+ await inputFile.init();
29
+ const url = `v${version}/predict`;
30
+ return await super._request(url, inputFile, includeWords);
31
+ }
32
+ }
33
+
34
+ module.exports = APIReceipt;
@@ -0,0 +1,63 @@
1
+ const https = require("https");
2
+ const { version: sdkVersion } = require("../../package.json");
3
+ const { URL } = require("url");
4
+ const FormData = require("form-data");
5
+
6
+ const request = (url, method, headers, input, includeWords = false) => {
7
+ return new Promise(function (resolve, reject) {
8
+ const form = new FormData();
9
+ let body;
10
+ headers["User-Agent"] = `mindee-node/${sdkVersion} node/${process.version}`;
11
+
12
+ if (["path", "stream"].includes(input.inputType)) {
13
+ const fileParams = { filename: input.filename };
14
+ form.append("file", input.fileObject, fileParams);
15
+ if (includeWords) form.append("include_mvision", "true");
16
+ headers = { ...headers, ...form.getHeaders() };
17
+ } else if (input.inputType === "base64") {
18
+ let body_obj = { file: input.fileObject };
19
+ if (includeWords) body_obj["include_mvision"] = "true";
20
+ body = JSON.stringify(body_obj);
21
+ headers["Content-Type"] = "application/json";
22
+ headers["Content-Length"] = body.length;
23
+ }
24
+
25
+ const uri = new URL(url);
26
+ const options = {
27
+ method: method,
28
+ headers: headers,
29
+ hostname: uri.hostname,
30
+ path: `${uri.pathname}${uri.search}`,
31
+ };
32
+
33
+ const req = https.request(options, function (res) {
34
+ let responseBody = [];
35
+
36
+ res.on("data", function (chunk) {
37
+ responseBody += chunk;
38
+ });
39
+
40
+ res.on("end", function () {
41
+ resolve({
42
+ ...res,
43
+ data: JSON.parse(responseBody),
44
+ });
45
+ });
46
+ });
47
+
48
+ req.on("error", (err) => {
49
+ reject(err);
50
+ });
51
+
52
+ if (["path", "stream"].includes(input.inputType)) {
53
+ form.pipe(req);
54
+ }
55
+
56
+ if (input.inputType === "base64") {
57
+ req.write(body);
58
+ req.end();
59
+ }
60
+ });
61
+ };
62
+
63
+ module.exports = request;
@@ -0,0 +1,62 @@
1
+ const Document = require("../documents").document;
2
+ const Receipt = require("../documents").receipt;
3
+ const Invoice = require("../documents").invoice;
4
+ const FinancialDocument = require("../documents").financialDocument;
5
+ const fs = require("fs").promises;
6
+
7
+ class Response {
8
+ constructor({
9
+ httpResponse,
10
+ documentType,
11
+ input,
12
+ error,
13
+ reconsctruted = false,
14
+ ...args
15
+ }) {
16
+ this.httpResponse = httpResponse;
17
+ this.documentType = documentType;
18
+ this.input = input;
19
+ if (!error && !reconsctruted) this.formatResponse();
20
+ if (reconsctruted === true) {
21
+ Object.assign(this, args);
22
+ }
23
+ }
24
+
25
+ async dump(path) {
26
+ return await fs.writeFile(path, JSON.stringify(Object.entries(this)));
27
+ }
28
+
29
+ static async load(path) {
30
+ const file = fs.readFile(path);
31
+ const args = JSON.parse(file);
32
+ return new Response({ reconsctruted: true, ...args });
33
+ }
34
+
35
+ formatResponse() {
36
+ const constructors = {
37
+ receipt: (params) => new Receipt(params),
38
+ invoice: (params) => new Invoice(params),
39
+ financialDocument: (params) => new FinancialDocument(params),
40
+ };
41
+ const predictions = this.httpResponse.data.predictions.entries();
42
+ this[`${this.documentType}s`] = [];
43
+
44
+ // Create a list of Document (Receipt, Invoice...) for each page of the input document
45
+ for (const [pageNumber, prediction] of predictions) {
46
+ this[`${this.documentType}s`].push(
47
+ constructors[this.documentType]({
48
+ apiPrediction: prediction,
49
+ inputFile: this.input,
50
+ pageNumber: pageNumber,
51
+ })
52
+ );
53
+ }
54
+
55
+ // Merge the list of Document into a unique Document
56
+ this[this.documentType] = Document.mergePages(
57
+ this[`${this.documentType}s`]
58
+ );
59
+ }
60
+ }
61
+
62
+ module.exports = Response;
@@ -0,0 +1,65 @@
1
+ const fs = require("fs").promises;
2
+
3
+ class Document {
4
+ /**
5
+ * Takes a list of Documents and return one Document where
6
+ * each field is set with the maximum probability field
7
+ * @param {Input} inputFile - input file given to parse the document
8
+ */
9
+ constructor(inputFile = undefined) {
10
+ this.filepath = undefined;
11
+ this.filename = undefined;
12
+ this.fileExtension = undefined;
13
+
14
+ if (inputFile != undefined) {
15
+ this.filepath = inputFile.filepath;
16
+ this.filename = inputFile.filename;
17
+ this.fileExtension = inputFile.fileExtension;
18
+ }
19
+ this.checklist = {};
20
+ }
21
+
22
+ clone() {
23
+ return JSON.parse(JSON.stringify(this));
24
+ }
25
+
26
+ /** return true if all checklist of the document if true */
27
+ checkAll() {
28
+ return this.checklist.every((item) => item == true);
29
+ }
30
+
31
+ /** Export document into a JSON file */
32
+ async dump(path) {
33
+ return await fs.writeFile(path, JSON.stringify(Object.entries(this)));
34
+ }
35
+
36
+ /** Create a Document from a JSON file */
37
+ static async load(path) {
38
+ const file = fs.readFile(path);
39
+ const args = JSON.parse(file);
40
+ return new Document({ reconsctruted: true, ...args });
41
+ }
42
+
43
+ /**
44
+ * Takes a list of Documents and return one Document where
45
+ * each field is set with the maximum probability field
46
+ * @param {Array<Document>} documents - A list of Documents
47
+ */
48
+ static mergePages(documents) {
49
+ const finalDocument = documents[0].clone();
50
+ const attributes = Object.getOwnPropertyNames(finalDocument);
51
+ for (const document of documents) {
52
+ for (const attribute of attributes) {
53
+ if (
54
+ document?.[attribute]?.probability >
55
+ finalDocument[attribute].probability
56
+ ) {
57
+ finalDocument[attribute] = document?.[attribute];
58
+ }
59
+ }
60
+ }
61
+ return finalDocument;
62
+ }
63
+ }
64
+
65
+ module.exports = Document;
@@ -0,0 +1,25 @@
1
+ const Field = require("./field");
2
+
3
+ class Amount extends Field {
4
+ /**
5
+ * @param {Object} prediction - Prediction object from HTTP response
6
+ * @param {String} valueKey - Key to use in the prediction dict
7
+ * @param {Boolean} reconstructed - Does the object is reconstructed (not extracted by the API)
8
+ * @param {Integer} pageNumber - Page number for multi pages pdf
9
+ */
10
+ constructor({
11
+ prediction,
12
+ valueKey = "amount",
13
+ reconstructed = false,
14
+ pageNumber = 0,
15
+ }) {
16
+ super({ prediction, valueKey, reconstructed, pageNumber });
17
+ this.value = +parseFloat(prediction[valueKey]).toFixed(3);
18
+ if (isNaN(this.value)) {
19
+ this.value = undefined;
20
+ this.probability = 0;
21
+ }
22
+ }
23
+ }
24
+
25
+ module.exports = Amount;
@@ -0,0 +1,29 @@
1
+ const Field = require("./field");
2
+
3
+ class DateField extends Field {
4
+ /**
5
+ * @param {Object} prediction - Prediction object from HTTP response
6
+ * @param {String} valueKey - Key to use in the prediction dict
7
+ * @param {Boolean} reconstructed - Does the object is reconstructed (not extracted by the API)
8
+ * @param {Integer} pageNumber - Page number for multi pages pdf
9
+ */
10
+ constructor({
11
+ prediction,
12
+ valueKey = "iso",
13
+ reconstructed = false,
14
+ pageNumber = 0,
15
+ }) {
16
+ super({ prediction, valueKey, reconstructed, pageNumber });
17
+ this.dateObject = new Date(this.value);
18
+ if (
19
+ !(this.dateObject instanceof Date) ||
20
+ isNaN(this.dateObject.valueOf())
21
+ ) {
22
+ this.dateObject = undefined;
23
+ this.probability = 0.0;
24
+ this.value = undefined;
25
+ }
26
+ }
27
+ }
28
+
29
+ module.exports = DateField;
@@ -0,0 +1,89 @@
1
+ class Field {
2
+ /**
3
+ * @param {Object} prediction - Prediction object from HTTP response
4
+ * @param {String} valueKey - Key to use in the prediction dict
5
+ * @param {Boolean} reconstructed - Does the object is reconstructed (not extracted by the API)
6
+ * @param {Integer} pageNumber - Page number for multi pages pdf
7
+ * @param {Array<String>} extraFields - Extra fields to get from the prediction and to set as attribute of the Field
8
+ */
9
+ constructor({
10
+ prediction,
11
+ valueKey = "value",
12
+ reconstructed = false,
13
+ extraFields,
14
+ pageNumber,
15
+ }) {
16
+ this.pageNumber = pageNumber;
17
+ this.reconstructed = reconstructed;
18
+ this.value = undefined;
19
+ this.probability = 0.0;
20
+ this.bbox = [];
21
+ if (valueKey in prediction && prediction[valueKey] !== "N/A") {
22
+ this.value = prediction[valueKey];
23
+ if (prediction.probability) this.probability = prediction.probability;
24
+ if (prediction.segmentation)
25
+ this.bbox = prediction.segmentation.bounding_box || [];
26
+ if (extraFields) {
27
+ for (const fieldName of extraFields) {
28
+ this[fieldName] = prediction[fieldName];
29
+ }
30
+ }
31
+ }
32
+ }
33
+
34
+ compare(other) {
35
+ if (this.value == null && other.value == null) return true;
36
+ else if (this.value == null || other.value == null) return false;
37
+ else {
38
+ if (typeof this.value == "string") {
39
+ return this.value.toLowerCase() === other.value.toLowerCase();
40
+ } else {
41
+ return this.value === other.value;
42
+ }
43
+ }
44
+ }
45
+
46
+ /**
47
+ @param {Array<Field>} array1 - first Array of Fields
48
+ @param {Array<Field>} array2 - second Array of Fields
49
+ @param {String} attr - Attribute to compare
50
+ @returns {Boolean} - true if all elements in array1 exist in array2 and vice-versa, false otherwise
51
+ */
52
+ static compareArrays(array1, array2, attr = "value") {
53
+ const list1 = array1.map((item) => item[attr]);
54
+ const list2 = array2.map((item) => item[attr]);
55
+ if (list1.length !== list2.length) return false;
56
+ for (const item1 of list1) {
57
+ if (!list2.includes(item1)) return false;
58
+ }
59
+ return true;
60
+ }
61
+
62
+ /**
63
+ * @param {Array<Field>} array - Array of Fields
64
+ * @returns {Number} product of all the fields probaility
65
+ */
66
+ static arrayProbability(array) {
67
+ let total = 1.0;
68
+ for (const field of array) {
69
+ total *= field.probability;
70
+ if (isNaN(total)) return 0.0;
71
+ }
72
+ return total;
73
+ }
74
+
75
+ /**
76
+ * @param {Array<Field>} array - Array of Fields
77
+ * @returns {Number} Sum of all the Fields values in the array
78
+ */
79
+ static arraySum(array) {
80
+ let total = 0;
81
+ for (const field of array) {
82
+ total += field.value;
83
+ if (isNaN(total)) return 0.0;
84
+ }
85
+ return total;
86
+ }
87
+ }
88
+
89
+ module.exports = Field;