diff --git a/src/utils/package-lock.json b/src/utils/package-lock.json index 1117b8c..13f16fa 100644 --- a/src/utils/package-lock.json +++ b/src/utils/package-lock.json @@ -11,7 +11,9 @@ "dependencies": { "better-sqlite3": "^11.10.0", "fastify": "^5.0.0", - "fastify-plugin": "^5.0.1" + "fastify-plugin": "^5.0.1", + "typescript-result": "3.1.1", + "uuidv7": "^1.0.2" }, "devDependencies": { "@types/better-sqlite3": "^7.6.13", @@ -2122,6 +2124,15 @@ "node": ">=14.17" } }, + "node_modules/typescript-result": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/typescript-result/-/typescript-result-3.1.1.tgz", + "integrity": "sha512-KQ7SdBa1UcZCu+qI7pgZ1FdzBIlGiyoHTaQQDshtsKCKiPdzLlkU6OWkJuhjzo+xl+y+dOVO+2HyyEpODNDHyw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -2135,6 +2146,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuidv7": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uuidv7/-/uuidv7-1.0.2.tgz", + "integrity": "sha512-8JQkH4ooXnm1JCIhqTMbtmdnYEn6oKukBxHn1Ic9878jMkL7daTI7anTExfY18VRCX7tcdn5quzvCb6EWrR8PA==", + "license": "Apache-2.0", + "bin": { + "uuidv7": "cli.js" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/src/utils/package.json b/src/utils/package.json index f1b8ea3..6667144 100644 --- a/src/utils/package.json +++ b/src/utils/package.json @@ -19,7 +19,9 @@ "dependencies": { "better-sqlite3": "^11.10.0", "fastify": "^5.0.0", - "fastify-plugin": "^5.0.1" + "fastify-plugin": "^5.0.1", + "typescript-result": "3.1.1", + "uuidv7": "^1.0.2" }, "devDependencies": { "@types/better-sqlite3": "^7.6.13", diff --git a/src/utils/src/database.ts b/src/utils/src/database.ts index 4393fb5..78cfaf1 100644 --- a/src/utils/src/database.ts +++ b/src/utils/src/database.ts @@ -6,45 +6,97 @@ // By: maiboyer +#+ +:+ +#+ // // +#+#+#+#+#+ +#+ // // Created: 2025/06/17 17:06:31 by maiboyer #+# #+# // -// Updated: 2025/06/20 00:11:43 by maiboyer ### ########.fr // +// Updated: 2025/07/17 16:28:48 by maiboyer ### ########.fr // // // // ************************************************************************** // import fp from 'fastify-plugin' import { FastifyInstance } from 'fastify' import sqlite from 'better-sqlite3' +import { Result } from 'typescript-result' import initSql from "./init.sql.js" +import { newUUIDv7, UUIDv7 } from './uuid.js' + +export class DBUserExists extends Error { + public readonly type = 'db-user-exists'; +} + +// 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 db: sqlite.Database; + private st: Map = new Map(); - constructor(db_path: string) { - this.db = sqlite(db_path, {}); - this.db.pragma('journal_mode = WAL'); - this.db.exec(initSql); - } + /** + * 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))(); + } - destroy(): void { - this.db?.close(); - } + /** + * 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 createUser(user: string): Result { + const st = this.prepare('INSERT INTO users VALUES (?, ?) RETURNING id'); + + try { + st.get(newUUIDv7(), user) + } + catch (e: any) { + console.log(e) + console.log(typeof e) + } + return Result.ok(newUUIDv7()); + } } // When using .decorate you have to specify added properties for Typescript declare module 'fastify' { - export interface FastifyInstance { - db: Database; - } + export interface FastifyInstance { + db: Database; + } } export type DatabaseOption = { - path: string; + path: string; }; export const uDatabase = fp(async function( - _fastify: FastifyInstance, - _options: DatabaseOption) { + _fastify: FastifyInstance, + _options: DatabaseOption) { }); diff --git a/src/utils/src/init.sql b/src/utils/src/init.sql index 9ec752f..287c138 100644 --- a/src/utils/src/init.sql +++ b/src/utils/src/init.sql @@ -1,4 +1,9 @@ -- this file will make sure that the database is always up to date with the correct schema -- when editing this file, make sure to always include stuff like `IF NOT EXISTS` such as to not throw error -- NEVER DROP ANYTHING IN THIS FILE -CREATE TABLE IF NOT EXISTS users (name STRING); +CREATE TABLE IF NOT EXISTS users ( + id STRING UNIQUE PRIMARY KEY, -- UUIDv7 as a string + name STRING UNIQUE, -- name of the user + token STRING UNIQUE, -- the token of the user (aka the cookie) +); + diff --git a/src/utils/src/uuid.ts b/src/utils/src/uuid.ts new file mode 100644 index 0000000..adf6c13 --- /dev/null +++ b/src/utils/src/uuid.ts @@ -0,0 +1,35 @@ +// ************************************************************************** // +// // +// ::: :::::::: // +// uuid.ts :+: :+: :+: // +// +:+ +:+ +:+ // +// By: maiboyer +#+ +:+ +#+ // +// +#+#+#+#+#+ +#+ // +// Created: 2025/06/20 17:41:01 by maiboyer #+# #+# // +// Updated: 2025/06/20 17:44:29 by maiboyer ### ########.fr // +// // +// ************************************************************************** // + +import { Result } from "typescript-result"; +import { uuidv7 } from "uuidv7"; + +export class InvalidUUID extends Error { + public readonly type = 'invalid-uuid'; +}; + +export type UUIDv7 = string & { readonly __brand: unique symbol }; +const uuidv7Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +export function isUUIDv7(value: string): value is UUIDv7 { + return uuidv7Regex.test(value); +} + +export function toUUIDv7(value: string): Result { + if (!isUUIDv7(value)) return Result.error(new InvalidUUID()); + + return Result.ok(value as UUIDv7); +} + +export function newUUIDv7(): UUIDv7 { + return uuidv7() as UUIDv7; +}