Skip to content

Commit efeacdb

Browse files
committed
Merge branch 'william/project-structure'
2 parents ed5dc27 + de85977 commit efeacdb

26 files changed

+594
-233
lines changed

.gitignore

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1-
.idea
2-
node_modules
1+
# Build output:
2+
/lib/
3+
/logs/
4+
/serverConfig.json
5+
/pushServerConfig.json
36

4-
build
5-
serverConfig.json
6-
tsconfig.tsbuildinfo
7-
logs
7+
# Package managers:
8+
node_modules/
9+
npm-debug.log
10+
package-lock.json
11+
yarn-error.log
12+
13+
# Editors:
14+
.DS_Store
15+
.idea/
16+
.vscode/

README.md

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,54 @@
1-
edge-notifications
1+
# edge-push-server
2+
3+
This server sends push notifications to Edge client apps. It contains an HTTP server that clients can use to register for notifications, and a background process that checks for price changes and actually sends the messages.
4+
5+
## Setup
6+
7+
This server requires a working copies of Node.js, Yarn, PM2, and CouchDB. We also recommend using Caddy to terminate SSL connections.
8+
9+
### Set up logging
10+
11+
Run these commands as a server admin:
12+
13+
```sh
14+
touch /var/log/pushServer.log
15+
touch /var/log/priceDaemon.log
16+
chown edgy /var/log/pushServer.log /var/log/priceDaemon.log
17+
cp ./docs/logrotate /etc/logrotate.d/pushServer
18+
```
19+
20+
### Manage server using `pm2`
21+
22+
First, tell pm2 how to run the server script:
23+
24+
```sh
25+
# install:
26+
pm2 start pm2.json
27+
pm2 save
28+
29+
# check status:
30+
pm2 monit
31+
tail -f /var/log/pushServer.log
32+
tail -f /var/log/priceDaemon.log
33+
34+
# manage:
35+
pm2 reload pm2.json
36+
pm2 restart pm2.json
37+
pm2 stop pm2.json
38+
39+
pm2 restart pushServer // Just the HTTP server
40+
pm2 restart priceDaemon // Just the price checker
41+
```
42+
43+
### Updating
44+
45+
To update the code running on the production server, use the following procedure:
46+
47+
```sh
48+
git pull
49+
yarn
50+
yarn prepare
51+
pm2 restart pm2.json
52+
```
53+
54+
Each deployment should come with its own version bump, changelog update, and git tag.

docs/logrotate

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# /etc/logrotate.d/pushServer
2+
3+
/var/log/pushServer.log {
4+
copytruncate
5+
daily
6+
missingok
7+
notifempty
8+
rotate 10
9+
}
10+
11+
/var/log/priceDaemon.log {
12+
copytruncate
13+
daily
14+
missingok
15+
notifempty
16+
rotate 10
17+
}

package.json

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "edge-push-notifications",
2+
"name": "edge-push-server",
33
"version": "1.0.0",
44
"private": true,
55
"description": "",
@@ -8,15 +8,15 @@
88
"author": "",
99
"main": "build/index.js",
1010
"scripts": {
11-
"compile": "tsc",
12-
"fix": "npm run lint -- --fix",
13-
"lint": "eslint --ext .js,.ts .",
14-
"precommit": "lint-staged && tsc",
15-
"prepare": "husky install",
16-
"price:script:pm2": "pm2 start build/price-script/index.js --name price-script --log logs/price-script.log --time",
17-
"price:script": "node build/index.js",
18-
"server:pm2": "pm2 start build/server/index.js --name server --log logs/server.log --time",
19-
"server": "node build/server.js"
11+
"build": "sucrase -q -t typescript,imports -d ./lib ./src",
12+
"clean": "rimraf lib",
13+
"fix": "eslint . --fix",
14+
"lint": "eslint .",
15+
"precommit": "lint-staged && npm-run-all types prepare",
16+
"prepare": "husky install && npm-run-all clean build",
17+
"start-server": "node -r sucrase/register src/server/index.ts",
18+
"start-price": "node -r sucrase/register src/price-script/index.ts",
19+
"types": "tsc"
2020
},
2121
"lint-staged": {
2222
"*.{js,ts}": "eslint"
@@ -25,12 +25,13 @@
2525
"@pm2/io": "^4.3.5",
2626
"axios": "^0.21.2",
2727
"body-parser": "^1.19.0",
28+
"cleaner-config": "^0.1.7",
2829
"cleaners": "^0.3.12",
2930
"cors": "^2.8.5",
31+
"edge-server-tools": "^0.2.11",
3032
"express": "^4.17.1",
3133
"firebase-admin": "^8.12.1",
32-
"nano": "^9.0.5",
33-
"node-schedule": "^1.3.2"
34+
"nano": "^9.0.5"
3435
},
3536
"devDependencies": {
3637
"@types/cors": "^2.8.7",
@@ -49,8 +50,9 @@
4950
"eslint-plugin-standard": "^4.0.1",
5051
"husky": "^7.0.0",
5152
"lint-staged": "^10.4.0",
53+
"npm-run-all": "^4.1.5",
5254
"prettier": "^2.1.2",
53-
"ts-node": "^9.0.0",
55+
"sucrase": "^3.21.0",
5456
"typescript": "^4.7.3"
5557
}
5658
}

