Alex Mikhalev
6 years ago
12 changed files with 242 additions and 35 deletions
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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."); |
|
||||||
} |
|
||||||
} |
|
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
} |
} |
||||||
|
Loading…
Reference in new issue