Skip to content

Commit

Permalink
Almost routes
Browse files Browse the repository at this point in the history
  • Loading branch information
swansontec committed Aug 2, 2022
1 parent 6cea6b1 commit d89f1fc
Show file tree
Hide file tree
Showing 11 changed files with 392 additions and 193 deletions.
2 changes: 1 addition & 1 deletion src/db/couchConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function makeLegacyConnection(connection: ServerScope): DbConnection {
},

async getDeviceById(deviceId: string): Promise<DeviceRow | undefined> {
return await getDeviceById(connection, deviceId)
return await getDeviceById(connection, deviceId, '', new Date())
},

async getEventsByDeviceId(deviceId: string): Promise<PushEventRow[]> {
Expand Down
40 changes: 27 additions & 13 deletions src/db/couchDevices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import {
import { ServerScope } from 'nano'

import { DeviceRow } from '../types/dbTypes'
import { asBase64 } from '../types/pushApiTypes'
import { Device, DeviceState } from '../types/pushTypes'
import { asBase64 } from '../types/pushCleaners'
import { Device, DeviceStatus } from '../types/pushTypes'

/**
* An API key, as stored in Couch.
Expand All @@ -26,11 +26,10 @@ export const asCouchDevice = asCouchDoc<Omit<Device, 'deviceId'>>(
asObject({
appId: asString,
created: asDate,
deviceId: asString,

deviceToken: asOptional(asString), // Current token for notifications.

enableLegacyPrices: asBoolean, // For legacy v1 API.
// Status:
deviceToken: asOptional(asString),
enableLegacyPrices: asBoolean,
rootLoginIds: asArray(asBase64),
visited: asDate
})
Expand All @@ -42,17 +41,32 @@ export const devicesSetup: DatabaseSetup = {
name: 'push-devices'
}

/**
* Looks up a device by its id.
* If the device does not exist in the database, creates a fresh row.
*/
export async function getDeviceById(
connection: ServerScope,
deviceId: string
): Promise<DeviceRow | undefined> {
deviceId: string,
appId: string,
date: Date
): Promise<DeviceRow> {
const db = connection.use(devicesSetup.name)
const raw = db.get(deviceId).catch(error => {
if (asMaybeNotFoundError(error) != null) return
throw error
})

if (raw == null) return
if (raw == null)
return makeDeviceRow(connection, {
doc: {
appId,
created: date,
enableLegacyPrices: false,
rootLoginIds: [],
visited: date
}
})
return makeDeviceRow(connection, raw)
}

Expand All @@ -65,16 +79,16 @@ function makeDeviceRow(connection: ServerScope, raw: unknown): DeviceRow {
return {
device,

async updateState(state: DeviceState): Promise<void> {
// TODO: Merge our state with the remote state
async updateStatus(status: Partial<DeviceStatus>): Promise<void> {
// TODO: Merge our status with the remote status
// date = Math.max(date1, date2), etc...

// Assume that the last document we fetched is still current:
let remote = base
while (true) {
// Write to the database:
const doc: CouchDevice = {
doc: { ...device, ...state },
doc: { ...device, ...status },
id: remote.id,
rev: remote.rev
}
Expand All @@ -85,7 +99,7 @@ function makeDeviceRow(connection: ServerScope, raw: unknown): DeviceRow {
// If that worked, the merged document is now the latest:
if (response?.ok === true) {
base = doc
Object.assign(device, state)
Object.assign(device, status)
return
}

Expand Down
49 changes: 21 additions & 28 deletions src/db/couchPushEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {
asArray,
asBoolean,
asDate,
asEither,
asNull,
asObject,
asOptional,
asString,
Expand All @@ -19,42 +21,33 @@ import { ServerScope } from 'nano'
import { base64 } from 'rfc4648'

import { PushEventRow } from '../types/dbTypes'
import { asBase64, asPushTrigger } from '../types/pushApiTypes'
import { PushEvent, PushEventState } from '../types/pushTypes'
import {
asBase64,
asPushEffect,
asPushEventState,
asPushTrigger
} from '../types/pushCleaners'
import { PushEvent, PushEventStatus } from '../types/pushTypes'

/**
* An API key, as stored in Couch.
* A push event, as stored in Couch.
*/
export const asCouchPushEvent = asCouchDoc<Omit<PushEvent, 'eventId'>>(
export const asCouchPushEvent = asCouchDoc<PushEvent>(
asObject({
// Identity:
created: asDate,
eventId: asString,
deviceId: asOptional(asString),
loginId: asOptional(asBase64),

// Event:
broadcast: asOptional(
asArray(
asObject({
pluginId: asString,
rawTx: asBase64
})
)
),
push: asOptional(
asObject({
title: asOptional(asString),
body: asOptional(asString),
data: asObject(asString)
})
),
effects: asArray(asPushEffect),
recurring: asBoolean,
trigger: asPushTrigger,

// State flags:
active: asBoolean, // Watch the trigger when true
triggered: asOptional(asDate),
broadcasted: asOptional(asDate),
pushed: asOptional(asDate)
// Status:
state: asPushEventState,
effectErrors: asOptional(asArray(asEither(asString, asNull))),
triggered: asOptional(asDate)
})
)
const wasCouchPushEvent = uncleaner(asCouchPushEvent)
Expand Down Expand Up @@ -217,7 +210,7 @@ function makePushEventRow(connection: ServerScope, raw: unknown): PushEventRow {
return {
event,

async updateState(state: PushEventState): Promise<void> {
async updateStatus(status: Partial<PushEventStatus>): Promise<void> {
// TODO: Merge our state with the remote state
// date = Math.max(date1, date2), etc...

Expand All @@ -226,7 +219,7 @@ function makePushEventRow(connection: ServerScope, raw: unknown): PushEventRow {
while (true) {
// Write to the database:
const doc: CouchPushEvent = {
doc: { ...event, ...state },
doc: { ...event, ...status },
id: remote.id,
rev: remote.rev
}
Expand All @@ -239,7 +232,7 @@ function makePushEventRow(connection: ServerScope, raw: unknown): PushEventRow {
// If that worked, the merged document is now the latest:
if (response?.ok === true) {
base = doc
Object.assign(event, state)
Object.assign(event, status)
return
}

Expand Down
48 changes: 48 additions & 0 deletions src/middleware/withDevice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { asMaybe } from 'cleaners'
import { Serverlet } from 'serverlet'

import { getApiKeyByKey } from '../db/couchApiKeys'
import { getDeviceById } from '../db/couchDevices'
import { asPushRequestBody } from '../types/pushApiTypes'
import { DbRequest, DeviceRequest } from '../types/requestTypes'
import { errorResponse } from '../types/responseTypes'

/**
* Parses the request payload and looks up the device.
* Legacy routes do not use this one.
*/
export const withDevice =
(server: Serverlet<DeviceRequest>): Serverlet<DbRequest> =>
async request => {
const { connection, date, log, req } = request

// Parse the common request body:
const body = asMaybe(asPushRequestBody)(req.body)
if (body == null) {
return errorResponse('Bad request body', { status: 400 })
}

// Look up the key in the database:
const apiKey = await log.debugTime(
'getApiKeyByKey',
getApiKeyByKey(connection, body.apiKey)
)
if (apiKey == null) {
return errorResponse('Incorrect API key', { status: 401 })
}

// Look up the device in the database:
const deviceRow = await log.debugTime(
'getDeviceById',
getDeviceById(connection, body.deviceId, body.appId, date)
)

// Pass that along:
return await server({
...request,
apiKey,
appId: body.appId,
deviceRow,
payload: body.data
})
}
91 changes: 70 additions & 21 deletions src/routes/deviceRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,87 @@
import { asMaybe, uncleaner } from 'cleaners'
import { ServerScope } from 'nano'
import { Serverlet } from 'serverlet'

import { PushEvent } from '../types/pushTypes'
import { ApiRequest } from '../types/requestTypes'
import { jsonResponse } from '../types/responseTypes'
import { getEventsByDeviceId } from '../db/couchPushEvents'
import {
asDeviceCheckinPayload,
asDevicePayload,
asDeviceUpdatePayload
} from '../types/pushApiTypes'
import { Device } from '../types/pushTypes'
import { DeviceRequest } from '../types/requestTypes'
import { errorResponse, jsonResponse } from '../types/responseTypes'

const wasDevicePayload = uncleaner(asDevicePayload)
type DevicePayload = ReturnType<typeof asDevicePayload>

/**
* POST /v2/device
*/
export const deviceFetchRoute: Serverlet<ApiRequest> = async request => {
return jsonResponse({})
}
export interface DevicePayload {
deviceId: string
deviceToken: string
events: PushEvent[]
rootLoginIds: Uint8Array[] // asArray(asBase64)
created: Date
visited: Date
export const deviceFetchRoute: Serverlet<DeviceRequest> = async request => {
const { connection, deviceRow } = request

return jsonResponse(
wasDevicePayload(await makeDevicePayload(connection, deviceRow.device))
)
}

/**
* POST /v2/device/checkin
*/
export const deviceCheckinRoute: Serverlet<ApiRequest> = async request => {
return jsonResponse({})
export const deviceCheckinRoute: Serverlet<DeviceRequest> = async request => {
const { connection, date, deviceRow, payload } = request

const clean = asMaybe(asDeviceCheckinPayload)(payload)
if (clean == null) {
return errorResponse('Incorrect device checkin payload', { status: 400 })
}

await deviceRow.updateStatus({
deviceToken: clean.deviceToken,
visited: date
})

return jsonResponse(
wasDevicePayload(await makeDevicePayload(connection, deviceRow.device))
)
}

/**
* POST /v2/device/update
*/
export const deviceUpdateRoute: Serverlet<ApiRequest> = async request => {
return jsonResponse({})
export const deviceUpdateRoute: Serverlet<DeviceRequest> = async request => {
const { connection, date, deviceRow, payload } = request

const clean = asMaybe(asDeviceUpdatePayload)(payload)
if (clean == null) {
return errorResponse('Incorrect device update payload', { status: 400 })
}

await deviceRow.updateStatus({
deviceToken: clean.deviceToken,
rootLoginIds: clean.rootLoginIds,
visited: date
})

// TODO:
// clean.createEvents
// clean.removeEvents

return jsonResponse(
wasDevicePayload(await makeDevicePayload(connection, deviceRow.device))
)
}
export interface DeviceUpdatePayload {
rootLoginIds: Uint8Array[] // asArray(asBase64)
events: PushEvent[]
deviceToken: string

async function makeDevicePayload(
connection: ServerScope,
device: Device
): Promise<DevicePayload> {
const eventRows = await getEventsByDeviceId(connection, device.deviceId)

return {
deviceToken: device.deviceToken,
rootLoginIds: device.rootLoginIds,
events: eventRows.map(row => row.event)
}
}
Loading

0 comments on commit d89f1fc

Please sign in to comment.