pm2.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"apps": [
3+
{
4+
"name": "pushServer",
5+
"script": "./lib/server/index.js",
6+
"out_file": "/var/log/pushServer.log"
7+
},
8+
{
9+
"name": "priceDaemon",
10+
"script": "./lib/price-script/index.js",
11+
"out_file": "/var/log/priceDaemon.log"
12+
}
13+
]
14+
}

serverConfig.json.sample

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/NotificationManager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import * as io from '@pm2/io'
2-
import * as admin from 'firebase-admin'
1+
import io from '@pm2/io'
2+
import admin from 'firebase-admin'
33

44
import { ApiKey } from './models'
55

src/couchSetup.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { asArray, asMaybe, asNumber, asObject, asString } from 'cleaners'
2+
import {
3+
asReplicatorSetupDocument,
4+
DatabaseSetup,
5+
setupDatabase,
6+
SetupDatabaseOptions,
7+
syncedDocument
8+
} from 'edge-server-tools'
9+
import { ServerScope } from 'nano'
10+
11+
import { serverConfig } from './serverConfig'
12+
13+
// ---------------------------------------------------------------------------
14+
// Synced documents
15+
// ---------------------------------------------------------------------------
16+
17+
/**
18+
* Live-updating server options stored in the `push-settings` database.
19+
*/
20+
const asSettings = asObject({
21+
apiKeys: asMaybe(
22+
asArray(
23+
asObject({
24+
name: asString,
25+
apiKey: asString
26+
})
27+
),
28+
[]
29+
),
30+
priceCheckInMinutes: asMaybe(asNumber, 5)
31+
})
32+
33+
export const syncedReplicators = syncedDocument(
34+
'replicators',
35+
asReplicatorSetupDocument
36+
)
37+
38+
export const syncedSettings = syncedDocument('settings', asSettings.withRest)
39+
40+
// ---------------------------------------------------------------------------
41+
// Databases
42+
// ---------------------------------------------------------------------------
43+
44+
export const settingsSetup: DatabaseSetup = {
45+
name: 'push-settings',
46+
syncedDocuments: [syncedReplicators, syncedSettings]
47+
}
48+
49+
const apiKeysSetup: DatabaseSetup = { name: 'db_api_keys' }
50+
51+
const thresholdsSetup: DatabaseSetup = { name: 'db_currency_thresholds' }
52+
53+
const devicesSetup: DatabaseSetup = { name: 'db_devices' }
54+
55+
const usersSetup: DatabaseSetup = {
56+
name: 'db_user_settings'
57+
// documents: {
58+
// '_design/filter': makeJsDesign('by-currency', ?),
59+
// '_design/map': makeJsDesign('currency-codes', ?)
60+
// }
61+
}
62+
63+
const defaultsSetup: DatabaseSetup = {
64+
name: 'defaults'
65+
// syncedDocuments: ['thresholds']
66+
}
67+
68+
// ---------------------------------------------------------------------------
69+
// Setup routine
70+
// ---------------------------------------------------------------------------
71+
72+
export async function setupDatabases(
73+
connection: ServerScope,
74+
disableWatching: boolean = false
75+
): Promise<void> {
76+
const { currentCluster } = serverConfig
77+
const options: SetupDatabaseOptions = {
78+
currentCluster,
79+
replicatorSetup: syncedReplicators,
80+
disableWatching
81+
}
82+
83+
await setupDatabase(connection, settingsSetup, options)
84+
await Promise.all([
85+
setupDatabase(connection, apiKeysSetup, options),
86+
setupDatabase(connection, thresholdsSetup, options),
87+
setupDatabase(connection, devicesSetup, options),
88+
setupDatabase(connection, usersSetup, options),
89+
setupDatabase(connection, defaultsSetup, options)
90+
])
91+
}

