From 70d72f44193fb7cfec61d31cd5133f79478ddf25 Mon Sep 17 00:00:00 2001 From: Maieul BOYER Date: Wed, 13 Aug 2025 15:46:20 +0200 Subject: [PATCH] feat(database): split stuff into multiple files --- src/@shared/package.json | 1 + src/@shared/src/auth/index.ts | 11 ++ src/@shared/src/database/index.ts | 76 +---------- src/@shared/src/database/mixin/_base.ts | 64 +++++++++ src/@shared/src/database/mixin/_template.ts | 21 +++ src/@shared/src/database/mixin/session.ts | 27 ++++ src/@shared/src/database/mixin/user.ts | 35 +++++ src/@shared/src/database/users.ts | 7 - src/package-lock.json | 141 ++++++++++++++++++++ 9 files changed, 307 insertions(+), 76 deletions(-) create mode 100644 src/@shared/src/auth/index.ts create mode 100644 src/@shared/src/database/mixin/_base.ts create mode 100644 src/@shared/src/database/mixin/_template.ts create mode 100644 src/@shared/src/database/mixin/session.ts create mode 100644 src/@shared/src/database/mixin/user.ts delete mode 100644 src/@shared/src/database/users.ts diff --git a/src/@shared/package.json b/src/@shared/package.json index 05f6322..18fb06d 100644 --- a/src/@shared/package.json +++ b/src/@shared/package.json @@ -9,6 +9,7 @@ "author": "", "license": "ISC", "dependencies": { + "@fastify/jwt": "^9.1.0", "better-sqlite3": "^11.10.0", "fastify": "^5.0.0", "fastify-plugin": "^5.0.1", diff --git a/src/@shared/src/auth/index.ts b/src/@shared/src/auth/index.ts new file mode 100644 index 0000000..4b085f4 --- /dev/null +++ b/src/@shared/src/auth/index.ts @@ -0,0 +1,11 @@ +import fastifyJwt from "@fastify/jwt"; +import { FastifyPluginAsync } from "fastify"; +import fp from 'fastify-plugin' + +export const jwtPlugin = fp(async (fastify, _opts) => { + let env = process.env.JWT_SECRET; + if (env === undefined || env === null) + throw "JWT_SECRET is not defined" + void fastify.register(fastifyJwt, { secret: env }); +}); + diff --git a/src/@shared/src/database/index.ts b/src/@shared/src/database/index.ts index 04bcaae..925dd5b 100644 --- a/src/@shared/src/database/index.ts +++ b/src/@shared/src/database/index.ts @@ -1,77 +1,14 @@ import fp from 'fastify-plugin' import { FastifyInstance } from 'fastify' -import sqlite from 'better-sqlite3' -// @ts-ignore: this file is included using vite, typescript doesn't know how to include this... -import initSql from "./init.sql?raw" +import { Base } from "./mixin/_base"; +import { UserDb } from "./mixin/user"; +import { SessionDb } from "./mixin/session"; -/** - * represent a unique user (by its ID.) - * Having this means that the user does exist (aka it hasn't been deleted) - */ -export type UserID = number & { readonly __brand: unique symbol }; -/** - * The full representation of an user - * - * @property id [UserID]: The id of the user (unique) - * @property name [string]: The username of the user (unique) - * @property password [?string]: The password hash of the user (if password is defined) - */ -export type DbUser = { - readonly id: UserID, - readonly name: string, - readonly password: string | null, -}; - -// Only way to use the database. Everything must be done through this. -// Prefer to use prepared statement `using this.db.prepare` -export class Database { - private db: sqlite.Database; - private st: Map = new Map(); - - - /** - * Create a new instance of the database, and init it to a known state - * the file ./init.sql will be ran onto the database, creating any table that might be missing - */ - constructor(db_path: string) { - this.db = sqlite(db_path, {}); - this.db.pragma('journal_mode = WAL'); - this.db.transaction(() => this.db.exec(initSql))(); +class Database extends UserDb(SessionDb(Base as any)) { + constructor(path: string) { + super(path); } - - /** - * close the database - */ - destroy(): void { - // remove any statement from the cache - this.st.clear(); - // close the database - this.db?.close(); - } - - /** - * use this to create queries. This will create statements (kinda expensive) and cache them - * since they will be cached, this means that they are only created once, - * otherwise they'll be just spat out from the cache - * the statements are cached by the {query} argument, - * meaning that if you try to make two identiqual statement, but with different {query} they won't be cached - * - * @example this.prepare('SELECT * FROM users WHERE id = ?') - * @example this.prepare('SELECT * FROM users LIMIT 100 OFFSET ?') - */ - private prepare(query: string): sqlite.Statement { - let st = this.st.get(query); - if (st !== undefined) return st; - - st = this.db.prepare(query); - this.st.set(query, st); - return st; - } - - public getUser(user: UserID): DbUser { - - }; } // When using .decorate you have to specify added properties for Typescript @@ -90,6 +27,7 @@ export const useDatabase = fp(async function( _options: DatabaseOption) { f.log.info("Database has been hooked up to fastify ?!"); f.log.warn("TODO: actually hook up database to fastify..."); + f.decorate('db', new Database(_options.path)); }); export default useDatabase; diff --git a/src/@shared/src/database/mixin/_base.ts b/src/@shared/src/database/mixin/_base.ts new file mode 100644 index 0000000..959c3e3 --- /dev/null +++ b/src/@shared/src/database/mixin/_base.ts @@ -0,0 +1,64 @@ +import sqlite from "better-sqlite3"; + +// @ts-ignore: this file is included using vite, typescript doesn't know how to include this... +import initSql from "../init.sql?raw" + +export type MixinBase = new (...args: any[]) => { + constructor(db_path: string): void, + destroy(): void, + prepare(s: string): sqlite.Statement, + db: sqlite.Database, +} & T; + + +// Only way to use the database. Everything must be done through this. +// Prefer to use prepared statement `using this.db.prepare` +// +// this is the base, meaning that no actual query are made from this file. +// To create a new query function, go open another file, create a class that inherit from this class +// in the `index.ts` file, import the new class, and make the `Database` class inherit it +export class Base { + private db: sqlite.Database; + private st: Map = new Map(); + + + /** + * Create a new instance of the database, and init it to a known state + * the file ./init.sql will be ran onto the database, creating any table that might be missing + */ + constructor(db_path: string) { + console.log("NEW DB :)"); + this.db = sqlite(db_path, {}); + this.db.pragma('journal_mode = WAL'); + this.db.transaction(() => this.db.exec(initSql))(); + } + + /** + * close the database + */ + public destroy(): void { + // remove any statement from the cache + this.st.clear(); + // close the database + this.db?.close(); + } + + /** + * use this to create queries. This will create statements (kinda expensive) and cache them + * since they will be cached, this means that they are only created once, + * otherwise they'll be just spat out from the cache + * the statements are cached by the {query} argument, + * meaning that if you try to make two identiqual statement, but with different {query} they won't be cached + * + * @example this.prepare('SELECT * FROM users WHERE id = ?') + * @example this.prepare('SELECT * FROM users LIMIT 100 OFFSET ?') + */ + protected prepare(query: string): sqlite.Statement { + let st = this.st.get(query); + if (st !== undefined) return st; + + st = this.db.prepare(query); + this.st.set(query, st); + return st; + } +} diff --git a/src/@shared/src/database/mixin/_template.ts b/src/@shared/src/database/mixin/_template.ts new file mode 100644 index 0000000..115e506 --- /dev/null +++ b/src/@shared/src/database/mixin/_template.ts @@ -0,0 +1,21 @@ +//import sqlite from "better-sqlite3" +import { MixinBase } from "./_base" + +// never use this directly +export const TemplateDb = function (Base: TBase) { + return class extends Base { + constructor(...args: any[]) { + if (args.length != 1 && !(args[0] instanceof String)) + throw "Invalid arguments to mixing class" + super(args[0]); + } + } +} + +export type TemplateId = number & { readonly __brand: unique symbol }; + +export type TemplateType = { + readonly id: TemplateId, + readonly field: string, + readonly field2: number, +}; diff --git a/src/@shared/src/database/mixin/session.ts b/src/@shared/src/database/mixin/session.ts new file mode 100644 index 0000000..cb9f724 --- /dev/null +++ b/src/@shared/src/database/mixin/session.ts @@ -0,0 +1,27 @@ +//import sqlite from "better-sqlite3" +import { MixinBase } from "./_base" + +// never use this directly + +export const SessionDb = function (Base: TBase) { + return class extends Base { + constructor(...args: any[]) { + if (args.length != 1 && !(args[0] instanceof String)) + throw "Invalid arguments to mixing class" + super(args[0]); + } + + public getSessionFromId(id: SessionId): Session | null { + return null + } + } +} + +export type SessionId = number & { readonly __brand: unique symbol }; + +export type Session = { + readonly id: SessionId, + readonly name: string, + readonly salt: string, + readonly password: string, +}; diff --git a/src/@shared/src/database/mixin/user.ts b/src/@shared/src/database/mixin/user.ts new file mode 100644 index 0000000..26f14b9 --- /dev/null +++ b/src/@shared/src/database/mixin/user.ts @@ -0,0 +1,35 @@ +//import sqlite from "better-sqlite3" +import { MixinBase } from "./_base" + +// never use this directly + + +export const UserDb = function (Base: TBase) { + return class extends Base { + constructor(...args: any[]) { + if (args.length != 1 && !(args[0] instanceof String)) + throw "Invalid arguments to mixing class" + super(args[0]); + } + + private userFromRow(row: any): User { + throw "TODO: User from Row" + } + + public getUser(id: UserId): User | null { + return null + } + public setUser(id: UserId, partialUser: Partial>): User | null { + return null + } + } +} + +export type UserId = number & { readonly __brand: unique symbol }; + +export type User = { + readonly id: UserId, + readonly name: string, + readonly salt: string, + readonly password: string, +}; diff --git a/src/@shared/src/database/users.ts b/src/@shared/src/database/users.ts deleted file mode 100644 index e22c244..0000000 --- a/src/@shared/src/database/users.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Database } from "@shared/database"; - -export type UserID = Number & { readonly __brand: unique symbol }; - -export async function getUser(this: Database, id: UserID) { - console.log(this); -} diff --git a/src/package-lock.json b/src/package-lock.json index 3bc28f5..57bea85 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -20,6 +20,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@fastify/jwt": "^9.1.0", "better-sqlite3": "^11.10.0", "fastify": "^5.0.0", "fastify-plugin": "^5.0.1", @@ -640,6 +641,29 @@ "integrity": "sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==", "license": "MIT" }, + "node_modules/@fastify/jwt": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@fastify/jwt/-/jwt-9.1.0.tgz", + "integrity": "sha512-CiGHCnS5cPMdb004c70sUWhQTfzrJHAeTywt7nVw6dAiI0z1o4WRvU94xfijhkaId4bIxTCOjFgn4sU+Gvk43w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "@lukeed/ms": "^2.0.2", + "fast-jwt": "^5.0.0", + "fastify-plugin": "^5.0.0", + "steed": "^1.1.3" + } + }, "node_modules/@fastify/merge-json-schemas": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", @@ -1688,6 +1712,18 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -1765,6 +1801,12 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -2050,6 +2092,15 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -2164,6 +2215,21 @@ "rfdc": "^1.2.0" } }, + "node_modules/fast-jwt": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/fast-jwt/-/fast-jwt-5.0.6.tgz", + "integrity": "sha512-LPE7OCGUl11q3ZgW681cEU2d0d2JZ37hhJAmetCgNyW8waVaJVZXhyFF6U2so1Iim58Yc7pfxJe2P7MNetQH2g==", + "license": "Apache-2.0", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "asn1.js": "^5.4.1", + "ecdsa-sig-formatter": "^1.0.11", + "mnemonist": "^0.40.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/fast-querystring": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", @@ -2204,6 +2270,18 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fastfall": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/fastfall/-/fastfall-1.5.1.tgz", + "integrity": "sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==", + "license": "MIT", + "dependencies": { + "reusify": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fastify": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.4.0.tgz", @@ -2286,6 +2364,16 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fastparallel": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz", + "integrity": "sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4", + "xtend": "^4.0.2" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -2295,6 +2383,16 @@ "reusify": "^1.0.4" } }, + "node_modules/fastseries": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/fastseries/-/fastseries-1.7.2.tgz", + "integrity": "sha512-dTPFrPGS8SNSzAt7u/CbMKCJ3s01N04s4JFbORHcmyvVfVKmbhMD1VtRbh5enGHxkaQDqWyLefiKOGGmohGDDQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.0", + "xtend": "^4.0.0" + } + }, "node_modules/fdir": { "version": "6.4.6", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", @@ -2743,6 +2841,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -2783,6 +2887,15 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/mnemonist": { + "version": "0.40.3", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.3.tgz", + "integrity": "sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ==", + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.4" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2827,6 +2940,12 @@ "node": ">=10" } }, + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "license": "MIT" + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -3588,6 +3707,19 @@ "node": ">= 0.8" } }, + "node_modules/steed": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/steed/-/steed-1.1.3.tgz", + "integrity": "sha512-EUkci0FAUiE4IvGTSKcDJIQ/eRUP2JJb56+fvZ4sdnguLTqIdKjSxUe138poW8mkvKWXW2sFPrgTsxqoISnmoA==", + "license": "MIT", + "dependencies": { + "fastfall": "^1.5.0", + "fastparallel": "^2.2.0", + "fastq": "^1.3.0", + "fastseries": "^1.7.0", + "reusify": "^1.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -4104,6 +4236,15 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",