Browse Source

feat: Add user command to manage users

develop
Alex Mikhalev 6 years ago
parent
commit
18c35ac8b9
  1. 4
      common/ErrorCode.ts
  2. 2
      common/tsconfig.json
  3. 20
      server/Database.ts
  4. 21
      server/ManageCommand.ts
  5. 14
      server/UniqueConstraintError.ts
  6. 11
      server/commands/manage.ts
  7. 129
      server/commands/user.ts
  8. 9
      server/entities/User.ts
  9. 1
      server/index.ts
  10. 16
      server/repositories/SprinklersDeviceRepository.ts
  11. 37
      server/repositories/UserRepository.ts
  12. 9
      server/state.ts

4
common/ErrorCode.ts

@ -7,9 +7,10 @@ export enum ErrorCode {
BadToken = 105, BadToken = 105,
Unauthorized = 106, Unauthorized = 106,
NoPermission = 107, NoPermission = 107,
NotImplemented = 108,
NotFound = 109, NotFound = 109,
NotUnique = 110,
Internal = 200, Internal = 200,
NotImplemented = 201,
Timeout = 300, Timeout = 300,
ServerDisconnected = 301, ServerDisconnected = 301,
BrokerDisconnected = 302 BrokerDisconnected = 302
@ -22,6 +23,7 @@ export function toHttpStatus(errorCode: ErrorCode): number {
case ErrorCode.Parse: case ErrorCode.Parse:
case ErrorCode.Range: case ErrorCode.Range:
case ErrorCode.InvalidData: case ErrorCode.InvalidData:
case ErrorCode.NotUnique:
return 400; // Bad request return 400; // Bad request
case ErrorCode.Unauthorized: case ErrorCode.Unauthorized:
case ErrorCode.BadToken: case ErrorCode.BadToken:

2
common/tsconfig.json

@ -14,6 +14,6 @@
} }
}, },
"include": [ "include": [
"./**/*.ts" "./**/*.ts",
] ]
} }

20
server/Database.ts

@ -3,7 +3,7 @@ import { Connection, createConnection, getConnectionOptions } from "typeorm";
import logger from "@common/logger"; import logger from "@common/logger";
import { User } from "./entities"; import { SprinklersDevice, User } from "./entities";
import { SprinklersDeviceRepository, UserRepository } from "./repositories/"; import { SprinklersDeviceRepository, UserRepository } from "./repositories/";
export class Database { export class Database {
@ -24,16 +24,21 @@ export class Database {
Object.assign(options, { Object.assign(options, {
entities: [path.resolve(__dirname, "entities", "*.js")] entities: [path.resolve(__dirname, "entities", "*.js")]
}); });
if (options.synchronize) {
logger.warn("synchronizing database schema");
}
this._conn = await createConnection(options); this._conn = await createConnection(options);
this.users = this._conn.getCustomRepository(UserRepository); this.users = this._conn.getCustomRepository(UserRepository);
this.sprinklersDevices = this._conn.getCustomRepository( this.sprinklersDevices = this._conn.getCustomRepository(
SprinklersDeviceRepository SprinklersDeviceRepository
); );
logger.info("connected to database");
} }
async disconnect() { async disconnect() {
if (this._conn) { if (this._conn) {
return this._conn.close(); await this._conn.close();
logger.info("disconnected from database");
} }
} }
@ -41,18 +46,20 @@ export class Database {
const NUM = 100; const NUM = 100;
const users: User[] = []; const users: User[] = [];
for (let i = 0; i < NUM; i++) { for (let i = 0; i < NUM; i++) {
const username = "alex" + i; const username = "alex" + i % 50;
let user = await this.users.findByUsername(username); let user = await this.users.findByUsername(username);
if (!user) { // if (!user) {
user = await this.users.create({ user = await this.users.create({
name: "Alex Mikhalev" + i, name: "Alex Mikhalev" + i,
username username
}); });
} // }
await user.setPassword("kakashka" + i); await user.setPassword("kakashka" + i);
users.push(user); users.push(user);
} }
const devices: SprinklersDevice[] = [];
for (let i = 0; i < NUM; i++) { for (let i = 0; i < NUM; i++) {
const name = "Test" + i; const name = "Test" + i;
let device = await this.sprinklersDevices.findByName(name); let device = await this.sprinklersDevices.findByName(name);
@ -63,13 +70,14 @@ export class Database {
name, name,
deviceId: "grinklers" + (i === 1 ? "" : i) deviceId: "grinklers" + (i === 1 ? "" : i)
}); });
await this.sprinklersDevices.save(device); devices.push(device);
for (let j = 0; j < 5; j++) { for (let j = 0; j < 5; j++) {
const userIdx = (i + j * 10) % NUM; const userIdx = (i + j * 10) % NUM;
const user = users[userIdx]; const user = users[userIdx];
user.devices = (user.devices || []).concat([device]); user.devices = (user.devices || []).concat([device]);
} }
} }
await this.sprinklersDevices.save(devices);
logger.info("inserted/updated devices"); logger.info("inserted/updated devices");
await this.users.save(users); await this.users.save(users);

