diff --git a/.gitignore b/.gitignore index c4b6723..070419f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # dependencies /node_modules +/backend/node_modules +/backend/dist /.pnp .pnp.* .yarn/* diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..3a45b69 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,1109 @@ +{ + "name": "sixflags-backend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sixflags-backend", + "version": "0.1.0", + "dependencies": { + "@hono/node-server": "^2.0.0", + "better-sqlite3": "^12.8.0", + "hono": "^4.7.0", + "node-cron": "^3.0.3" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/node": "^22", + "@types/node-cron": "^3.0.11", + "tsx": "^4.21.0", + "typescript": "^5" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.0.tgz", + "integrity": "sha512-n3GfHwwCvHCkGmOwKfxUPOlbfzuO64Sbc5XC4NGPIXxkuOnJrdgExdRKmHfF924r914WRJPT397GdqLvdYTeyQ==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-cron": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", + "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.9.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.9.0.tgz", + "integrity": "sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/hono": { + "version": "4.12.14", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", + "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "license": "ISC", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..e683749 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,24 @@ +{ + "name": "sixflags-backend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@hono/node-server": "^2.0.0", + "better-sqlite3": "^12.8.0", + "hono": "^4.7.0", + "node-cron": "^3.0.3" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/node": "^22", + "@types/node-cron": "^3.0.11", + "tsx": "^4.21.0", + "typescript": "^5" + } +} diff --git a/backend/src/db/index.ts b/backend/src/db/index.ts new file mode 100644 index 0000000..b78b5e4 --- /dev/null +++ b/backend/src/db/index.ts @@ -0,0 +1,39 @@ +import Database from "better-sqlite3"; +import path from "path"; +import fs from "fs"; + +const DATA_DIR = path.join(process.cwd(), "data"); +const DB_PATH = path.join(DATA_DIR, "parks.db"); + +let _db: Database.Database | null = null; + +export function getDb(): Database.Database { + if (_db) return _db; + fs.mkdirSync(DATA_DIR, { recursive: true }); + _db = new Database(DB_PATH); + _db.pragma("journal_mode = WAL"); + _db.exec(` + CREATE TABLE IF NOT EXISTS park_days ( + park_id TEXT NOT NULL, + date TEXT NOT NULL, + is_open INTEGER NOT NULL DEFAULT 0, + hours_label TEXT, + special_type TEXT, + scraped_at TEXT NOT NULL, + PRIMARY KEY (park_id, date) + ) + `); + try { + _db.exec(`ALTER TABLE park_days ADD COLUMN special_type TEXT`); + } catch { + // Column already exists + } + return _db; +} + +export function closeDb(): void { + if (_db) { + _db.close(); + _db = null; + } +} diff --git a/backend/src/db/queries.ts b/backend/src/db/queries.ts new file mode 100644 index 0000000..61c1fc6 --- /dev/null +++ b/backend/src/db/queries.ts @@ -0,0 +1,165 @@ +import type Database from "better-sqlite3"; +import { getDb } from "./index"; + +export interface DayData { + isOpen: boolean; + hoursLabel: string | null; + specialType: string | null; +} + +interface DayRow { + park_id: string; + date: string; + is_open: number; + hours_label: string | null; + special_type: string | null; +} + +function rowToDayData(row: DayRow): DayData { + return { + isOpen: row.is_open === 1, + hoursLabel: row.hours_label, + specialType: row.special_type, + }; +} + +export function upsertDay( + parkId: string, + date: string, + isOpen: boolean, + hoursLabel?: string, + specialType?: string, +): void { + getDb() + .prepare( + `INSERT INTO park_days (park_id, date, is_open, hours_label, special_type, scraped_at) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT (park_id, date) DO UPDATE SET + is_open = excluded.is_open, + hours_label = excluded.hours_label, + special_type = excluded.special_type, + scraped_at = excluded.scraped_at + WHERE park_days.date >= date('now')`, + ) + .run(parkId, date, isOpen ? 1 : 0, hoursLabel ?? null, specialType ?? null, new Date().toISOString()); +} + +export function getDateRange( + startDate: string, + endDate: string, +): Record> { + const rows = getDb() + .prepare( + `SELECT park_id, date, is_open, hours_label, special_type + FROM park_days + WHERE date >= ? AND date <= ?`, + ) + .all(startDate, endDate) as DayRow[]; + + const result: Record> = {}; + for (const row of rows) { + if (!result[row.park_id]) result[row.park_id] = {}; + result[row.park_id][row.date] = rowToDayData(row); + } + return result; +} + +export function getParkMonthData( + parkId: string, + year: number, + month: number, +): Record { + const prefix = `${year}-${String(month).padStart(2, "0")}`; + const rows = getDb() + .prepare( + `SELECT park_id, date, is_open, hours_label, special_type + FROM park_days + WHERE park_id = ? AND date LIKE ? || '-%' + ORDER BY date`, + ) + .all(parkId, prefix) as DayRow[]; + + const result: Record = {}; + for (const row of rows) { + result[row.date] = rowToDayData(row); + } + return result; +} + +export function getMonthCalendar( + year: number, + month: number, +): Record { + const prefix = `${year}-${String(month).padStart(2, "0")}`; + const rows = getDb() + .prepare( + `SELECT park_id, date, is_open + FROM park_days + WHERE date LIKE ? || '-%' + ORDER BY date`, + ) + .all(prefix) as { park_id: string; date: string; is_open: number }[]; + + const result: Record = {}; + for (const row of rows) { + if (!result[row.park_id]) result[row.park_id] = []; + const day = parseInt(row.date.slice(8), 10); + result[row.park_id][day - 1] = row.is_open === 1; + } + return result; +} + +export function getDayData(parkId: string, date: string): DayData | null { + const row = getDb() + .prepare( + `SELECT park_id, date, is_open, hours_label, special_type + FROM park_days + WHERE park_id = ? AND date = ?`, + ) + .get(parkId, date) as DayRow | undefined; + + return row ? rowToDayData(row) : null; +} + +export function isMonthScraped( + parkId: string, + year: number, + month: number, + staleAfterMs: number, +): boolean { + const daysInMonth = new Date(year, month, 0).getDate(); + const lastDay = `${year}-${String(month).padStart(2, "0")}-${String(daysInMonth).padStart(2, "0")}`; + const today = new Date().toISOString().slice(0, 10); + + if (lastDay < today) return true; + + const prefix = `${year}-${String(month).padStart(2, "0")}`; + const row = getDb() + .prepare( + `SELECT MAX(scraped_at) AS last_scraped + FROM park_days + WHERE park_id = ? AND date LIKE ? || '-%'`, + ) + .get(parkId, prefix) as { last_scraped: string | null }; + + if (!row.last_scraped) return false; + return Date.now() - new Date(row.last_scraped).getTime() < staleAfterMs; +} + +export function getLastScrapeTime(): string | null { + const row = getDb() + .prepare(`SELECT MAX(scraped_at) AS last_scraped FROM park_days`) + .get() as { last_scraped: string | null }; + return row.last_scraped; +} + +export function getParkDayCount(): number { + const row = getDb() + .prepare(`SELECT COUNT(*) AS count FROM park_days`) + .get() as { count: number }; + return row.count; +} + +export function transact(fn: () => void): void { + getDb().transaction(fn)(); +} diff --git a/backend/src/index.ts b/backend/src/index.ts new file mode 100644 index 0000000..f329ad7 --- /dev/null +++ b/backend/src/index.ts @@ -0,0 +1,38 @@ +import { Hono } from "hono"; +import { serve } from "@hono/node-server"; +import { cors } from "hono/cors"; +import { logger } from "hono/logger"; + +import { getDb } from "./db/index"; +import { startScheduler } from "./services/scheduler"; + +import calendarRoutes from "./routes/calendar"; +import parksRoutes from "./routes/parks"; +import ridesRoutes from "./routes/rides"; +import statusRoutes from "./routes/status"; +import scrapeRoutes from "./routes/scrape"; + +const PORT = parseInt(process.env.PORT ?? "3001", 10); + +const app = new Hono(); + +app.use("*", logger()); +app.use("*", cors()); + +app.route("/api/calendar", calendarRoutes); +app.route("/api/parks", parksRoutes); +app.route("/api/parks", ridesRoutes); +app.route("/api/status", statusRoutes); +app.route("/api/scrape", scrapeRoutes); + +// Initialize database on startup +getDb(); +console.log("[backend] database initialized"); + +// Start cron scheduler +startScheduler(); + +// Start HTTP server +serve({ fetch: app.fetch, port: PORT }, (info) => { + console.log(`[backend] listening on http://localhost:${info.port}`); +}); diff --git a/backend/src/routes/calendar.ts b/backend/src/routes/calendar.ts new file mode 100644 index 0000000..7e0a726 --- /dev/null +++ b/backend/src/routes/calendar.ts @@ -0,0 +1,160 @@ +import { Hono } from "hono"; +import { PARKS } from "../../../lib/parks"; +import { QUEUE_TIMES_IDS } from "../../../lib/queue-times-map"; +import { getCoasterSet } from "../../../lib/coaster-data"; +import { getTodayLocal, isWithinOperatingWindow, getOperatingStatus } from "../../../lib/env"; +import { fetchToday } from "../../../lib/scrapers/sixflags"; +import { fetchLiveRides } from "../../../lib/scrapers/queuetimes"; +import { getDateRange, getParkMonthData, type DayData } from "../db/queries"; +import { TtlCache } from "../services/cache"; + +const todayCache = new TtlCache<{ date: string; isOpen: boolean; hoursLabel?: string; specialType?: string } | null>(5 * 60 * 1000); +const ridesCache = new TtlCache<{ openRides: number; openCoasters: number } | null>(5 * 60 * 1000); + +const app = new Hono(); + +app.get("/week", async (c) => { + const startParam = c.req.query("start"); + if (!startParam || !/^\d{4}-\d{2}-\d{2}$/.test(startParam)) { + return c.json({ error: "Missing or invalid ?start=YYYY-MM-DD" }, 400); + } + + const weekDates = Array.from({ length: 7 }, (_, i) => { + const d = new Date(startParam + "T00:00:00"); + d.setDate(d.getDate() + i); + return d.toISOString().slice(0, 10); + }); + const endDate = weekDates[6]; + const today = getTodayLocal(); + + const data = getDateRange(startParam, endDate); + + // Merge live today data + if (weekDates.includes(today)) { + await Promise.all( + PARKS.map(async (p) => { + let live = todayCache.get(p.id); + if (live === null && !todayCache.get(p.id + "_checked")) { + live = await fetchToday(p.apiId).catch(() => null); + todayCache.set(p.id, live); + todayCache.set(p.id + "_checked", true as any); + } + if (!live) return; + if (!data[p.id]) data[p.id] = {}; + data[p.id][today] = { + isOpen: live.isOpen, + hoursLabel: live.hoursLabel ?? null, + specialType: live.specialType ?? null, + }; + }), + ); + } + + const currentWeekStart = (() => { + const d = new Date(today + "T00:00:00"); + d.setDate(d.getDate() - d.getDay()); + return d.toISOString().slice(0, 10); + })(); + const isCurrentWeek = startParam === currentWeekStart; + + // Live status for today + let rideCounts: Record = {}; + let coasterCounts: Record = {}; + let openParkIds: string[] = []; + let closingParkIds: string[] = []; + let weatherDelayParkIds: string[] = []; + + if (weekDates.includes(today)) { + const openTodayParks = PARKS.filter((p) => { + const dayData = data[p.id]?.[today]; + if (!dayData?.isOpen || !dayData.hoursLabel) return false; + return isWithinOperatingWindow(dayData.hoursLabel, p.timezone); + }); + openParkIds = openTodayParks.map((p) => p.id); + closingParkIds = openTodayParks + .filter((p) => { + const dayData = data[p.id]?.[today]; + return dayData?.hoursLabel ? getOperatingStatus(dayData.hoursLabel, p.timezone) === "closing" : false; + }) + .map((p) => p.id); + + const trackedParks = openTodayParks.filter((p) => QUEUE_TIMES_IDS[p.id]); + const results = await Promise.all( + trackedParks.map(async (p) => { + let cached = ridesCache.get(p.id); + if (cached === null) { + const coasterSet = getCoasterSet(p.id); + const result = await fetchLiveRides(QUEUE_TIMES_IDS[p.id], coasterSet).catch(() => null); + cached = result + ? { + openRides: result.rides.filter((r) => r.isOpen).length, + openCoasters: result.rides.filter((r) => r.isOpen && r.isCoaster).length, + } + : null; + ridesCache.set(p.id, cached); + } + return { id: p.id, ...(cached ?? { openRides: 0, openCoasters: 0 }) }; + }), + ); + + weatherDelayParkIds = results.filter(({ openRides }) => openRides === 0).map(({ id }) => id); + rideCounts = Object.fromEntries(results.filter(({ openRides }) => openRides > 0).map(({ id, openRides }) => [id, openRides])); + coasterCounts = Object.fromEntries(results.filter(({ openCoasters }) => openCoasters > 0).map(({ id, openCoasters }) => [id, openCoasters])); + } + + const scrapedCount = Object.values(data).reduce((sum, parkData) => sum + Object.keys(parkData).length, 0); + + c.header("Cache-Control", "public, max-age=120, stale-while-revalidate=300"); + return c.json({ + weekStart: startParam, + weekDates, + today, + isCurrentWeek, + data, + rideCounts, + coasterCounts, + openParkIds, + closingParkIds, + weatherDelayParkIds, + hasCoasterData: true, + scrapedCount, + }); +}); + +app.get("/:parkId/month", async (c) => { + const parkId = c.req.param("parkId"); + const monthParam = c.req.query("month"); + + if (!monthParam || !/^\d{4}-\d{2}$/.test(monthParam)) { + return c.json({ error: "Missing or invalid ?month=YYYY-MM" }, 400); + } + + const [yearStr, monthStr] = monthParam.split("-"); + const year = parseInt(yearStr); + const month = parseInt(monthStr); + + if (month < 1 || month > 12) { + return c.json({ error: "Month must be 1-12" }, 400); + } + + const monthData = getParkMonthData(parkId, year, month); + const today = getTodayLocal(); + + // Merge live today if viewing current month + const park = PARKS.find((p) => p.id === parkId); + if (park) { + const liveToday = await fetchToday(park.apiId).catch(() => null); + if (liveToday) { + monthData[today] = { + isOpen: liveToday.isOpen, + hoursLabel: liveToday.hoursLabel ?? null, + specialType: liveToday.specialType ?? null, + }; + } + } + + c.header("Cache-Control", "public, max-age=300, stale-while-revalidate=600"); + return c.json({ parkId, year, month, monthData, today }); +}); + +export default app; diff --git a/backend/src/routes/parks.ts b/backend/src/routes/parks.ts new file mode 100644 index 0000000..1c746d6 --- /dev/null +++ b/backend/src/routes/parks.ts @@ -0,0 +1,19 @@ +import { Hono } from "hono"; +import { PARKS, PARK_MAP } from "../../../lib/parks"; + +const app = new Hono(); + +app.get("/", (c) => { + c.header("Cache-Control", "public, max-age=3600"); + return c.json({ parks: PARKS }); +}); + +app.get("/:id", (c) => { + const park = PARK_MAP.get(c.req.param("id")); + if (!park) return c.json({ error: "Park not found" }, 404); + + c.header("Cache-Control", "public, max-age=3600"); + return c.json(park); +}); + +export default app; diff --git a/backend/src/routes/rides.ts b/backend/src/routes/rides.ts new file mode 100644 index 0000000..c55ed32 --- /dev/null +++ b/backend/src/routes/rides.ts @@ -0,0 +1,66 @@ +import { Hono } from "hono"; +import { PARK_MAP } from "../../../lib/parks"; +import { QUEUE_TIMES_IDS } from "../../../lib/queue-times-map"; +import { getCoasterSet } from "../../../lib/coaster-data"; +import { getTodayLocal, isWithinOperatingWindow } from "../../../lib/env"; +import { fetchLiveRides } from "../../../lib/scrapers/queuetimes"; +import { scrapeRidesForDay } from "../../../lib/scrapers/sixflags"; +import { getDayData } from "../db/queries"; +import { TtlCache } from "../services/cache"; +import type { LiveRidesResult } from "../../../lib/scrapers/queuetimes"; + +const liveRidesCache = new TtlCache(5 * 60 * 1000); + +const app = new Hono(); + +app.get("/:id/rides", async (c) => { + const id = c.req.param("id"); + const park = PARK_MAP.get(id); + if (!park) return c.json({ error: "Park not found" }, 404); + + const today = getTodayLocal(); + const todayData = getDayData(id, today); + const withinWindow = todayData?.hoursLabel + ? isWithinOperatingWindow(todayData.hoursLabel, park.timezone) + : false; + + const queueTimesId = QUEUE_TIMES_IDS[id]; + let liveRides: LiveRidesResult | null = null; + + if (queueTimesId) { + liveRides = liveRidesCache.get(id); + if (liveRides === null) { + const coasterSet = getCoasterSet(id); + liveRides = await fetchLiveRides(queueTimesId, coasterSet).catch(() => null); + if (liveRides) liveRidesCache.set(id, liveRides); + } + + if (liveRides && !withinWindow) { + liveRides = { + ...liveRides, + rides: liveRides.rides.map((r) => ({ ...r, isOpen: false, waitMinutes: 0 })), + }; + } + } + + const isWeatherDelay = + withinWindow && liveRides !== null && liveRides.rides.length > 0 && liveRides.rides.every((r) => !r.isOpen); + + let scheduleFallback = null; + if (!liveRides) { + scheduleFallback = await scrapeRidesForDay(park.apiId, today).catch(() => null); + } + + c.header("Cache-Control", "public, max-age=60, stale-while-revalidate=120"); + return c.json({ + parkId: id, + today, + parkOpenToday: !!(todayData?.isOpen && todayData?.hoursLabel), + withinWindow, + isWeatherDelay, + liveRides, + scheduleFallback, + }); +}); + +export default app; diff --git a/backend/src/routes/scrape.ts b/backend/src/routes/scrape.ts new file mode 100644 index 0000000..7deb7ee --- /dev/null +++ b/backend/src/routes/scrape.ts @@ -0,0 +1,33 @@ +import { Hono } from "hono"; +import { scrapeToday, scrapeCurrentMonth, scrapeUpcomingMonths, scrapeFullYear } from "../services/scraper"; + +const app = new Hono(); + +app.post("/trigger", async (c) => { + const scope = c.req.query("scope") ?? "today"; + + let result; + switch (scope) { + case "today": + result = await scrapeToday(); + break; + case "month": + result = await scrapeCurrentMonth(); + break; + case "upcoming": + result = await scrapeUpcomingMonths(); + break; + case "full": + result = await scrapeFullYear(); + break; + case "force": + result = await scrapeFullYear(true); + break; + default: + return c.json({ error: "Invalid scope. Use: today, month, upcoming, full, force" }, 400); + } + + return c.json(result); +}); + +export default app; diff --git a/backend/src/routes/status.ts b/backend/src/routes/status.ts new file mode 100644 index 0000000..9edd583 --- /dev/null +++ b/backend/src/routes/status.ts @@ -0,0 +1,21 @@ +import { Hono } from "hono"; +import { PARKS } from "../../../lib/parks"; +import { getLastScrapeTime, getParkDayCount } from "../db/queries"; +import { getLastScrapeResult } from "../services/scraper"; + +const app = new Hono(); + +app.get("/", (c) => { + return c.json({ + status: "ok", + uptime: Math.floor(process.uptime()), + parks: PARKS.length, + database: { + totalDays: getParkDayCount(), + lastScrape: getLastScrapeTime(), + }, + lastScrapeResult: getLastScrapeResult(), + }); +}); + +export default app; diff --git a/backend/src/services/cache.ts b/backend/src/services/cache.ts new file mode 100644 index 0000000..3a2a0b4 --- /dev/null +++ b/backend/src/services/cache.ts @@ -0,0 +1,35 @@ +interface CacheEntry { + data: T; + expiresAt: number; +} + +export class TtlCache { + private store = new Map>(); + + constructor(private defaultTtlMs: number) {} + + get(key: string): T | null { + const entry = this.store.get(key); + if (!entry) return null; + if (Date.now() > entry.expiresAt) { + this.store.delete(key); + return null; + } + return entry.data; + } + + set(key: string, data: T, ttlMs?: number): void { + this.store.set(key, { + data, + expiresAt: Date.now() + (ttlMs ?? this.defaultTtlMs), + }); + } + + clear(): void { + this.store.clear(); + } + + get size(): number { + return this.store.size; + } +} diff --git a/backend/src/services/scheduler.ts b/backend/src/services/scheduler.ts new file mode 100644 index 0000000..74bc580 --- /dev/null +++ b/backend/src/services/scheduler.ts @@ -0,0 +1,39 @@ +import cron from "node-cron"; +import { scrapeToday, scrapeCurrentMonth, scrapeUpcomingMonths, scrapeFullYear } from "./scraper"; + +let initialized = false; + +export function startScheduler(): void { + if (initialized) return; + initialized = true; + + // Tier 1: Today — every hour during operating season (Mar-Dec) + cron.schedule("0 * * 3-12 *", async () => { + console.log(`[scheduler] tier-1: scraping today @ ${new Date().toISOString()}`); + await scrapeToday().catch((err) => console.error("[scheduler] tier-1 error:", err)); + }); + + // Tier 2: This week — every 6 hours, current month for all parks + cron.schedule("0 */6 * * *", async () => { + console.log(`[scheduler] tier-2: scraping current month @ ${new Date().toISOString()}`); + await scrapeCurrentMonth().catch((err) => console.error("[scheduler] tier-2 error:", err)); + }); + + // Tier 3: Upcoming — twice daily (3 AM, 3 PM), current + next month + cron.schedule("0 3,15 * * *", async () => { + console.log(`[scheduler] tier-3: scraping upcoming months @ ${new Date().toISOString()}`); + await scrapeUpcomingMonths().catch((err) => console.error("[scheduler] tier-3 error:", err)); + }); + + // Tier 4: Full season — once daily at 3 AM + cron.schedule("0 3 * * *", async () => { + console.log(`[scheduler] tier-4: scraping full year @ ${new Date().toISOString()}`); + await scrapeFullYear().catch((err) => console.error("[scheduler] tier-4 error:", err)); + }); + + console.log("[scheduler] cron jobs registered"); + console.log(" tier-1: today — hourly (Mar-Dec)"); + console.log(" tier-2: current month — every 6h"); + console.log(" tier-3: upcoming — 3 AM + 3 PM"); + console.log(" tier-4: full year — 3 AM daily"); +} diff --git a/backend/src/services/scraper.ts b/backend/src/services/scraper.ts new file mode 100644 index 0000000..62b2d67 --- /dev/null +++ b/backend/src/services/scraper.ts @@ -0,0 +1,143 @@ +import { PARKS } from "../../../lib/parks"; +import { scrapeMonth, fetchToday, RateLimitError } from "../../../lib/scrapers/sixflags"; +import { upsertDay, isMonthScraped, getDayData, transact } from "../db/queries"; +import { parseStalenessHours } from "../../../lib/env"; + +const DELAY_MS = 1000; +const STALE_AFTER_MS = parseStalenessHours(process.env.PARK_HOURS_STALENESS_HOURS, 72) * 60 * 60 * 1000; + +function sleep(ms: number) { + return new Promise((r) => setTimeout(r, ms)); +} + +export interface ScrapeResult { + scope: string; + fetched: number; + skipped: number; + errors: number; + updated: number; + startedAt: string; + finishedAt: string; +} + +let lastScrapeResult: ScrapeResult | null = null; + +export function getLastScrapeResult(): ScrapeResult | null { + return lastScrapeResult; +} + +export async function scrapeToday(): Promise { + const startedAt = new Date().toISOString(); + let fetched = 0; + let skipped = 0; + let errors = 0; + let updated = 0; + + for (const park of PARKS) { + try { + const live = await fetchToday(park.apiId); + if (!live) { + skipped++; + continue; + } + fetched++; + + const existing = getDayData(park.id, live.date); + if ( + existing && + existing.isOpen === live.isOpen && + existing.hoursLabel === (live.hoursLabel ?? null) && + existing.specialType === (live.specialType ?? null) + ) { + continue; + } + + upsertDay(park.id, live.date, live.isOpen, live.hoursLabel, live.specialType); + updated++; + console.log(`[today] ${park.shortName}: updated (${live.isOpen ? "open" : "closed"}${live.hoursLabel ? " " + live.hoursLabel : ""})`); + } catch { + errors++; + } + await sleep(500); + } + + const result: ScrapeResult = { + scope: "today", + fetched, + skipped, + errors, + updated, + startedAt, + finishedAt: new Date().toISOString(), + }; + lastScrapeResult = result; + console.log(`[today] done: ${fetched} fetched, ${updated} updated, ${skipped} skipped, ${errors} errors`); + return result; +} + +export async function scrapeMonths(monthList: { year: number; month: number }[], force = false): Promise { + const startedAt = new Date().toISOString(); + let fetched = 0; + let skipped = 0; + let errors = 0; + + for (const park of PARKS) { + for (const { year, month } of monthList) { + if (!force && isMonthScraped(park.id, year, month, STALE_AFTER_MS)) { + skipped++; + continue; + } + + try { + const days = await scrapeMonth(park.apiId, year, month); + transact(() => { + for (const d of days) { + upsertDay(park.id, d.date, d.isOpen, d.hoursLabel, d.specialType); + } + }); + fetched++; + console.log(`[month] ${park.shortName} ${year}-${String(month).padStart(2, "0")}: ${days.filter((d) => d.isOpen).length} open days`); + } catch (err) { + if (err instanceof RateLimitError) { + console.log(`[month] ${park.shortName}: rate limited`); + } else { + console.error(`[month] ${park.shortName}: error — ${err instanceof Error ? err.message : err}`); + } + errors++; + } + await sleep(DELAY_MS); + } + } + + const result: ScrapeResult = { + scope: `months(${monthList.map((m) => `${m.year}-${String(m.month).padStart(2, "0")}`).join(",")})`, + fetched, + skipped, + errors, + updated: fetched, + startedAt, + finishedAt: new Date().toISOString(), + }; + lastScrapeResult = result; + console.log(`[month] done: ${fetched} fetched, ${skipped} skipped, ${errors} errors`); + return result; +} + +export async function scrapeCurrentMonth(): Promise { + const now = new Date(); + return scrapeMonths([{ year: now.getFullYear(), month: now.getMonth() + 1 }]); +} + +export async function scrapeUpcomingMonths(): Promise { + const now = new Date(); + const current = { year: now.getFullYear(), month: now.getMonth() + 1 }; + const next = new Date(now.getFullYear(), now.getMonth() + 1, 1); + const nextMonth = { year: next.getFullYear(), month: next.getMonth() + 1 }; + return scrapeMonths([current, nextMonth]); +} + +export async function scrapeFullYear(force = false): Promise { + const year = new Date().getFullYear(); + const months = Array.from({ length: 12 }, (_, i) => ({ year, month: i + 1 })); + return scrapeMonths(months, force); +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..55e821d --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "outDir": "dist", + "rootDir": "..", + "baseUrl": "..", + "paths": { + "@lib/*": ["lib/*"] + }, + "skipLibCheck": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true + }, + "include": ["src/**/*.ts", "../lib/**/*.ts"], + "exclude": ["node_modules", "dist"] +}