152 Commits

Author SHA1 Message Date
efarooqui
6c1e287148 Merge branch 'develop' of https://www.github.com/forensic-architecture/datasheet-server into feature/json-api-export 2021-03-01 16:06:52 -08:00
Ebrahem Farooqui
679407f421 adding env vars for test and lint 2021-03-01 16:05:16 -08:00
efarooqui
32f3f41a82 Minor comment; running new ci file for testing 2021-02-22 10:40:54 -08:00
Ebrahem Farooqui
3eebf811fb Incorrect syntax for unpacking head ref 2021-02-22 09:50:46 -08:00
Ebrahem Farooqui
be03a3983a Adding branch to checkout
Adding the correct branch to checkout with for workflow
2021-02-22 09:47:05 -08:00
efarooqui
b91068c346 Linting errors 2021-02-17 11:36:36 -08:00
efarooqui
d6512e2cde Replacing empty string with null 2021-02-17 11:33:39 -08:00
efarooqui
3e2b9f2a1d Redundant error message in copy file 2021-02-09 16:58:13 -08:00
efarooqui
3c391e5dfa Working error handling; refactored a bit but same functionality 2021-02-09 16:50:58 -08:00
efarooqui
7c963eb1d0 Working file export; need to test out error handling and flows 2021-02-09 15:38:54 -08:00
efarooqui
fb77e1c365 Adding fileDest to retrieveAll function in Controller 2021-02-08 18:37:11 -08:00
efarooqui
13a4b11259 New export route working to query all blueprints and add to one data object; need to write to file 2021-02-08 17:33:09 -08:00
efarooqui
7bafcb0343 Make new route in api instead of going through npm script; new retrieveAll call for controller 2021-02-08 11:49:43 -08:00
Lachlan Kermode
d6d565a0fc Topic/cd workflow (#66)
* scaffold cd dispatch

* fix bug

update

update

update

* repo -> runtime_args

* remove travis

* only run on commits to develop
2021-01-19 22:07:10 +01:00
efarooqui
ca104e4abe Merge branch 'develop' of https://www.github.com/forensic-architecture/datasheet-server into develop 2021-01-18 20:50:14 -08:00
Ebrahem Farooqui
c18c935b54 CI Workflow (#58)
* Create main.yml

Initial action ci file

* Linting fixes

Co-authored-by: efarooqui <efarooqui@pandora.com>
2021-01-19 15:17:32 +13:00
efarooqui
828a739d72 Merge branch 'develop' of https://www.github.com/forensic-architecture/datasheet-server into develop 2020-12-08 19:32:49 -08:00
Juan Camilo González
ac80c8f256 Feature/fix duplicate associations sample xlsx (#63)
* Release v0.5.0 (#61)

* remove explicit ID from events in timemap setup

* Fixed internal anchor link headline

* update fmt

* Bump lodash from 4.17.15 to 4.17.19

Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>

* Wrote validation functions and getter to grab appropriate validation function

* Collapsed filters and narratives into associations; modified default tabs to reflect

* Refactoring events to have only associations; created associations tab and related export tab

* Clean up with event related associations

* Delete validation for now; not relevant to this PR

* Bump node-fetch from 2.6.0 to 2.6.1

Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1)

Signed-off-by: dependabot[bot] <support@github.com>

* Removed gsheets tab in config, unused import in package json

* try/catch to prioritise local.config.js

* Removing export categories endpoint

* Removed categories from list and using test_export_events

* Test associations

* Removed comment regarding categories endpoint

* Removing unnecessary test_export_events endpoint

* Linting threw error stating that we use path module to join instead of +

* generalise LocalFetcher to support many sheet types (xlsx, ods, etc)

* rm getFileExt

* use tab as delimiter in intermediate representation

* Bump googleapis from 32.0.0 to 39.1.0

Bumps [googleapis](https://github.com/googleapis/google-api-nodejs-client) from 32.0.0 to 39.1.0.
- [Release notes](https://github.com/googleapis/google-api-nodejs-client/releases)
- [Changelog](https://github.com/googleapis/google-api-nodejs-client/blob/master/CHANGELOG.md)
- [Commits](https://github.com/googleapis/google-api-nodejs-client/compare/v32.0.0...v39.1.0)

Signed-off-by: dependabot[bot] <support@github.com>

* Fixed tests

* fix example sheet

Co-authored-by: Christoph Knoth <github@christoph-knoth.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: efarooqui <efarooqui@pandora.com>
Co-authored-by: Ebrahem Farooqui <ebefarooqui@gmail.com>
Co-authored-by: Lachlan <Kermode>

* Spin up demo fixes

Co-authored-by: Lachlan Kermode <lachiekermode@gmail.com>
Co-authored-by: Christoph Knoth <github@christoph-knoth.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: efarooqui <efarooqui@pandora.com>
Co-authored-by: Ebrahem Farooqui <ebefarooqui@gmail.com>
2020-12-07 11:12:26 +01:00
Lachlan Kermode
79999342d1 Merge pull request #53 from forensic-architecture/dependabot/npm_and_yarn/googleapis-39.1.0
Bump googleapis from 32.0.0 to 39.1.0
2020-11-25 13:48:54 +00:00
efarooqui
224a5e6fe4 Reverting deeprows changes 2020-11-24 14:48:55 -08:00
Lachlan Kermode
6f7a5a19ad Merge pull request #59 from forensic-architecture/fix/timemap_data
fix example sheet
2020-11-20 10:17:43 +00:00
Lachlan Kermode
386407138f fix example sheet 2020-11-20 11:15:37 +01:00
efarooqui
17607cf69f Merge branch 'develop' of https://www.github.com/forensic-architecture/datasheet-server into develop 2020-11-16 08:55:00 -08:00
efarooqui
f3f574380c Fixed tests 2020-11-16 08:54:50 -08:00
dependabot[bot]
64f7775a83 Bump googleapis from 32.0.0 to 39.1.0
Bumps [googleapis](https://github.com/googleapis/google-api-nodejs-client) from 32.0.0 to 39.1.0.
- [Release notes](https://github.com/googleapis/google-api-nodejs-client/releases)
- [Changelog](https://github.com/googleapis/google-api-nodejs-client/blob/master/CHANGELOG.md)
- [Commits](https://github.com/googleapis/google-api-nodejs-client/compare/v32.0.0...v39.1.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-12 19:32:05 +00:00
Lachlan Kermode
7dd9d69d1f Merge pull request #57 from forensic-architecture/topic/better-local
generalise LocalFetcher to support many sheet types (xlsx, ods, etc)
2020-11-12 19:31:16 +00:00
Lachlan
848d5b20f7 use tab as delimiter in intermediate representation 2020-11-12 13:25:39 +00:00
Lachlan
f5ada3d326 rm getFileExt 2020-11-12 10:12:25 +00:00
Lachlan
8dad1de61e generalise LocalFetcher to support many sheet types (xlsx, ods, etc) 2020-11-12 10:06:02 +00:00
Ebrahem Farooqui
eaa4d1f2c1 Merge pull request #56 from forensic-architecture/feature/refactor-categories
Feature/refactor categories
2020-10-20 15:22:26 -07:00
efarooqui
29182f8ec2 Linting threw error stating that we use path module to join instead of + 2020-10-19 09:02:03 -07:00
efarooqui
1c600bc222 Removing unnecessary test_export_events endpoint 2020-10-19 08:56:31 -07:00
efarooqui
8492c5cbaa Removed comment regarding categories endpoint 2020-10-15 21:33:13 -07:00
efarooqui
2e1d098d12 Test associations 2020-10-14 08:12:56 -07:00
efarooqui
e5288c2599 Removed categories from list and using test_export_events 2020-10-13 08:45:41 -07:00
efarooqui
ea52a8bd8c Removing export categories endpoint 2020-10-07 09:03:30 -07:00
Lachlan Kermode
5068ea8543 Merge pull request #54 from forensic-architecture/feature/refactor-filters-and-narratives-to-associations
Feature/refactor filters and narratives to associations
2020-09-23 17:12:14 +01:00
Lachlan Kermode
1e2a991708 try/catch to prioritise local.config.js 2020-09-23 18:08:40 +02:00
efarooqui
35f8460eb4 Removed gsheets tab in config, unused import in package json 2020-09-14 08:17:48 -07:00
Lachlan Kermode
98ec281973 Merge pull request #55 from forensic-architecture/dependabot/npm_and_yarn/node-fetch-2.6.1
Bump node-fetch from 2.6.0 to 2.6.1
2020-09-14 10:35:32 +02:00
dependabot[bot]
2a11bf1ec7 Bump node-fetch from 2.6.0 to 2.6.1
Bumps [node-fetch](https://github.com/bitinn/node-fetch) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/bitinn/node-fetch/releases)
- [Changelog](https://github.com/node-fetch/node-fetch/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/bitinn/node-fetch/compare/v2.6.0...v2.6.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-13 00:13:08 +00:00
efarooqui
75831cbb52 Delete validation for now; not relevant to this PR 2020-09-11 08:28:55 -07:00
efarooqui
9dd2a66ce1 Clean up with event related associations 2020-09-11 08:25:52 -07:00
efarooqui
ae12de5933 Refactoring events to have only associations; created associations tab and related export tab 2020-09-09 21:23:49 -07:00
efarooqui
5c25a8d1d0 Collapsed filters and narratives into associations; modified default tabs to reflect 2020-08-25 08:55:19 -07:00
efarooqui
fba74d8e9c Wrote validation functions and getter to grab appropriate validation function 2020-08-11 21:40:34 -07:00
Lachlan Kermode
f3115007e2 Merge pull request #52 from forensic-architecture/dependabot/npm_and_yarn/lodash-4.17.19
Bump lodash from 4.17.15 to 4.17.19
2020-07-21 09:57:07 +02:00
Lachlan Kermode
70149b905f Merge pull request #51 from christophknoth/patch-1
Fixed internal anchor link headline
2020-07-21 09:56:48 +02:00
dependabot[bot]
afa52bffb6 Bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-21 04:11:55 +00:00
Lachlan Kermode
533ab6e6f9 update fmt 2020-07-15 09:58:56 +02:00
Christoph Knoth
d06f4a5b68 Fixed internal anchor link headline 2020-06-30 12:30:30 +02:00
Lachlan Kermode
95cb7a6f80 remove explicit ID from events in timemap setup 2020-06-19 12:07:57 +02:00
Lachlan Kermode
e7718f18c7 update readme 2020-06-18 11:04:30 +02:00
Lachlan Kermode
f43f0f322c update default sheet 2020-06-18 10:10:41 +02:00
Lachlan Kermode
33a3c57036 update 2020-06-17 19:10:40 +02:00
Lachlan Kermode
ce3475b147 fix empty rows 2020-06-17 18:58:44 +02:00
Lachlan Kermode
83e6897b75 fix for windows npm 2020-06-17 18:03:56 +02:00
Lachlan Kermode
a620b17090 stop tracking 2020-06-17 12:14:22 +02:00
FA Internal SSH
c273e681a4 make config cleaner 2020-06-17 08:37:49 +00:00
FA Internal SSH
1d0735ffb6 switch to npm 2020-06-17 08:30:17 +00:00
FA Internal SSH
b1aa8e6703 update prefixedTabs 2020-06-17 08:29:47 +00:00
Lachlan Kermode
4663e23940 add update button 2020-06-14 19:53:04 +02:00
Lachlan Kermode
b48345a5d4 rm git hooks 2020-06-14 19:31:04 +02:00
Lachlan Kermode
59157d44ba add basic hbs view 2020-06-14 19:27:23 +02:00
Lachlan Kermode
8a91c6af56 update config 2020-06-14 18:58:22 +02:00
Lachlan Kermode
63c26bdb12 add handlebars for blueprints 2020-06-02 14:58:22 +02:00
Lachlan Kermode
aa30489be9 Update sheets_config.js 2020-05-02 16:04:45 +02:00
Lachlan Kermode
d113181bb4 fix differential args for fetchers 2020-04-26 17:17:34 +02:00
Lachlan Kermode
f34b9224aa gitignore sheets_config.js 2020-04-12 12:05:53 +02:00
Lachlan Kermode
ebe2930a53 Update sheets_config.js 2020-04-12 12:04:44 +02:00
Lachlan Kermode
84b6ecf05f correct example config 2020-04-12 12:04:19 +02:00
Lachlan Kermode
1b5e0ebecf Merge pull request #50 from forensic-architecture/topic/xlsx
Topic/xlsx
2020-04-06 14:16:56 +02:00
Lachlan Kermode
f659b46233 all excel data as strings 2020-03-27 18:42:15 +01:00
Lachlan Kermode
5d7eb0af05 add xlsx support
by factoring out a Fetcher class
2020-03-27 12:30:21 +01:00
Lachlan Kermode
2e343a17dd enc 2020-03-26 06:45:49 +01:00
Lachlan Kermode
5feeff589a update sheets_config 2020-03-26 06:45:06 +01:00
Lachlan Kermode
eeb040c6ef Merge pull request #44 from forensic-architecture/dependabot/npm_and_yarn/js-yaml-3.13.1
Bump js-yaml from 3.12.0 to 3.13.1
2020-03-26 06:43:17 +01:00
Lachlan Kermode
e217b89a02 Merge pull request #45 from forensic-architecture/dependabot/npm_and_yarn/mixin-deep-1.3.2
Bump mixin-deep from 1.3.1 to 1.3.2
2020-03-26 06:43:06 +01:00
Lachlan Kermode
9767e76336 Merge pull request #46 from forensic-architecture/dependabot/npm_and_yarn/esm-3.2.25
Bump esm from 3.0.84 to 3.2.25
2020-03-26 06:42:54 +01:00
Lachlan Kermode
57c65ed5f4 Merge pull request #47 from forensic-architecture/dependabot/npm_and_yarn/axios-0.18.1
Bump axios from 0.18.0 to 0.18.1
2020-03-26 06:42:42 +01:00
Lachlan Kermode
21a597fa5b Merge pull request #48 from forensic-architecture/dependabot/npm_and_yarn/lodash.merge-4.6.2
Bump lodash.merge from 4.6.1 to 4.6.2
2020-03-26 06:42:30 +01:00
Lachlan Kermode
7a19356bb4 Merge pull request #49 from forensic-architecture/dependabot/npm_and_yarn/lodash-4.17.15
Bump lodash from 4.17.11 to 4.17.15
2020-03-26 06:42:17 +01:00
dependabot[bot]
77ab257ed3 Bump lodash from 4.17.11 to 4.17.15
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.15.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.15)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-26 05:41:06 +00:00
dependabot[bot]
0e7547c58c Bump esm from 3.0.84 to 3.2.25
Bumps [esm](https://github.com/standard-things/esm) from 3.0.84 to 3.2.25.
- [Release notes](https://github.com/standard-things/esm/releases)
- [Commits](https://github.com/standard-things/esm/compare/3.0.84...3.2.25)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-26 05:41:05 +00:00
dependabot[bot]
d4a74b5d70 Bump axios from 0.18.0 to 0.18.1
Bumps [axios](https://github.com/axios/axios) from 0.18.0 to 0.18.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.18.1/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.18.0...v0.18.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-26 05:41:05 +00:00
dependabot[bot]
e33417381a Bump lodash.merge from 4.6.1 to 4.6.2
Bumps [lodash.merge](https://github.com/lodash/lodash) from 4.6.1 to 4.6.2.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-26 05:41:05 +00:00
dependabot[bot]
dd5428d7b6 Bump js-yaml from 3.12.0 to 3.13.1
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.12.0 to 3.13.1.
- [Release notes](https://github.com/nodeca/js-yaml/releases)
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.12.0...3.13.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-26 05:41:03 +00:00
dependabot[bot]
f83251e308 Bump mixin-deep from 1.3.1 to 1.3.2
Bumps [mixin-deep](https://github.com/jonschlinkert/mixin-deep) from 1.3.1 to 1.3.2.
- [Release notes](https://github.com/jonschlinkert/mixin-deep/releases)
- [Commits](https://github.com/jonschlinkert/mixin-deep/compare/1.3.1...1.3.2)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-26 05:41:03 +00:00
Lachlan Kermode
0374955469 Merge pull request #43 from forensic-architecture/dependabot/npm_and_yarn/acorn-6.4.1
Bump acorn from 6.0.4 to 6.4.1
2020-03-26 06:40:40 +01:00
Lachlan Kermode
c294e4964e Merge pull request #42 from forensic-architecture/dependabot/npm_and_yarn/eslint-utils-1.4.3
Bump eslint-utils from 1.3.1 to 1.4.3
2020-03-26 06:40:30 +01:00
dependabot[bot]
1c2abded7b Bump acorn from 6.0.4 to 6.4.1
Bumps [acorn](https://github.com/acornjs/acorn) from 6.0.4 to 6.4.1.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/6.0.4...6.4.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-14 18:57:07 +00:00
dependabot[bot]
25a57f95d3 Bump eslint-utils from 1.3.1 to 1.4.3
Bumps [eslint-utils](https://github.com/mysticatea/eslint-utils) from 1.3.1 to 1.4.3.
- [Release notes](https://github.com/mysticatea/eslint-utils/releases)
- [Commits](https://github.com/mysticatea/eslint-utils/compare/v1.3.1...v1.4.3)

Signed-off-by: dependabot[bot] <support@github.com>
2019-11-02 03:40:28 +00:00
Lachlan Kermode
13f93a2b04 Merge pull request #38 from forensic-architecture/issue/blueprints-json-formatting-#30
Issue/blueprints json formatting #30
2019-05-23 17:03:46 +01:00
Lachlan Kermode
fdcf4f28d0 encrypt with maintainer secret 2019-05-23 16:40:29 +01:00
SAM LUDFORD
9deb5aae3f multiple resources collapsed into single blueprint 2019-05-23 11:43:10 +01:00
SAM LUDFORD
8f397e395d list of list of blueprints flattened into single list 2019-05-23 11:31:16 +01:00
Lachlan Kermode
824b672a5e license fix 2019-05-16 12:13:18 +01:00
Lachlan Kermode
1c82292344 update license 2019-05-16 12:10:05 +01:00
Lachlan Kermode
2632661bbe .env.enc for travis tests 2019-05-15 15:52:18 +01:00
Lachlan Kermode
849a0ce9ed update readme with new wiki 2019-05-15 15:47:09 +01:00
Lachlan Kermode
63b66f2cff Merge pull request #36 from forensic-architecture/topic/add-columns
Topic/add columns
2019-02-14 16:18:30 +00:00
Lachlan Kermode
ff30d1be18 tree root -> _root 2019-02-14 16:11:38 +00:00
Lachlan Kermode
57bcfb5d40 add encrypt 2019-01-17 16:03:21 +00:00
Lachlan Kermode
30f747bcaf add blueprinter 2019-01-17 16:02:21 +00:00
Lachlan Kermode
3271fd0f05 new encrypt 2019-01-15 13:07:59 +00:00
Lachlan Kermode
965482633b Merge pull request #35 from forensic-architecture/fix/deep-bps
Fix/deep bps
2019-01-04 18:04:54 +01:00
Lachlan Kermode
f32c1f8a02 💄 2019-01-04 12:18:12 +00:00
Lachlan Kermode
a124d2c9bf deep regex non-greedy 2019-01-04 12:16:58 +00:00
Lachlan Kermode
11fec88030 lint 2018-12-29 14:52:23 +01:00
Lachlan Kermode
7b3a6514d8 change permissions to encrypt.sh 2018-12-29 14:50:46 +01:00
Lachlan Kermode
b6d917553c add deepids as default for sources 2018-12-29 13:28:11 +01:00
Lachlan Kermode
cd6a4f56a7 Merge pull request #33 from forensic-architecture/topic/doc-improve
WIP: Topic/doc improve
2018-12-20 15:56:51 +00:00
Lachlan Kermode
8d2c5d261c writeup architecture doc 2018-12-20 14:15:54 +00:00
Lachlan Kermode
92a3b4cb96 add basic architecture doc 2018-12-19 17:55:27 +00:00
Lachlan Kermode
fd728708af add architecture graphic 2018-12-19 17:52:35 +00:00
Lachlan Kermode
d26f6b96c9 modify docs to reflect new config 2018-12-19 16:41:16 +00:00
Lachlan Kermode
440b139aa1 Merge pull request #32 from forensic-architecture/topic/server-process-test
Topic/server process test
2018-12-19 11:32:07 +00:00
Lachlan Kermode
e573348679 refactor travis encrypt to manual process before push 2018-12-18 15:54:44 +00:00
Lachlan Kermode
651f768c5b only require travis encryption for maintainers 2018-12-18 15:00:05 +00:00
Joshua Lieberman
063586735c adding regex to get rid of encryption information when sharing between multiple travis configs 2018-12-18 14:51:52 +00:00
Lachlan Kermode
9ffc7c88d0 update travis config 2018-12-17 10:52:31 +00:00
Lachlan Kermode
ddeeb3f588 refactor encrypt script to be more readable 2018-12-17 10:49:12 +00:00
Lachlan Kermode
9f96e6ea8d update encrypt 2018-12-15 16:22:33 +00:00
Lachlan Kermode
b06c8536b9 rm old config.js 2018-12-15 11:07:30 +00:00
Lachlan Kermode
4328ceb464 encrypt script for travis 2018-12-15 10:59:20 +00:00
Lachlan Kermode
64ca47ee8e amend serverProcess test to update first 2018-12-15 10:58:28 +00:00
Lachlan Kermode
909b9fd21b run bash in pre-push hook 2018-12-15 09:35:53 +00:00
Lachlan Kermode
b6b7299d81 add encrypted key 2018-12-14 18:44:15 +00:00
Lachlan Kermode
3388d18eb3 fix travis decrypt logic 2018-12-14 18:43:55 +00:00
Lachlan Kermode
837139eb78 add encrypted key 2018-12-14 18:41:16 +00:00
Lachlan Kermode
c49cb2b59e fix encrypt script 2018-12-14 18:40:53 +00:00
Lachlan Kermode
1f2a2953b1 fix lint 2018-12-14 18:23:48 +00:00
Lachlan Kermode
18ddc6c48b leverage ava's async features to ensure order in server_process tests 2018-12-14 17:50:00 +00:00
Lachlan Kermode
95a501aba2 fix lint and prev commit errors 2018-12-14 17:24:50 +00:00
Lachlan Kermode
82b4dceef0 rm test/lint from encrypt 2018-12-14 16:59:47 +00:00
Lachlan Kermode
219adc1e5e refactor to scripts 2018-12-14 16:56:37 +00:00
Joshua
5f4943d1d5 new server test with precommit hook to secure .env files, set up CI environment (#5)
* new server test with precommit hook to secure .env files, set up CI environment (#3)

* [TESTS] added all registered routes to api server test

* [WIP] abstracting config to env where it makes sense, refactoring elsewhere, adding more tests

* [WIP] fixed tests so they fail as expected

* [WIP]

* [DEBUG] fixed issues with the env configuration, added correct tests

* [MISC] env didn't get readded on last precommit

* [TESTS] added longer wait time for server as sometimtimes tests fail arbitrarily
2018-12-14 15:59:12 +00:00
Lachlan Kermode
1515f17461 Merge pull request #29 from forensic-architecture/topic/add-deeprows
Topic/add deeprows
2018-12-14 13:20:20 +00:00
Lachlan Kermode
5d8a6b1927 update example.config.js 2018-12-14 09:56:18 +00:00
Lachlan Kermode
f47fc311c1 update tests 2018-12-13 17:10:14 +00:00
Lachlan Kermode
b37e49880a fix lint 2018-12-13 16:49:23 +00:00
Lachlan Kermode
bbee0c2896 functional deeprows blueprinter 2018-12-13 16:44:56 +00:00
Lachlan Kermode
df239c8f58 rm debugging logic 2018-12-13 15:59:03 +00:00
Lachlan Kermode
5431b2be3f abstract generic logic from blueprinters to blueprinters.js
The logic in the files in the 'blueprinters' folder is now _only_ the data transformation logic. Instead of taking in arguments like the sheetId, the tabName, and the sheetName, the function now takes a single argument: the list of lists that represents the raw data from the sheet.

This setup gives datasheet-server greater value, as it allows developers to only specify the transformation logic, and not worry about the other stuff that datasheet server is doing.
2018-12-13 15:56:54 +00:00
Lachlan Kermode
7636db4f41 Merge pull request #28 from forensic-architecture/fix/ids-retrieveFrag-docs
Fix/ids retrieve frag docs
2018-12-13 12:44:38 +00:00
Lachlan Kermode
84237fcf14 fix lint 2018-12-13 12:42:53 +00:00
Lachlan Kermode
bbea550c87 fix ids blueprinter 2018-12-13 12:35:27 +00:00
Lachlan Kermode
f909abfdc0 fix retrieveFrag call 2018-12-12 16:07:25 +00:00
Lachlan Kermode
b2276c694e export errors on default obj 2018-12-12 16:07:25 +00:00
Lachlan Kermode
4151d68f2e update docs Interface.js, abstract model layer errors 2018-12-12 16:07:25 +00:00
Lachlan Kermode
f104754cf9 Merge pull request #26 from jlieb10/develop
generalized name in package.json
2018-12-11 15:39:26 +00:00
Joshua Lieberman
a52c305e35 [MISC] generalized name in package.json 2018-12-11 15:14:37 +00:00
46 changed files with 8466 additions and 4840 deletions

4
.env.example Normal file
View File

@@ -0,0 +1,4 @@
PORT=4040
MAPBOX_TOKEN=pk.ANOTHERLONGSTRING.pMXNkIn0.xjjmguLIeX-r8FWomVG8Tg
SERVICE_ACCOUNT_EMAIL="DUMMY-SERVICE-WORKER-NAME@DUMMY-PROJECT-NAME.iam.gserviceaccount.com"
SERVICE_ACCOUNT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\A-vErY-l0nG-sTr1Ng\n-----END PRIVATE KEY-----\n"

19
.github/workflows/cd.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: CD
on:
push:
branches: [ develop ]
# pull_request:
# branches: [ develop ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Trigger CD build
uses: peter-evans/repository-dispatch@v1
with:
token: ${{ secrets.CI_DISPATCH_TOKEN }}
repository: forensic-architecture/configs
event-type: remote-build
client-payload: '{"runtime_args": "datasheet", "branch": "${GITHUB_REF##*/}"}'

25
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: CI
on:
push:
branches: [ develop ]
pull_request:
branches: [ develop ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.head_ref }}
- uses: actions/setup-node@v2-beta
with:
node-version: '12'
- run: npm install
- run: npm test
env:
CI: true
- run: npm run lint
env:
CI: true

9
.gitignore vendored
View File

@@ -5,6 +5,13 @@
.DS_Store
*.swp
.env
*service-account-key\.json
src/config.js
/yarn-error.log
*.pem
.travis.yml.old
tags
tags.lock
tags.temp
src/config.js
src/local.config.js

View File

@@ -1,15 +0,0 @@
language: node_js
node_js:
- "stable"
cache:
directories:
- node_modules
before_script:
- npm install -g yarn
- cp ./src/example.config.js ./src/config.js
install:
- yarn
script:
- yarn build
- yarn lint
- yarn test

View File

@@ -45,11 +45,32 @@ WIP
### Before making changes
WIP. Here we'll describe what the expected process and workflow is when making code changes, with regards to branching, forking and so on.
1. If you are a contributor, you will need to create a fork of this repository
on your own GitHub handle, as you will not have commit access to the
forensic architecture repo.
2. Create a new branch from _develop_ (not master or staging). The branch
should be prefixed with 'topic/' if you are intending to submit a feature
('enhancement' tag in the issue), or with 'fix/' if you are fixing a bug
('bug' tag in the issue).
All of your commits go in this branch. When the feature/fix is complete, follow
the instructions below to submit a PR for the branch.
### Submitting changes as Pull Requests
WIP
In order to submit a branch as a PR, you'll need to install the [Travis CLI](https://github.com/travis-ci/travis.rb). The documentation for this is a little shifty: if you're developing on a Mac, you can easily install it with `brew install travis`. The Travis CLI is necessary so that you can encrypt your service account credentials and use them while testing in Travis CI.
To do this, you need to run one extra command before you push commits
to a remote branch:
```
npm run travis-encrypt
```
This command encrypts your private key and service account email in .env in
such a way that they can still be used while running tests on Travis' server.
This command will add a commit to your branch that modifies the binary file
.env.enc, and updates your Travis config accordingly. After running this
command, you should be able to pass the pre-push check and run tests in the
Travis server.
## Additional resources

View File

@@ -10,11 +10,11 @@ RUN cd /www; yarn
COPY . /www
WORKDIR /www
RUN yarn build
RUN mkdir -p temp
RUN mkdir -p data
# set your port
ENV PORT 8080
EXPOSE 8080
ENV PORT 4040
EXPOSE 4040
# start command as per package.json
CMD ["yarn", "start"]

20
LICENSE
View File

@@ -1,20 +0,0 @@
The MIT License (MIT)
Copyright (c) 2018 Forensic Architecture
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

66
LICENSE.md Normal file
View File

@@ -0,0 +1,66 @@
Do No Harm License
**Preamble**
Most software today is developed with little to no thought of how it will be used, or the consequences for our society and planet.
As software developers, we engineer the infrastructure of the 21st century. We recognise that our infrastructure has great power to shape the world and the lives of those we share it with, and we choose to consciously take responsibility for the social and environmental impacts of what we build.
We envisage a world free from injustice, inequality, and the reckless destruction of lives and our planet. We reject slavery in all its forms, whether by force, indebtedness, or by algorithms that hack human vulnerabilities. We seek a world where humankind is at peace with our neighbours, nature, and ourselves. We want our work to enrich the physical, mental and spiritual wellbeing of all society.
We build software to further this vision of a just world, or at the very least, to not put that vision further from reach.
**Terms**
*Copyright* (c) 2019 Forensic Architecture. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
4. This software must not be used by any organisation, website, product or service that:
a) lobbies for, promotes, or derives a majority of income from actions that support or contribute to:
* sex trafficking
* human trafficking
* slavery
* indentured servitude
* gambling
* tobacco
* adversely addictive behaviours
* nuclear energy
* warfare
* weapons manufacturing
* war crimes
* violence (except when required to protect public safety)
* burning of forests
* deforestation
* hate speech or discrimination based on age, gender, gender identity, race, sexuality, religion, nationality
b) lobbies against, or derives a majority of income from actions that discourage or frustrate:
* peace
* access to the rights set out in the Universal Declaration of Human Rights and the Convention on the Rights of the Child
* peaceful assembly and association (including worker associations)
* a safe environment or action to curtail the use of fossil fuels or prevent climate change
* democratic processes
5. All redistribution of source code or binary form, including any modifications must be under these terms. You must inform recipients that the code is governed by these conditions, and how they can obtain a copy of this license. You may not attempt to alter the conditions of who may/may not use this software.
We define:
**Forests** to be 0.5 or more hectares of trees that were either planted more than 50 years ago or were not planted by humans or human made equipment.
**Deforestation** to be the clearing, burning or destruction of 0.5 or more hectares of forests within a 1 year period.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**Attribution**
Do No Harm License [Contributor Covenant][homepage], (pre 1.0),
available at https://github.com/raisely/NoHarm
[homepage]: https://github.com/raisely/NoHarm

View File

@@ -34,45 +34,12 @@ Datasheet server is a Node server developed at [Forensic Architecture](https://f
Querying data directly from spreadsheets is brittle, as it relies on the maintenance of a rigid structure in the sheets at all times. By putting Datasheet Server as a proxy that sits in between source sheets and their consumers, it is possible to dynamically modify sheets without breaking applications. A data admin can then use Datasheet Server to ensure that applications always receive eligible data, without foregoing the spreadsheets as sources of truth.
To see how to get a local instance of datasheet server running in practice, see [this wiki](https://github.com/forensic-architecture/timemap/wiki/Setting-up-a-local-instance-of-Timemap) explaining how to use it to feed data from a Google Sheet to a local instance of [Timemap](https://github.com/forensic-architecture/timemap).
### Design Concepts
The codebase currently only supports Google Sheets as a source, and a REST-like format as a query language. It is designed, however, with extensibility in mind.
**Sources**
A source represents a sheet-like collection of data, such as a Google Sheet. A source has one or more **tabs**, each of which contains a 2-dimensional grids of cells. Each cell contains a body of text (a string).
**Resources**
The data from sources are made available as resources, which are structured blocks of data that are granularly accessible through a query language. Resources are the outfacing aspect of Datasheet Server, and represent the only kind of data that can be queried by applications. Each resource is configured with one or more **query languages**. (Currently only a REST-like query language supported.)
**Blueprints**
Blueprints are a data structure that represent the way that infromation from **sources** are to be turned into **resources**. For each tab in a source, there is a corresponding Blueprint. Blueprints are created through a [blueprinter function](/src/blueprinters) invoked on the raw data from a source tab.
Blueprints are JSON objects. There have two forms:
1. _desaturated_ -- describes the resources and query languages available on data from a source tab.
2. _saturated_ -- both describes resources available on data from a source etab, and contains that data.
A desaturated Blueprint can be saturated by retrieving its data from the server's **model layer**, which stores tab data from sources.
A JSON catalogue of the available blueprints (desaturated) in a server is available at `/api/blueprints`.
## [Configuration](#configuration)
Copy the [example.config.js](/src/example.config.js) in the [src](/src) directory into a file named 'config.js'. Modify the options in this file accordingly:
| Option | Description | Type |
| ------- | ----------- | ---- |
| port | The port at which the server will make data available. | integer |
| googleSheets | The configuration object for [Google Sheet](https://www.google.co.uk/sheets/about/) data sources. See the [Sources](#source-google-sheets) section below. | object |
To see how to get a local instance of datasheet server running in practice, see [this wiki](https://github.com/forensic-architecture/timemap/wiki/running-timemap-and-datasheet-server-locally) explaining how to use it to feed data from a Google Sheet to a local instance of [Timemap](https://github.com/forensic-architecture/timemap).
#### [Sources](#sources)
###### [Google Sheets](#source-google-sheets)
In order to make the data from a Sheet accessible to the server, you need to [create a service account](https://cloud.google.com/iam/docs/creating-managing-service-accounts). Once created, give the service account email access to each Sheet from which you want to serve data. ('View Only' access is sufficient, as the server never modifies data.)
Sources are specified in [src/config.js](https://github.com/forensic-architecture/datasheet-server/blob/develop/src/config.js). Datasheet server currently only supports Google Sheets as a source.
| Option | Description | Type |
| ------ | ----------- | ---- |
| email | The email address of the service account. This is available in the downloadable service account JSON in the `client_email` field. | string |
| privateKey | The private key associated with the service account. This is available in the downloadable service account JSON in the `private_key` field. | string |
###### [Google Sheets](#source-google-sheets)
| sheets | A list of objects, one for each sheet that is being used as a source. Each sheet object has a `name` (String), an `id` (String), and a `tabs` (object) field, which are explained below. | object |
Each Google Sheet being used as a as source requires a corresponding object in `sheets`. The object should be structured as follows:
@@ -80,41 +47,23 @@ Each Google Sheet being used as a as source requires a corresponding object in `
| Option | Description | Type |
| ------ | ----------- | ---- |
| name | Used to refer to data served from this source | string |
| id | The ID of the sheet in Google. (You can find it in the address bar when the Sheet is open in a browser. It is the string that follows 'spreadsheets/d/'). | string |
| path/id | The path to the XLSX sheet on your local, or the ID of the sheet in Google. (You can find it in the address bar when the Sheet is open in a browser. It is the string that follows 'spreadsheets/d/'). | string |
| tabs | An object that maps each tab in the source to one or more Blueprinters. All of the Blueprinters in the [blueprinters folder](/lib/blueprinters) are available through a single import as at the top of [example.config.js](/src/example.config.js). <br> To correctly associate a Blueprinter, the object key needs to be _the tab name with all lowercase letters, and spaces replaced by a '-'_. For example, if the tab name in Google Sheets is 'Info About SHEEP', the object key should be 'info-about-sheep'. <br> The value should be the Blueprinter function that you want to use for the data in that tab. If you require more than one endpoint for a single tab, you can support multiple blueprinters by making the item an array. See the example of a configuration object below. <br>TODO: no Blueprinter is used by default. | object |
###### Example Configuration Object
```js
import BP from './lib/blueprinters'
export default {
port: 4040,
googleSheets: {
email: 'project-name@reliable-baptist-23338.iam.gserviceaccount.com',
privateKey: 'SOME_PRIVATE_KEY',
sheets: [
{
name: 'example',
id: '1s-vfBR8Uy-B-TLO_C5Ozw4z-L0E3hdP8ohMV761ouRI',
tabs: {
'objects': BP.byRow,
'fruit': [BP.byRow, BP.byID],
}
},
]
}
}
```
See src/config.js for an example configuration sheet.
## [Quickstart](#quickstart)
## [Quickstart](#quickstart)
Clone the repository to your local:
```
git clone https://www.github.com/forensic-architecture/datasheet-server
```
Follow the steps in the [configuration](#configuration) section of this
document.
### Run with Docker
To create a new instance of the server with [Docker](https://www.docker.com/) installed, clone the repository, create a `config.js`, and build the image:
To create a new instance of the server with [Docker](https://www.docker.com/) installed, ensure that you have followed the steps in the quickstart guide above, then and build an image locally. (Note that Docker must be installed):
```sh
docker build -t datasheet-server .
```
@@ -152,4 +101,4 @@ If you have any questions or just want to chat, please join our team [fa_open_so
## License
TimeMap is distributed under the MIT License.
Datasheet Server is distributed under the [MIT License](https://github.com/forensic-architecture/datasheet-server/blob/develop/LICENSE).

3
data/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*
!.gitignore
!timemap_data.xlsx

BIN
data/timemap_data.xlsx Normal file

Binary file not shown.

41
docs/architecture.md Normal file
View File

@@ -0,0 +1,41 @@
# Architecture
![](datasheet-server-graphic.jpg)
Datasheet Server uses the architecture diagrammed above to allow effective
management of the sheets it represents.
It exposes an API with four endpoints:
* `/:sheet/:tab/:resource` - the primary means of accessing data in the
server. The name of a sheet is specified in
[_config.js](https://github.com/forensic-architecture/datasheet-server/blob/develop/src/config.js#L7).
Each tab on the sheet is in lower case, with spaces replaced by
underscores. The resource name is the same as the name of the blueprinter
specified for the tab is config.js.
* `/:sheet/:tab/:resource/:id` - when the resource is a list, items in the list
can be accessed individually as well.
* `/update` - when this route is queried with a GET request, each of the
fetchers in the server will update their models from the relevant sheet.
* `/blueprints` - a JSON object that represents all available sheets, tabs, and
resources in the server.
# Controller
The [controller](https://github.com/forensic-architecture/datasheet-server/blob/develop/src/lib/Controller.js)
manages all of the fetchers in the server. Its `update` triggers the update
mechanisms of all the `update`s in the fetchers it manages. Its `retrieve` and
`retrieveFrag` methods find the appropriate fetcher, and trigger its
respectively named method. Its `bluerprints` method collects the blueprints
from all fetchers, and presents them as a single list.
# Fetcher
The [fetcher](https://github.com/forensic-architecture/datasheet-server/blob/develop/src/lib/Fetcher.js)
is the most complex component in datasheet server. Its responsiblity is to
interface with a sheet, and store the data from that sheet in a model. It also
makes information from that model available to the controller.
# Model
The model layer is an [interface](https://github.com/forensic-architecture/datasheet-server/blob/develop/src/models/Interface.js)
that specifies how stored data must be made available to a fetcher. The
implementation of the model layer ([StoreJson](https://github.com/forensic-architecture/datasheet-server/blob/develop/src/models/StoreJson.js)
is one example) determines the time and space complexity of retrieval.

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

22
docs/design-concepts.md Normal file
View File

@@ -0,0 +1,22 @@
### Design Concepts
The codebase currently only supports Google Sheets as a source, and a REST-like format as a query language. It is designed, however, with extensibility in mind.
**Sources**
A source represents a sheet-like collection of data, such as a Google Sheet. A source has one or more **tabs**, each of which contains a 2-dimensional grids of cells. Each cell contains a body of text (a string).
**Resources**
The data from sources are made available as resources, which are structured blocks of data that are granularly accessible through a query language. Resources are the outfacing aspect of Datasheet Server, and represent the only kind of data that can be queried by applications. Each resource is configured with one or more **query languages**. (Currently only a REST-like query language supported.)
**Blueprints**
Blueprints are a data structure that represent the way that infromation from **sources** are to be turned into **resources**. For each tab in a source, there is a corresponding Blueprint. Blueprints are created through a [blueprinter function](/src/blueprinters) invoked on the raw data from a source tab.
Blueprints are JSON objects. There have two forms:
1. _desaturated_ -- describes the resources and query languages available on data from a source tab.
2. _saturated_ -- both describes resources available on data from a source etab, and contains that data.
A desaturated Blueprint can be saturated by retrieving its data from the server's **model layer**, which stores tab data from sources.
A JSON catalogue of the available blueprints (desaturated) in a server is available at `/api/blueprints`.

19
docs/gsheet-config.md Normal file
View File

@@ -0,0 +1,19 @@
## [Configuration](#configuration)
Copy the example environment file:
```
cp .env.example .env
```
Inside this file, you will need to modify at least the `SERVICE_ACCOUNT_EMAIL` and `SERVICE_ACCOUNT_PRIVATE_KEY` fields. These fields refer to the credentials of a [Google service account](https://cloud.google.com/iam/docs/understanding-service-accounts). Google requires that developers create these when attempting to access their services programmatically, so that they can attribute requests to users. Service accounts also contain identity information, which means that asset owners can allow certain service accounts access to certain sheets, just as one might differentially grant certain users access to certain cloud assets.
Once you have [created a service account](https://support.google.com/a/answer/7378726?hl=en), create and download an [API key for that account](https://cloud.google.com/iam/docs/creating-managing-service-account-keys). The JSON file for the API key that you download when you create it contains both a service account private key, and an email associated with the service account: add these respectively in the strings in .env for `SERVICE_ACCOUNT_PRIVATE_KEY` and `SERVICE_ACCOUNT_EMAIL`.
The last thing to do is to grant the service account access to the sheet that
datasheet-server is pulling from. You can add a service account to a sheet as
you would any other Google user: just enter the email address associated. (Note
that this step is not necessary if you are accessing a publicly available
sheet.)
Other configuration options, such as the port at which the server will expose
resources, are also modifiable from the .env file.

7461
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,17 @@
{
"name": "grenfell-server",
"name": "datasheet-server",
"version": "0.3.0",
"description": "Starter project for an ES6 RESTful Express API",
"main": "dist",
"type": "module",
"scripts": {
"dev": "NODE_ENV=development nodemon -w src --exec \"babel-node src\"",
"build": "NODE_ENV=production npx babel src -d dist",
"dev": "env NODE_ENV=development nodemon -w src --exec \"babel-node src\"",
"build": "env NODE_ENV=production npx babel src -d dist",
"start": "node dist",
"lint": "standard \"src/**/*.js\" \"test/**/*/js\"",
"lint": "standard \"src/**/*.js\" \"test/**/*.js\"",
"test-watch": "ava --watch",
"test": "ava --verbose"
"test": "ava --verbose",
"travis-encrypt": "./scripts/encrypt.sh"
},
"repository": {
"type": "git",
@@ -21,21 +23,26 @@
"body-parser": "^1.13.3",
"compression": "^1.5.2",
"cors": "^2.8.5",
"dotenv": "^6.1.0",
"express": "^4.13.3",
"express-graphql": "^0.6.12",
"googleapis": "^32.0.0",
"express-handlebars": "^4.0.4",
"file-system": "^2.2.2",
"googleapis": "^39.1.0",
"graphql": "^0.13.2",
"morgan": "^1.8.0",
"mz": "^2.7.0",
"node-fetch": "^2.2.0",
"node-fetch": "^2.6.1",
"object-hash": "^1.3.0",
"ramda": "^0.25.0",
"resource-router-middleware": "^0.6.0"
"resource-router-middleware": "^0.6.0",
"xlsx": "^0.16.8"
},
"devDependencies": {
"@babel/cli": "^7.1.2",
"@babel/core": "^7.1.2",
"@babel/node": "^7.0.0",
"@babel/polyfill": "^7.0.0",
"@babel/preset-env": "^7.1.0",
"@babel/register": "^7.0.0",
"ava": "1.0.0-beta.8",
@@ -52,7 +59,8 @@
"test/**/*.js"
],
"require": [
"@babel/register"
"@babel/register",
"@babel/polyfill"
]
},
"bugs": {

View File

@@ -4,6 +4,7 @@ import copy from '../copy/en'
export default ({ config, controller }) => {
let api = Router()
const fileDest = config.EXPORT_FILE_DEST || null
api.get('/', (req, res) => {
res.json({
@@ -12,7 +13,42 @@ export default ({ config, controller }) => {
})
api.get('/blueprints', (req, res) => {
res.json(controller.blueprints())
const bps = controller.blueprints()
res.render('blueprints', {
bps: bps.map(bp => ({
source: bp.sheet.name,
tab: bp.name,
urls: bp.urls
}))
})
})
api.get('/export', (req, res) => {
controller
.retrieveAll(fileDest)
.then(msg =>
res.json({
success: msg
})
)
.catch(err =>
res.status(404)
.send({ error: err.message, err })
)
})
api.get('/update', (req, res) => {
controller
.update()
.then(msg =>
res.json({
success: msg
})
)
.catch(err =>
res.status(404)
.send({ error: err.message, err })
)
})
api.get('/:sheet/:tab/:resource/:frag', (req, res) => {
@@ -27,8 +63,9 @@ export default ({ config, controller }) => {
})
api.get('/:sheet/:tab/:resource', (req, res) => {
const { sheet, tab, resource } = req.params
controller
.retrieve(req.params.sheet, req.params.tab, req.params.resource)
.retrieve(sheet, tab, resource)
.then(data => res.json(data))
.catch(err =>
res.status(err.status || 404)
@@ -36,30 +73,16 @@ export default ({ config, controller }) => {
)
})
api.get('/update', (req, res) => {
controller
.update()
.then(msg =>
res.json({
success: msg
})
)
.catch(err =>
res.status(404)
.send({ error: err.message })
)
})
// ERROR routes. Note that it is important that these come AFTER routes
// like /update, so that the regex does not greedily match these routes.
api.get('/:sheet', (req, res) => {
res.status(404)
res.status(400)
.send({ error: copy.errors.onlysheet })
})
api.get('/:sheet/:tab', (req, res) => {
res.status(404)
res.status(400)
.send({ error: copy.errors.onlyTab })
})

View File

@@ -1,37 +1,27 @@
import R from 'ramda'
import { defaultBlueprint, defaultResource } from '../lib/blueprinters'
/**
* byColumn - generate a Blueprint from a data sheet by column. Each column
* name is a resheet, and all values in that column are the resheet items.
* Each resource item is an object with values labelled according
* to column names specified in the sheet's first row. If two or more
* column names are the same except for a different integer at the end
* (e.g. 'tag1', and 'tag2'), then the values of those two columns are
* aggregated into a list, which is the value of the prefix's key ('tag').
* Any values in those columns that are empty will NOT be added to the list.
*
* @param {type} data - list of lists representing sheet data.
* @return {type} Blueprint
* generated.
* @param {type} data list of lists representing sheet data.
* @return {type} Array the structured data.
*/
function columns (tabName, sheetName, sheetId, data) {
// Define Blueprint props
const bp = R.clone(defaultBlueprint)
bp.sheet = {
name: sheetName,
id: sheetId
}
bp.name = tabName
export default (data) => {
const columnNames = data[0]
const columns = columnNames.map(name => ([]))
// column names define resources
const labels = data[0]
labels.forEach(label => {
bp.resources[label] = R.clone(defaultResource)
})
// remaining rows as data
data.forEach((row, idx) => {
if (idx === 0) return
labels.forEach((label, idx) => {
bp.resources[label].data.push(row[idx])
row.forEach((item, idx) => {
columns[idx].push(item)
})
})
return bp
}
export default columns
return columns.map((column, idx) => ({
name: columnNames[idx],
items: column
}))
}

View File

@@ -0,0 +1,22 @@
import deeprows from './deeprows'
/**
* Each resource item is an object with values labelled according
* to column names specified in the sheet's first row. If two or more
* column names are the same except for a different integer at the end
* (e.g. 'tag1', and 'tag2'), then the values of those two columns are
* aggregated into a list, which is the value of the prefix's key ('tag').
* Any values in those columns that are empty will NOT be added to the list.
*
* @param {type} data list of lists representing sheet data.
* @return {type} Object the structured data.
*/
export default (data) => {
const output = {}
deeprows(data).forEach(row => {
output[row.id] = row
})
return output
}

View File

@@ -0,0 +1,72 @@
import { fmtObj } from '../lib/util'
/**
* Each resource item is an object with values labelled according
* to column names specified in the sheet's first row. If two or more
* column names are the same except for a different integer at the end
* (e.g. 'tag1', and 'tag2'), then the values of those two columns are
* aggregated into a list, which is the value of the prefix's key ('tag').
* Any values in those columns that are empty will NOT be added to the list.
*
* @param {type} data list of lists representing sheet data.
* @return {type} Array the structured data.
*/
export default (data) => {
const itemLabels = data[0]
const baseFmt = fmtObj(itemLabels)
const output = []
// create a structure to indicate which columns needs to be aggregated
const endsWithNumber = new RegExp('(.*?)[0-9]+$')
const structure = {
__flat: []
}
itemLabels.forEach(label => {
const matches = label.match(endsWithNumber)
if (!matches) {
structure.__flat.push(label)
} else {
const labelPrefix = `${matches[1]}s`
if (labelPrefix in structure) {
structure[labelPrefix].push(label)
} else {
structure[labelPrefix] = [ label ]
}
}
})
// generate the value for deep labels using the structure created
data.forEach((row, idx) => {
if (idx === 0) return
const baseRow = baseFmt(row)
const deepRow = {}
// generate deep row labels using structure
Object.keys(structure)
.forEach(newLabel => {
if (newLabel !== '__flat') {
const oldLabels = structure[newLabel]
// only add new value if not ''
const labelValues = []
oldLabels.forEach(l => {
const vl = baseRow[l]
if (vl !== '') {
labelValues.push(vl)
}
})
deepRow[newLabel] = labelValues
}
})
// move values for flat labels over from base
structure.__flat.forEach(label => {
deepRow[label] = baseRow[label]
})
if (!Object.keys(deepRow).every(k => deepRow[k] === '')) {
output.push(deepRow)
}
})
return output
}

View File

@@ -1,39 +1,16 @@
import R from 'ramda'
import { fmtObj } from '../lib/util'
import { defaultBlueprint, defaultResource } from '../lib/blueprinters'
/**
* 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.
* 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 into the data list at idx = id.
*
* @param {type} data list of lists representing sheet data.
* @param {type} label="groups" name of resource in blueprint.
* @param {type} name="" name of blueprint.
* @return {type} Blueprint
* @param {type} data list of lists representing sheet data.
* @return {type} Array the structured data.
*/
export default function groups (
tabName,
sheetName,
sheetId,
data,
label = 'groups'
) {
// Define Blueprint
const bp = R.clone(defaultBlueprint)
bp.sheet = {
name: sheetName,
id: sheetId
}
bp.name = tabName
// Column names define resources
export default (data) => {
const itemLabels = data[0]
const fmt = fmtObj(itemLabels)
bp.resources[label] = R.clone(defaultResource)
bp.resources[label].data = []
const output = []
const dataGroups = {}
data.forEach((row, idx) => {
@@ -45,12 +22,14 @@ export default function groups (
dataGroups[group].push(fmt(row))
}
})
Object.keys(dataGroups).forEach(groupKey => {
bp.resources[label].data.push({
group: groupKey,
group_label: dataGroups[groupKey][0].group_label,
data: dataGroups[groupKey]
Object.keys(dataGroups)
.forEach(groupKey => {
output.push({
group: groupKey,
group_label: dataGroups[groupKey][0].group_label,
data: dataGroups[groupKey]
})
})
})
return bp
return output
}

View File

@@ -1,42 +1,22 @@
import R from 'ramda'
import { fmtObj } from '../lib/util'
import { defaultBlueprint, defaultResource } from '../lib/blueprinters'
/**
* 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.
* Each resource item is an object with values labelled according to column
* names. Items are inserted in the data list at idx = id.
* Very similar to the rows blueprinter, but inserts each row as a value in
* an object, where the value in the 'id' column of the row will be used as
* the search key
*
* @param {type} data list of lists representing sheet data.
* @param {type} label="ids" name of resource in blueprint.
* @param {type} name="" name of blueprint.
* @return {type} Blueprint
* @param {type} data list of lists representing sheet data.
* @return {type} Object the structured data.
*/
export default function ids (
tabName,
sheetName,
sheetId,
data,
label = 'ids'
) {
// Define Blueprint
const bp = R.clone(defaultBlueprint)
bp.sheet = {
name: sheetName,
id: sheetId
}
bp.name = tabName
// Column names define resources
export default (data) => {
const itemLabels = data[0]
const fmt = fmtObj(itemLabels)
bp.resources[label] = R.clone(defaultResource)
bp.resources[label].data = []
const output = {}
data.forEach((row, idx) => {
if (idx === 0) return
bp.resources[label].data[fmt(row).id] = fmt(row)
output[fmt(row).id] = fmt(row)
})
return bp
return output
}

View File

@@ -1,41 +1,21 @@
import R from 'ramda'
import { fmtObj } from '../lib/util'
import { defaultBlueprint, defaultResource } from '../lib/blueprinters'
/**
* 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
* item is an object with values labelled according to column names.
* Each resource item is an object with values labelled according
* to column names specified in the sheet's first row.
*
* @param {type} data list of lists representing sheet data.
* @param {type} label="rows" name of resource in blueprint.
* @param {type} name="" name of blueprint.
* @return {type} Blueprint
* @param {type} data list of lists representing sheet data.
* @return {type} Array the structured data.
*/
export default function rows (
tabName,
sheetName,
sheetId,
data,
label = 'rows'
) {
// Define Blueprint
const bp = R.clone(defaultBlueprint)
bp.sheet = {
name: sheetName,
id: sheetId
}
bp.name = tabName
// Column names define resources
export default (data) => {
const itemLabels = data[0]
const fmt = fmtObj(itemLabels)
bp.resources[label] = R.clone(defaultResource)
bp.resources[label].data = []
const output = []
data.forEach((row, idx) => {
if (idx === 0) return
bp.resources[label].data.push(fmt(row))
output.push(fmt(row))
})
return bp
return output
}

View File

@@ -1,38 +1,12 @@
import R from 'ramda'
import { defaultBlueprint, defaultResource } from '../lib/blueprinters'
/**
* 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.
* Each resource item is an object with values labelled according to column
* names. Items are inserted in the data list at idx = id.
* Each resource item is inserted into a tree. TODO: describe layout.
*
* @param {type} data list of lists representing sheet data.
* @param {type} label="groups" name of resource in blueprint.
* @param {type} name="" name of blueprint.
* @return {type} Blueprint
* @param {type} data list of lists representing sheet data.
* @return {type} Array the structured data.
*/
export default function tree (
tabName,
sheetName,
sheetId,
data,
label = 'tree'
) {
// Define Blueprint
const bp = R.clone(defaultBlueprint)
bp.sheet = {
name: sheetName,
id: sheetId
}
bp.name = tabName
// Column names define resources
bp.resources[label] = R.clone(defaultResource)
bp.resources[label].data = {}
export default (data) => {
const tree = {
key: 'tags',
key: '_root',
children: {}
}
@@ -62,6 +36,5 @@ export default function tree (
}
})
bp.resources[label].data = tree
return bp
return tree
}

12
src/config.js Normal file
View File

@@ -0,0 +1,12 @@
import { timemap } from './lib'
export default {
gsheets: [],
xlsx: [
{
name: 'timemap_data',
path: 'data/timemap_data.xlsx',
tabs: timemap.default
}
]
}

View File

@@ -1,13 +1,19 @@
export default {
errors: {
update: 'The server could not update. Check your API credentials and internet connection and try again.',
export: {
fileMissing: 'The server could not export. Check that you provided a file path to export to and try again.',
writeFailed: 'The server could not export the data to the file. There is an issue with the data format'
},
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.',
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]}' in this sheet.`,
noFragment: prts => `Fragment index does not exist`
noFragment: prts => `Fragment index does not exist`,
modelLayer: prts => `Something went wrong at the model layer`
},
success: {
update: 'All sheets updated'
update: 'All sheets updated',
export: dest => `All resources exported to the file: ${dest}`
}
}

View File

@@ -1,21 +0,0 @@
import BP from './lib/blueprinters'
export default {
port: 4040,
googleSheets: {
email: 'SOME_SERVICE_ACCOUNT_EMAIL',
privateKey: 'SOME_SERVICE_ACCOUNT_PRIVATE_KEY',
sheets: [
{
name: 'example',
id: '1UC7DkCFeUXHfpUxUGruExwFbP4pqVBdJLOKfo6wDDGk',
tabs: {
export_events: [BP.byId, BP.byRow],
export_categories: [BP.byGroup, BP.byRow],
export_sites: BP.byRow,
export_tags: BP.byTree
}
}
]
}
}

View File

@@ -1,12 +1,21 @@
import http from 'http'
import path from 'path'
import express from 'express'
import initialize from './initialize'
import middleware from './middleware'
import api from './api'
import config from './config'
import dotenv from 'dotenv'
const hbs = require('express-handlebars')
dotenv.config()
let app = express()
app.server = http.createServer(app)
app.engine('.hbs', hbs({
extname: '.hbs',
defaultLayout: 'default'
}))
app.set('view engine', '.hbs')
// enable cross origin requests explicitly in development
if (process.env.NODE_ENV === 'development') {
@@ -15,6 +24,8 @@ if (process.env.NODE_ENV === 'development') {
app.use(cors())
}
const config = process.env
initialize(controller => {
app.use(
middleware({
@@ -30,8 +41,11 @@ initialize(controller => {
})
)
app.server.listen(process.env.PORT || config.port, () => {
console.log(`Started on port ${app.server.address().port}`)
app.use(express.static(path.join(__dirname, 'public')))
app.server.listen(process.env.PORT || 4040, () => {
console.log('===========================================')
console.log(`Server running on port ${app.server.address().port}`)
})
})

View File

@@ -1,37 +1,43 @@
import StoreJson from './models/StoreJson'
import Fetcher from './lib/Fetcher'
import fetchers from './lib/Fetcher'
import Controller from './lib/Controller'
import config from './config'
import R from 'ramda'
const { googleSheets } = config
const { sheets, privateKey, email } = googleSheets
const isntNull = n => n !== null
const filterNull = ls => R.filter(isntNull, ls)
const flattenfilterNull = ls => filterNull(R.flatten(ls))
let themFetchers
function authenticate (_fetcher) {
return _fetcher.fetcher.authenticate(email, privateKey).then(msg => {
console.log(msg)
return true
})
let config
try {
config = require('./local.config.js').default
} catch (_) {
config = require('./config.js').default
}
export default callback => {
const fetchers = sheets.map(sheet => {
return {
name: sheet.name,
fetcher: new Fetcher(new StoreJson(), sheet.name, sheet.id, sheet.tabs)
}
})
Promise.all(fetchers.map(authenticate))
.then(() => {
console.log(`===================`)
console.log(`grant access to: ${email}`)
console.log(`===================`)
// NB: reformat fetchers as config for controller
const config = {}
fetchers.forEach(fetcher => {
config[fetcher.name] = fetcher.fetcher
return Promise.resolve().then(() => {
return Object.keys(config).map(fType => {
// skip config attrs that don't have corresponding fetchers
if (!(fType in fetchers)) return null
const FFetcher = fetchers[fType]
return config[fType].map(sheet => {
const otherArgs = { ...sheet }
delete otherArgs.name
delete otherArgs.tabs
return {
name: sheet.name,
fetcher: new FFetcher(new StoreJson(), sheet.name, sheet.tabs, ...Object.values(otherArgs))
}
})
})
})
.then(res => {
themFetchers = flattenfilterNull(res)
})
.then(() => Promise.all(themFetchers.map(f => f.fetcher.authenticate(process.env))))
.then(fetchers => {
const config = R.zipObj(themFetchers.map(f => f.name), fetchers)
const controller = new Controller(config)
callback(controller)
})
@@ -39,7 +45,7 @@ export default callback => {
console.log(err)
console.log(
`ERROR: the server couldn't connect to all of the sheets you provided. Ensure you have granted access to ${
email
process.env.SERVICE_ACCOUNT_EMAIL
} on ALL listed sheets.`
)
})

17
src/lib.js Normal file
View File

@@ -0,0 +1,17 @@
import BP from './lib/blueprinters'
function prefixedTabs (prefix, cfg) {
if (!cfg) cfg = {}
const prf = key => cfg[key] ? `${prefix}_` : ''
return {
[`${prf('events')}export_events`]: BP.deeprows,
[`${prf('associations')}export_associations`]: BP.deeprows,
[`${prf('sources')}export_sources`]: BP.deepids,
[`${prf('sites')}export_sites`]: BP.rows
}
}
export const timemap = {
default: prefixedTabs(),
prefixedTabs
}

View File

@@ -1,4 +1,5 @@
import copy from '../copy/en'
import { exportToFile } from '../lib/util'
/**
* Controller
@@ -16,16 +17,22 @@ class Controller {
blueprints () {
return Object.keys(this.fetchers).map(
sheet => this.fetchers[sheet].blueprints
)
).reduce((acc, curr) => acc.concat(curr))
}
rebuildBlueprintsAsync () {
Object.values(this.fetchers).forEach(t => t._buildBlueprintsAsync())
}
update () {
const me = this
return Promise.all(
Object.keys(this.fetchers).map(sheet => {
return this.fetchers[sheet].update()
})
).then(results => {
if (results.every(r => r)) {
me.rebuildBlueprintsAsync()
return copy.success.update
} else {
throw new Error(copy.errors.update)
@@ -33,6 +40,37 @@ class Controller {
})
}
// Controller function to retrieve all blueprints and export to user defined file location
retrieveAll (fileDest) {
if (!fileDest) return Promise.reject(new Error(copy.errors.export.fileMissing))
const indexedData = {}
const urls = []
const bps = this.blueprints()
return Promise.all(
bps.map(bp => {
const resource = Object.keys(bp.resources)[0]
urls.push(bp.urls[0])
return this.retrieve(bp.sheet.name, bp.name, resource)
})
).then(async results => {
if (results.every(res => res)) {
urls.forEach((item, idx) => {
indexedData[item] = results[idx]
})
try {
const message = await exportToFile(fileDest, indexedData)
return message
} catch (e) {
return Promise.reject(e)
}
} else {
throw new Error(copy.errors.export.writeFailed)
}
})
}
retrieve (sheet, tab, resource) {
if (this._sheetExists(sheet)) {
const fetcher = this.fetchers[sheet]

View File

@@ -1,45 +1,39 @@
// FetcherTwo class interfaces with Google Sheet, and saves to a specified db
import { google } from 'googleapis'
import R from 'ramda'
import { createHash } from 'crypto'
import { buildDesaturated } from './blueprinters'
import {
fmtName,
fmtBlueprinterTitles,
isFunction
} from './util'
import { createHash } from 'crypto'
import R from 'ramda'
/* GsheetFetcher deps */
import { google } from 'googleapis'
/* LocalFetcher deps */
import X from 'xlsx'
class Fetcher {
constructor (db, sheetName, sheetId, blueprinters) {
constructor (db, name, bps) {
/*
* The database that the fetcher should use. This should be an instance of a model-compliant class.
* See models/Interface.js for the specifications for a model-compliant class.
*/
this.db = db
/*
* ID of the Google Sheet where the data is sheetd. Note that the privateKey.clientEmail
* loaded here must be added to the sheet as an editor.
*/
this.sheetId = sheetId
/*
* The name of the sheet. This will prefix tabs saved in the database.
*/
this.sheetName = sheetName
this.sheetName = name
/*
* A unique ID for the Fetcher to identify its elements in the model layer
*/
this.id = createHash('md5').update(sheetName).update(sheetId).digest('hex')
const bpsstring = Object.keys(bps).join(';')
this.id = createHash('md5').update(name).update(bpsstring).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.blueprinters = fmtBlueprinterTitles(bps)
/*
* This object is the canonical represenation for the data that a Fetcher
* proxies. When the fetcher is initialized, its model layer (db) is indexed,
@@ -49,13 +43,6 @@ class Fetcher {
*/
this.blueprints = null
this._buildBlueprintsAsync() // NB: modifies this.blueprints on completion
/*
* Google API setup
*/
this.API = google.sheets('v4')
this.auth = null
/** curry to allow convenient syntax with map */
this._saveViaBlueprinter = R.curry(this._saveViaBlueprinter.bind(this))
}
@@ -66,20 +53,33 @@ class Fetcher {
const allParts = allUrls.reduce((acc, url) => {
if (url.startsWith(this.id)) {
const parts = url.split('/')
acc.push([ parts[1], parts[2] ])
return acc
} else {
return acc
let duplicateTab = acc.reduce((tabFound, p) => {
return tabFound || p[0] === parts[1]
}, false)
if (duplicateTab) {
acc.forEach(p => {
if (p[0] === parts[1]) {
p[1].push(parts[2])
}
})
} else {
acc.push([ parts[1], [ parts[2] ] ])
}
}
return acc
}, [])
return allParts
.map(parts => buildDesaturated(
this.sheetId,
this.sheetName,
parts[0],
parts[1]
))
.map(parts => {
const bp = buildDesaturated(
this.sheetId,
this.sheetName,
parts[0],
parts[1]
)
bp.urls = Object.keys(bp.resources).map(r => `/api/${bp.sheet.name}/${bp.name}/${r}`)
return bp
})
})
.then(res => {
this.blueprints = res
@@ -91,9 +91,9 @@ class Fetcher {
*/
_saveViaBlueprinter (tab, data, blueprinter) {
const saturatedBp = blueprinter(
tab,
this.sheetName,
this.sheetId,
this.sheetName,
tab,
data
)
@@ -104,20 +104,80 @@ class Fetcher {
)
}
save (_tab, data) {
const tab = fmtName(_tab)
if (Object.keys(this.blueprinters).indexOf(tab) > -1) {
const bpConfig = this.blueprinters[tab]
if (isFunction(bpConfig)) {
// if bpConfig specifies a single blueprinter
return this._saveViaBlueprinter(tab, data, bpConfig)
} else {
// if bpConfig specifies an array of blueprinters
return bpConfig.map(this._saveViaBlueprinter(tab, data))
}
} else {
// 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 = fmtName(tab)
const url = `${this.id}/${tab}/${resource}`
return this.db.load(url, this.blueprints[title])
}
retrieveFrag (tab, resource, frag) {
const title = fmtName(tab)
const url = `${this.id}/${tab}/${resource}/${frag || ''}`
return this.db.load(url, this.blueprints[title])
}
/** Run on startup. Should be overridden if explicit auth is required **/
authenticate (env) {
return Promise.resolve(this)
}
}
class GsheetFetcher extends Fetcher {
constructor (db, name, bps, sheetId) {
super(db, name, bps)
this.type = 'Google Sheet'
if (arguments.length < 4) throw Error('You must provide the sheet ID')
/*
* ID of the Google Sheet where the data is sheetd. Note that the privateKey.clientEmail
* loaded here must be added to the sheet as an editor.
*/
this.sheetId = sheetId
/*
* Google API setup
*/
this.API = google.sheets('v4')
this.auth = null
}
/** returns a Promise that resolves if access is granted to the account, and rejects otherwise. */
authenticate (clientEmail, privateKey) {
const googleAuth = new google.auth.JWT(clientEmail, null, privateKey, [
authenticate (env) {
const googleAuth = new google.auth.JWT(env.SERVICE_ACCOUNT_EMAIL, null, env.SERVICE_ACCOUNT_PRIVATE_KEY, [
'https://www.googleapis.com/auth/spreadsheets'
])
this.auth = googleAuth
const { sheetId } = this
const me = this
return new Promise((resolve, reject) => {
googleAuth.authorize(function (err) {
if (err) {
reject(err)
} else {
resolve(`Connected to ${sheetId}.`)
console.log(`Connected to ${me.sheetName}. (${me.type} with ID ${sheetId}).`)
console.log(` grant access to: ${process.env.SERVICE_ACCOUNT_EMAIL}`)
resolve(me)
}
})
})
@@ -159,39 +219,32 @@ class Fetcher {
.then(() => true)
.catch(() => false)
}
}
save (_tab, data) {
const tab = fmtName(_tab)
if (Object.keys(this.blueprinters).indexOf(tab) > -1) {
const bpConfig = this.blueprinters[tab]
if (isFunction(bpConfig)) {
// if bpConfig specifies a single blueprinter
return this._saveViaBlueprinter(tab, data, bpConfig)
} else {
// if bpConfig specifies an array of blueprinters
return bpConfig.map(this._saveViaBlueprinter(tab, data))
}
} else {
// NB: if a blueprinter is not specified for a tab,
// just skip it.
return true
}
class LocalFetcher extends Fetcher {
constructor (db, name, bps, path) {
super(db, name, bps)
this.path = path
this.update().then(res =>
console.log(`${res ? 'Successful' : 'Couldn\'t'} update ${name}`)
)
}
// NB: could combine these functions by checking kwargs length
retrieve (tab, resource) {
const title = fmtName(tab)
const url = `${this.id}/${tab}/${resource}`
return this.db.load(url, this.blueprints[title])
}
retrieveFrag (tab, resource, frag) {
const title = fmtName(tab)
const url = `${this.sheetName}/${tab}/${resource}/${frag}`
return this.db.load(url, this.blueprints[title])
update () {
const wb = X.readFile(this.path)
wb.SheetNames.forEach(name => {
const sh = wb.Sheets[name]
const csv = X.utils.sheet_to_csv(sh, { FS: '\t' })
const ll = csv.split('\n').map(line => line.split('\t'))
this.save(name, ll)
})
return Promise.resolve(true)
}
}
export default Fetcher
export default {
'gsheets': GsheetFetcher,
'xlsx': LocalFetcher,
'ods': LocalFetcher,
'local': LocalFetcher
}

View File

@@ -15,22 +15,39 @@ export const defaultResource = {
data: []
}
export function buildDesaturated (sheetId, sheetName, tab, resource) {
export function buildDesaturated (sheetId, sheetName, tab, resources) {
const bp = R.clone(defaultBlueprint)
bp.sheet.name = sheetName
bp.sheet.id = sheetId
bp.name = tab
bp.resources[resource] = null
bp.resources = resources.reduce((acc, r) => {
acc[r] = null
return acc
}, {})
return bp
}
const buildBlueprinter = R.curry((datafierName, datafier, sheetId, sheetName, tabName, data) => {
const bp = R.clone(defaultBlueprint)
bp.sheet = {
name: sheetName,
id: sheetId
}
bp.name = tabName
bp.resources[datafierName] = R.clone(defaultResource)
bp.resources[datafierName].data = datafier(data)
return bp
})
// import all default exports from 'blueprinters' folder
const allBps = {}
const REL_PATH_TO_BPS = '../blueprinters'
const normalizedPath = path.join(__dirname, REL_PATH_TO_BPS)
fs.readdirSync(normalizedPath).forEach(file => {
const bpName = file.replace('.js', '')
allBps[bpName] = require(`${REL_PATH_TO_BPS}/${file}`).default
const datafier = require(`${REL_PATH_TO_BPS}/${file}`).default
allBps[bpName] = buildBlueprinter(bpName, datafier)
})
// NB: revert to ES5 'module.exports' required to make blueprinters from

19
src/lib/errors.js Normal file
View File

@@ -0,0 +1,19 @@
import copy from '../copy/en'
export function modelLayerGeneric (parts) {
return new Error(copy.errors.modelLayer(parts))
}
export function noFragment (parts) {
return new Error(copy.errors.noFragment(parts))
}
export function noResource (parts) {
return new Error(copy.errors.noResource(parts))
}
export default {
modelLayerGeneric,
noFragment,
noResource
}

View File

@@ -1,4 +1,6 @@
import R from 'ramda'
import { promises as fs } from 'file-system'
import copy from '../copy/en'
/* eslint-disable */
String.prototype.replaceAll = function (search, replacement) {
@@ -14,6 +16,18 @@ function camelize (str) {
})
}
export async function exportToFile (fileDest, data) {
const stringifiedData = JSON.stringify(data, null, 2)
const filePath = `${fileDest}/export.json`
try {
await fs.writeFile(filePath, stringifiedData)
return copy.success.export(filePath)
} catch (err) {
throw new Error(copy.errors.export.writeFailed)
}
}
export const fmtObj = R.curry(
(
columnNames,
@@ -37,7 +51,8 @@ export const fmtObj = R.curry(
}
}
columnNames.forEach((columnName, idx) => {
obj[fmtColName(columnName)] = row[idx]
const value = row[idx] ? row[idx] : ''
obj[fmtColName(columnName)] = value
})
return obj
}

View File

@@ -1,5 +1,4 @@
import { Router } from 'express'
import { mapboxAccessToken } from '../config'
import morgan from 'morgan'
import mapbox from './mapbox'
@@ -10,8 +9,8 @@ export default ({ config, db }) => {
/* logging middleware */
routes.use(morgan('dev'))
if (mapboxAccessToken) {
routes.get('/mapbox/:z/:y/:x', mapbox(mapboxAccessToken))
if (process.env.MAPBOX_TOKEN) {
routes.get('/mapbox/:z/:y/:x', mapbox(process.env.MAPBOX_TOKEN))
}
return routes

View File

@@ -1,30 +1,41 @@
/* eslint-disable */
/**
* Model is a class whose sole responsibility is to save and load blueprints.
* It allows for different storage mechanisms for different kinds of blueprints.
* Model is a class whose sole responsibility is to save and load data through a custom URL format.
* As an interfacce, it allows for different storage mechanisms, and different scale/performance for different kinds of data.
*
* ERRORS:
* When a load function fails, it should throw either:
* 1. A __ error if the resource doesn't exist on that sheet/tab.
* 2. A __ error if a fragment lookup fails because it doesn't exist.
* 3. A __ error if something else goes wrong.
* 1. noResource(parts) if the resource doesn't exist on that sheet/tab.
* 2. noFragment(parts) if a fragment lookup fails because it doesn't exist.
* 3. modelLayerGeneric(parts) if something else goes wrong.
*
* This is a WIP layer. See StoreJson.js for an example in action.
*/
class Model {
/**
* save - save a Blueprint, using the information it contains.
*
* @param {type} blueprint the Blueprint to be saved.
* @return {type} Promise which returns True.
* Index the data stored by this model, returning a list of the available URLs.
* @return {Promise(boolean)} Unpacks to a list of available URLs if successful, throws an error otherwise.
*/
save (blueprint) {}
index () {}
/**
* Save data at a URL. The URL is in the format
* /:fetcherID/:tab/:resource
* Fetcher IDs must be unique, tabs and resources can be duplicated across
* different fetchers.
*
* @param {string} url - the URL at which to save the data.
* @param {object} data - the data to be saved.
* @return {Promise(boolean)} Unpacks to true if the update was successful, false if otherwise.
*/
save (url, data) {}
/**
* load - load a resource from a data model, using a Blueprint object as
* well as a REST-like URL of the format /:source/:tab/:resource.
*
* @param {type} url String that represents the path to resource.
* @param {type} blueprint Blueprint object (desaturated?).
* @return {type} Object containing the resource data.
* Load data from a URL, in the format
* /:fetcherID/:tab/:resource
*
* @param {string} url - the URL at which to load the data.
* @return {Promise(object)} a Promise that unpacks to the data retrieved. An error will be thrown if the URL is invalid.
*/
load (url, blueprint) {}
load (url) {}
}

View File

@@ -1,7 +1,7 @@
import fs from 'mz/fs'
import copy from '../copy/en'
import errors from '../lib/errors'
const STORAGE_DIRNAME = 'temp'
const STORAGE_DIRNAME = 'data'
function partsFromFilename (fname) {
const body = fname.slice(0, -5)
@@ -45,7 +45,7 @@ class StoreJson {
if (!isNaN(id) && id >= 0 && id < data.length) {
return data[id]
} else {
throw new Error(copy.errors.noFragment(parts))
throw errors.noFragment(parts)
}
} else {
// Do a lookup if fragment is included to filter a relevant item
@@ -53,12 +53,12 @@ class StoreJson {
if (!isNaN(index) && index >= 0 && index < data.length) {
return data.filter((vl, idx) => idx === index)[0]
} else {
throw new Error(copy.errors.noFragment(parts))
throw errors.noFragment(parts)
}
}
})
} else {
return Promise.reject(new Error(copy.errors.noResource(parts)))
return Promise.reject(errors.noResource(parts))
}
}

2
temp/.gitignore vendored
View File

@@ -1,2 +0,0 @@
*
!.gitignore

View File

@@ -1,12 +1,11 @@
import test from 'ava'
import R from 'ramda'
import {
defaultBlueprint,
defaultResource,
columns,
rows
defaultBlueprint
} from '../src/lib/blueprinters'
import rows from '../src/blueprinters/rows'
import deeprows from '../src/blueprinters/deeprows'
const egInput1 = [
['h1', 'h2', 'h3'],
[1, 2, 3],
@@ -25,41 +24,20 @@ test('defaultBlueprint exports', t => {
t.deepEqual(expected, defaultBlueprint)
})
test('columns blueprinter generates expected output', t => {
const actual = columns('eg ColumnBlueprint', 'egSheetName', 'egSheetId', egInput1)
const expected = R.clone(defaultBlueprint)
expected.name = 'eg ColumnBlueprint'
expected.sheet = {
id: 'egSheetId',
name: 'egSheetName'
}
expected.resources['h1'] = R.clone(defaultResource)
expected.resources['h1'].data = [1, 4]
expected.resources['h2'] = R.clone(defaultResource)
expected.resources['h2'].data = [2, 5]
expected.resources['h3'] = R.clone(defaultResource)
expected.resources['h3'].data = [3, 6]
test('rows blueprinter', t => {
const expected = [
{ h1: 1, h2: 2, h3: 3 },
{ h1: 4, h2: 5, h3: 6 }
]
const actual = rows(egInput1)
t.deepEqual(expected, actual)
})
test('rows blueprinter generates expected output', t => {
const actual = rows('egRowBlueprint', 'egSheetName', 'egSheetId', egInput1, 'items')
const expected = R.clone(defaultBlueprint)
expected.name = 'egRowBlueprint'
expected.sheet = {
id: 'egSheetId',
name: 'egSheetName'
}
expected.resources['items'] = R.clone(defaultResource)
expected.resources['items'].data = [{
h1: 1,
h2: 2,
h3: 3
},
{
h1: 4,
h2: 5,
h3: 6
}]
test('deeprows blueprinter', t => {
const expected = [
{ 'hs': [1, 2, 3] },
{ 'hs': [4, 5, 6] }
]
const actual = deeprows(egInput1)
t.deepEqual(expected, actual)
})

90
test/serverProcess.js Normal file
View File

@@ -0,0 +1,90 @@
import test from 'ava'
import fetch from 'node-fetch'
import childProcess from 'child_process'
const SERVER_LAUNCH_WAIT_TIME = 10 * 1000
const SERVER_ROOT = 'http://localhost:4040'
let serverProc = null
let serverExited = false
function checkStatus (res) {
if (res.ok) {
return res
} else {
throw new Error('Route is not present')
}
}
/* SETUP: launch a development server with a wait time */
test.before.cb(t => {
console.log('SETUP: launching server and updating...')
serverProc = childProcess.spawn('yarn', ['dev'], {
cwd: '.',
stdio: 'ignore'
})
serverProc.on('exit', function (code, signal) {
serverExited = true
})
function pingUpdate () {
const expected = {
success: 'All sheets updated'
}
return fetch(`${SERVER_ROOT}/api/update`)
.then(checkStatus)
.then(res => res.json())
.then(json => {
t.deepEqual(json, expected)
t.end()
})
}
setTimeout(pingUpdate, SERVER_LAUNCH_WAIT_TIME)
})
/* CLEANUP: kill the server */
test.after(function () {
console.log('killing server...')
serverProc.kill('SIGKILL')
})
test('should launch', t => {
t.false(serverExited)
})
const passUrls = [
'/api/'
]
const failUrls = [
// /:sheet
'/api/example',
// /:sheet/:tab
'/api/example/events'
]
passUrls.forEach(function (url) {
test(`should respond successfully to request for ${url}`, t => {
return fetch(`${SERVER_ROOT}${url}`)
.then(checkStatus)
.then(res => res.json())
.then(json => {
console.info('JSON: ', json)
t.pass()
})
})
})
failUrls.forEach(function (url) {
test(`should respond with 404 for ${url}`, t => {
return fetch(`${SERVER_ROOT}${url}`)
.then(res => {
if (!res.ok) {
t.pass()
} else {
t.fail()
}
})
})
})

91
views/blueprints.hbs Normal file
View File

@@ -0,0 +1,91 @@
<h1>Available Endpoints</h1>
<hr>
<div class="main-container">
{{#each bps}}
<div class="blueprint-container">
<div class="bp-header">
<div class="bp-tab">{{ tab }}</div>
<div class="bp-source">{{ source }}</div>
</div>
{{#each urls}}
<div><a target="_blank" href="http://localhost:4040{{ this }}">{{ this }}</a></div>
{{/each}}
</div>
{{ else }}
<div>No endpoints found. Have you updated?</div>
{{/each}}
</div>
<a class="bp-update-container" target="_blank" href="http://localhost:4040/api/update">
<div class="bp-button">Update</div>
</div>
<style>
:root {
--grey: #8a8a8a;
--btn-width: 200px;
}
.main-container {
display: flex;
flex-wrap: wrap;
}
.blueprint-container {
display: flex;
flex: 1 0 31%;
flex-direction: column;
border: 1px solid var(--grey);
padding: 1em;
margin: 1em;
}
.bp-header {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.bp-tab {
font-size: 24pt;
}
.bp-source {
color: var(--grey);
}
.bp-update-container {
position: fixed;
top: 2px;
right: 2em;
display: flex;
justify-content: center;
align-items: center;
}
.bp-button {
transition: all 0.3s ease;
text-transform: uppercase;
font-size: 24pt;
font-weight: bold;
width: var(--btn-width);
display: flex;
justify-content: center;
align-items: center;
border: 3px solid black;
padding: .5em;
text-decoration: none;
text-underline: none;
color: black;
}
.bp-update-container:hover {
background-color: black;
transition: all 0.3s ease;
cursor: pointer;
}
.bp-update-container:hover div {
color: white;
}
</style>

15
views/layouts/default.hbs Normal file
View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Datasheet Server</title>
<meta name="HandheldFriendly" content="True" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
</head>
<body>
<div class="container">
{{{body}}}
</div>
</body>
</html>

4361
yarn.lock

File diff suppressed because it is too large Load Diff