From 5fe2036f0b6a86bedc4d014b5f29b3ec8edd3253 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Sat, 2 Nov 2024 11:32:36 +1100 Subject: [PATCH] immutable application settings framework --- .../migration.sql | 7 ++ prisma/schema.prisma | 10 ++- .../config/application-configuration.ts | 85 +++++++++++++++++++ server/plugins/app-setup.ts | 17 ++++ server/plugins/{setup.ts => user-setup.ts} | 0 5 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 prisma/migrations/20241102000813_create_application_configuration/migration.sql create mode 100644 server/internal/config/application-configuration.ts create mode 100644 server/plugins/app-setup.ts rename server/plugins/{setup.ts => user-setup.ts} (100%) diff --git a/prisma/migrations/20241102000813_create_application_configuration/migration.sql b/prisma/migrations/20241102000813_create_application_configuration/migration.sql new file mode 100644 index 0000000..e07ad83 --- /dev/null +++ b/prisma/migrations/20241102000813_create_application_configuration/migration.sql @@ -0,0 +1,7 @@ +-- CreateTable +CREATE TABLE "ApplicationSettings" ( + "timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "enabledAuthencationMechanisms" "AuthMec"[], + + CONSTRAINT "ApplicationSettings_pkey" PRIMARY KEY ("timestamp") +); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9d23408..9101b34 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,6 +13,12 @@ datasource db { url = env("DATABASE_URL") } +model ApplicationSettings { + timestamp DateTime @id @default(now()) + + enabledAuthencationMechanisms AuthMec[] +} + model User { id String @id @default(uuid()) username String @unique @@ -45,8 +51,8 @@ model Invitation { id String @id @default(uuid()) isAdmin Boolean @default(false) - username String? - email String? + username String? + email String? } enum ClientCapabilities { diff --git a/server/internal/config/application-configuration.ts b/server/internal/config/application-configuration.ts new file mode 100644 index 0000000..b495cb7 --- /dev/null +++ b/server/internal/config/application-configuration.ts @@ -0,0 +1,85 @@ +import { ApplicationSettings, AuthMec } from "@prisma/client"; +import prisma from "../db/database"; + +class ApplicationConfiguration { + // Reference to the currently selected application configuration + private currentApplicationSettings: ApplicationSettings; + private applicationStateProxy: object; + private dirty: boolean = false; + private dirtyPromise: Promise | undefined = undefined; + + constructor() { + // @ts-expect-error + this.currentApplicationSettings = {}; + this.applicationStateProxy = {}; + } + + private buildApplicationSettingsProxy() { + const appConfig = this; + const proxy = new Proxy(this.currentApplicationSettings, { + get: (target, key: keyof ApplicationSettings) => { + return appConfig.currentApplicationSettings[key]; + }, + set: (target, key: keyof ApplicationSettings, value) => { + if (JSON.stringify(value) === JSON.stringify(appConfig.currentApplicationSettings[key])) return true; + appConfig.currentApplicationSettings[key] = value; + + const deepAppConfigCopy: Omit & { + timestamp?: Date; + } = JSON.parse(JSON.stringify(appConfig.currentApplicationSettings)); + + delete deepAppConfigCopy["timestamp"]; + + appConfig.dirty = true; + appConfig.dirtyPromise = prisma.applicationSettings.create({ + data: deepAppConfigCopy, + }); + return true; + }, + deleteProperty: (target, key: keyof ApplicationSettings) => { + return false; + }, + }); + this.applicationStateProxy = proxy; + } + + // Default application configuration + async initialiseConfiguration() { + const initialState = await prisma.applicationSettings.create({ + data: { + enabledAuthencationMechanisms: [AuthMec.Simple], + }, + }); + + console.log("created configuration"); + + this.currentApplicationSettings = initialState; + this.buildApplicationSettingsProxy(); + } + + async pullConfiguration() { + const latestState = await prisma.applicationSettings.findFirst({ + orderBy: { + timestamp: "desc", + }, + }); + + if (!latestState) throw new Error("No application configuration to pull"); + + this.currentApplicationSettings = latestState; + this.buildApplicationSettingsProxy(); + console.log("pulled configuration"); + } + + async waitForWrite() { + if (this.dirty) { + await this.dirtyPromise; + } + } + + useApplicationConfiguration(): ApplicationSettings { + return this.applicationStateProxy as ApplicationSettings; + } +} + +export const applicationSettings = new ApplicationConfiguration(); \ No newline at end of file diff --git a/server/plugins/app-setup.ts b/server/plugins/app-setup.ts new file mode 100644 index 0000000..f11e6de --- /dev/null +++ b/server/plugins/app-setup.ts @@ -0,0 +1,17 @@ +import { + applicationSettings, +} from "../internal/config/application-configuration"; +import prisma from "../internal/db/database"; + +export default defineNitroPlugin(async (nitro) => { + const applicationSettingsCount = await prisma.applicationSettings.count({}); + if (applicationSettingsCount > 0) { + await applicationSettings.pullConfiguration(); + } else { + await applicationSettings.initialiseConfiguration(); + } + + nitro.hooks.hookOnce("close", async () => { + await applicationSettings.waitForWrite(); + }); +}); diff --git a/server/plugins/setup.ts b/server/plugins/user-setup.ts similarity index 100% rename from server/plugins/setup.ts rename to server/plugins/user-setup.ts