mirror of
https://github.com/bellingcat/datasheet-server.git
synced 2026-06-10 12:28:34 +03:00
Compare commits
2 Commits
main
...
feature/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5d922ef16 | ||
|
|
1971128b18 |
19
.github/workflows/cd.yml
vendored
19
.github/workflows/cd.yml
vendored
@@ -1,19 +0,0 @@
|
||||
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
25
.github/workflows/ci.yml
vendored
@@ -1,25 +0,0 @@
|
||||
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
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -14,5 +14,3 @@ tags
|
||||
tags.lock
|
||||
tags.temp
|
||||
src/config.js
|
||||
src/local.config.js
|
||||
src/config.js
|
||||
|
||||
14
.travis.yml
Normal file
14
.travis.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- stable
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
before_script:
|
||||
- npm install -g yarn
|
||||
install:
|
||||
- yarn
|
||||
script:
|
||||
- yarn build
|
||||
- yarn lint
|
||||
- yarn test
|
||||
10
Dockerfile
10
Dockerfile
@@ -4,17 +4,17 @@ LABEL authors="Lachlan Kermode <lk@forensic-architecture.org>"
|
||||
|
||||
# Install app dependencies
|
||||
COPY package.json /www/package.json
|
||||
RUN cd /www; npm install
|
||||
RUN cd /www; yarn
|
||||
|
||||
# Copy app source
|
||||
COPY . /www
|
||||
WORKDIR /www
|
||||
RUN npm run build
|
||||
RUN yarn build
|
||||
RUN mkdir -p data
|
||||
|
||||
# set your port
|
||||
ENV PORT 4040
|
||||
EXPOSE 4040
|
||||
ENV PORT 8080
|
||||
EXPOSE 8080
|
||||
|
||||
# start command as per package.json
|
||||
CMD ["npm", "start"]
|
||||
CMD ["yarn", "start"]
|
||||
|
||||
@@ -59,11 +59,6 @@ Clone the repository to your local:
|
||||
git clone https://www.github.com/forensic-architecture/datasheet-server
|
||||
```
|
||||
|
||||
Copy [src/example.config.js](src/example.config.js) into `src/config.js` and modify
|
||||
```
|
||||
cp src/example.config.js src/config.js
|
||||
```
|
||||
|
||||
Follow the steps in the [configuration](#configuration) section of this
|
||||
document.
|
||||
|
||||
|
||||
Binary file not shown.
11434
package-lock.json
generated
11434
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -26,15 +26,16 @@
|
||||
"express": "^4.13.3",
|
||||
"express-graphql": "^0.6.12",
|
||||
"express-handlebars": "^4.0.4",
|
||||
"googleapis": "^39.1.0",
|
||||
"googleapis": "^32.0.0",
|
||||
"graphql": "^0.13.2",
|
||||
"moment": "^2.27.0",
|
||||
"morgan": "^1.8.0",
|
||||
"mz": "^2.7.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-fetch": "^2.3.0",
|
||||
"node-xlsx": "^0.15.0",
|
||||
"object-hash": "^1.3.0",
|
||||
"ramda": "^0.25.0",
|
||||
"resource-router-middleware": "^0.6.0",
|
||||
"xlsx": "^0.16.8"
|
||||
"resource-router-middleware": "^0.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.1.2",
|
||||
|
||||
11
scripts/check-branch.sh
Executable file
11
scripts/check-branch.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
ENCRYPT_MESSAGE="\nThe .env.enc file has not changed its contents from the version on origin/develop.\nTo ensure that tests pass in Travis, you need to encrypt the contents of .env before pushing to the remote, so that the Travis server can use your service account credentials.\nEnsure that the Travis CLI is installed on your local, run\n\n\tnpm run travis-encrypt\n\nand then push to the remote again.\nIf you don't care whether your build passes on Travis, you can run:\n\n\tgit push --no-verify\n\nand bypass this check.\n\n"
|
||||
|
||||
# check whether .env.enc has changed
|
||||
if [ -z "`git diff origin/develop -- .env.enc`" ]; then
|
||||
echo $ENCRYPT_MESSAGE
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
38
scripts/encrypt.sh
Executable file
38
scripts/encrypt.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
echo "Encrypting .env file for Travis..."
|
||||
|
||||
# confirm that the user has the core repo as origin, and is therefore a maintainer
|
||||
# if [ -z `git config --get remote.origin.url | grep "forensic-architecture/datasheet-server"` ]; then
|
||||
# echo "Travis encryption not required for satellite contributors, continuing.."
|
||||
# exit 0
|
||||
# fi
|
||||
|
||||
# confirm travis is installed
|
||||
if [ ! hash travis 2>/dev/null ]; then
|
||||
echo "============================================================================================"
|
||||
echo "ERROR: Travis CLI is not installed on your local. Please install from:"
|
||||
echo "\thttps://github.com/travis-ci/travis.rb"
|
||||
echo "After installing, make sure that you login with:"
|
||||
echo "\ttravis login --pro"
|
||||
echo "============================================================================================"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# confirm there is a .env file to encrypt
|
||||
if [ ! -f .env ]; then
|
||||
echo "============================================================================================"
|
||||
echo "ERROR: You must create a .env file and add your credentials. See .env.example for an example"
|
||||
echo "============================================================================================"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# regex to match and delete 'before_install' and everything after it
|
||||
# necessary to delete these lines to get Travis to build for multiple accounts
|
||||
echo "creating new .travis.yml configuration"
|
||||
sed -i.old '/^before_install.*/,$ d' .travis.yml
|
||||
echo "old config file saved as .travis.yml.old"
|
||||
|
||||
travis encrypt-file .env --add --force --org
|
||||
git add .env.enc
|
||||
git add .travis.yml
|
||||
echo ".env.enc created and added to commit"
|
||||
@@ -30,10 +30,10 @@ export default ({ config, controller }) => {
|
||||
success: msg
|
||||
})
|
||||
)
|
||||
.catch(err => {
|
||||
.catch(err =>
|
||||
res.status(404)
|
||||
.send({ error: err.message, err })
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
api.get('/:sheet/:tab/:resource/:frag', (req, res) => {
|
||||
@@ -58,6 +58,17 @@ export default ({ config, controller }) => {
|
||||
)
|
||||
})
|
||||
|
||||
// api.get('/:sheet/:tab/:resource/validate', (req, res) => {
|
||||
// const { sheet, tab, resource } = req.params
|
||||
// controller
|
||||
// .retrieve(sheet, tab, resource)
|
||||
// .then(data => res.json(data))
|
||||
// .catch(err =>
|
||||
// res.status(err.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.
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fmtObj } from '../lib/util'
|
||||
import { getColumnValidation } from '../lib/validation'
|
||||
|
||||
/**
|
||||
* Each resource item is an object with values labelled according
|
||||
@@ -35,7 +36,6 @@ export default (data) => {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// generate the value for deep labels using the structure created
|
||||
data.forEach((row, idx) => {
|
||||
if (idx === 0) return
|
||||
@@ -61,6 +61,8 @@ export default (data) => {
|
||||
|
||||
// move values for flat labels over from base
|
||||
structure.__flat.forEach(label => {
|
||||
const validatedLabel = getColumnValidation(label, baseRow[label])
|
||||
console.log(validatedLabel)
|
||||
deepRow[label] = baseRow[label]
|
||||
})
|
||||
if (!Object.keys(deepRow).every(k => deepRow[k] === '')) {
|
||||
|
||||
18
src/config.js
Normal file
18
src/config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { timemap } from './lib'
|
||||
|
||||
export default {
|
||||
gsheets: [
|
||||
{
|
||||
name: 'us2020',
|
||||
id: '1I_pgyTQJlIorTIEHBxw1mM1STn-SrIi66FKYxut61iM',
|
||||
tabs: timemap.default
|
||||
}
|
||||
],
|
||||
xlsx: [
|
||||
{
|
||||
name: 'timemap_data',
|
||||
path: 'data/timemap_data.xlsx',
|
||||
tabs: timemap.default
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { timemap } from './lib'
|
||||
|
||||
export default {
|
||||
gsheets: [],
|
||||
xlsx: [
|
||||
{
|
||||
name: 'timemap_data',
|
||||
path: 'data/timemap_data.xlsx',
|
||||
tabs: timemap.default
|
||||
}
|
||||
],
|
||||
cors: {
|
||||
active: false,
|
||||
corsOptions: { // meaningless unless active=true
|
||||
origin: 'http://example.com',
|
||||
optionsSuccessStatus: 200 // some legacy browsers (IE11, various SmartTVs) choke on 204
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/index.js
19
src/index.js
@@ -1,5 +1,4 @@
|
||||
import http from 'http'
|
||||
import path from 'path'
|
||||
import express from 'express'
|
||||
import initialize from './initialize'
|
||||
import middleware from './middleware'
|
||||
@@ -7,14 +6,6 @@ import api from './api'
|
||||
import dotenv from 'dotenv'
|
||||
const hbs = require('express-handlebars')
|
||||
|
||||
|
||||
let configJS
|
||||
try {
|
||||
configJS = require('./local.config.js').default
|
||||
} catch (_) {
|
||||
configJS = require('./config.js').default
|
||||
}
|
||||
|
||||
dotenv.config()
|
||||
|
||||
let app = express()
|
||||
@@ -25,17 +16,13 @@ app.engine('.hbs', hbs({
|
||||
}))
|
||||
app.set('view engine', '.hbs')
|
||||
|
||||
// enable cross origin requests explicitly in development OR if active in prod
|
||||
const cors = require('cors')
|
||||
// enable cross origin requests explicitly in development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const cors = require('cors')
|
||||
console.log('Enabling CORS in development...')
|
||||
app.use(cors())
|
||||
} else if (configJS.cors.active === true) {
|
||||
console.log('Enabling CORS in from config.js in prod...')
|
||||
app.use(cors(configJS.cors.corsOptions))
|
||||
}
|
||||
|
||||
|
||||
const config = process.env
|
||||
|
||||
initialize(controller => {
|
||||
@@ -53,7 +40,7 @@ initialize(controller => {
|
||||
})
|
||||
)
|
||||
|
||||
app.use(express.static(path.join(__dirname, 'public')))
|
||||
app.use(express.static(__dirname + '/public'))
|
||||
|
||||
app.server.listen(process.env.PORT || 4040, () => {
|
||||
console.log('===========================================')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import StoreJson from './models/StoreJson'
|
||||
import fetchers from './lib/Fetcher'
|
||||
import Controller from './lib/Controller'
|
||||
import config from './config'
|
||||
import R from 'ramda'
|
||||
|
||||
const isntNull = n => n !== null
|
||||
@@ -8,13 +9,6 @@ const filterNull = ls => R.filter(isntNull, ls)
|
||||
const flattenfilterNull = ls => filterNull(R.flatten(ls))
|
||||
let themFetchers
|
||||
|
||||
let config
|
||||
try {
|
||||
config = require('./local.config.js').default
|
||||
} catch (_) {
|
||||
config = require('./config.js').default
|
||||
}
|
||||
|
||||
export default callback => {
|
||||
return Promise.resolve().then(() => {
|
||||
return Object.keys(config).map(fType => {
|
||||
|
||||
@@ -5,9 +5,10 @@ function prefixedTabs (prefix, cfg) {
|
||||
const prf = key => cfg[key] ? `${prefix}_` : ''
|
||||
return {
|
||||
[`${prf('events')}export_events`]: BP.deeprows,
|
||||
[`${prf('associations')}export_associations`]: BP.deeprows,
|
||||
[`${prf('categories')}export_categories`]: [BP.groups, BP.rows],
|
||||
[`${prf('filters')}export_filters`]: BP.tree,
|
||||
[`${prf('narratives')}export_narratives`]: BP.rows,
|
||||
[`${prf('sources')}export_sources`]: BP.deepids,
|
||||
[`${prf('shapes')}export_shapes`]: BP.deeprows,
|
||||
[`${prf('sites')}export_sites`]: BP.rows
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import copy from '../copy/en'
|
||||
|
||||
/**
|
||||
* Controller class
|
||||
* Controller
|
||||
*
|
||||
*/
|
||||
class Controller {
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import R from 'ramda'
|
||||
import { createHash } from 'crypto'
|
||||
// FetcherTwo class interfaces with Google Sheet, and saves to a specified db
|
||||
import { google } from 'googleapis'
|
||||
import { buildDesaturated } from './blueprinters'
|
||||
import {
|
||||
fmtName,
|
||||
fmtBlueprinterTitles,
|
||||
isFunction
|
||||
} from './util'
|
||||
|
||||
/* GsheetFetcher deps */
|
||||
import { google } from 'googleapis'
|
||||
/* LocalFetcher deps */
|
||||
import X from 'xlsx'
|
||||
import { createHash } from 'crypto'
|
||||
import R from 'ramda'
|
||||
import xlsx from 'node-xlsx'
|
||||
import fs from 'fs'
|
||||
|
||||
class Fetcher {
|
||||
constructor (db, name, bps) {
|
||||
@@ -139,6 +138,7 @@ class Fetcher {
|
||||
|
||||
/** Run on startup. Should be overridden if explicit auth is required **/
|
||||
authenticate (env) {
|
||||
console.log(`Connected to ${this.sheetName}. No explicit authentication required for ${this.type}s.`)
|
||||
return Promise.resolve(this)
|
||||
}
|
||||
}
|
||||
@@ -217,29 +217,31 @@ class GsheetFetcher extends Fetcher {
|
||||
})
|
||||
.then(this._buildBlueprintsAsync())
|
||||
.then(() => true)
|
||||
.catch((err) => {
|
||||
console.log(`Error fetching gsheets: ${err.message} `)
|
||||
return false
|
||||
})
|
||||
.catch(() => false)
|
||||
}
|
||||
}
|
||||
|
||||
class LocalFetcher extends Fetcher {
|
||||
class XlsxFetcher extends Fetcher {
|
||||
constructor (db, name, bps, path) {
|
||||
super(db, name, bps)
|
||||
this.type = 'XLSX File'
|
||||
this.path = path
|
||||
this.update().then(res =>
|
||||
console.log(`${res ? 'Successful' : 'Couldn\'t'} update ${name}`)
|
||||
)
|
||||
this.isRemote = false
|
||||
|
||||
if (this.path.startsWith('https')) {
|
||||
this.isRemote = true
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
const data = xlsx.parse(fs.readFileSync(this.path))
|
||||
data.forEach(tab => {
|
||||
const stringyData = tab.data.map(row =>
|
||||
row.map(d =>
|
||||
typeof (d) === 'number' ? d.toString() : d
|
||||
)
|
||||
)
|
||||
this.save(tab.name, stringyData)
|
||||
})
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
@@ -247,7 +249,5 @@ class LocalFetcher extends Fetcher {
|
||||
|
||||
export default {
|
||||
'gsheets': GsheetFetcher,
|
||||
'xlsx': LocalFetcher,
|
||||
'ods': LocalFetcher,
|
||||
'local': LocalFetcher
|
||||
'xlsx': XlsxFetcher
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import R from 'ramda'
|
||||
import R from 'ramda';
|
||||
|
||||
/* eslint-disable */
|
||||
String.prototype.replaceAll = function (search, replacement) {
|
||||
|
||||
20
src/lib/validation.js
Normal file
20
src/lib/validation.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import moment from 'moment';
|
||||
|
||||
const DATE_FORMAT = "MM/DD/YYYY";
|
||||
const TIME_REGEX = new RegExp("^([01]\d|2[0-3]):?([0-5]\d)$")
|
||||
|
||||
|
||||
export const getColumnValidation = (colName, value) => {
|
||||
switch(colName) {
|
||||
case 'longitude':
|
||||
return isFinite(value) && Math.abs(value) <= 180;
|
||||
case 'latitude':
|
||||
return isFinite(value) && Math.abs(value) <= 90;
|
||||
case 'date':
|
||||
return moment(value, DATE_FORMAT, true).isValid();
|
||||
case 'time':
|
||||
return TIME_REGEX.test(value);
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ class StoreJson {
|
||||
|
||||
save (url, data) {
|
||||
const parts = url.split('/')
|
||||
|
||||
return fs.writeFile(
|
||||
`${STORAGE_DIRNAME}/${parts[0]}__${parts[1]}__${parts[2]}.json`,
|
||||
JSON.stringify(data)
|
||||
|
||||
@@ -12,8 +12,6 @@ const egInput1 = [
|
||||
[4, 5, 6]
|
||||
]
|
||||
|
||||
// Test default blueprint exports
|
||||
// Smoke tests
|
||||
test('defaultBlueprint exports', t => {
|
||||
const expected = {
|
||||
sheet: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import fetch from 'node-fetch'
|
||||
import childProcess from 'child_process'
|
||||
|
||||
const SERVER_LAUNCH_WAIT_TIME = 10 * 1000
|
||||
const SERVER_ROOT = ''
|
||||
const SERVER_ROOT = 'http://localhost:4040'
|
||||
let serverProc = null
|
||||
let serverExited = false
|
||||
function checkStatus (res) {
|
||||
@@ -54,7 +54,14 @@ test('should launch', t => {
|
||||
})
|
||||
|
||||
const passUrls = [
|
||||
'/api/'
|
||||
// /
|
||||
'/api/',
|
||||
// /blueprints
|
||||
'/api/blueprints',
|
||||
// /:sheet/:tab/:resource
|
||||
'/api/example/export_events/rows',
|
||||
// /:sheet/:tab/:resource/:frag
|
||||
'/api/example/export_events/rows/1'
|
||||
]
|
||||
|
||||
const failUrls = [
|
||||
@@ -70,7 +77,6 @@ passUrls.forEach(function (url) {
|
||||
.then(checkStatus)
|
||||
.then(res => res.json())
|
||||
.then(json => {
|
||||
console.info('JSON: ', json)
|
||||
t.pass()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
<div class="bp-source">{{ source }}</div>
|
||||
</div>
|
||||
{{#each urls}}
|
||||
<div><a target="_blank" href="{{ this }}">{{ this }}</a></div>
|
||||
<div><a target="_blank" href="http://localhost:4040{{ this }}">{{ this }}</a></div>
|
||||
<div class="bp-validate-button" target="_blank" href="http://localhost:4040{{ this }}">Validate</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{ else }}
|
||||
@@ -16,7 +17,7 @@
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<a class="bp-update-container" target="_blank" href="/api/update">
|
||||
<a class="bp-update-container" target="_blank" href="http://localhost:4040/api/update">
|
||||
<div class="bp-button">Update</div>
|
||||
</div>
|
||||
|
||||
@@ -25,6 +26,7 @@
|
||||
:root {
|
||||
--grey: #8a8a8a;
|
||||
--btn-width: 200px;
|
||||
--validate-btn-width: 150px;
|
||||
}
|
||||
.main-container {
|
||||
display: flex;
|
||||
@@ -79,6 +81,21 @@
|
||||
color: black;
|
||||
}
|
||||
|
||||
.bp-validate-button {
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
font-size: 12pt;
|
||||
font-weight: bold;
|
||||
width: var(--validate-btn-width);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 3px solid red;
|
||||
padding: .5em;
|
||||
text-decoration: none;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.bp-update-container:hover {
|
||||
background-color: black;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
Reference in New Issue
Block a user