· 6 years ago · Sep 16, 2019, 06:30 AM
1/* eslint eqeqeq: "off" */
2var _ = require('lodash')
3var async = require('async')
4
5/**
6 * Helper module to query Airtable data on a specified base, table, and view.
7 * @param {Object} airtableBase Configured Airtable object
8 * @param {String} tableName Table name to query.
9 * @param {String} viewName View name to query
10 * @return {Object} returns a new AirtableData object.
11 */
12module.exports = function (airtableBase, tableName, viewName) {
13 // Internal Scope Object
14 var ISO = {
15 fn: {},
16 var: {},
17 data: {}
18 }
19
20 // Airtable API fails if too many calls made in parallel, use a job queue
21 // to limit it
22 // (Should the queue be a singleton/ESO?)
23 function jobQueueFn (makePromise, errCallback) {
24 makePromise(errCallback)
25 }
26
27 // Wraps one Promise in another one that pushes to the job queue. Save
28 // intermediate data locally because async.queue() only passes errors
29 function pushFnToQueue (fn) {
30 return new Promise((resolve, reject) => {
31 let dataIfSuccess = null
32 let errCallback = (err) => {
33 if (err) reject(err)
34 else resolve(dataIfSuccess)
35 }
36
37 ISO.var.jobQueue.push((errCallback) => {
38 return fn().then((data) => {
39 dataIfSuccess = data
40 errCallback()
41 })
42 .catch(errCallback)
43 }, errCallback)
44 })
45 }
46
47 function _queryAllData () {
48 return new Promise((resolve, reject) => {
49 // Query the specific Table and View as per the Airtable documentation.
50 ISO.var.Airtable(ISO.var.tableName).select({
51 view: ISO.var.viewName
52 }).eachPage((records, fetchNextPage) => {
53 // Iterate over each record and add to tableData
54 _.map(records, (record) => { ISO.data.tableData.push(record) })
55 // If there are more records, will call this function again.
56 // If there are no more records, will call next 'done' function.
57 fetchNextPage()
58 }, (err) => {
59 // Done
60 if (err) {
61 // Something happened.
62 reject(err)
63 } else {
64 resolve(ISO.data.tableData)
65 }
66 })
67 })
68 }
69
70 function _save (id, data) {
71 return new Promise((resolve, reject) => {
72 ISO.var.Airtable(ISO.var.tableName).update(id, data, (err, record) => {
73 if (err) reject(err)
74 else resolve(record)
75 })
76 })
77 }
78
79 function _create (data) {
80 return new Promise((resolve, reject) => {
81 ISO.var.Airtable(ISO.var.tableName).create(data, (err, record) => {
82 if (err) reject(err)
83 else resolve(record)
84 })
85 })
86 }
87
88 /**
89 * @class AirtableData
90 * @param {Object} AirtableBase A configured Airtable base object.
91 * @param {String} tableName Specific table to pull data from.
92 * @param {String} viewName Specific view name to pull data from.
93 */
94 class AirtableData {
95 constructor (AirtableBase, tableName, viewName) {
96 ISO.var.tableName = tableName
97 ISO.var.viewName = viewName
98 ISO.var.Airtable = AirtableBase
99 ISO.var.jobConcurrency = 3
100 ISO.var.jobQueue = async.queue(jobQueueFn, ISO.var.jobConcurrency)
101 ISO.data.tableData = []
102 ISO.fn = this
103 }
104
105 /**
106 * Grab all data from the specified table and view.
107 * @return {Promise} Promise object, if successful contains all Airtable data in a Array.
108 */
109 queryAllData () {
110 return pushFnToQueue(() => { return _queryAllData() })
111 }
112
113 /**
114 * Retreive a specific airtable row. If column is supplied, it will only return that column value.
115 * @param {String} id of the row to get
116 * @param {String} column
117 * @return {Promise} Promise object, if successful contains Airtable row data or row column data.
118 */
119 get (id, column = null) {
120 return new Promise((resolve, reject) => {
121 let findRow = (rId) => {
122 // We want to cast in this case, unknown what the ID may be so do not
123 // do a strict ===
124 let rowData = _.find(ISO.data.tableData, (o) => { return o.id == rId })
125
126 if (rowData && column) {
127 resolve(rowData.get(column))
128 } else if (rowData) {
129 resolve(rowData)
130 } else {
131 if (column) {
132 reject(new Error(`No data row '${column}' with id ${id} found.`))
133 } else {
134 reject(new Error(`No data with id ${id} found.`))
135 }
136 }
137 }
138
139 if (ISO.data.tableData.length === 0) {
140 ISO.fn.queryAllData()
141 .then((data) => {
142 ISO.data.tableData = data
143
144 findRow(id)
145 })
146 .catch((err) => {
147 reject(err)
148 })
149 } else {
150 findRow(id)
151 }
152 })
153 }
154
155 /**
156 * Update a specific Airtable row with data.
157 * @param id {String} Airtable row id
158 * @param data {Object} Airtable key pair of Column Name and data
159 * @return {Promise} Returns a promise that resolves with the record data if successful.
160 */
161 save (id, data) {
162 return pushFnToQueue(() => { return _save(id, data) })
163 }
164
165 /**
166 * Create a new Airtable record.
167 * @param data {Object} Airtable key pair of Column Name and data
168 * @return {Promise} Returns a promise that resolves with the record data if successful.
169 */
170 create (data) {
171 return pushFnToQueue(() => { return _create(data) })
172 }
173
174 /**
175 * Resolves only when all jobs in the queue are finished. This can only be called once per set of jobs. Resolves immediately if no jobs are currently running.
176 * @return {Promise} Resolves with no return value when queue is empty
177 */
178 waitForJobs () {
179 return new Promise((resolve, reject) => {
180 // Not sure whether or not pause/resume is necessary
181 ISO.var.jobQueue.pause()
182 if (ISO.var.jobQueue.idle()) resolve()
183 else ISO.var.jobQueue.drain = resolve
184 ISO.var.jobQueue.resume()
185 })
186 }
187 }
188
189 // Return a new AirtableData class anytime we use the method function.
190 return new AirtableData(airtableBase, tableName, viewName)
191}