Skip to content

Commit

Permalink
Merge pull request #543 from OneArmyWorld/master
Browse files Browse the repository at this point in the history
WIP backend refactor & v2 migration test
  • Loading branch information
chrismclarke authored Aug 20, 2019
2 parents 6106242 + d06838e commit a556c3a
Show file tree
Hide file tree
Showing 46 changed files with 1,463 additions and 825 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ You can find useful links to learn more about these technologies [in the resourc
- Run the component documentation
`yarn storybook`

We use [BrowserStack](https://www.browserstack.com/) to test our platform on multiple devices and browsers.
Note: Builds are currently tested on Chrome/Firefox. If your browser is not
supported, then consider contributing.

Expand Down
19 changes: 16 additions & 3 deletions functions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,22 @@ Simply make a PR and once approved the function will be deployed

## Testing locally

If the code is built you can run firebase serve from the main repo and the functions will also be made available. More info: https://firebase.google.com/docs/functions/local-emulator

Note, this will require authentication for the firebase project. You can request to be added to the project from any of the admins.
```
cd functions
npm run start
```

This spins up concurrent tasks to build and watch for changes to the typescript code, and run
the firebase emulator which will hot-reload when the compiled code changes. This combination
should mean that changes to functions can be tested locally, via the given endpoint, e.g.
`http://localhost:5001/precious-plastics-v4-dev/us-central1/api`
More info: https://firebase.google.com/docs/functions/local-emulator

It is recommended that you use a good API testing software, such as [Postman](https://www.getpostman.com/) or [Insomnia](https://insomnia.rest/) for this

Note, this will require authentication for the firebase project. You can request to be added to the project from any of the admins. Once authenticated, you can login to firebase within your own console
and the relevant config will automatically be made available
(viewable with command `firebase functions:config:get`)

This also only works for specific triggers (namely the api endpoints). If you want to
test a functions triggered in other ways you may first want to create an api endpoint
Expand Down
18 changes: 11 additions & 7 deletions functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,24 @@
"scripts": {
"lint": "tslint --project tsconfig.json",
"build": "tsc",
"watch": "./node_modules/.bin/tsc --watch",
"copyDevConfig": "firebase functions:config:get > .runtimeconfig.json",
"copyDevConfigWin": "firebase functions:config:get | ac .runtimeconfig.json",
"serve": "npm run copyDevConfig && npm run build && firebase serve --only functions",
"serve": "concurrently --kill-others \"npm run watch\" \"firebase emulators:start\"",
"shell": "npm run build && firebase functions:shell",
"deploy:dev": "firebase use default && firebase deploy --only functions",
"start": "npm run shell",
"start": "npm run copyDevConfig && npm run serve",
"logs": "firebase functions:log"
},
"main": "lib/index.js",
"main": "./lib/functions/src/index.js",
"dependencies": {
"axios": "^0.18.1",
"body-parser": "^1.18.3",
"cors": "^2.8.5",
"dateformat": "^3.0.3",
"express": "^4.16.4",
"firebase-admin": "7.2.0",
"firebase-functions": "2.2.1",
"firebase-admin": "8.3.0",
"firebase-functions": "^3.2.0",
"fs-extra": "^7.0.1",
"google-auth-library": "^2.0.1",
"googleapis": "^35.0.0",
Expand All @@ -29,10 +31,12 @@
"devDependencies": {
"@types/axios": "^0.14.0",
"@types/cors": "^2.8.5",
"@types/dateformat": "^3.0.0",
"@types/fs-extra": "^5.0.5",
"@types/sharp": "^0.22.1",
"tslint": "5.15.0",
"typescript": "3.2.2"
"concurrently": "^4.1.1",
"tslint": "^5.12.0",
"typescript": "^3.2.2"
},
"engines": {
"node": "8"
Expand Down
48 changes: 26 additions & 22 deletions functions/src/Firebase/databaseBackup.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,49 @@
import Axios from 'axios'
import { getAccessToken } from '../Utils/auth.utils'
import { config } from 'firebase-functions'
import { SERVICE_ACCOUNT_CONFIG } from '../config/config'
import * as google from 'google-auth-library'
import * as dateformat from 'dateformat'

/* Cloud function to automatically backup the firebase database adapted from:
https://thatweirddeveloper.com/how-to-back-up-cloud-firestore-periodically
More examples at:
https://firebase.google.com/docs/firestore/solutions/schedule-export
Note this requires use of a service account to access drive storage,
which is accessed from environment variables
*/

// rest reference: https://cloud.google.com/firestore/docs/reference/rest/v1beta2/projects.databases/exportDocuments
export const BackupDatabase = async () => {
console.log('executing database backup')
const scopes = [
'https://www.googleapis.com/auth/datastore',
'https://www.googleapis.com/auth/cloud-platform',
]
const accessToken = await getAccessToken(scopes)
// TODO - no longer using util auth, should revise code to see if still relevant or not
const auth = await google.auth.getClient({
scopes: ['https://www.googleapis.com/auth/datastore'],
})
const accessTokenResponse = await auth.getAccessToken()
const accessToken = accessTokenResponse.token
const headers = {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + accessToken,
}

console.log('access token received', accessToken)
const PROJECT_ID = config().project_id
console.log('project id', PROJECT_ID)
const PROJECT_ID = SERVICE_ACCOUNT_CONFIG.project_id

const url = `https://firestore.googleapis.com/v1beta1/projects/${PROJECT_ID}/databases/(default):exportDocuments`
// use axios to send post request as promise
console.log('posting', url)
const timestamp: string = new Date().toString()
let res
const timestamp = dateformat(Date.now(), 'yyyy-mm-dd-HH-MM-ss')
const body = {
outputUriPrefix: `gs://${PROJECT_ID}.appspot.com/backups/${timestamp}`,
}
try {
res = await Axios({
url: url,
method: 'post',
headers: {
Authorization: `Bearer ${accessToken}`,
},
data: {
outputUriPrefix: `gs://${PROJECT_ID}.appspot.com/backups/${timestamp}`,
},
})
const response = await Axios.post(url, body, { headers: headers })
return { status: 200, message: response.data }
} catch (error) {
res = 404
return { status: 500, message: error.response.data }
}
return res
}

// const createSub = async () => {
Expand Down
Empty file removed functions/src/Firebase/test.json
Empty file.
50 changes: 0 additions & 50 deletions functions/src/exports/_deprecated.ts

This file was deleted.

7 changes: 5 additions & 2 deletions functions/src/exports/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as functions from 'firebase-functions'
import * as bodyParser from 'body-parser'
import * as cors from 'cors'
import * as express from 'express'
import { upgradeDBAll } from '../upgrade/dbV1Upgrade'

console.log('api init')
const app = express()
Expand Down Expand Up @@ -40,8 +41,10 @@ app.all('*', async (req, res, next) => {
// *** NOTE currently all request types handled the same, i.e. GET/POST
// will likely change behaviour in future when required
switch (endpoint) {
case 'avatar':
console.log('avatar test')
case 'dbV1Upgrade':
console.log('upgrading db v1')
const upgradeStatus = await upgradeDBAll()
res.send(upgradeStatus)
break
default:
res.send('invalid api endpoint')
Expand Down
5 changes: 3 additions & 2 deletions functions/src/exports/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ Add/change schedule from `./functions-cron/appengine/cron.yaml`

import * as functions from 'firebase-functions'
import DHSite from '../DaveHakkensNL'
import { BackupDatabase } from '../Firebase/databaseBackup'

export const weeklyTasks = functions.pubsub
.topic('weekly-tick')
.onPublish(async (message, context) => {
console.log('weekly tick', message, context)
// await BackupDatabase()
console.log('backup complete')
const backupStatus = await BackupDatabase()
console.log(backupStatus)
})

export const dailyTasks = functions.pubsub
Expand Down
7 changes: 7 additions & 0 deletions functions/src/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// tslint:disable no-implicit-dependencies
// Models can be imported from the main package for use here
// NOTE 1 - this requires adjustment main src in package.json
// NOTE 2 - shorthand @OAModels notation defined in tsconfig
import { IDBEndpoint, IDbDoc } from '@OAModels/common.models'

export { IDBEndpoint, IDbDoc }
94 changes: 94 additions & 0 deletions functions/src/upgrade/dbV1Upgrade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { db } from '../Firebase/firestoreDB'

/* Temporary function used to migrate from db V1 docs to V2
The breaking change comes from converting timestamps to strings
(explained here: https://github.com/OneArmyWorld/onearmy/issues/343 )
*/

const mappings: DBMapping = {
discussions: 'v2_discussions',
eventsV1: 'v2_events',
howtosV1: 'v2_howtos',
tagsV1: 'v2_tags',
users: 'v2_users',
}

export const upgradeDBAll = async () => {
const promises = Object.keys(mappings).map(async endpoint => {
const upgradedDocs = await upgradeDBEndpoint(endpoint)
return upgradedDocs
})
const updates = await Promise.all(promises)
console.log('upgrade complete')
return updates
}

const upgradeDBEndpoint = async (endpoint: string) => {
console.log('upgrading endpoint', endpoint)
const snap = await db.collection(endpoint).get()
const docs = snap.empty ? [] : snap.docs.map(d => d.data())
console.log(`|${endpoint}|: [${docs.length}] docs to upgrade`)
const batch = db.batch()
// remove deleted docs and upgrade
const v2Docs = docs.filter(d => !d._deleted).map(d => upgradeV1Doc(d))
const v2Endpoint = mappings[endpoint]
v2Docs.forEach(v2Doc => {
const ref = db.doc(`${v2Endpoint}/${v2Doc._id}`)
batch.set(ref, v2Doc)
})
console.log(`|${endpoint}|: upgrade ready`)
await batch.commit()
return v2Docs
}

function upgradeV1Doc(doc: any) {
try {
// upgrade timestamps on all docs
const metaFields = ['_created', '_modified']
metaFields.forEach(field => {
doc[field] = _upgradeDate(doc[field])
})
// upgrade other specific fields
const optionalFields = ['_lastResponse', 'year', 'date']
optionalFields.forEach(field => {
// ignore case where field set to null (discussions)
if (doc.hasOwnProperty(field) && doc[field]) {
doc[field] = _upgradeDate(doc[field])
}
})
return doc
} catch (error) {
console.log(doc)
throw new Error('unable to upgrade doc')
}
}

function _upgradeDate(date: any) {
if (typeof date === 'string') {
return new Date(date).toISOString()
} else if (
date.hasOwnProperty('_seconds') &&
date.hasOwnProperty('_nanoseconds')
) {
const millis = date._seconds * 1000 + date._nanoseconds / 1e6
return new Date(millis).toISOString()
} else {
throw new Error(`no date upgrade method: ${JSON.stringify(date)}`)
}
}

type DBMapping = { [key in IDBEndpointV1]: IDBEndpointV2 }

type IDBEndpointV1 =
| 'howtosV1'
| 'users'
| 'discussions'
| 'tagsV1'
| 'eventsV1'

type IDBEndpointV2 =
| 'v2_howtos'
| 'v2_users'
| 'v2_discussions'
| 'v2_tags'
| 'v2_events'
8 changes: 7 additions & 1 deletion functions/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
"outDir": "lib",
"sourceMap": true,
"target": "es6",
"typeRoots": ["node_modules/@types"]
"jsx": "react",
"typeRoots": ["node_modules/@types"],
"baseUrl": "./",
"paths": {
"@OAModels/*": ["../src/models/*"]
},
"skipLibCheck": true
},
"include": ["src/**/*.ts", "spec/**/*.ts"]
}
10 changes: 2 additions & 8 deletions functions/tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"no-floating-promises": true,

// Do not allow any imports for modules that are not in package.json. These will almost certainly fail when
// deployed.
// deployed
"no-implicit-dependencies": true,

// The 'this' keyword can only be used inside of classes.
Expand All @@ -65,11 +65,8 @@
// Disallow control flow statements, such as return, continue, break, and throw in finally blocks.
"no-unsafe-finally": true,

// Do not allow variables to be used before they are declared.
"no-use-before-declare": true,

// Expressions must always return a value. Avoids common errors like const myValue = functionReturningVoid();
"no-void-expression": [true, "ignore-arrow-function-shorthand"],
"no-void-expression": [false, "ignore-arrow-function-shorthand"],

// Disallow duplicate imports in the same file.
"no-duplicate-imports": true,
Expand Down Expand Up @@ -106,9 +103,6 @@
// Warns if function overloads could be unified into a single function with optional or rest parameters.
"unified-signatures": { "severity": "warning" },

// Warns if code has an import or variable that is unused.
"no-unused-variable": { "severity": "warning" },

// Prefer const for values that will not change. This better documents code.
"prefer-const": { "severity": "warning" },

Expand Down
Loading

0 comments on commit a556c3a

Please sign in to comment.