21
server/ManageCommand.ts

@ -0,0 +1,21 @@
import Command from "@oclif/command";
import { Database, ServerState } from ".";
export default abstract class ManageCommand extends Command {
state!: ServerState;
database!: Database;
async connect() {
this.state = new ServerState();
await this.state.startDatabase();
this.database = this.state.database;
}
async finally(e: Error | undefined) {
if (this.state) {
await this.state.stopDatabase();
}
await super.finally(e);
}
}

14
server/UniqueConstraintError.ts

@ -0,0 +1,14 @@
import { QueryFailedError } from "typeorm";
import ApiError from "@common/ApiError";
import { ErrorCode } from "@common/ErrorCode";
export default class UniqueConstraintError extends ApiError {
static is(err: any): err is QueryFailedError {
return err && err.name === "QueryFailedError" && (err as any).code === 23505; // unique constraint error
}
constructor(err: QueryFailedError) {
super(`Unsatisfied unique constraint: ${(err as any).detail}`, ErrorCode.NotUnique, err);
}
}

11
server/commands/manage.ts

@ -1,11 +0,0 @@
import Command from "@oclif/command";
import { createApp, ServerState, WebSocketApi } from "../";
import log from "@common/logger";
export default class ManageCommand extends Command {
run(): Promise<any> {
throw new Error("Method not implemented.");
}
}

129
server/commands/user.ts

@ -0,0 +1,129 @@
import { flags } from "@oclif/command";
import { ux } from "cli-ux";
import { capitalize } from "lodash";
import { FindConditions } from "typeorm";
import ManageCommand from "../ManageCommand";
import { Input } from "@oclif/parser/lib/flags";
import { User } from "@server/entities";
type UserFlags = (typeof UserCommand)["flags"] extends Input<infer F>
? F
: never;
type Action = "create" | "update" | "delete";
export default class UserCommand extends ManageCommand {
static description = "Manage users";
static flags = {
create: flags.boolean({
char: "c",
exclusive: ["update", "delete", "id"],
dependsOn: ["username"],
description: "Create a new user"
}),
update: flags.boolean({
char: "u",
exclusive: ["create", "delete"],
description: "Update an existing user (by --id or --username)"
}),
delete: flags.boolean({
char: "d",
exclusive: ["create", "update"],
description: "Delete a user (by --id or --username)"
}),
id: flags.integer({
description: "The id of the user to update or delete",
}),
username: flags.string({
description: "The username of the user to create or update"
}),
name: flags.string({
description: "The name of the user, when creating or updating"
}),
passwordPrompt: flags.boolean({
char: "p",
description:
"Prompts for the password of the user when creating or updating"
})
};
getAction(flags: UserFlags): Action {
if (flags.create) return "create";
else if (flags.update) return "update";
else if (flags.delete) return "delete";
else {
this.error("Must specify an action (--create, --update, --delete)", {
exit: false
});
return this._help();
}
}
getFindConditions(flags: UserFlags, action: Action): FindConditions<User> {
if (flags.id != null) {
return { id: flags.id };
} else if (flags.username) {
return { username: flags.username };
} else {
this.error(`Must specify either --id or --username to ${action}`, {
exit: false
});
return this._help();
}
}
async getOrDeleteUser(flags: UserFlags, action: Action): Promise<User | never> {
if (action === "create") {
return this.database.users.create();
} else {
const findConditions = this.getFindConditions(flags, action);
if (action === "delete") {
this.log("findConditions: ", findConditions)
const result = await this.database.users.delete(findConditions);
this.log(`Deleted user: `, result);
return this.exit();
} else {
const user = await this.database.users.findOneUser(findConditions);
if (!user) {
return this.error(`The specified user does not exist`);
}
return user;
}
}
}
async run() {
const parseResult = this.parse(UserCommand);
const flags = parseResult.flags;
const action = this.getAction(flags);
await this.connect();
const user = await this.getOrDeleteUser(flags, action);
if (flags.id != null && flags.username) {
user.username = flags.username;
}
if (flags.name) {
user.name = flags.name;
}
if (flags.passwordPrompt || flags.create) {
const password = await ux.prompt("Enter a password to assign the user", {
type: "hide"
});
await user.setPassword(password);
}
try {
await this.database.users.save(user);
this.log(`${capitalize(action)}d user id ${user.id} (${user.username})`);
} catch (e) {
console.log(e)
throw e;
}
}
}

9
server/entities/User.ts

