-
Notifications
You must be signed in to change notification settings - Fork 3
/
airpuck.js
425 lines (368 loc) · 18.3 KB
/
airpuck.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
/*
* Version 2.0
* 10-18-2019
* Copyright 2019 justKD
* MIT License
*/
// go to airtable api - https://airtable.com/api
// select the base you want to work with
// check show api key
// check the authentication section, and see something like:
//
// $ curl https://api.airtable.com/v0/appqAIFn2dZxGMIty/Table%201?api_key=keyRvIqOJ3I393sB3
//
// in the example url above:
// Base ID = appqAIFn2dZxGMIty
// Table Name = able 1 (after removing the escaped space)
// Api Key = keyRvIqOJ3I393sB3
const Airpuck = {
Table: class {
/**
* Initialization options that can be passed to the `Airpuck.Table()` constuctor.
* @typedef {object} AirpuckTableOptions
* @property {string} name - The table name exactly as it appears in Airtable.
* @property {string} baseID - The ID of the intended base.
* @property {string} apiKey - The appropriate API key.
* @property {number} maxRecords - The number of max
*/
/**
* This reflects a specific table in an Airtable base.
* @param {AirpuckTableOptions} options - An `AirpuckTableOptions` object or just the table name.`
* @param {Function=} onReady - Callback function run after successful table initialization.
* @details Will automatically attempt to identify all fields, but the Airtable API does not report empty fields. If no records
* have zero empty fields, Airpuck will not be able to accurately identify all fields.
*/
constructor(options, onReady) {
/** Instance properties. */
const _props = {
options: {},
fields: [],
records: [],
endpoint: '',
}
/** Instance state. */
const _state = {
ready: false,
}
/** Set parameters as properties. */
const setOptions = (_ => {
if (options) {
_props.options.name = options.name
_props.options.baseID = options.baseID
_props.options.apiKey = options.apiKey
}
})()
/** Creates a record in Airtable format with known fields. */
this.record = class {
constructor() {
this.fields = {}
_props.fields.forEach(field => this.fields[field] = '')
}
}
/** Helper class to simplify and consolidate XHR calls. */
const XHR = class {
/**
* Initialization options that can be passed to the `new XHR()` constructor.
* @typedef {object} SimpleXHROptions
* @property {string} endpoint
* @property {string=} bearer
*/
constructor() {
this.endpoint = _props.endpoint
this.bearer = _props.options.apiKey
this.status = null
this.response = null
const _request = (type, success, fail, send, recordID) => {
const xhr = new XMLHttpRequest()
if (recordID) xhr.open(type, this.endpoint + '/' + recordID)
else xhr.open(type, this.endpoint)
xhr.setRequestHeader('Content-Type', 'application/json')
if (this.bearer) xhr.setRequestHeader('Authorization', 'Bearer ' + this.bearer)
xhr.onload = _ => {
this.status = xhr.status
if (xhr.status === 200) {
this.response = JSON.parse(xhr.response)
if (success) success()
} else {
console.log(xhr.status)
if (fail) fail()
}
}
if (send) {
xhr.send(JSON.stringify(send))
} else xhr.send()
}
this.GET = (success, fail) => _request('GET', success, fail)
this.POST = (send, success, fail) => _request('POST', success, fail, send)
this.PATCH = (recordID, send, success, fail) => _request('PATCH', success, fail, send, recordID)
this.PUT = (recordID, send, success, fail) => _request('PUT', success, fail, send, recordID)
this.DELETE = (recordID, success, fail) => _request('DELETE', success, fail, null, recordID)
}
}
/** Hold private functions. */
const _private = {
/** Format a string for URL compliance. */
encodeForURL: string => encodeURIComponent(string),
/** Create the Airtable REST endpoint for the given base and table. */
getEndpoint: (baseID, tableName) => "https://api.airtable.com/v0/" + baseID + "/" + _private.encodeForURL(tableName),
/** Retrieve non-empty fields for the last record with the most populated fields and store in `_props.records`. */
getFields: _ => {
if (_props.records.length > 0) {
let record = _props.records[0]
_props.records.forEach(rec => {
if (Object.keys(rec.fields).length > Object.keys(record.fields).length) record = rec
})
_props.fields = Object.keys(record.fields)
}
},
/** Table initialization. The callback is the `onReady` parameter passed to the constructor. */
init: callback => {
if (_props.options.name && _props.options.baseID && _props.options.apiKey) {
_props.endpoint = _private.getEndpoint(_props.options.baseID, _props.options.name)
const completeCallback = _ => {
_private.getFields()
callback()
}
_public.pull(completeCallback)
} else console.log('skip init - options required')
},
/** Generate the public API from the `_public` functions. */
generateAPI: _ => Object.keys(_api).forEach(key => this[key] = _api[key]),
}
/** Hold public functions. */
const _public = {
/**
* Loops until the `new Airtable.table()` initialization sequence is complete or fails if `loop.max` is reached first.
* @param {function=} callback - Function called after successful initialization.
*/
ready: callback => {
if (_state.ready) callback()
else {
const loop = {
interval: 100,
max: 200,
count: 0,
}
setTimeout(_ => {
loop.count++
if (loop.count < loop.max) _public.ready(callback)
else console.log('timeout: could not initialize table')
}, loop.interval)
}
},
/**
* Returns an array of Airtable records that match the field:value pair.
* @param {string} field - The target field key with exact syntax/capitalization.
* @param {any} value - The value to match.
* @returns {array} An array of Airtable records that match the criteria.
*/
getRecordsByField: (field, value) => {
const found = []
Object.values(_props.records).forEach(record => {
if (record.fields[field]) {
if (record.fields[field] == value) {
found.push(record)
}
}
})
return found
},
/**
* Returns an Airtable record whose `id` value matches the passed value.
* @param {string} id - The `id` of the intended record. If it is unknown, see `getRecordsByField()`.
* @returns {object} An Airtable record object that matches the criteria.
*/
getRecordByID: id => {
let found = null
Object.values(_props.records).forEach(record => {
if (record.id == id) found = record
})
return found
},
/**
* Pulls the current data from Airtable and updates the local store found in `table.records()`.
* @param {function=} callback - Function called following successful pull.
*/
pull: callback => {
if (_props.options.name && _props.options.baseID && _props.options.apiKey) {
const xhr = new XHR()
xhr.GET(_ => {
_props.records = xhr.response.records
if (callback) callback(xhr.response)
}, _ => console.log('pull error'))
} else console.log('pull error - options required')
},
/**
* Add a new record to the table. Must be a valid Airtable object. Automatically updates the local store upon success.
* @param {object} record - An object properly formatted for Airtable. Must have a `fields` property. See `new table.record()`.
* @param {function=} callback - Function called following successful add.
*/
add: (record, callback) => {
const xhr = new XHR()
xhr.POST(record, _ => {
_props.records[Object.keys(_props.records).length] = xhr.response // update the local store with the new record
if (callback) callback(xhr.response)
}, _ => console.log('add error'))
},
/**
* Update an existing record. Automatically updates the local store upon success.
* @param {object} record - An object properly formatted for Airtable. Must have an `id` property. See `getRecordByField()` and `getRecordByID()`.
* @param {function=} callback - Function called following successful update.
*/
update: (record, callback) => {
// only try to update if a valid record exists
let found = false
_props.records.forEach(rec => {
if (rec.id == record.id) {
found = true
const formattedRecord = {
fields: record.fields,
}
const xhr = new XHR()
xhr.PATCH(record.id, formattedRecord, _ => {
_props.records.forEach(rec => { // update the local store with the changed record
if (rec.id == record.id) rec.fields = record.fields
})
if (callback) callback(xhr.response)
})
}
})
if (!found) console.log('no record found for that id')
},
/** Delete an existing record. Automatically updates the local store upon success.
* @param {object} record - An object properly formatted for Airtable. Must have an `id` property. See `getRecordByField()` and `getRecordByID()`.
* @param {function=} callback - Function called following successful delete.
*/
delete: (record, callback) => {
// only try to delete if a valid record exists
let found = false
_props.records.forEach(rec => {
if (rec.id == record.id) {
found = true
const xhr = new XHR()
xhr.DELETE(record.id, _ => {
_props.records.forEach((rec, index) => { // update the local store with by deleting the record
if (rec.id == record.id) _props.records.splice(index, 1)
})
if (callback) callback(xhr.response)
})
}
})
if (!found) console.log('no record found for that id')
},
/**
* Format an attachment object that can be passed to attachment fields. Airtable attachment fields require an array of attachment objects.
* @param {string} url - The URL (local or remote) of the attachment file. Airtable will download it and keep its own copy.
* @param {string=} filename - Optionally, you can rename the file before sending it to Airtable.
* @returns {object}
*/
attachment: (url, filename) => {
const attachment = {}
attachment.url = url
if (filename) attachment.filename = filename
return attachment
},
}
/** Functions for the public API. */
const _api = {
pull: callback => _public.pull(callback),
ready: callback => _public.ready(callback),
getRecordsByField: (field, value) => _public.getRecordsByField(field, value),
getRecordByID: id => _public.getRecordByID(id),
add: (record, callback) => _public.add(record, callback),
update: (record, callback) => _public.update(record, callback),
delete: (record, callback) => _public.delete(record, callback),
attachment: (url, filename) => _public.attachment(url, filename),
records: _ => _props.records,
fields: _ => _props.fields,
endpoint: _ => _props.endpoint,
options: _ => _props.options,
}
_private.generateAPI()
_private.init(_ => {
_state.ready = true
if (onReady) onReady()
})
}
// END CONSTRUCTOR
},
// END TABLE
/* ******************** */
// JSDoc for public API
/**
* @name Table#pull
* @function @memberof Table
* @description Pulls the current data from Airtable and updates the local store found in `table.records()`.
* @param {function=} callback - Function called following successful pull.
*/
/**
* @name Table#ready
* @function @memberof Table
* @description Loops until the `new Airtable.table()` initialization sequence is complete or fails if `loop.max` is reached first.
* @param {function=} callback - Function called after successful initialization.
*/
/**
* @name Table#getRecordsByField
* @function @memberof Table
* @description Returns an array of Airtable records that match the field:value pair.
* @param {string} field - The target field key with exact syntax/capitalization.
* @param {any} value - The value to match.
* @returns {array} An array of Airtable records that match the criteria.
*/
/**
* @name Table#getRecordByID
* @function @memberof Table
* @description Returns an Airtable record whose `id` value matches the passed value.
* @param {string} id - The `id` of the intended record. If it is unknown, see `getRecordsByField()`.
* @returns {object} An Airtable record object that matches the criteria.
*/
/**
* @name Table#add
* @function @memberof Table
* @description Add a new record to the table. Must be a valid Airtable object. Also updates the local store on success.
* @param {object} record - An object properly formatted for Airtable. Must have a `fields` property. See `new table.record()`.
* @param {function=} callback - Function called following successful add.
*/
/**
* @name Table#update
* @function @memberof Table
* @description Update an existing record. Also updates the local store on success.
* @param {object} record - An object properly formatted for Airtable. Must have an `id` property. See `getRecordByField()` and `getRecordByID()`.
* @param {function=} callback - Function called following successful update.
*/
/**
* @name Table#delete
* @function @memberof Table
* @description Delete an existing record. Also updates the local store on success.
* @param {object} record - An object properly formatted for Airtable. Must have an `id` property. See `getRecordByField()` and `getRecordByID()`.
* @param {function=} callback - Function called following successful delete.
*/
/**
* @name Table#attachment
* @function @memberof Table
* @description Format an attachment object that can be passed to attachment fields. Airtable attachment fields require an array of attachment objects.
* @param {string} url - The URL (local or remote) of the attachment file. Airtable will download it and keep its own copy.
* @param {string=} filename - Optionally, you can rename the file before sending it to Airtable.
* @returns {object}
*/
/**
* @name Table#records
* @function @memberof Table
* @returns {array} Local record store.
*/
/**
* @name Table#fields
* @function @memberof Table
* @returns {array} Known field keys.
*/
/**
* @name Table#endpoint
* @function @memberof Table
* @returns {string} Airtable REST endpoint.
*/
/**
* @name Table#options
* @function @memberof Table
* @returns {AirpuckTableOptions} Options originally passed to the constructor.
*/
}