Updating errors and logging in a lot of places. Changing some static methods of Reservation to instance methods
This commit is contained in:
parent
983c785029
commit
6dfa37272e
7 changed files with 123 additions and 59 deletions
|
|
@ -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}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue