mirror of
https://github.com/bellingcat/datasheet-server.git
synced 2026-06-11 21:08:33 +03:00
better saving schema for model layer
previously, the model layer had a confused interface, where it saved via blueprints, but loaded via a URL logic. This commit modifies Fetchers to save consistently via a URL
This commit is contained in:
@@ -9,7 +9,7 @@ class Controller {
|
||||
this.fetchers = fetchers
|
||||
}
|
||||
|
||||
sheetExists (sheet) {
|
||||
_sheetExists (sheet) {
|
||||
return (Object.keys(this.fetchers).indexOf(sheet) >= 0)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ class Controller {
|
||||
return this.fetchers[sheet].update()
|
||||
})
|
||||
).then(results => {
|
||||
console.log(results)
|
||||
if (results.every(r => r)) {
|
||||
return copy.success.update
|
||||
} else {
|
||||
@@ -34,7 +35,7 @@ class Controller {
|
||||
}
|
||||
|
||||
retrieve (sheet, tab, resource) {
|
||||
if (this.sheetExists(sheet)) {
|
||||
if (this._sheetExists(sheet)) {
|
||||
const fetcher = this.fetchers[sheet]
|
||||
return fetcher.retrieve(tab, resource)
|
||||
} else {
|
||||
@@ -43,7 +44,7 @@ class Controller {
|
||||
}
|
||||
|
||||
retrieveFrag (sheet, tab, resource, frag) {
|
||||
if (this.sheetExists(sheet)) {
|
||||
if (this._sheetExists(sheet)) {
|
||||
const fetcher = this.fetchers[sheet]
|
||||
return fetcher.retrieveFrag(tab, resource, frag)
|
||||
} else {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// FetcherTwo class interfaces with Google Sheet, and saves to a specified db
|
||||
import { google } from 'googleapis'
|
||||
import {
|
||||
fmtSheetTitle,
|
||||
fmtName,
|
||||
fmtBlueprinterTitles,
|
||||
bp,
|
||||
isFunction
|
||||
} from './util'
|
||||
import { byRow } from './blueprinters'
|
||||
import { createHash } from 'crypto'
|
||||
import R from 'ramda'
|
||||
|
||||
class Fetcher {
|
||||
@@ -28,39 +27,62 @@ class Fetcher {
|
||||
*/
|
||||
this.sheetName = sheetName
|
||||
|
||||
/*
|
||||
* A unique ID for the Fetcher to identify its elements in the model layer
|
||||
*/
|
||||
this.id = createHash('md5').update(sheetName).update(sheetId).digest('hex')
|
||||
|
||||
/*
|
||||
* These are the available tabs for storing and retrieving data.
|
||||
* Each blueprinter is a function that returns a Blueprint from a
|
||||
* list of lists (which will be retrieved from gsheets).
|
||||
*/
|
||||
this.blueprinters = fmtBlueprinterTitles(blueprinters)
|
||||
this.blueprints = {}
|
||||
Object.keys(this.blueprinters).forEach(key => {
|
||||
this.blueprints[key] = null
|
||||
})
|
||||
|
||||
/*
|
||||
* This object is the canonical represenation for the data that a Fetcher
|
||||
* proxies. When the fetcher is initialized, its model layer (db) is indexed,
|
||||
* and this object populated accordingly. Whenever the fetcher updates, this
|
||||
* data structure updates as well. It is the model layer that determines the
|
||||
* performance of indexing the blueprints.
|
||||
*/
|
||||
this.blueprints = this._indexDbForBlueprints()
|
||||
.then(res => res)
|
||||
|
||||
/*
|
||||
* Google API setup
|
||||
*/
|
||||
this.sheets = google.sheets('v4')
|
||||
this.API = google.sheets('v4')
|
||||
this.auth = null
|
||||
|
||||
/**
|
||||
* saveBp is a curried function that takes in a title and
|
||||
* a blueprinter. NB: it sits here in the constructor as
|
||||
* I am not sure how to curry a class method with Ramda.
|
||||
*/
|
||||
this._saveBp = R.curry((tab, title, data, blueprinter) => {
|
||||
const saturatedBp = blueprinter(
|
||||
tab,
|
||||
this.sheetName,
|
||||
this.sheetId,
|
||||
data
|
||||
/** curry to allow convenient syntax with map */
|
||||
this._saveViaBlueprinter = R.curry(this._saveViaBlueprinter)
|
||||
}
|
||||
|
||||
/** save data under a given tab name via its blueprinter, which generates
|
||||
* its resource name. Note that this is curried in the constructor.
|
||||
*/
|
||||
_saveViaBlueprinter (tab, data, blueprinter) {
|
||||
const saturatedBp = blueprinter(
|
||||
tab,
|
||||
this.sheetName,
|
||||
this.sheetId,
|
||||
data
|
||||
)
|
||||
|
||||
return Promise.all(
|
||||
Object.keys(saturatedBp.routes).map(route =>
|
||||
this.db.save(`${this.id}/${tab}/${route}`, saturatedBp.routes[route].data)
|
||||
)
|
||||
const blueprint = bp(saturatedBp) // TODO: come up with better semantics.
|
||||
this.blueprints[title] = blueprint
|
||||
return this.db.save(saturatedBp)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/** index the db and produce appropriate blueprints structure **/
|
||||
_indexDbForBlueprints () {
|
||||
return this.db.index()
|
||||
.then(res => {
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
/** returns a Promise that resolves if access is granted to the account, and rejects otherwise. */
|
||||
@@ -85,14 +107,14 @@ class Fetcher {
|
||||
update () {
|
||||
let tabTitles
|
||||
/* Retrieve all available routes on a given sheet, and store formatted copies of it where a formatter is available */
|
||||
return this.sheets.spreadsheets
|
||||
return this.API.spreadsheets
|
||||
.get({
|
||||
auth: this.auth,
|
||||
spreadsheetId: this.sheetId
|
||||
})
|
||||
.then(response => {
|
||||
tabTitles = response.data.sheets.map(sheet => sheet.properties.title)
|
||||
return this.sheets.spreadsheets.values.batchGet({
|
||||
return this.API.spreadsheets.values.batchGet({
|
||||
auth: this.auth,
|
||||
spreadsheetId: this.sheetId,
|
||||
ranges: tabTitles
|
||||
@@ -100,12 +122,15 @@ class Fetcher {
|
||||
})
|
||||
.then(results => {
|
||||
const tabData = results.data.valueRanges
|
||||
|
||||
return Promise.all(
|
||||
tabData.map((tab, idx) => {
|
||||
const { values } = tab
|
||||
|
||||
if (values === undefined) {
|
||||
return Promise.resolve({})
|
||||
}
|
||||
|
||||
const name = tabTitles[idx]
|
||||
return this.save(name, values)
|
||||
})
|
||||
@@ -115,31 +140,34 @@ class Fetcher {
|
||||
.catch(() => false)
|
||||
}
|
||||
|
||||
save (tab, data) {
|
||||
const title = fmtSheetTitle(tab)
|
||||
if (Object.keys(this.blueprinters).indexOf(title) > -1) {
|
||||
const bpConfig = this.blueprinters[title]
|
||||
save (_tab, data) {
|
||||
const tab = fmtName(_tab)
|
||||
|
||||
if (Object.keys(this.blueprinters).indexOf(tab) > -1) {
|
||||
const bpConfig = this.blueprinters[tab]
|
||||
if (isFunction(bpConfig)) {
|
||||
return this._saveBp(tab, title, data, bpConfig)
|
||||
// if bpConfig specifies a single blueprinter
|
||||
return this._saveViaBlueprinter(tab, data, bpConfig)
|
||||
} else {
|
||||
return bpConfig.map(this._saveBp(tab, title, data))
|
||||
// if bpConfig specifies an array of blueprinters
|
||||
return bpConfig.map(this._saveViaBlueprinter(tab, data))
|
||||
}
|
||||
} else {
|
||||
// If it can't find a blueprinter for the tab title, default to byRow
|
||||
return this.db.save(byRow(tab, this.sheetName, this.sheetId, data))
|
||||
// NB: if a blueprinter is not specified for a tab,
|
||||
// just skip it.
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// NB: could combine these functions by checking kwargs length
|
||||
retrieve (tab, resource) {
|
||||
const title = fmtSheetTitle(tab)
|
||||
const title = fmtName(tab)
|
||||
const url = `${this.sheetName}/${tab}/${resource}`
|
||||
return this.db.load(url, this.blueprints[title])
|
||||
}
|
||||
|
||||
retrieveFrag (tab, resource, frag) {
|
||||
const title = fmtSheetTitle(tab)
|
||||
const title = fmtName(tab)
|
||||
const url = `${this.sheetName}/${tab}/${resource}/${frag}`
|
||||
return this.db.load(url, this.blueprints[title])
|
||||
}
|
||||
|
||||
@@ -55,24 +55,24 @@ export const idxSearcher = R.curry((attrName, searchValue, myArray) => {
|
||||
|
||||
/* more site specific functions. TODO: maybe move to another folder? */
|
||||
|
||||
export function fmtSheetTitle (name) {
|
||||
export function fmtName (name) {
|
||||
return name.replaceAll(' ', '-').toLowerCase()
|
||||
}
|
||||
|
||||
export function fmtBlueprinterTitles (tabs) {
|
||||
const obj = {}
|
||||
Object.keys(tabs).forEach(tab => {
|
||||
const name = fmtSheetTitle(tab)
|
||||
const name = fmtName(tab)
|
||||
obj[name] = tabs[tab]
|
||||
})
|
||||
return obj
|
||||
}
|
||||
|
||||
export function deriveFilename (sheet, tab) {
|
||||
return `${fmtSheetTitle(sheet)}-${fmtSheetTitle(tab)}.json`
|
||||
return `${fmtName(sheet)}-${fmtName(tab)}.json`
|
||||
}
|
||||
|
||||
export function bp (full) {
|
||||
export function desaturate (full) {
|
||||
const blueprint = {
|
||||
name: R.clone(full.name),
|
||||
sheet: R.clone(full.sheet),
|
||||
|
||||
Reference in New Issue
Block a user