From 9aadf8e0233654b414cc88e82f8c12c92a9206f6 Mon Sep 17 00:00:00 2001 From: Alex Mikhalev Date: Fri, 13 Jul 2018 00:04:59 -0600 Subject: [PATCH] Started adding some device stuff for database --- app/components/DeviceView.tsx | 26 ++- ...bsocketClient.ts => WebSocketRpcClient.ts} | 2 +- app/state/AppState.ts | 15 +- common/sprinklersRpc/ConnectionState.ts | 14 +- common/sprinklersRpc/SprinklersDevice.ts | 2 +- common/sprinklersRpc/mqtt/index.ts | 13 +- server/models/Database.ts | 14 ++ server/models/SprinklersDevice.ts | 152 ++++++++++++++++++ server/models/User.ts | 12 +- server/models/index.ts | 3 + server/state.ts | 4 +- 11 files changed, 223 insertions(+), 34 deletions(-) rename app/sprinklersRpc/{websocketClient.ts => WebSocketRpcClient.ts} (99%) create mode 100644 server/models/SprinklersDevice.ts create mode 100644 server/models/index.ts diff --git a/app/components/DeviceView.tsx b/app/components/DeviceView.tsx index 0e4658f..2625ba4 100644 --- a/app/components/DeviceView.tsx +++ b/app/components/DeviceView.tsx @@ -10,31 +10,27 @@ import "./DeviceView.scss"; const ConnectionState = observer(({ connectionState, className }: { connectionState: ConState, className?: string }) => { - const connected = connectionState.isConnected; - const classes = classNames({ - connectionState: true, - connected: connected === true, - disconnected: connected === false, - unknown: connected === null, - }, className); + const connected = connectionState.isDeviceConnected; let connectionText: string; let iconName: SemanticICONS = "unlinkify"; + let clazzName: string = "disconnected"; if (connected) { connectionText = "Connected"; iconName = "linkify"; - } else if (connected === null) { - connectionText = "Unknown"; - iconName = "question"; + clazzName = "connected"; + } else if (connected === false) { + connectionText = "Device Disconnected"; } else if (connectionState.noPermission) { connectionText = "No permission for this device"; iconName = "ban"; - } else if (connectionState.serverToBroker) { - connectionText = "Device Disconnected"; - } else if (connectionState.clientToServer) { - connectionText = "Broker Disconnected"; + } else if (connectionState.clientToServer === false) { + connectionText = "Disconnected from server"; } else { - connectionText = "Server Disconnected"; + connectionText = "Unknown"; + iconName = "question"; + clazzName = "unknown"; } + const classes = classNames("connectionState", clazzName, className); return (
  diff --git a/app/sprinklersRpc/websocketClient.ts b/app/sprinklersRpc/WebSocketRpcClient.ts similarity index 99% rename from app/sprinklersRpc/websocketClient.ts rename to app/sprinklersRpc/WebSocketRpcClient.ts index f005554..cd7d209 100644 --- a/app/sprinklersRpc/websocketClient.ts +++ b/app/sprinklersRpc/WebSocketRpcClient.ts @@ -91,7 +91,7 @@ export class WebSocketRpcClient implements s.SprinklersRPC { private reconnectTimer: number | null = null; get connected(): boolean { - return this.connectionState.isConnected || false; + return this.connectionState.isServerConnected || false; } constructor(tokenStore: TokenStore, webSocketUrl: string = DEFAULT_URL) { diff --git a/app/state/AppState.ts b/app/state/AppState.ts index c65854c..8a3252a 100644 --- a/app/state/AppState.ts +++ b/app/state/AppState.ts @@ -2,9 +2,11 @@ import { createBrowserHistory, History } from "history"; import { computed } from "mobx"; import { RouterStore, syncHistoryWithStore } from "mobx-react-router"; -import { WebSocketRpcClient } from "@app/sprinklersRpc/websocketClient"; +import { WebSocketRpcClient } from "@app/sprinklersRpc/WebSocketRpcClient"; import HttpApi from "@app/state/HttpApi"; import { UiStore } from "@app/state/UiStore"; +import ApiError from "@common/ApiError"; +import { ErrorCode } from "@common/ErrorCode"; import log from "@common/logger"; export default class AppState { @@ -29,9 +31,14 @@ export default class AppState { try { await this.httpApi.tokenStore.grantRefresh(); } catch (err) { - log.warn({ err }, "could not refresh access token. erasing token"); - this.tokenStore.clear(); - this.history.push("/login"); + if (err instanceof ApiError && err.code === ErrorCode.BadToken) { + log.warn({ err }, "refresh is bad for some reason, erasing"); + this.tokenStore.clear(); + this.history.push("/login"); + } else { + log.error({ err }, "could not refresh access token"); + // TODO: some kind of error page? + } } } else { this.history.push("/login"); diff --git a/common/sprinklersRpc/ConnectionState.ts b/common/sprinklersRpc/ConnectionState.ts index 236ce3c..76629b4 100644 --- a/common/sprinklersRpc/ConnectionState.ts +++ b/common/sprinklersRpc/ConnectionState.ts @@ -45,18 +45,28 @@ export class ConnectionState { return false; } - @computed get isConnected(): boolean | null { + @computed get isDeviceConnected(): boolean | null { if (this.hasPermission === false) { return false; } + if (this.serverToBroker === false || this.clientToServer === false) { + return null; + } if (this.brokerToDevice != null) { return this.brokerToDevice; } + return null; + } + + @computed get isServerConnected(): boolean | null { + if (this.hasPermission === false) { + return false; + } if (this.serverToBroker != null) { return this.serverToBroker; } if (this.clientToServer != null) { - return this.clientToServer; + return this.brokerToDevice; } return null; } diff --git a/common/sprinklersRpc/SprinklersDevice.ts b/common/sprinklersRpc/SprinklersDevice.ts index bdb64a6..0ef6162 100644 --- a/common/sprinklersRpc/SprinklersDevice.ts +++ b/common/sprinklersRpc/SprinklersDevice.ts @@ -12,7 +12,7 @@ export abstract class SprinklersDevice { @observable sectionRunner: SectionRunner; @computed get connected(): boolean { - return this.connectionState.isConnected || false; + return this.connectionState.isDeviceConnected || false; } sectionConstructor: typeof Section = Section; diff --git a/common/sprinklersRpc/mqtt/index.ts b/common/sprinklersRpc/mqtt/index.ts index 7a32d9d..e4d30ce 100644 --- a/common/sprinklersRpc/mqtt/index.ts +++ b/common/sprinklersRpc/mqtt/index.ts @@ -7,6 +7,7 @@ import * as s from "@common/sprinklersRpc"; import * as requests from "@common/sprinklersRpc/deviceRequests"; import * as schema from "@common/sprinklersRpc/schema"; import { seralizeRequest } from "@common/sprinklersRpc/schema/requests"; +import { getRandomId } from "@common/utils"; const log = logger.child({ source: "mqtt" }); @@ -14,14 +15,14 @@ interface WithRid { rid: number; } -export class MqttApiClient implements s.SprinklersRPC { +export class MqttRpcClient implements s.SprinklersRPC { readonly mqttUri: string; client!: mqtt.Client; @observable connectionState: s.ConnectionState = new s.ConnectionState(); devices: Map = new Map(); get connected(): boolean { - return this.connectionState.isConnected || false; + return this.connectionState.isServerConnected || false; } constructor(mqttUri: string) { @@ -30,11 +31,11 @@ export class MqttApiClient implements s.SprinklersRPC { } private static newClientId() { - return "sprinklers3-MqttApiClient-" + Math.round(Math.random() * 1000); + return "sprinklers3-MqttApiClient-" + getRandomId(); } start() { - const clientId = MqttApiClient.newClientId(); + const clientId = MqttRpcClient.newClientId(); log.info({ mqttUri: this.mqttUri, clientId }, "connecting to mqtt broker with client id"); this.client = mqtt.connect(this.mqttUri, { clientId, connectTimeout: 5000, reconnectPeriod: 5000, @@ -127,14 +128,14 @@ const handler = (test: RegExp) => }; class MqttSprinklersDevice extends s.SprinklersDevice { - readonly apiClient: MqttApiClient; + readonly apiClient: MqttRpcClient; readonly prefix: string; handlers!: IHandlerEntry[]; private nextRequestId: number = Math.floor(Math.random() * 1000000000); private responseCallbacks: Map = new Map(); - constructor(apiClient: MqttApiClient, prefix: string) { + constructor(apiClient: MqttRpcClient, prefix: string) { super(); this.sectionConstructor = MqttSection; this.sectionRunnerConstructor = MqttSectionRunner; diff --git a/server/models/Database.ts b/server/models/Database.ts index ec2efb4..d50e30e 100644 --- a/server/models/Database.ts +++ b/server/models/Database.ts @@ -1,6 +1,7 @@ import * as r from "rethinkdb"; import logger from "@common/logger"; +import { SprinklersDevice, UserSprinklersDevice } from "./SprinklersDevice"; import { User } from "./User"; export class Database { @@ -43,6 +44,12 @@ export class Database { if (tables.indexOf(User.tableName) === -1) { await User.createTable(this); } + if (tables.indexOf(SprinklersDevice.tableName) === -1) { + await SprinklersDevice.createTable(this); + } + if (tables.indexOf(UserSprinklersDevice.tableName) === -1) { + await UserSprinklersDevice.createTable(this); + } const alex = new User(this, { name: "Alex Mikhalev", username: "alex", @@ -53,5 +60,12 @@ export class Database { const alex2 = await User.loadByUsername(this, "alex"); logger.info("password valid: " + await alex2!.comparePassword("kakashka")); + + const device = new SprinklersDevice(this, { + name: "test", + }); + await device.createOrUpdate(); + + device.addToUser } } diff --git a/server/models/SprinklersDevice.ts b/server/models/SprinklersDevice.ts new file mode 100644 index 0000000..2b7f131 --- /dev/null +++ b/server/models/SprinklersDevice.ts @@ -0,0 +1,152 @@ +import * as r from "rethinkdb"; +import { createModelSchema, primitive, serialize, update } from "serializr"; + +import { Database } from "./Database"; +import { User } from "./User"; + +export interface ISprinklersDevice { + id: string | undefined; + name: string; +} + +export class SprinklersDevice implements ISprinklersDevice { + static readonly tableName = "SprinklersDevices"; + + id: string | undefined; + name: string = ""; + + private db: Database; + private _table: r.Table | null = null; + + constructor(db: Database, data?: Partial) { + this.db = db; + if (data) { + update(this, data); + } + } + + static async createTable(db: Database) { + await db.db.tableCreate(SprinklersDevice.tableName).run(db.conn); + await db.db.table(SprinklersDevice.tableName).indexCreate("name").run(db.conn); + } + + static async loadAll(db: Database): Promise { + const cursor = await db.db.table(SprinklersDevice.tableName) + .run(db.conn); + const users = await cursor.toArray(); + return users.map((data) => { + return new SprinklersDevice(db, data); + }); + } + + static async load(db: Database, id: string): Promise { + const data = await db.db.table(SprinklersDevice.tableName) + .get(id) + .run(db.conn); + if (data == null) { + return null; + } + return new SprinklersDevice(db, data); + } + + private static getTable(db: Database): r.Table { + return db.db.table(this.tableName); + } + + private get table() { + if (!this._table) { + this._table = SprinklersDevice.getTable(this.db); + } + return this._table; + } + + async create() { + const data = serialize(this); + delete data.id; + const result = await this.table + .insert(data) + .run(this.db.conn); + this.id = result.generated_keys[0]; + } + + async createOrUpdate() { + const data = serialize(this); + delete data.id; + const device = this.table.filter(r.row("name").eq(this.name)); + const nameDoesNotExist = device.isEmpty(); + const a: r.WriteResult = await r.branch(nameDoesNotExist, + this.table.insert(data) as r.Expression, + device.nth(0).update(data) as r.Expression) + .run(this.db.conn); + if (a.inserted > 0) { + this.id = a.generated_keys[0]; + return true; + } else { + return false; + } + } + + async addToUser(user: User | number) { + const userId = (typeof user === "number") ? user : user.id; + const userDevice = new UserSprinklersDevice(this.db, { + + }); + } + + toJSON(): any { + return serialize(this); + } +} + +createModelSchema(SprinklersDevice, { + id: primitive(), + name: primitive(), +}); + +export interface IUserSprinklersDevice { + id: string | undefined; + userId: string; + sprinklersDeviceId: string; +} + +export class UserSprinklersDevice implements IUserSprinklersDevice { + static readonly tableName = "UserSprinklersDevices"; + + id: string | undefined; + + userId: string = ""; + sprinklersDeviceId: string = ""; + + private db: Database; + private _table: r.Table | null = null; + + constructor(db: Database) { + this.db = db; + } + + static async createTable(db: Database) { + await db.db.tableCreate(UserSprinklersDevice.tableName).run(db.conn); + await db.db.table(UserSprinklersDevice.tableName).indexCreate("userId").run(db.conn); + await db.db.table(UserSprinklersDevice.tableName).indexCreate("sprinklersDeviceId").run(db.conn); + } + + private static getTable(db: Database): r.Table { + return db.db.table(this.tableName); + } + + async create() { + const data = serialize(this); + delete data.id; + const result = await this.table + .insert(data) + .run(this.db.conn); + this.id = result.generated_keys[0]; + } + + private get table() { + if (!this._table) { + this._table = UserSprinklersDevice.getTable(this.db); + } + return this._table; + } +} diff --git a/server/models/User.ts b/server/models/User.ts index 4c8fe44..41c48ea 100644 --- a/server/models/User.ts +++ b/server/models/User.ts @@ -14,7 +14,7 @@ export interface IUser { const HASH_ROUNDS = 10; export class User implements IUser { - static readonly tableName = "users"; + static readonly tableName = "Users"; id: string | undefined; username: string = ""; @@ -72,9 +72,10 @@ export class User implements IUser { async create() { const data = serialize(this); delete data.id; - await this.table + const result = await this.table .insert(data) .run(this.db.conn); + this.id = result.generated_keys[0]; } async createOrUpdate() { @@ -86,7 +87,12 @@ export class User implements IUser { this.table.insert(data) as r.Expression, user.nth(0).update(data) as r.Expression) .run(this.db.conn); - return a.inserted > 0; + if (a.inserted > 0) { + this.id = a.generated_keys[0]; + return true; + } else { + return false; + } } async setPassword(newPassword: string): Promise { diff --git a/server/models/index.ts b/server/models/index.ts new file mode 100644 index 0000000..c3de530 --- /dev/null +++ b/server/models/index.ts @@ -0,0 +1,3 @@ +export { User, IUser } from "./User"; +export { SprinklersDevice, ISprinklersDevice, UserSprinklersDevice } from "./SprinklersDevice"; +export { Database } from "./Database"; diff --git a/server/state.ts b/server/state.ts index 6b5b175..c86e8a0 100644 --- a/server/state.ts +++ b/server/state.ts @@ -3,7 +3,7 @@ import * as mqtt from "@common/sprinklersRpc/mqtt"; import { Database } from "./models/Database"; export class ServerState { - mqttClient: mqtt.MqttApiClient; + mqttClient: mqtt.MqttRpcClient; database: Database; constructor() { @@ -11,7 +11,7 @@ export class ServerState { if (!mqttUrl) { throw new Error("Must specify a MQTT_URL to connect to"); } - this.mqttClient = new mqtt.MqttApiClient(mqttUrl); + this.mqttClient = new mqtt.MqttRpcClient(mqttUrl); this.database = new Database(); }