src/models/ApiKey.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import { asBoolean, asMap, asObject, asOptional, asString } from 'cleaners'
2-
import * as Nano from 'nano'
2+
import Nano from 'nano'
33

4+
import { serverConfig } from '../serverConfig'
45
import { Base } from '.'
56

6-
// eslint-disable-next-line @typescript-eslint/no-var-requires
7-
const CONFIG = require('../../serverConfig.json')
8-
9-
const nanoDb = Nano(CONFIG.dbFullpath)
7+
const nanoDb = Nano(serverConfig.couchUri)
108
const dbDevices = nanoDb.db.use('db_api_keys')
119

1210
const asApiKey = asObject({

src/models/CurrencyThreshold.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import { asBoolean, asMap, asNumber, asObject, asOptional } from 'cleaners'
2-
import * as Nano from 'nano'
2+
import Nano from 'nano'
33

4+
import { serverConfig } from '../serverConfig'
45
import { Base } from '.'
56
import { Defaults } from './Defaults'
67

7-
// eslint-disable-next-line @typescript-eslint/no-var-requires
8-
const CONFIG = require('../../serverConfig.json')
9-
10-
const nanoDb = Nano(CONFIG.dbFullpath)
8+
const nanoDb = Nano(serverConfig.couchUri)
119
const dbCurrencyThreshold = nanoDb.db.use('db_currency_thresholds')
1210

1311
const asThreshold = asObject({

src/models/Defaults.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import { asMap } from 'cleaners'
2-
import * as Nano from 'nano'
2+
import Nano from 'nano'
33

4+
import { serverConfig } from '../serverConfig'
45
import { Base } from '.'
56

6-
// eslint-disable-next-line @typescript-eslint/no-var-requires
7-
const CONFIG = require('../../serverConfig.json')
8-
9-
const nanoDb = Nano(CONFIG.dbFullpath)
7+
const nanoDb = Nano(serverConfig.couchUri)
108
const dbCurrencyThreshold = nanoDb.db.use('defaults')
119

1210
export class Defaults extends Base {

src/models/Device.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import { asNumber, asObject, asOptional, asString } from 'cleaners'
2-
import * as Nano from 'nano'
2+
import Nano from 'nano'
33

4+
import { serverConfig } from '../serverConfig'
45
import { Base } from '.'
56

6-
// eslint-disable-next-line @typescript-eslint/no-var-requires
7-
const CONFIG = require('../../serverConfig.json')
8-
9-
const nanoDb = Nano(CONFIG.dbFullpath)
7+
const nanoDb = Nano(serverConfig.couchUri)
108
const dbDevices = nanoDb.db.use<ReturnType<typeof asDevice>>('db_devices')
119

1210
const asDevice = asObject({

src/models/User.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import { asBoolean, asMap, asObject, asOptional } from 'cleaners'
2-
import * as Nano from 'nano'
2+
import Nano from 'nano'
33

4+
import { serverConfig } from '../serverConfig'
45
import { Base } from '.'
56
import { Device } from './Device'
67

7-
// eslint-disable-next-line @typescript-eslint/no-var-requires
8-
const CONFIG = require('../../serverConfig.json')
9-
10-
const nanoDb = Nano(CONFIG.dbFullpath)
8+
const nanoDb = Nano(serverConfig.couchUri)
119
const dbUserSettings =
1210
nanoDb.db.use<ReturnType<typeof asUser>>('db_user_settings')
1311

src/models/base.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { asObject, Cleaner } from 'cleaners'
2-
import * as Nano from 'nano'
2+
import Nano from 'nano'
33

44
const asModelData = asObject<Nano.MaybeDocument>({})
55

src/price-script/checkPriceChanges.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as io from '@pm2/io'
1+
import io from '@pm2/io'
22
import { MetricType } from '@pm2/io/build/main/services/metrics'
33

44
import { CurrencyThreshold, Device, User } from '../models'

src/price-script/fetchThresholdPrices.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import * as io from '@pm2/io'
2-
import Counter from '@pm2/io/build/main/utils/metrics/counter'
1+
import io from '@pm2/io'
32

43
import { CurrencyThreshold } from '../models'
54
import { NotificationPriceChange } from './checkPriceChanges'
65
import { getPrice } from './prices'
76

87
const SLEEP_TIMEOUT = 1000 // in milliseconds
98

9+
type Counter = ReturnType<typeof io.counter>
1010
const processMetrics: { [id: string]: Counter | undefined } = {}
1111

1212
async function sleep(ms = SLEEP_TIMEOUT) {

0 commit comments

Comments
 (0)