@ -5,7 +5,11 @@ import {
Entity, Entity,
JoinTable, JoinTable,
ManyToMany, ManyToMany,
PrimaryGeneratedColumn PrimaryGeneratedColumn,
BeforeInsert,
BeforeUpdate,
Index,
InsertEvent
} from "typeorm"; } from "typeorm";
import { IUser } from "@common/httpApi"; import { IUser } from "@common/httpApi";
@ -18,7 +22,8 @@ export class User implements IUser {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id!: number; id!: number;
@Column({ unique: true }) @Column()
@Index("user_username_unique", { unique: true })
username: string = ""; username: string = "";
@Column() @Column()

1
server/index.ts

@ -5,5 +5,6 @@ import "./env";
import "./configureLogger"; import "./configureLogger";
export { ServerState } from "./state"; export { ServerState } from "./state";
export { Database } from "./Database";
export { createApp } from "./express"; export { createApp } from "./express";
export { WebSocketApi } from "./sprinklersRpc/"; export { WebSocketApi } from "./sprinklersRpc/";

16
server/repositories/SprinklersDeviceRepository.ts

@ -1,6 +1,7 @@
import { EntityRepository, Repository } from "typeorm"; import { DeepPartial, EntityRepository, Repository, SaveOptions } from "typeorm";
import { SprinklersDevice, User } from "@server/entities"; import { SprinklersDevice, User } from "@server/entities";
import UniqueConstraintError from "@server/UniqueConstraintError";
@EntityRepository(SprinklersDevice) @EntityRepository(SprinklersDevice)
export class SprinklersDeviceRepository extends Repository<SprinklersDevice> { export class SprinklersDeviceRepository extends Repository<SprinklersDevice> {
@ -39,4 +40,17 @@ export class SprinklersDeviceRepository extends Repository<SprinklersDevice> {
} }
return user.devices![0]; return user.devices![0];
} }
save<T extends DeepPartial<SprinklersDevice>>(entities: T[], options?: SaveOptions): Promise<T[]>;
save<T extends DeepPartial<SprinklersDevice>>(entity: T, options?: SaveOptions): Promise<T>;
async save(entity: any, options?: SaveOptions): Promise<any> {
try {
return await super.save(entity, options);
} catch (e) {
if (UniqueConstraintError.is(e)) {
throw new UniqueConstraintError(e);
}
throw e;
}
}
} }

37
server/repositories/UserRepository.ts

@ -1,33 +1,50 @@
import { EntityRepository, FindOneOptions, Repository } from "typeorm"; import { EntityRepository, FindOneOptions, Repository, DeepPartial, FindConditions, SaveOptions } from "typeorm";
import ApiError from "@common/ApiError";
import { User } from "@server/entities"; import { User } from "@server/entities";
export interface FindUserOptions { export interface FindUserOptions extends FindOneOptions<User> {
devices: boolean; devices: boolean;
} }
function applyDefaultOptions( function computeOptions(
options?: Partial<FindUserOptions> options?: Partial<FindUserOptions>
): FindOneOptions<User> { ): FindOneOptions<User> {
const opts: FindUserOptions = { devices: false, ...options }; const opts: FindUserOptions = { devices: false, ...options };
const relations = [opts.devices && "devices"].filter(Boolean) as string[]; const { devices, ...rest } = opts;
return { relations }; const relations = [devices && "devices"].filter(Boolean) as string[];
return { relations, ...rest };
} }
@EntityRepository(User) @EntityRepository(User)
export class UserRepository extends Repository<User> { export class UserRepository extends Repository<User> {
findAll(options?: Partial<FindUserOptions>) { findAll(options?: Partial<FindUserOptions>) {
const opts = applyDefaultOptions(options); const opts = computeOptions(options);
return super.find(opts); return super.find(opts);
} }
findOneUser(conditions: FindConditions<User>, options?: Partial<FindUserOptions>) {
const opts = computeOptions(options);
return super.findOne(conditions, opts);
}
findById(id: number, options?: Partial<FindUserOptions>) { findById(id: number, options?: Partial<FindUserOptions>) {
const opts = applyDefaultOptions(options); return this.findOneUser({ id }, options);
return super.findOne(id, opts);
} }
findByUsername(username: string, options?: Partial<FindUserOptions>) { findByUsername(username: string, options?: Partial<FindUserOptions>) {
const opts = applyDefaultOptions(options); return this.findOneUser({ username }, options);
return this.findOne({ username }, opts);
} }
// async checkAndSave(entity: User): Promise<User> {
// return this.manager.transaction(manager => {
// let query = manager.createQueryBuilder<User>(User, "user", manager.queryRunner!);
// if (entity.id != null) {
// query = query.where("user.id <> :id", { id: entity.id })
// }
// query
// return manager.save(entity);
// });
// }
} }

9
server/state.ts

@ -23,13 +23,20 @@ export class ServerState {
async startDatabase() { async startDatabase() {
await this.database.connect(); await this.database.connect();
logger.info("connected to database");
if (process.env.INSERT_TEST_DATA) { if (process.env.INSERT_TEST_DATA) {
try {
await this.database.insertTestData(); await this.database.insertTestData();
logger.info("inserted test data"); logger.info("inserted test data");
} catch (e) {
logger.error(e, "error inserting test data");
} }
} }
}
async stopDatabase() {
await this.database.disconnect();
}
async startMqtt() { async startMqtt() {
this.mqttClient.username = SUPERUSER; this.mqttClient.username = SUPERUSER;

Loading…
Cancel
Save