Merge pull request #22 from forensic-architecture/topic/build-bp

Topic/build bp closes #1
This commit is contained in:
Lachlan Kermode
2018-12-07 16:40:00 +00:00
committed by GitHub
12 changed files with 189 additions and 119 deletions

View File

@@ -1,5 +1,5 @@
import R from 'ramda' import R from 'ramda'
import { defaultBlueprint, defaultRoute } from '../lib/blueprinters' import { defaultBlueprint, defaultResource } from '../lib/blueprinters'
/** /**
* byColumn - generate a Blueprint from a data sheet by column. Each column * byColumn - generate a Blueprint from a data sheet by column. Each column
@@ -9,7 +9,7 @@ import { defaultBlueprint, defaultRoute } from '../lib/blueprinters'
* @return {type} Blueprint * @return {type} Blueprint
* generated. * generated.
*/ */
export default function byColumn (tabName, sheetName, sheetId, data) { function columns (tabName, sheetName, sheetId, data) {
// Define Blueprint props // Define Blueprint props
const bp = R.clone(defaultBlueprint) const bp = R.clone(defaultBlueprint)
bp.sheet = { bp.sheet = {
@@ -18,18 +18,20 @@ export default function byColumn (tabName, sheetName, sheetId, data) {
} }
bp.name = tabName bp.name = tabName
// column names define routes // column names define resources
const labels = data[0] const labels = data[0]
labels.forEach(label => { labels.forEach(label => {
bp.routes[label] = R.clone(defaultRoute) bp.resources[label] = R.clone(defaultResource)
}) })
// remaining rows as data // remaining rows as data
data.forEach((row, idx) => { data.forEach((row, idx) => {
if (idx === 0) return if (idx === 0) return
labels.forEach((label, idx) => { labels.forEach((label, idx) => {
bp.routes[label].data.push(row[idx]) bp.resources[label].data.push(row[idx])
}) })
}) })
return bp return bp
} }
export default columns

View File

@@ -1,9 +1,9 @@
import R from 'ramda' import R from 'ramda'
import { fmtObj } from '../lib/util' import { fmtObj } from '../lib/util'
import { defaultBlueprint, defaultRoute } from '../lib/blueprinters' import { defaultBlueprint, defaultResource } from '../lib/blueprinters'
/** /**
* byGroup - generate a Blueprint from a data sheet grouped by a column called 'group' * groups - generate a Blueprint from a data sheet grouped by a column called 'group'
* The resource name defaults to 'groups', or a custom resource name can be passed. * The resource name defaults to 'groups', or a custom resource name can be passed.
* Each resource item is an object with values labelled according to column * Each resource item is an object with values labelled according to column
* names. Items are inserted in the data list at idx = id. * names. Items are inserted in the data list at idx = id.
@@ -13,7 +13,7 @@ import { defaultBlueprint, defaultRoute } from '../lib/blueprinters'
* @param {type} name="" name of blueprint. * @param {type} name="" name of blueprint.
* @return {type} Blueprint * @return {type} Blueprint
*/ */
export default function byGroup ( export default function groups (
tabName, tabName,
sheetName, sheetName,
sheetId, sheetId,
@@ -28,11 +28,11 @@ export default function byGroup (
} }
bp.name = tabName bp.name = tabName
// Column names define routes // Column names define resources
const itemLabels = data[0] const itemLabels = data[0]
const fmt = fmtObj(itemLabels) const fmt = fmtObj(itemLabels)
bp.routes[label] = R.clone(defaultRoute) bp.resources[label] = R.clone(defaultResource)
bp.routes[label].data = [] bp.resources[label].data = []
const dataGroups = {} const dataGroups = {}
@@ -46,7 +46,7 @@ export default function byGroup (
} }
}) })
Object.keys(dataGroups).forEach(groupKey => { Object.keys(dataGroups).forEach(groupKey => {
bp.routes[label].data.push({ bp.resources[label].data.push({
group: groupKey, group: groupKey,
group_label: dataGroups[groupKey][0].group_label, group_label: dataGroups[groupKey][0].group_label,
data: dataGroups[groupKey] data: dataGroups[groupKey]

View File

@@ -1,9 +1,9 @@
import R from 'ramda' import R from 'ramda'
import { fmtObj } from '../lib/util' import { fmtObj } from '../lib/util'
import { defaultBlueprint, defaultRoute } from '../lib/blueprinters' import { defaultBlueprint, defaultResource } from '../lib/blueprinters'
/** /**
* byId - generate a Blueprint from a data sheet by id, which is an integer. * ids - generate a Blueprint from a data sheet by id, which is an integer.
* The resource name defaults to 'ids', or a custom resource name can be passed. * The resource name defaults to 'ids', or a custom resource name can be passed.
* Each resource item is an object with values labelled according to column * Each resource item is an object with values labelled according to column
* names. Items are inserted in the data list at idx = id. * names. Items are inserted in the data list at idx = id.
@@ -13,7 +13,7 @@ import { defaultBlueprint, defaultRoute } from '../lib/blueprinters'
* @param {type} name="" name of blueprint. * @param {type} name="" name of blueprint.
* @return {type} Blueprint * @return {type} Blueprint
*/ */
export default function byId ( export default function ids (
tabName, tabName,
sheetName, sheetName,
sheetId, sheetId,
@@ -28,15 +28,15 @@ export default function byId (
} }
bp.name = tabName bp.name = tabName
// Column names define routes // Column names define resources
const itemLabels = data[0] const itemLabels = data[0]
const fmt = fmtObj(itemLabels) const fmt = fmtObj(itemLabels)
bp.routes[label] = R.clone(defaultRoute) bp.resources[label] = R.clone(defaultResource)
bp.routes[label].data = [] bp.resources[label].data = []
data.forEach((row, idx) => { data.forEach((row, idx) => {
if (idx === 0) return if (idx === 0) return
bp.routes[label].data[fmt(row).id] = fmt(row) bp.resources[label].data[fmt(row).id] = fmt(row)
}) })
return bp return bp
} }

View File

@@ -1,9 +1,9 @@
import R from 'ramda' import R from 'ramda'
import { fmtObj } from '../lib/util' import { fmtObj } from '../lib/util'
import { defaultBlueprint, defaultRoute } from '../lib/blueprinters' import { defaultBlueprint, defaultResource } from '../lib/blueprinters'
/** /**
* byRow - generate a Blueprint from a data sheet by row. The resource name * rows - generate a Blueprint from a data sheet by row. The resource name
* defaults to 'rows', or a custom resource name can be passed. Each resource * defaults to 'rows', or a custom resource name can be passed. Each resource
* item is an object with values labelled according to column names. * item is an object with values labelled according to column names.
* *
@@ -12,7 +12,7 @@ import { defaultBlueprint, defaultRoute } from '../lib/blueprinters'
* @param {type} name="" name of blueprint. * @param {type} name="" name of blueprint.
* @return {type} Blueprint * @return {type} Blueprint
*/ */
export default function byRow ( export default function rows (
tabName, tabName,
sheetName, sheetName,
sheetId, sheetId,
@@ -27,15 +27,15 @@ export default function byRow (
} }
bp.name = tabName bp.name = tabName
// Column names define routes // Column names define resources
const itemLabels = data[0] const itemLabels = data[0]
const fmt = fmtObj(itemLabels) const fmt = fmtObj(itemLabels)
bp.routes[label] = R.clone(defaultRoute) bp.resources[label] = R.clone(defaultResource)
bp.routes[label].data = [] bp.resources[label].data = []
data.forEach((row, idx) => { data.forEach((row, idx) => {
if (idx === 0) return if (idx === 0) return
bp.routes[label].data.push(fmt(row)) bp.resources[label].data.push(fmt(row))
}) })
return bp return bp
} }

View File

@@ -1,8 +1,8 @@
import R from 'ramda' import R from 'ramda'
import { defaultBlueprint, defaultRoute } from '../lib/blueprinters' import { defaultBlueprint, defaultResource } from '../lib/blueprinters'
/** /**
* byTree - generate a Blueprint from a data sheet grouped by a column called 'group' * tree - generate a Blueprint from a data sheet grouped by a column called 'group'
* The resource name defaults to 'groups', or a custom resource name can be passed. * The resource name defaults to 'groups', or a custom resource name can be passed.
* Each resource item is an object with values labelled according to column * Each resource item is an object with values labelled according to column
* names. Items are inserted in the data list at idx = id. * names. Items are inserted in the data list at idx = id.
@@ -12,7 +12,7 @@ import { defaultBlueprint, defaultRoute } from '../lib/blueprinters'
* @param {type} name="" name of blueprint. * @param {type} name="" name of blueprint.
* @return {type} Blueprint * @return {type} Blueprint
*/ */
export default function byTree ( export default function tree (
tabName, tabName,
sheetName, sheetName,
sheetId, sheetId,
@@ -27,9 +27,9 @@ export default function byTree (
} }
bp.name = tabName bp.name = tabName
// Column names define routes // Column names define resources
bp.routes[label] = R.clone(defaultRoute) bp.resources[label] = R.clone(defaultResource)
bp.routes[label].data = {} bp.resources[label].data = {}
const tree = { const tree = {
key: 'tags', key: 'tags',
@@ -62,6 +62,6 @@ export default function byTree (
} }
}) })
bp.routes[label].data = tree bp.resources[label].data = tree
return bp return bp
} }

View File

@@ -4,7 +4,7 @@ export default {
onlySheet: 'You cannot query a sheet directly. The URL needs to be in the format /:sheet/:tab/:resource.', onlySheet: 'You cannot query a sheet directly. The URL needs to be in the format /:sheet/:tab/:resource.',
onlyTab: 'You cannot query a tab directly. The URL needs to be in the format /:sheet/:tab/:resource.', onlyTab: 'You cannot query a tab directly. The URL needs to be in the format /:sheet/:tab/:resource.',
noSheet: sheet => `The sheet ${sheet} is not available in this server.`, noSheet: sheet => `The sheet ${sheet} is not available in this server.`,
noResource: prts => `The resource '${prts[2]}' does not exists in the tab '${prts[1]}' of the sheet '${prts[0]}'.`, noResource: prts => `The resource '${prts[2]}' does not exists in the tab '${prts[1]}' in this sheet.`,
noFragment: prts => `Fragment index does not exist` noFragment: prts => `Fragment index does not exist`
}, },
success: { success: {

View File

@@ -9,7 +9,7 @@ class Controller {
this.fetchers = fetchers this.fetchers = fetchers
} }
sheetExists (sheet) { _sheetExists (sheet) {
return (Object.keys(this.fetchers).indexOf(sheet) >= 0) return (Object.keys(this.fetchers).indexOf(sheet) >= 0)
} }
@@ -34,7 +34,7 @@ class Controller {
} }
retrieve (sheet, tab, resource) { retrieve (sheet, tab, resource) {
if (this.sheetExists(sheet)) { if (this._sheetExists(sheet)) {
const fetcher = this.fetchers[sheet] const fetcher = this.fetchers[sheet]
return fetcher.retrieve(tab, resource) return fetcher.retrieve(tab, resource)
} else { } else {
@@ -43,7 +43,7 @@ class Controller {
} }
retrieveFrag (sheet, tab, resource, frag) { retrieveFrag (sheet, tab, resource, frag) {
if (this.sheetExists(sheet)) { if (this._sheetExists(sheet)) {
const fetcher = this.fetchers[sheet] const fetcher = this.fetchers[sheet]
return fetcher.retrieveFrag(tab, resource, frag) return fetcher.retrieveFrag(tab, resource, frag)
} else { } else {

View File

@@ -1,12 +1,12 @@
// FetcherTwo class interfaces with Google Sheet, and saves to a specified db // FetcherTwo class interfaces with Google Sheet, and saves to a specified db
import { google } from 'googleapis' import { google } from 'googleapis'
import { buildDesaturated } from './blueprinters'
import { import {
fmtSheetTitle, fmtName,
fmtBlueprinterTitles, fmtBlueprinterTitles,
bp,
isFunction isFunction
} from './util' } from './util'
import { byRow } from './blueprinters' import { createHash } from 'crypto'
import R from 'ramda' import R from 'ramda'
class Fetcher { class Fetcher {
@@ -28,39 +28,80 @@ class Fetcher {
*/ */
this.sheetName = sheetName 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. * These are the available tabs for storing and retrieving data.
* Each blueprinter is a function that returns a Blueprint from a * Each blueprinter is a function that returns a Blueprint from a
* list of lists (which will be retrieved from gsheets). * list of lists (which will be retrieved from gsheets).
*/ */
this.blueprinters = fmtBlueprinterTitles(blueprinters) 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 = null
this._buildBlueprintsAsync() // NB: modifies this.blueprints on completion
/* /*
* Google API setup * Google API setup
*/ */
this.sheets = google.sheets('v4') this.API = google.sheets('v4')
this.auth = null this.auth = null
/** /** curry to allow convenient syntax with map */
* saveBp is a curried function that takes in a title and this._saveViaBlueprinter = R.curry(this._saveViaBlueprinter)
* a blueprinter. NB: it sits here in the constructor as }
* I am not sure how to curry a class method with Ramda.
*/ _buildBlueprintsAsync () {
this._saveBp = R.curry((tab, title, data, blueprinter) => { return this.db.index()
const saturatedBp = blueprinter( .then(allUrls => {
tab, const allParts = allUrls.reduce((acc, url) => {
this.sheetName, if (url.startsWith(this.id)) {
this.sheetId, const parts = url.split('/')
data acc.push([ parts[1], parts[2] ])
return acc
} else {
return acc
}
}, [])
return allParts
.map(parts => buildDesaturated(
this.sheetId,
this.sheetName,
parts[0],
parts[1]
))
})
.then(res => {
this.blueprints = res
})
}
/** 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.resources).map(route =>
this.db.save(`${this.id}/${tab}/${route}`, saturatedBp.resources[route].data)
) )
const blueprint = bp(saturatedBp) // TODO: come up with better semantics. )
this.blueprints[title] = blueprint
return this.db.save(saturatedBp)
})
} }
/** returns a Promise that resolves if access is granted to the account, and rejects otherwise. */ /** returns a Promise that resolves if access is granted to the account, and rejects otherwise. */
@@ -84,15 +125,15 @@ class Fetcher {
update () { update () {
let tabTitles let tabTitles
/* Retrieve all available routes on a given sheet, and store formatted copies of it where a formatter is available */ /* Retrieve all available resources on a given sheet, and store formatted copies of it where a formatter is available */
return this.sheets.spreadsheets return this.API.spreadsheets
.get({ .get({
auth: this.auth, auth: this.auth,
spreadsheetId: this.sheetId spreadsheetId: this.sheetId
}) })
.then(response => { .then(response => {
tabTitles = response.data.sheets.map(sheet => sheet.properties.title) tabTitles = response.data.sheets.map(sheet => sheet.properties.title)
return this.sheets.spreadsheets.values.batchGet({ return this.API.spreadsheets.values.batchGet({
auth: this.auth, auth: this.auth,
spreadsheetId: this.sheetId, spreadsheetId: this.sheetId,
ranges: tabTitles ranges: tabTitles
@@ -100,12 +141,15 @@ class Fetcher {
}) })
.then(results => { .then(results => {
const tabData = results.data.valueRanges const tabData = results.data.valueRanges
return Promise.all( return Promise.all(
tabData.map((tab, idx) => { tabData.map((tab, idx) => {
const { values } = tab const { values } = tab
if (values === undefined) { if (values === undefined) {
return Promise.resolve({}) return Promise.resolve({})
} }
const name = tabTitles[idx] const name = tabTitles[idx]
return this.save(name, values) return this.save(name, values)
}) })
@@ -115,31 +159,35 @@ class Fetcher {
.catch(() => false) .catch(() => false)
} }
save (tab, data) { save (_tab, data) {
const title = fmtSheetTitle(tab) const tab = fmtName(_tab)
if (Object.keys(this.blueprinters).indexOf(title) > -1) {
const bpConfig = this.blueprinters[title] if (Object.keys(this.blueprinters).indexOf(tab) > -1) {
const bpConfig = this.blueprinters[tab]
if (isFunction(bpConfig)) { if (isFunction(bpConfig)) {
return this._saveBp(tab, title, data, bpConfig) // if bpConfig specifies a single blueprinter
return this._saveViaBlueprinter(tab, data, bpConfig)
} else { } else {
return bpConfig.map(this._saveBp(tab, title, data)) // if bpConfig specifies an array of blueprinters
return bpConfig.map(this._saveViaBlueprinter(tab, data))
} }
} else { } else {
// If it can't find a blueprinter for the tab title, default to byRow // NB: if a blueprinter is not specified for a tab,
return this.db.save(byRow(tab, this.sheetName, this.sheetId, data)) // just skip it.
return true
} }
} }
// NB: could combine these functions by checking kwargs length // NB: could combine these functions by checking kwargs length
retrieve (tab, resource) { retrieve (tab, resource) {
const title = fmtSheetTitle(tab) const title = fmtName(tab)
const url = `${this.sheetName}/${tab}/${resource}` const url = `${this.id}/${tab}/${resource}`
return this.db.load(url, this.blueprints[title]) return this.db.load(url, this.blueprints[title])
} }
retrieveFrag (tab, resource, frag) { retrieveFrag (tab, resource, frag) {
const title = fmtSheetTitle(tab) const title = fmtName(tab)
const url = `${this.sheetName}/${tab}/${resource}/${frag}` const url = `${this.sheetName}/${tab}/${resource}/${frag}`
return this.db.load(url, this.blueprints[title]) return this.db.load(url, this.blueprints[title])
} }

View File

@@ -1,20 +1,29 @@
import path from 'path' import path from 'path'
import fs from 'fs' import fs from 'fs'
import R from 'ramda'
export const defaultBlueprint = { export const defaultBlueprint = {
name: null, name: null,
id: null, sheet: {
dialects: ['rest'], // supported dialects, can (eventually) be multiple name: null,
routes: {} id: null
},
resources: {}
} }
export const defaultRoute = { export const defaultResource = {
options: {
fragment: true
},
data: [] data: []
} }
export function buildDesaturated (sheetId, sheetName, tab, resource) {
const bp = R.clone(defaultBlueprint)
bp.sheet.name = sheetName
bp.sheet.id = sheetId
bp.name = tab
bp.resources[resource] = null
return bp
}
// import all default exports from 'blueprinters' folder // import all default exports from 'blueprinters' folder
const allBps = {} const allBps = {}
const REL_PATH_TO_BPS = '../blueprinters' const REL_PATH_TO_BPS = '../blueprinters'
@@ -28,5 +37,6 @@ fs.readdirSync(normalizedPath).forEach(file => {
// each file in blueprinters folder available for granular import from here. // each file in blueprinters folder available for granular import from here.
module.exports = Object.assign({ module.exports = Object.assign({
defaultBlueprint, defaultBlueprint,
defaultRoute defaultResource,
buildDesaturated
}, allBps) }, allBps)

View File

@@ -55,33 +55,33 @@ export const idxSearcher = R.curry((attrName, searchValue, myArray) => {
/* more site specific functions. TODO: maybe move to another folder? */ /* more site specific functions. TODO: maybe move to another folder? */
export function fmtSheetTitle (name) { export function fmtName (name) {
return name.replaceAll(' ', '-').toLowerCase() return name.replaceAll(' ', '-').toLowerCase()
} }
export function fmtBlueprinterTitles (tabs) { export function fmtBlueprinterTitles (tabs) {
const obj = {} const obj = {}
Object.keys(tabs).forEach(tab => { Object.keys(tabs).forEach(tab => {
const name = fmtSheetTitle(tab) const name = fmtName(tab)
obj[name] = tabs[tab] obj[name] = tabs[tab]
}) })
return obj return obj
} }
export function deriveFilename (sheet, tab) { 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 = { const blueprint = {
name: R.clone(full.name), name: R.clone(full.name),
sheet: R.clone(full.sheet), sheet: R.clone(full.sheet),
dialects: R.clone(full.dialects), dialects: R.clone(full.dialects),
routes: {} resources: {}
} }
Object.keys(full.routes).forEach(route => { Object.keys(full.resources).forEach(route => {
blueprint.routes[route] = { blueprint.resources[route] = {
options: R.clone(full.routes[route].options) options: R.clone(full.resources[route].options)
} }
}) })
return blueprint return blueprint

View File

@@ -1,20 +1,28 @@
import fs from 'mz/fs' import fs from 'mz/fs'
import { fmtSheetTitle } from '../lib/util'
import copy from '../copy/en' import copy from '../copy/en'
const STORAGE_DIRNAME = 'temp' const STORAGE_DIRNAME = 'temp'
function partsFromFilename (fname) {
const body = fname.slice(0, -5)
return body.split('__')
}
class StoreJson { class StoreJson {
save (bp) { index () {
return Promise.all( return Promise.resolve()
Object.keys(bp.routes).map(route => .then(() => fs.readdir(STORAGE_DIRNAME))
fs.writeFile( .then(files => files.filter(f => f.match(/.*\.json$/)))
`${STORAGE_DIRNAME}/${fmtSheetTitle( .then(jsons => jsons.map(partsFromFilename))
bp.sheet.name .then(parts => parts.map(p => `${p[0]}/${p[1]}/${p[2]}`))
)}__${fmtSheetTitle(bp.name)}__${route}.json`, }
JSON.stringify(bp.routes[route].data)
) save (url, data) {
) const parts = url.split('/')
return fs.writeFile(
`${STORAGE_DIRNAME}/${parts[0]}__${parts[1]}__${parts[2]}.json`,
JSON.stringify(data)
) )
} }

View File

@@ -2,9 +2,9 @@ import test from 'ava'
import R from 'ramda' import R from 'ramda'
import { import {
defaultBlueprint, defaultBlueprint,
defaultRoute, defaultResource,
byColumn, columns,
byRow rows
} from '../src/lib/blueprinters' } from '../src/lib/blueprinters'
const egInput1 = [ const egInput1 = [
@@ -15,41 +15,43 @@ const egInput1 = [
test('defaultBlueprint exports', t => { test('defaultBlueprint exports', t => {
const expected = { const expected = {
sheet: {
name: null,
id: null
},
name: null, name: null,
id: null, resources: {}
dialects: ['rest'],
routes: {}
} }
t.deepEqual(expected, defaultBlueprint) t.deepEqual(expected, defaultBlueprint)
}) })
test('byColumn blueprinter generates expected output', t => { test('columns blueprinter generates expected output', t => {
const actual = byColumn('eg ColumnBlueprint', 'egSheetName', 'egSheetId', egInput1) const actual = columns('eg ColumnBlueprint', 'egSheetName', 'egSheetId', egInput1)
const expected = R.clone(defaultBlueprint) const expected = R.clone(defaultBlueprint)
expected.name = 'eg ColumnBlueprint' expected.name = 'eg ColumnBlueprint'
expected.sheet = { expected.sheet = {
id: 'egSheetId', id: 'egSheetId',
name: 'egSheetName' name: 'egSheetName'
} }
expected.routes['h1'] = R.clone(defaultRoute) expected.resources['h1'] = R.clone(defaultResource)
expected.routes['h1'].data = [1, 4] expected.resources['h1'].data = [1, 4]
expected.routes['h2'] = R.clone(defaultRoute) expected.resources['h2'] = R.clone(defaultResource)
expected.routes['h2'].data = [2, 5] expected.resources['h2'].data = [2, 5]
expected.routes['h3'] = R.clone(defaultRoute) expected.resources['h3'] = R.clone(defaultResource)
expected.routes['h3'].data = [3, 6] expected.resources['h3'].data = [3, 6]
t.deepEqual(expected, actual) t.deepEqual(expected, actual)
}) })
test('byRow blueprinter generates expected output', t => { test('rows blueprinter generates expected output', t => {
const actual = byRow('egRowBlueprint', 'egSheetName', 'egSheetId', egInput1, 'items') const actual = rows('egRowBlueprint', 'egSheetName', 'egSheetId', egInput1, 'items')
const expected = R.clone(defaultBlueprint) const expected = R.clone(defaultBlueprint)
expected.name = 'egRowBlueprint' expected.name = 'egRowBlueprint'
expected.sheet = { expected.sheet = {
id: 'egSheetId', id: 'egSheetId',
name: 'egSheetName' name: 'egSheetName'
} }
expected.routes['items'] = R.clone(defaultRoute) expected.resources['items'] = R.clone(defaultResource)
expected.routes['items'].data = [{ expected.resources['items'].data = [{
h1: 1, h1: 1,
h2: 2, h2: 2,
h3: 3 h3: 3