From 4b61e73573daab8f7b6cec4c4370547132bad288 Mon Sep 17 00:00:00 2001 From: Oliver-Akins Date: Thu, 31 Aug 2023 19:12:34 -0600 Subject: [PATCH] Implement the majority of the API --- api/package-lock.json | 179 ++++++++++++++++++++++++++ api/package.json | 3 + api/src/endpoints/channels/watch.ts | 37 ++++++ api/src/endpoints/questions/add.ts | 56 ++++++++ api/src/endpoints/questions/list.ts | 25 ++++ api/src/endpoints/questions/update.ts | 77 +++++++++++ api/src/hapi.ts | 5 + api/src/main.ts | 28 +++- api/src/twitch-bot.ts | 34 +++++ api/src/types/database.d.ts | 7 + api/src/types/hapi.d.ts | 12 ++ api/src/types/utilities.ts | 6 + api/src/utils/database.ts | 0 database/init.sql | 9 +- docker-compose.yaml | 3 + 15 files changed, 475 insertions(+), 6 deletions(-) create mode 100644 api/src/endpoints/channels/watch.ts create mode 100644 api/src/endpoints/questions/add.ts create mode 100644 api/src/endpoints/questions/list.ts create mode 100644 api/src/endpoints/questions/update.ts create mode 100644 api/src/twitch-bot.ts create mode 100644 api/src/types/database.d.ts create mode 100644 api/src/types/hapi.d.ts create mode 100644 api/src/types/utilities.ts create mode 100644 api/src/utils/database.ts diff --git a/api/package-lock.json b/api/package-lock.json index d6db4ad..7eaf8a2 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -13,6 +13,8 @@ "glob": "^10.3.3", "joi": "^17.9.2", "module-alias": "^2.2.3", + "mysql2": "^3.6.0", + "tmi.js": "^1.8.5", "tsconfig-paths": "^4.2.0", "tslog": "^4.8.2" }, @@ -20,6 +22,7 @@ "@types/chai": "^4.3.5", "@types/mocha": "^10.0.1", "@types/node": "^20.4.7", + "@types/tmi.js": "^1.8.3", "chai": "^4.3.7", "mocha": "^10.2.0", "ts-mocha": "^10.0.0", @@ -521,6 +524,12 @@ "integrity": "sha512-bUBrPjEry2QUTsnuEjzjbS7voGWCc30W0qzgMf90GPeDGFRakvrz47ju+oqDAKCXLUCe39u57/ORMl/O/04/9g==", "dev": true }, + "node_modules/@types/tmi.js": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@types/tmi.js/-/tmi.js-1.8.3.tgz", + "integrity": "sha512-piKPU1DF+Lxnh9BV0gVpbJIMnKOQT6zT0o6NLE0DZgHNW7fGxReRRSp987+Ph1aYiyNWXXbxAeQAovTlEYC1hw==", + "dev": true + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -855,6 +864,14 @@ "node": ">=6" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -967,6 +984,14 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1050,6 +1075,17 @@ "he": "bin/he" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1125,6 +1161,11 @@ "node": ">=8" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -1238,6 +1279,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "node_modules/loupe": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", @@ -1402,6 +1448,51 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "node_modules/mysql2": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.0.tgz", + "integrity": "sha512-EWUGAhv6SphezurlfI2Fpt0uJEWLmirrtQR7SkbTHFC+4/mJBrPiSzHESHKAWKG7ALVD6xaG/NBjjd1DGJGQQQ==", + "dependencies": { + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru-cache": "^8.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "engines": { + "node": ">=16.14" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, "node_modules/nanoid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", @@ -1414,6 +1505,25 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1574,6 +1684,16 @@ } ] }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", @@ -1632,6 +1752,14 @@ "source-map": "^0.6.0" } }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -1717,6 +1845,18 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/tmi.js": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/tmi.js/-/tmi.js-1.8.5.tgz", + "integrity": "sha512-A9qrydfe1e0VWM9MViVhhxVgvLpnk7pFShVUWePsSTtoi+A1X+Zjdoa7OJd7/YsgHXGj3GkNEvnWop/1WwZuew==", + "dependencies": { + "node-fetch": "^2.6.1", + "ws": "^8.2.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1729,6 +1869,11 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-mocha": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/ts-mocha/-/ts-mocha-10.0.0.tgz", @@ -1920,6 +2065,20 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -1980,6 +2139,26 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/api/package.json b/api/package.json index 3171796..cc1c511 100644 --- a/api/package.json +++ b/api/package.json @@ -13,6 +13,8 @@ "glob": "^10.3.3", "joi": "^17.9.2", "module-alias": "^2.2.3", + "mysql2": "^3.6.0", + "tmi.js": "^1.8.5", "tsconfig-paths": "^4.2.0", "tslog": "^4.8.2" }, @@ -20,6 +22,7 @@ "@types/chai": "^4.3.5", "@types/mocha": "^10.0.1", "@types/node": "^20.4.7", + "@types/tmi.js": "^1.8.3", "chai": "^4.3.7", "mocha": "^10.2.0", "ts-mocha": "^10.0.0", diff --git a/api/src/endpoints/channels/watch.ts b/api/src/endpoints/channels/watch.ts new file mode 100644 index 0000000..94fad8c --- /dev/null +++ b/api/src/endpoints/channels/watch.ts @@ -0,0 +1,37 @@ +import { ServerRoute } from "@hapi/hapi"; +import boom from "@hapi/boom"; + +const route: ServerRoute = { + method: `POST`, path: `/{channel}`, + async handler(request, h) { + const { channel } = request.params; + const { bot, db } = request.server.app; + + let [ channels ] = await db.query>( + `select * from tqna.twitchs where channel = ?`, + [ channel ] + ); + if (channels.length > 0) { + return h.response().code(202); + }; + + try { + await bot.join(channel); + } catch { + throw boom.badRequest(`Could not connect to that channel`); + }; + + try { + await db.query( + `insert into tqna.twitchs ( channel ) values ( ? )`, + [ channel ] + ); + } catch { + await bot.part(channel); + throw boom.badImplementation(`Failed to save the channel`); + }; + + return h.response().code(204); + }, +}; +export default route; \ No newline at end of file diff --git a/api/src/endpoints/questions/add.ts b/api/src/endpoints/questions/add.ts new file mode 100644 index 0000000..fb1b775 --- /dev/null +++ b/api/src/endpoints/questions/add.ts @@ -0,0 +1,56 @@ +import { Query } from "~/types/utilities"; +import { ServerRoute } from "@hapi/hapi"; +import Joi from "joi"; + +const route: ServerRoute = { + method: `POST`, path: `/{channel}/questions`, + options: { + validate: { + params: Joi.object({ + channel: Joi.string().required(), + }), + payload: Joi.object({ + question: Joi.string().required(), + asker: Joi.string().required(), + }), + }, + }, + async handler(request, h) { + const { channel } = request.params; + const { db, log } = request.server.app; + const payload = request.payload as any; + log.debug(`Attempting to save a new question in channel: ${channel}`); + + let errored = false; + let question = null; + let conn = await db.getConnection(); + await conn.beginTransaction(); + + try { + await db.query( + `insert into tqna.questions ( channel, asker, question ) values ( ?, ?, ?)`, + [ channel, payload.asker, payload.question ] + ); + + let [ questions ] = await db.query>( + `select * from tqna.questions where id = last_insert_id() limit 1` + ); + question = questions[0]; + + await conn.commit(); + } catch { + log.error(`Failed to add the question`); + await conn.rollback(); + } finally { + conn.release(); + }; + + if (errored) { + throw new Error(); + }; + + log.debug(`Created new question in channel: ${channel}`); + return question; + }, +}; +export default route; \ No newline at end of file diff --git a/api/src/endpoints/questions/list.ts b/api/src/endpoints/questions/list.ts new file mode 100644 index 0000000..7c6e987 --- /dev/null +++ b/api/src/endpoints/questions/list.ts @@ -0,0 +1,25 @@ +import { ServerRoute } from "@hapi/hapi"; +import Joi from "joi"; + +const route: ServerRoute = { + method: `GET`, path: `/{channel}/questions`, + options: { + validate: { + params: Joi.object({ + channel: Joi.string().required(), + }), + }, + }, + async handler(request) { + const { channel } = request.params; + const { db, log } = request.server.app; + log.debug(`Listing questions for channel: ${channel}`); + + let [ questions ] = await db.query( + `select * from tqna.questions as q where q.channel = ?`, + [ channel ] + ); + return questions; + }, +}; +export default route; \ No newline at end of file diff --git a/api/src/endpoints/questions/update.ts b/api/src/endpoints/questions/update.ts new file mode 100644 index 0000000..e1cc557 --- /dev/null +++ b/api/src/endpoints/questions/update.ts @@ -0,0 +1,77 @@ +import { Query } from "~/types/utilities"; +import { ServerRoute } from "@hapi/hapi"; +import Joi from "joi"; + +const route: ServerRoute = { + method: `POST`, path: `/{channel}/questions/{question_id}`, + options: { + validate: { + params: Joi.object({ + channel: Joi + .string() + .required(), + question_id: Joi + .number() + .positive() + .integer() + .not(0) + .required(), + }), + payload: Joi.object({ + question: Joi.string().optional(), + asker: Joi.string().optional(), + answered: Joi.boolean().optional(), + id: Joi.forbidden(), + }) + .min(1), + }, + }, + async handler(request, h) { + const { channel, question_id } = request.params; + const { db, log } = request.server.app; + const payload = request.payload as any; + let question = null; + log.debug(`Updating question ${question_id} for channel ${channel}`); + + let conn = await db.getConnection(); + await conn.beginTransaction(); + + try { + let setters = []; + let values = []; + for (const key in payload) { + let v = payload[key]; + if (v.startsWith(`__`)) { + setters.push(`${key} = ${v}`); + } else { + setters.push(`${key} = ?`); + values.push(v); + }; + }; + + await db.query( + `update tqna.questions + ( ${setters.join(`, `)} ) + where channel = ? and id = ? + limit 1`, + [...values, channel, question_id ] + ); + + let [ questions ] = await db.query>( + `select * from tqna.questions where id = ? limit 1`, + [ question_id ] + ); + question = questions[0]; + + await conn.commit(); + } catch { + log.error(`Failed to add the question`); + await conn.rollback(); + } finally { + conn.release(); + }; + + return question; + }, +}; +export default route; \ No newline at end of file diff --git a/api/src/hapi.ts b/api/src/hapi.ts index 80ce56e..aaba203 100644 --- a/api/src/hapi.ts +++ b/api/src/hapi.ts @@ -1,11 +1,15 @@ import { Server } from "@hapi/hapi"; import { globSync } from "glob"; import path from "path"; +import { log } from "./main"; const server = new Server({ port: 6969, host: `0.0.0.0`, + debug: { + request: [ `*` ], + }, }); @@ -17,6 +21,7 @@ async function registerRoutes() { for (const file of files) { let route = (await import(path.join(__dirname, file))).default; server.route(route); + log.debug(`Registered route: ${route.method} ${route.path}`); }; }; diff --git a/api/src/main.ts b/api/src/main.ts index 3f815dc..faa8c3b 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -2,10 +2,12 @@ // at the top of this file as the first statement import "module-alias/register"; +import { start as startBot } from "~/twitch-bot"; +import { start as startApi } from "~/hapi"; +import mysql from "mysql2/promise"; import { Logger } from "tslog"; -import { start } from "~/hapi"; -export const isDev = process.env.NODE_ENV?.startsWith(`dev`); +export const isDev = !!process.env.NODE_ENV?.startsWith(`dev`); let logLevel = 4; if (process.env.LOG_LEVEL) { @@ -22,8 +24,26 @@ export const log = new Logger({ async function main() { - let server = await start(); - log.info(`Server listening`) + + const db = mysql.createPool({ + host: process.env.MYSQL_HOST, + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD, + database: `tqna`, + namedPlaceholders: true, + }); + + + let server = await startApi(); + server.app.log = log; + server.app.isDev = isDev; + server.app.db = db; + log.info(`Server listening`); + + + let tmi = await startBot(db, server); + server.app.bot = tmi; + log.info(`Bot listening`); }; if (require.main === module) { diff --git a/api/src/twitch-bot.ts b/api/src/twitch-bot.ts new file mode 100644 index 0000000..29a0bfe --- /dev/null +++ b/api/src/twitch-bot.ts @@ -0,0 +1,34 @@ +import { Pool, RowDataPacket } from "mysql2/promise"; +import { Server } from "@hapi/hapi"; +import { log } from "./main"; +import tmi from "tmi.js"; + +export async function start(db: Pool, server: Server) { + let [ channels ] = await db.query>({ + sql: `select channel from tqna.twitchs`, + rowsAsArray: true + }); + + const client = new tmi.Client({ + channels: [...new Set(channels.flat())] as string[], + }); + + client.on(`message`, async (channel, user, message, self) => { + log.silly(`Twitch message: [c:${channel}] [u:${user}] ${message}`); + if (self) { return }; + + if (message.match(/^[Q]:/i)) { + await server.inject({ + method: `POST`, url: `/${channel.slice(1)}/questions`, + payload: { + question: message.slice(2).trim(), + asker: user["display-name"] ?? user.username, + }, + }); + }; + }); + + await client.connect(); + + return client; +}; \ No newline at end of file diff --git a/api/src/types/database.d.ts b/api/src/types/database.d.ts new file mode 100644 index 0000000..7b0f67b --- /dev/null +++ b/api/src/types/database.d.ts @@ -0,0 +1,7 @@ +interface Question { + readonly __id: number; + channel: number; + asker: string; + question: string; + answered: boolean; +} \ No newline at end of file diff --git a/api/src/types/hapi.d.ts b/api/src/types/hapi.d.ts new file mode 100644 index 0000000..142e896 --- /dev/null +++ b/api/src/types/hapi.d.ts @@ -0,0 +1,12 @@ +import mysql from "mysql2/promise"; +import { Client } from "tmi.js"; +import { Logger } from "tslog"; + +declare module "@hapi/hapi" { + interface ServerApplicationState { + db: mysql.Pool; + log: Logger; + isDev: boolean; + bot: Client; + } +} \ No newline at end of file diff --git a/api/src/types/utilities.ts b/api/src/types/utilities.ts new file mode 100644 index 0000000..f4debe9 --- /dev/null +++ b/api/src/types/utilities.ts @@ -0,0 +1,6 @@ +import type { RowDataPacket } from "mysql2/promise"; + +export type Query = + P extends true + ? (Partial & RowDataPacket)[] + : (T & RowDataPacket)[] \ No newline at end of file diff --git a/api/src/utils/database.ts b/api/src/utils/database.ts new file mode 100644 index 0000000..e69de29 diff --git a/database/init.sql b/database/init.sql index 9cd4b60..998c2fa 100644 --- a/database/init.sql +++ b/database/init.sql @@ -1,9 +1,14 @@ create database if not exists tqna; +create table if not exists tqna.twitchs ( + `id` integer not null auto_increment primary key, + `channel` text +); + create table if not exists tqna.questions ( - `__id` integer not null auto_increment primary key, + `id` integer not null auto_increment primary key, `channel` text, `asker` text, `question` text, - `answered` boolean + `answered` boolean not null default false ); \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 3c59359..e4e99a3 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -11,6 +11,9 @@ services: - "6969:6969" environment: NODE_ENV: "dev" + MYSQL_HOST: "db" + MYSQL_USER: "root" + MYSQL_PASSWORD: "root" healthcheck: disable: true volumes: