diff --git a/package-lock.json b/package-lock.json index cc488b7..cce9dfe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,6 @@ "requires": true, "packages": { "": { - "name": "autobaan", "version": "1.0.0", "license": "ISC", "dependencies": { @@ -17,6 +16,9 @@ "@babel/preset-env": "^7.16.4", "@babel/preset-typescript": "^7.16.0", "@rollup/plugin-babel": "^5.3.0", + "@rollup/plugin-commonjs": "^21.0.1", + "@rollup/plugin-node-resolve": "^13.0.6", + "@rollup/plugin-typescript": "^8.3.0", "@types/aws-lambda": "^8.10.85", "@types/jest": "^27.0.2", "@types/puppeteer": "^5.4.4", @@ -26,6 +28,7 @@ "eslint": "^8.2.0", "jest": "^27.3.1", "prettier": "^2.4.1", + "rollup": "^2.60.1", "typescript": "^4.4.4" }, "engines": { @@ -2445,6 +2448,71 @@ } } }, + "node_modules/@rollup/plugin-commonjs": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-21.0.1.tgz", + "integrity": "sha512-EA+g22lbNJ8p5kuZJUYyhhDK7WgJckW5g4pNN7n4mAFUM96VuwUnNT3xr2Db2iCZPI1pJPbGyfT5mS9T1dHfMg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "commondir": "^1.0.1", + "estree-walker": "^2.0.1", + "glob": "^7.1.6", + "is-reference": "^1.2.1", + "magic-string": "^0.25.7", + "resolve": "^1.17.0" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^2.38.3" + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.0.6.tgz", + "integrity": "sha512-sFsPDMPd4gMqnh2gS0uIxELnoRUp5kBl5knxD2EO0778G1oOJv4G1vyT2cpWz75OU2jDVcXhjVUuTAczGyFNKA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "rollup": "^2.42.0" + } + }, + "node_modules/@rollup/plugin-typescript": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.3.0.tgz", + "integrity": "sha512-I5FpSvLbtAdwJ+naznv+B4sjXZUcIvLLceYpITAn7wAP8W0wqc5noLdGIp9HGVntNhRWXctwPYrSSFQxtl0FPA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "resolve": "^1.17.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "rollup": "^2.14.0", + "tslib": "*", + "typescript": ">=3.7.0" + } + }, "node_modules/@rollup/pluginutils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", @@ -2612,6 +2680,15 @@ "@types/node": "*" } }, + "node_modules/@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -3361,6 +3438,18 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/builtin-modules": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", + "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -3496,6 +3585,12 @@ "node": ">= 0.8" } }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4879,6 +4974,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4894,6 +4995,15 @@ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -6936,6 +7046,15 @@ "node": ">=10" } }, + "node_modules/magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.4" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -7649,11 +7768,10 @@ } }, "node_modules/rollup": { - "version": "2.60.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.60.0.tgz", - "integrity": "sha512-cHdv9GWd58v58rdseC8e8XIaPUo8a9cgZpnCMMDGZFDZKEODOiPPEQFXLriWr/TjXzhPPmG5bkAztPsOARIcGQ==", + "version": "2.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.60.1.tgz", + "integrity": "sha512-akwfnpjY0rXEDSn1UTVfKXJhPsEBu+imi1gqBA1ZkHGydUnkV/fWCC90P7rDaLEW8KTwBcS1G3N4893Ndz+jwg==", "dev": true, - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -7803,6 +7921,12 @@ "node": ">=0.10.0" } }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -10211,6 +10335,53 @@ "@rollup/pluginutils": "^3.1.0" } }, + "@rollup/plugin-commonjs": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-21.0.1.tgz", + "integrity": "sha512-EA+g22lbNJ8p5kuZJUYyhhDK7WgJckW5g4pNN7n4mAFUM96VuwUnNT3xr2Db2iCZPI1pJPbGyfT5mS9T1dHfMg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "commondir": "^1.0.1", + "estree-walker": "^2.0.1", + "glob": "^7.1.6", + "is-reference": "^1.2.1", + "magic-string": "^0.25.7", + "resolve": "^1.17.0" + }, + "dependencies": { + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + } + } + }, + "@rollup/plugin-node-resolve": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.0.6.tgz", + "integrity": "sha512-sFsPDMPd4gMqnh2gS0uIxELnoRUp5kBl5knxD2EO0778G1oOJv4G1vyT2cpWz75OU2jDVcXhjVUuTAczGyFNKA==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + } + }, + "@rollup/plugin-typescript": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.3.0.tgz", + "integrity": "sha512-I5FpSvLbtAdwJ+naznv+B4sjXZUcIvLLceYpITAn7wAP8W0wqc5noLdGIp9HGVntNhRWXctwPYrSSFQxtl0FPA==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "resolve": "^1.17.0" + } + }, "@rollup/pluginutils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", @@ -10369,6 +10540,15 @@ "@types/node": "*" } }, + "@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -10895,6 +11075,12 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "builtin-modules": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", + "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", + "dev": true + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -11004,6 +11190,12 @@ "delayed-stream": "~1.0.0" } }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -12038,6 +12230,12 @@ "is-extglob": "^2.1.1" } }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -12050,6 +12248,15 @@ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true }, + "is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "requires": { + "@types/estree": "*" + } + }, "is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -13574,6 +13781,15 @@ "yallist": "^4.0.0" } }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -14108,11 +14324,10 @@ } }, "rollup": { - "version": "2.60.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.60.0.tgz", - "integrity": "sha512-cHdv9GWd58v58rdseC8e8XIaPUo8a9cgZpnCMMDGZFDZKEODOiPPEQFXLriWr/TjXzhPPmG5bkAztPsOARIcGQ==", + "version": "2.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.60.1.tgz", + "integrity": "sha512-akwfnpjY0rXEDSn1UTVfKXJhPsEBu+imi1gqBA1ZkHGydUnkV/fWCC90P7rDaLEW8KTwBcS1G3N4893Ndz+jwg==", "dev": true, - "peer": true, "requires": { "fsevents": "~2.3.2" } @@ -14209,6 +14424,12 @@ } } }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", diff --git a/package.json b/package.json index 0ea086b..f1d32d4 100644 --- a/package.json +++ b/package.json @@ -2,15 +2,19 @@ "name": "autobaan", "version": "1.0.0", "description": "", - "main": "index.js", + "private": true, "engines": { "node": "14.x" }, "scripts": { + "build": "rm -r ./dist/* && rollup -c", + "build:reservationScheduler": "rm -r ./dist/reservationScheduler/ || : && rollup -c ./src/lambdas/reservationScheduler/rollup.config.js", + "package:reservationScheduler": "rm ./deploy/reservationScheduler.zip || : && mkdir ./deploy || : && zip deploy/reservationScheduler.zip -j dist/reservationScheduler/*", "test": "jest", + "test:clear-cache": "jest --clearCache", + "test:clean": "npm run test:clear-cache && npm run test", "lint": "eslint src/ --ext ts", "prettier": "prettier src -w", - "build": "tsc", "local": "npx ts-node src/local.ts", "zip": "mkdir deploy && zip deploy/reservation-lambda.zip -r dist" }, @@ -25,6 +29,9 @@ "@babel/preset-env": "^7.16.4", "@babel/preset-typescript": "^7.16.0", "@rollup/plugin-babel": "^5.3.0", + "@rollup/plugin-commonjs": "^21.0.1", + "@rollup/plugin-node-resolve": "^13.0.6", + "@rollup/plugin-typescript": "^8.3.0", "@types/aws-lambda": "^8.10.85", "@types/jest": "^27.0.2", "@types/puppeteer": "^5.4.4", @@ -34,6 +41,7 @@ "eslint": "^8.2.0", "jest": "^27.3.1", "prettier": "^2.4.1", + "rollup": "^2.60.1", "typescript": "^4.4.4" } } diff --git a/src/common/logger.ts b/src/common/logger.ts index 9ba3730..d8542f5 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -5,10 +5,38 @@ export enum LogLevel { } export class Logger { + private static instance: LoggerInstance + + public static instantiate( + correlationId: string, + level = LogLevel.ERROR + ): LoggerInstance { + Logger.instance = new LoggerInstance(correlationId, level) + return Logger.instance + } + + public static getInstance(): LoggerInstance { + return Logger.instance + } + + public static debug(message: string, details?: unknown): void { + Logger.getInstance().debug(message, details) + } + + public static info(message: string, details?: unknown): void { + Logger.getInstance().info(message, details) + } + + public static error(message: string, details?: unknown): void { + Logger.getInstance().error(message, details) + } +} + +export class LoggerInstance { private readonly correlationId: string private readonly level: LogLevel - constructor(correlationId: string, level = LogLevel.ERROR) { + public constructor(correlationId: string, level = LogLevel.ERROR) { this.correlationId = correlationId this.level = level } diff --git a/src/common/request.ts b/src/common/request.ts index eb57d49..0a81079 100644 --- a/src/common/request.ts +++ b/src/common/request.ts @@ -4,7 +4,7 @@ dayjs.extend(isSameOrBefore) import { DateRange, Opponent } from './reservation' -export interface ReservationRequest { +export interface ReservationRequest extends Record { username: string password: string dateRange: DateRange @@ -28,20 +28,30 @@ export class ValidationError extends Error { this.code = code } } - /** * Validates an incoming request body and converts to ReservationRequest * @param body String of request body * @returns ReservationRequest */ -export const validateRequest = (body: string): ReservationRequest => { +export const validateStringRequest = (body: string): ReservationRequest => { const request = validateRequestBody(body) validateRequestDateRange(request.dateRange) validateRequestOpponent(request.opponent) return request } -const validateRequestBody = (body?: string): ReservationRequest => { +export const validateJSONRequest = ( + body: Record +): ReservationRequest => { + const request = validateRequestBody(body) + validateRequestDateRange(request.dateRange) + validateRequestOpponent(request.opponent) + return request +} + +const validateRequestBody = ( + body?: string | Record +): ReservationRequest => { if (body === undefined) { throw new ValidationError( 'Invalid request', @@ -49,7 +59,21 @@ const validateRequestBody = (body?: string): ReservationRequest => { ) } - const jsonBody = transformRequestBody(body) + let jsonBody: ReservationRequest + if (typeof body === 'string') { + jsonBody = transformRequestBody(body) + } else { + const { username, password, dateRange, opponent } = body + jsonBody = { + username: username as string, + password: password as string, + dateRange: convertDateRangeStringToObject( + dateRange as { start: string; end: string } + ), + opponent: opponent as Opponent, + } + } + const { username, password, opponent, dateRange } = jsonBody if ( @@ -77,14 +101,15 @@ const transformRequestBody = (body: string): ReservationRequest => { try { json = JSON.parse(body) } catch (err) { + console.error(err) throw new ValidationError( 'Invalid request', ValidationErrorCode.INVALID_JSON ) } - const startTime = json.dateRange?.start ?? 'invalid' - const endTime = json.dateRange?.end ?? 'invalid' - const dateRange: DateRange = { start: dayjs(startTime), end: dayjs(endTime) } + const start = json.dateRange?.start ?? 'invalid' + const end = json.dateRange?.end ?? 'invalid' + const dateRange: DateRange = convertDateRangeStringToObject({ start, end }) return { username: json.username, password: json.password, @@ -93,6 +118,14 @@ const transformRequestBody = (body: string): ReservationRequest => { } } +const convertDateRangeStringToObject = ({ + start, + end, +}: { + start: string + end: string +}): DateRange => ({ start: dayjs(start), end: dayjs(end) }) + const validateRequestDateRange = (dateRange: DateRange): void => { // checking that both dates are valid const { start, end } = dateRange diff --git a/src/common/runner.ts b/src/common/runner.ts index 6045251..c61b57c 100644 --- a/src/common/runner.ts +++ b/src/common/runner.ts @@ -34,7 +34,7 @@ export class Runner { BrowserConnectOptions ): Promise { this.browser = await puppeteer.launch(options) - this.page = await this.browser.newPage() + this.page = await this.browser?.newPage() await this.login() return await this.makeReservations() } diff --git a/src/lambdas/reservationHandler.ts b/src/lambdas/reservationHandler/index.ts similarity index 100% rename from src/lambdas/reservationHandler.ts rename to src/lambdas/reservationHandler/index.ts diff --git a/src/lambdas/reservationHandler/rollup.config.js b/src/lambdas/reservationHandler/rollup.config.js new file mode 100644 index 0000000..626045e --- /dev/null +++ b/src/lambdas/reservationHandler/rollup.config.js @@ -0,0 +1,20 @@ +// rollup.config.js +import path from 'path' + +import typescript from '@rollup/plugin-typescript' +import { nodeResolve } from '@rollup/plugin-node-resolve' +import commonjs from '@rollup/plugin-commonjs' + +export default { + input: path.join(__dirname, 'index.ts'), + output: { + file: './dist/reservationHandler/index.js', + format: 'cjs', + sourcemap: true, + }, + plugins: [ + typescript(), + nodeResolve(), + commonjs(), + ] +} \ No newline at end of file diff --git a/src/lambdas/reservationScheduler.ts b/src/lambdas/reservationScheduler/index.ts similarity index 50% rename from src/lambdas/reservationScheduler.ts rename to src/lambdas/reservationScheduler/index.ts index 96637d2..865e49b 100644 --- a/src/lambdas/reservationScheduler.ts +++ b/src/lambdas/reservationScheduler/index.ts @@ -1,13 +1,10 @@ import { Context, Handler } from 'aws-lambda' import { Dayjs } from 'dayjs' -import { Logger, LogLevel } from '../common/logger' -import { Reservation } from '../common/reservation' -import { - validateRequest, - ReservationRequest, -} from '../common/request' -import { scheduleDateToRequestReservation } from '../common/schedule' +import { Logger, LogLevel } from '../../common/logger' +import { Reservation } from '../../common/reservation' +import { ReservationRequest, validateJSONRequest } from '../../common/request' +import { scheduleDateToRequestReservation } from '../../common/schedule' export interface ScheduledReservationRequest { reservationRequest: ReservationRequest @@ -18,21 +15,29 @@ export interface ReservationSchedulerResult { scheduledReservationRequest?: ScheduledReservationRequest } -const handler: Handler = async ( - payload: string, - context: Context, +export interface ReservationSchedulerInput + extends Omit { + dateRange: { start: string; end: string } +} + +export const handler: Handler< + ReservationSchedulerInput, + ReservationSchedulerResult +> = async ( + payload: ReservationSchedulerInput, + context: Context ): Promise => { - const logger = new Logger(context.awsRequestId, LogLevel.DEBUG) - logger.debug('Handling event', { payload }) + Logger.instantiate(context.awsRequestId, LogLevel.DEBUG) + Logger.debug('Handling event', { payload }) let reservationRequest: ReservationRequest try { - reservationRequest = validateRequest(payload) + reservationRequest = validateJSONRequest(payload) } catch (err) { - logger.error('Failed to validate request', { err }) + Logger.error('Failed to validate request', { err }) throw err } - logger.debug('Successfully validated request', { reservationRequest }) + Logger.debug('Successfully validated request', { reservationRequest }) const res = new Reservation( reservationRequest.dateRange, @@ -40,11 +45,11 @@ const handler: Handler = async ( ) if (!res.isAvailableForReservation()) { - logger.debug('Reservation date is more than 7 days away') + Logger.debug('Reservation date is more than 7 days away') const scheduledDay = scheduleDateToRequestReservation( reservationRequest.dateRange.start ) - logger.info( + Logger.info( `Scheduling reservation request for ${scheduledDay.format('YYYY-MM-DD')}` ) return { @@ -55,10 +60,8 @@ const handler: Handler = async ( } } - logger.info('Reservation request can be performed now') + Logger.info('Reservation request can be performed now') return { scheduledReservationRequest: { reservationRequest }, } } - -export default handler diff --git a/src/lambdas/reservationScheduler/rollup.config.js b/src/lambdas/reservationScheduler/rollup.config.js new file mode 100644 index 0000000..48dc60b --- /dev/null +++ b/src/lambdas/reservationScheduler/rollup.config.js @@ -0,0 +1,20 @@ +// rollup.config.js +import path from 'path' + +import typescript from '@rollup/plugin-typescript' +import { nodeResolve } from '@rollup/plugin-node-resolve' +import commonjs from '@rollup/plugin-commonjs' + +export default { + input: path.join(__dirname, 'index.ts'), + output: { + file: './dist/reservationScheduler/index.js', + format: 'cjs', + sourcemap: true, + }, + plugins: [ + typescript(), + nodeResolve(), + commonjs(), + ] +} \ No newline at end of file diff --git a/src/local.ts b/src/local.ts index a979e17..acd2c8d 100644 --- a/src/local.ts +++ b/src/local.ts @@ -1,12 +1,13 @@ -import { IncomingRequest } from './common/request' +import dayjs from 'dayjs' +import { ReservationRequest } from './common/request' import { Reservation } from './common/reservation' import { Runner } from './common/runner' -const run = async (request: IncomingRequest) => { - const { username, password, dateTimes, opponent } = request - const reservations = dateTimes.map((dt) => new Reservation(dt, opponent)) +const run = async (request: ReservationRequest) => { + const { username, password, dateRange, opponent } = request + const reservation = new Reservation(dateRange, opponent) - const runner = new Runner(username, password, reservations) + const runner = new Runner(username, password, [reservation]) await runner.run({ headless: false }) } @@ -38,26 +39,13 @@ const [endHour, endMinute] = endTime.split(':').map((t) => Number.parseInt(t)) run({ username: username, password: password, - dateTimes: [ - { - year: Number.parseInt(year), - month: Number.parseInt(month), - day: Number.parseInt(day), - timeRange: { - start: { - hour: startHour, - minute: startMinute, - }, - end: { - hour: endHour, - minute: endMinute, - }, - }, - }, - ], + dateRange: { + start: dayjs(`${year}-${month}-${day}T${startHour}:${startMinute}`), + end: dayjs(`${year}-${month}-${day}T${endHour}:${endMinute}`), + }, opponent: { - id: opponentId, name: opponentName, + id: opponentId, }, }) .then(() => console.log('Success')) diff --git a/tests/common/logger.test.ts b/tests/common/logger.test.ts new file mode 100644 index 0000000..9fa1f4e --- /dev/null +++ b/tests/common/logger.test.ts @@ -0,0 +1,44 @@ +import { Logger, LogLevel } from '../../src/common/logger' + +describe('Logger', () => { + test('should create a single instance of LoggerInstance', () => { + const a = Logger.instantiate('abc', LogLevel.DEBUG) + const b = Logger.getInstance() + + expect(a).toStrictEqual(b) + }) + + test('should log messages', () => { + const consoleLogSpy = jest.fn() + const consoleErrorSpy = jest.fn() + jest.spyOn(console, 'log').mockImplementation(consoleLogSpy) + jest.spyOn(console, 'error').mockImplementation(consoleErrorSpy) + + Logger.instantiate('abc', LogLevel.DEBUG) + Logger.debug('first') + Logger.info('second') + Logger.error('third', { errorMessage: 'test' }) + + expect(consoleLogSpy).toHaveBeenCalledTimes(2) + expect(consoleLogSpy).toHaveBeenNthCalledWith( + 1, '[%s] %s: %s', 'abc', 'DEBUG', 'first' + ) + expect(consoleLogSpy).toHaveBeenNthCalledWith( + 2, '[%s] %s: %s', 'abc', 'INFO', 'second' + ) + expect(consoleErrorSpy).toHaveBeenCalledTimes(1) + expect(consoleErrorSpy).toHaveBeenCalledWith( + '[%s] %s: %s - %O', 'abc', 'ERROR', 'third', { "errorMessage": "test" } + ) + }) + + test('should log only when level is >= LogLevel of LoggerInstance', () => { + const consoleLogSpy = jest.fn() + jest.spyOn(console, 'log').mockImplementationOnce(consoleLogSpy) + + Logger.instantiate('abc', LogLevel.INFO) + Logger.debug('should\'t appear') + + expect(consoleLogSpy).not.toHaveBeenCalled() + }) +}) \ No newline at end of file diff --git a/tests/common/request.test.ts b/tests/common/request.test.ts index 8359a71..57285e9 100644 --- a/tests/common/request.test.ts +++ b/tests/common/request.test.ts @@ -1,13 +1,14 @@ import dayjs from 'dayjs' import { - validateRequest, + validateJSONRequest, + validateStringRequest, ValidationError, ValidationErrorCode, } from '../../src/common/request' describe('request', () => { - describe('validateRequest', () => { + describe('validateStringRequest', () => { test('should return ReservationRequest', () => { const body = JSON.stringify({ username: 'collin', @@ -22,11 +23,11 @@ describe('request', () => { } }) - expect(() => validateRequest(body)).not.toThrow() + expect(() => validateStringRequest(body)).not.toThrow() }) test('should fail for undefined body', () => { - expect(() => validateRequest(undefined)).toThrowError(new ValidationError('Invalid request', ValidationErrorCode.UNDEFINED_REQUEST_BODY)) + expect(() => validateStringRequest(undefined)).toThrowError(new ValidationError('Invalid request', ValidationErrorCode.UNDEFINED_REQUEST_BODY)) }) test('should fail for invalid json', () => { @@ -43,7 +44,7 @@ describe('request', () => { } }` - expect(() => validateRequest(body)).toThrowError(new ValidationError('Invalid request', ValidationErrorCode.INVALID_JSON)) + expect(() => validateStringRequest(body)).toThrowError(new ValidationError('Invalid request', ValidationErrorCode.INVALID_JSON)) }) test.each([ @@ -60,7 +61,7 @@ describe('request', () => { { username: 'collin', password: '1qaz2wsx', dateRange: { start: '1', end: '1' }, opponent: { id: '123', name: '' } }, { username: 'collin', password: '1qaz2wsx', dateRange: { start: '1', end: '1' }, opponent: { id: '123' } }, ])('should fail for body missing required values', (body) => { - expect(() => validateRequest(JSON.stringify(body))).toThrowError(new ValidationError('Invalid request', ValidationErrorCode.INVALID_REQUEST_BODY)) + expect(() => validateStringRequest(JSON.stringify(body))).toThrowError(new ValidationError('Invalid request', ValidationErrorCode.INVALID_REQUEST_BODY)) }) test('should fail for invalid date range', () => { @@ -77,7 +78,7 @@ describe('request', () => { } }) - expect(() => validateRequest(body)).toThrowError(new ValidationError('Invalid request', ValidationErrorCode.INVALID_DATE_RANGE)) + expect(() => validateStringRequest(body)).toThrowError(new ValidationError('Invalid request', ValidationErrorCode.INVALID_DATE_RANGE)) }) test.each([ @@ -97,7 +98,7 @@ describe('request', () => { } }) - expect(() => validateRequest(body)).toThrowError(new ValidationError('Invalid request', ValidationErrorCode.INVALID_START_OR_END_DATE)) + expect(() => validateStringRequest(body)).toThrowError(new ValidationError('Invalid request', ValidationErrorCode.INVALID_START_OR_END_DATE)) }) test('should not fail if no opponent is provided', () => { @@ -110,7 +111,7 @@ describe('request', () => { }, }) - expect(() => validateRequest(body)).not.toThrow() + expect(() => validateStringRequest(body)).not.toThrow() }) test.each([ @@ -129,7 +130,26 @@ describe('request', () => { opponent, }) - expect(() => validateRequest(body)).toThrowError(new ValidationError('Invalid request', ValidationErrorCode.INVALID_OPPONENT)) + expect(() => validateStringRequest(body)).toThrowError(new ValidationError('Invalid request', ValidationErrorCode.INVALID_OPPONENT)) + }) + }) + + describe('validateJSONRequest', () => { + test('should return ReservationRequest', () => { + const body = { + username: 'collin', + password: '123abc', + dateRange: { + start: '2021-12-25T12:34:56Z', + end: '2021-12-25T12:45:56Z' + }, + opponent: { + id: '123', + name: 'collin', + } + } + + expect(() => validateJSONRequest(body)).not.toThrow() }) }) }) \ No newline at end of file diff --git a/tests/lambdas/reservationScheduler.test.ts b/tests/lambdas/reservationScheduler.test.ts index 7bac066..1f95dbe 100644 --- a/tests/lambdas/reservationScheduler.test.ts +++ b/tests/lambdas/reservationScheduler.test.ts @@ -1,6 +1,6 @@ import dayjs from 'dayjs' import { ValidationError, ValidationErrorCode } from '../../src/common/request' -import handler, { ReservationSchedulerResult } from '../../src/lambdas/reservationScheduler' +import { handler, ReservationSchedulerInput, ReservationSchedulerResult } from '../../src/lambdas/reservationScheduler' jest.mock('../../src/common/logger') @@ -9,12 +9,12 @@ describe('reservationScheduler', () => { const start = dayjs().add(15, 'minutes') const end = start.add(15, 'minutes') - const payload = '{' + - '"username": "collin",' + - '"password": "password",' + - `"dateRange": { "start": "${start.toISOString()}", "end": "${end.toISOString()}" },` + - '"opponent": { "id": "123", "name": "collin" }' + - '}' + const payload: ReservationSchedulerInput = { + username: "collin", + password: "password", + dateRange: { start: start.toISOString(), end: end.toISOString() }, + opponent: { id: "123", name: "collin" } + } // @ts-expect-error - Stubbing AWS context await expect(handler(payload, { awsRequestId: '1234' }, undefined)).resolves @@ -32,12 +32,12 @@ describe('reservationScheduler', () => { test('should handle valid requests outside of reservation window', async () => { const start = dayjs().add(15, 'days') const end = start.add(15, 'minutes') - const payload = '{' + - '"username": "collin",' + - '"password": "password",' + - `"dateRange": { "start": "${start.toISOString()}", "end": "${end.toISOString()}" },` + - '"opponent": { "id": "123", "name": "collin" }' + - '}' + const payload: ReservationSchedulerInput = { + username: "collin", + password: "password", + dateRange: { start: start.toISOString(), end: end.toISOString() }, + opponent: { id: "123", name: "collin" } + } // @ts-expect-error - Stubbing AWS context await expect(handler(payload, { awsRequestId: '1234' }, undefined)).resolves.toMatchObject({ @@ -56,16 +56,16 @@ describe('reservationScheduler', () => { test('should throw error for invalid requests', async () => { const start = dayjs().add(15, 'days') const end = start.add(15, 'minutes') - const payload = '{invalidJson' + - '"username": "collin",' + - '"password": "password",' + - `"dateRange": { "start": "${start.format()}", "end": "${end.format()}" },` + - '"opponent": { "id": "123", "name": "collin" }' + - '}' + + const payload: ReservationSchedulerInput = { + password: "password", + dateRange: { start: start.toISOString(), end: end.toISOString() }, + opponent: { id: "123", name: "collin" } + } // @ts-expect-error - Stubbing AWS context await expect(handler(payload, { awsRequestId: '1234' }, undefined)) .rejects - .toThrowError(new ValidationError('Invalid request', ValidationErrorCode.INVALID_JSON)) + .toThrowError(new ValidationError('Invalid request', ValidationErrorCode.INVALID_REQUEST_BODY)) }) }) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 9fcf415..f29017c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,19 +2,19 @@ "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { - "lib": ["es2021"], - "module": "commonjs", - "target": "es2021", + "lib": ["esnext"], + "module": "esnext", + "moduleResolution": "node", + "target": "es6", + "sourceMap": true, "strict": true, "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "outDir": "dist/" + "outDir": "./dist/" }, "include": [ - "src/**/*.ts" + "./src/**/*.ts" ] } \ No newline at end of file