Updating errors and logging in a lot of places. Changing some static methods of Reservation to instance methods

This commit is contained in:
Collin Duncan 2023-01-30 12:38:42 +01:00
parent 983c785029
commit 6dfa37272e
No known key found for this signature in database
7 changed files with 123 additions and 59 deletions

View file

@ -85,3 +85,9 @@ export class Logger {
this.log(LogLevel.ERROR, message, details) this.log(LogLevel.ERROR, message, details)
} }
} }
export class LoggableError extends Error {
toString() {
return `${this.name} - ${this.message}\n${this.stack}`
}
}

View file

@ -76,15 +76,16 @@ export class Reservation {
.subtract(RESERVATION_AVAILABLE_WITHIN_DAYS, 'days') .subtract(RESERVATION_AVAILABLE_WITHIN_DAYS, 'days')
} }
public toString() { public toString(obfuscate = false) {
return JSON.stringify(this.format()) return JSON.stringify(this.format(obfuscate))
} }
public format() { public format(obfuscate = false) {
return { return {
id: this.id,
user: { user: {
username: this.user.username, username: this.user.username,
password: this.user.password, password: obfuscate ? '???' : this.user.password,
}, },
opponent: this.opponent, opponent: this.opponent,
booked: this.booked, booked: this.booked,
@ -98,6 +99,7 @@ export class Reservation {
public serializeToJson(): SerializedReservation { public serializeToJson(): SerializedReservation {
return { return {
id: this.id,
user: this.user, user: this.user,
opponent: this.opponent, opponent: this.opponent,
booked: this.booked, booked: this.booked,
@ -126,7 +128,7 @@ export class Reservation {
return dates.map((date) => dayjs(date)) return dates.map((date) => dayjs(date))
} }
public static async save(res: Reservation) { public async save() {
await run( await run(
` `
INSERT INTO reservations INSERT INTO reservations
@ -151,24 +153,34 @@ export class Reservation {
) )
`, `,
[ [
res.id, this.id,
res.user.username, this.user.username,
res.user.password, this.user.password,
res.dateRange.start.format('YYYY-MM-DD HH:mm:ss'), this.dateRange.start.format('YYYY-MM-DD HH:mm:ss'),
res.dateRange.end.format('YYYY-MM-DD HH:mm:ss'), this.dateRange.end.format('YYYY-MM-DD HH:mm:ss'),
res.opponent.id, this.opponent.id,
res.opponent.name, this.opponent.name,
] ]
) )
} }
public static async delete(res: Reservation) { public async delete() {
await run( await run(
` `
DELETE FROM reservations DELETE FROM reservations
WHERE id = ? WHERE id = ?
`, `,
[res.id] [this.id]
)
}
public static async deleteById(id: string) {
await run(
`
DELETE FROM reservations
WHERE id = ?
`,
[id]
) )
} }
@ -273,6 +285,43 @@ export class Reservation {
return [] return []
} }
public static async fetchByPage(pageNumber: number, pageSize = 50): Promise<Reservation[]> {
const response = await all<SqlReservation>(
`
SELECT *
FROM reservations
ORDER BY date_range_start ASC
LIMIT ?
OFFSET ?;
`,
[pageSize, pageSize * pageNumber]
)
if (response.length > 0) {
return response.map(
(sqlReservation) =>
new Reservation(
{
username: sqlReservation.username,
password: sqlReservation.password,
},
{
start: dayjs(sqlReservation.date_range_start),
end: dayjs(sqlReservation.date_range_end),
},
{
id: sqlReservation.opponent_id,
name: sqlReservation.opponent_name,
},
undefined,
sqlReservation.id
)
)
}
return []
}
} }
export interface SerializedDateRange { export interface SerializedDateRange {
@ -281,6 +330,7 @@ export interface SerializedDateRange {
} }
export interface SerializedReservation { export interface SerializedReservation {
id: string
user: User user: User
opponent: Opponent opponent: Opponent
booked: boolean booked: boolean

View file

@ -1,6 +1,6 @@
import { asyncLocalStorage as l } from './logger' import { asyncLocalStorage as l, LoggableError } from './logger'
import { Reservation } from './reservation' import { Reservation } from './reservation'
import { LoggableError, Runner } from './runner' import { Runner } from './runner'
let runner: Runner | undefined let runner: Runner | undefined
const getRunner = () => { const getRunner = () => {
@ -24,15 +24,17 @@ export const reserve = async (reservation?: Reservation): Promise<boolean> => {
l.getStore()?.info('No reservation to perform') l.getStore()?.info('No reservation to perform')
return true return true
} }
l.getStore()?.debug('Trying to perform reservation', { reservationToPerform }) l.getStore()?.debug('Trying to perform reservation', { reservationToPerform: reservationToPerform.toString(true) })
const runner = getRunner() const runner = getRunner()
try { try {
await runner.run(reservationToPerform) await runner.run(reservationToPerform)
await Reservation.delete(reservationToPerform) await reservationToPerform.delete()
return true return true
} catch (error) { } catch (error) {
l.getStore()?.error('Failed to perform reservation', { error: (error as LoggableError).toString() }) l.getStore()?.error('Failed to perform reservation', {
error: (error as LoggableError).toString(),
})
return false return false
} }
} }

View file

@ -8,7 +8,7 @@ import puppeteer, {
LaunchOptions, LaunchOptions,
Page, Page,
} from 'puppeteer' } from 'puppeteer'
import { asyncLocalStorage as l } from './logger' import { asyncLocalStorage as l, LoggableError } from './logger'
import { Opponent, Reservation } from './reservation' import { Opponent, Reservation } from './reservation'
export class Runner { export class Runner {
@ -130,7 +130,7 @@ export class Runner {
} }
private async selectAvailableTime(res: Reservation): Promise<void> { private async selectAvailableTime(res: Reservation): Promise<void> {
l.getStore()?.debug('Selecting available time', res.format()) l.getStore()?.debug('Selecting available time', { reservation: res.toString(true) })
let freeCourt: ElementHandle | null | undefined let freeCourt: ElementHandle | null | undefined
let i = 0 let i = 0
while (i < res.possibleDates.length && !freeCourt) { while (i < res.possibleDates.length && !freeCourt) {
@ -152,7 +152,7 @@ export class Runner {
} }
private async selectOpponent(opponent: Opponent): Promise<void> { private async selectOpponent(opponent: Opponent): Promise<void> {
l.getStore()?.debug('Selecting opponent', opponent) l.getStore()?.debug('Selecting opponent', { opponent })
const player2Search = await this.page const player2Search = await this.page
?.waitForSelector('tr.res-make-player-2 > td > input') ?.waitForSelector('tr.res-make-player-2 > td > input')
.catch((e: Error) => { .catch((e: Error) => {
@ -173,20 +173,21 @@ export class Runner {
} }
private async confirmReservation(): Promise<void> { private async confirmReservation(): Promise<void> {
await this.page?.$('input#__make_submit').then((b) => b?.click()) await this.page
.catch((e: Error) => { throw new RunnerReservationConfirmButtonError(e) }) ?.$('input#__make_submit')
.then((b) => b?.click())
.catch((e: Error) => {
throw new RunnerReservationConfirmButtonError(e)
})
await this.page await this.page
?.waitForSelector('input#__make_submit2') ?.waitForSelector('input#__make_submit2')
.then((b) => b?.click()) .then((b) => b?.click())
.catch((e: Error) => { throw new RunnerReservationConfirmSubmitError(e) }) .catch((e: Error) => {
} throw new RunnerReservationConfirmSubmitError(e)
} })
export class LoggableError extends Error {
toString() {
return `${this.name} - ${this.message}\n${this.stack}`
} }
} }
export class RunnerError extends LoggableError { export class RunnerError extends LoggableError {
constructor(error: Error) { constructor(error: Error) {
super(error.message) super(error.message)
@ -215,4 +216,4 @@ export class RunnerOpponentSearchNetworkError extends RunnerError {}
export class RunnerOpponentSearchSelectionError extends RunnerError {} export class RunnerOpponentSearchSelectionError extends RunnerError {}
export class RunnerReservationConfirmButtonError extends RunnerError {} export class RunnerReservationConfirmButtonError extends RunnerError {}
export class RunnerReservationConfirmSubmitError extends RunnerError {} export class RunnerReservationConfirmSubmitError extends RunnerError {}

View file

@ -31,7 +31,7 @@ export const schedule = async (
} }
logger?.debug('Successfully validated request', { logger?.debug('Successfully validated request', {
reservation: reservation.format(), reservation: reservation.toString(true),
}) })
if (!reservation.isAvailableForReservation()) { if (!reservation.isAvailableForReservation()) {
@ -39,7 +39,7 @@ export const schedule = async (
'Reservation date is more than 7 days away; saving for later reservation' 'Reservation date is more than 7 days away; saving for later reservation'
) )
await Reservation.save(reservation) await reservation.save()
return { return {
scheduledReservation: { scheduledReservation: {

View file

@ -12,46 +12,50 @@ const getTaskConfig = (name: string): ScheduleOptions => ({
}) })
const logger = new Logger('cron', 'default', LogLevel.DEBUG) const logger = new Logger('cron', 'default', LogLevel.DEBUG)
let shouldContinue = true
export const startTasks = () => { export const startTasks = () => {
try { try {
const task = schedule( if (tasks.length === 0) {
'*/10 7 * * *', const task = schedule(
async (timestamp) => { '* 7 * * *',
asyncLocalStorage.run( async (timestamp) => {
new Logger('cron', v4(), LogLevel.DEBUG), asyncLocalStorage.run(
async () => { new Logger('cron', v4(), LogLevel.DEBUG),
if (shouldContinue) { async () => {
const childLogger = asyncLocalStorage.getStore() const childLogger = asyncLocalStorage.getStore()
childLogger?.info('Running cron job', { timestamp }) childLogger?.info('Running cron job', { timestamp })
try { try {
shouldContinue = await reserve() const result = await reserve()
if (!result) {
throw new Error('Failed to complete reservation')
}
childLogger?.info('Completed running cron job') childLogger?.info('Completed running cron job')
} catch (error) { } catch (error) {
shouldContinue = false
childLogger?.error('Error running cron job', { childLogger?.error('Error running cron job', {
error: (error as Error).message, error: (error as Error).message,
}) })
stopTasks()
} }
} }
} )
) },
}, getTaskConfig('reserver cron')
getTaskConfig('reserver cron') )
) logger.debug('Started cron task')
logger.debug('Started cron task') tasks.push(task)
tasks.push(task) }
} catch (error: any) { } catch (error) {
logger.error('Failed to start tasks', { error: error.message }) logger.error('Failed to start tasks', { error: (error as Error).message })
} }
} }
export const stopTasks = () => { export const stopTasks = () => {
try { try {
tasks.map((task) => task.stop()) if (tasks.length > 0) {
logger.debug('Stopped cron tasks') tasks.map((task) => task.stop())
} catch (error: any) { logger.debug('Stopped cron tasks')
logger.error('Failed to stop tasks', { error: error.message }) }
} catch (error) {
logger.error('Failed to stop tasks', { error: (error as Error).message })
} }
} }

View file

@ -5,6 +5,7 @@ import {
} from '../../../src/common/request' } from '../../../src/common/request'
import { Reservation } from '../../../src/common/reservation' import { Reservation } from '../../../src/common/reservation'
import { schedule, SchedulerInput } from '../../../src/common/scheduler' import { schedule, SchedulerInput } from '../../../src/common/scheduler'
import * as database from '../../../src/common/database'
jest.mock('../../../src/common/logger') jest.mock('../../../src/common/logger')
jest.mock('../../../src/common/reserver') jest.mock('../../../src/common/reserver')
@ -13,7 +14,7 @@ jest.useFakeTimers().setSystemTime(new Date('2022-01-01'))
describe('scheduler', () => { describe('scheduler', () => {
test('should handle valid requests within reservation window', async () => { test('should handle valid requests within reservation window', async () => {
jest.spyOn(Reservation, 'save').mockResolvedValueOnce() jest.spyOn(database, 'run').mockResolvedValueOnce()
const start = dayjs().add(15, 'minutes') const start = dayjs().add(15, 'minutes')
const end = start.add(15, 'minutes') const end = start.add(15, 'minutes')