feat(database): split stuff into multiple files

This commit is contained in:
Maieul BOYER 2025-08-13 15:46:20 +02:00 committed by Maix0
parent baf9dc54c6
commit 70d72f4419
9 changed files with 307 additions and 76 deletions

View file

@ -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",

View file

@ -0,0 +1,11 @@
import fastifyJwt from "@fastify/jwt";
import { FastifyPluginAsync } from "fastify";
import fp from 'fastify-plugin'
export const jwtPlugin = fp<FastifyPluginAsync>(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 });
});

View file

@ -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<string, sqlite.Statement> = 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<DatabaseOption>(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;

View file

@ -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<T = {}> = 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<string, sqlite.Statement> = 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;
}
}

View file

@ -0,0 +1,21 @@
//import sqlite from "better-sqlite3"
import { MixinBase } from "./_base"
// never use this directly
export const TemplateDb = function <TBase extends MixinBase>(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,
};

View file

@ -0,0 +1,27 @@
//import sqlite from "better-sqlite3"
import { MixinBase } from "./_base"
// never use this directly
export const SessionDb = function <TBase extends MixinBase>(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,
};

View file

@ -0,0 +1,35 @@
//import sqlite from "better-sqlite3"
import { MixinBase } from "./_base"
// never use this directly
export const UserDb = function <TBase extends MixinBase>(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<Omit<User, 'id'>>): 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,
};

View file

@ -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);
}