Replace all async reads and writes to the JSON database with the sync reads and writes to prevent a data corruption race condition where two requests are processed at the same time and write to the same file, or one reads while the other writes causing read of partially written data.
190 lines
5.2 KiB
TypeScript
190 lines
5.2 KiB
TypeScript
/*
|
|
SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
|
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
import { readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
import type { ApiAuthenticationProvider, ApiEvent, ApiSchedule, ApiSubscription, ApiUserType } from "~/shared/types/api";
|
|
import type { Id } from "~/shared/types/common";
|
|
|
|
export interface ServerSession {
|
|
id: Id,
|
|
access: ApiUserType,
|
|
accountId?: number,
|
|
authenticationProvider?: ApiAuthenticationProvider,
|
|
authenticationSlug?: string,
|
|
authenticationName?: string,
|
|
rotatesAtMs: number,
|
|
expiresAtMs?: number,
|
|
discardAtMs: number,
|
|
successor?: Id,
|
|
};
|
|
|
|
export interface ServerUser {
|
|
id: Id,
|
|
updatedAt: string,
|
|
deleted?: boolean,
|
|
type: ApiUserType,
|
|
name?: string,
|
|
interestedEventIds?: number[],
|
|
interestedEventSlotIds?: number[],
|
|
timezone?: string,
|
|
locale?: string,
|
|
}
|
|
|
|
export interface ServerAuthenticationMethod {
|
|
id: Id,
|
|
provider: ApiAuthenticationProvider,
|
|
slug: string,
|
|
name: string,
|
|
userId: Id,
|
|
}
|
|
|
|
// For this demo I'm just storing the runtime data in JSON files. When making
|
|
// this into proper application this should be replaced with an actual database.
|
|
|
|
const schedulePath = "data/schedule.json";
|
|
const subscriptionsPath = "data/subscriptions.json";
|
|
const usersPath = "data/users.json";
|
|
const nextUserIdPath = "data/next-user-id.json";
|
|
const sessionsPath = "data/sessions.json";
|
|
const nextSessionIdPath = "data/next-session-id.json";
|
|
const authMethodPath = "data/auth-method.json";
|
|
const nextAuthenticationMethodIdPath = "data/auth-method-id.json"
|
|
const nextEventIdPath = "data/next-event-id.json";
|
|
const eventsPath = "data/events.json";
|
|
|
|
function remove(path: string) {
|
|
try {
|
|
unlinkSync(path);
|
|
} catch (err: any) {
|
|
if (err.code !== "ENOENT") {
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function deleteDatabase() {
|
|
remove(schedulePath);
|
|
remove(subscriptionsPath);
|
|
remove(usersPath);
|
|
remove(sessionsPath);
|
|
}
|
|
|
|
function readJson<T>(filePath: string, fallback: T) {
|
|
let data: T extends () => infer R ? R : T;
|
|
try {
|
|
data = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
} catch (err: any) {
|
|
if (err.code !== "ENOENT")
|
|
throw err;
|
|
data = typeof fallback === "function" ? fallback() : fallback;
|
|
}
|
|
return data;
|
|
}
|
|
|
|
export function readSchedule() {
|
|
return readJson(schedulePath, (): ApiSchedule => ({
|
|
id: 111,
|
|
updatedAt: new Date().toISOString(),
|
|
}));
|
|
}
|
|
|
|
export function writeSchedule(schedule: ApiSchedule) {
|
|
writeFileSync(schedulePath, JSON.stringify(schedule, undefined, "\t") + "\n", "utf-8");
|
|
}
|
|
|
|
export function readSubscriptions() {
|
|
let subscriptions = readJson<ApiSubscription[]>(subscriptionsPath, []);
|
|
if (subscriptions.length && "keys" in subscriptions[0]) {
|
|
// Discard old format
|
|
subscriptions = [];
|
|
}
|
|
return subscriptions;
|
|
}
|
|
|
|
export function writeSubscriptions(subscriptions: ApiSubscription[]) {
|
|
writeFileSync(subscriptionsPath, JSON.stringify(subscriptions, undefined, "\t") + "\n", "utf-8");
|
|
}
|
|
|
|
export function readNextUserId() {
|
|
return readJson(nextUserIdPath, 0);
|
|
}
|
|
|
|
export function writeNextUserId(nextId: number) {
|
|
writeFileSync(nextUserIdPath, String(nextId), "utf-8");
|
|
}
|
|
|
|
export function nextUserId() {
|
|
let nextId = readJson(nextUserIdPath, 0);
|
|
if (nextId === 0) {
|
|
nextId = Math.max(...(readUsers()).map(user => user.id), -1) + 1;
|
|
}
|
|
writeFileSync(nextUserIdPath, String(nextId + 1), "utf-8");
|
|
return nextId;
|
|
}
|
|
|
|
export function readUsers() {
|
|
return readJson(usersPath, (): ServerUser[] => []);
|
|
}
|
|
|
|
export function writeUsers(users: ServerUser[]) {
|
|
writeFileSync(usersPath, JSON.stringify(users, undefined, "\t") + "\n", "utf-8");
|
|
}
|
|
|
|
export function readNextSessionId() {
|
|
return readJson(nextSessionIdPath, 0);
|
|
}
|
|
|
|
export function writeNextSessionId(nextId: number) {
|
|
writeFileSync(nextSessionIdPath, String(nextId), "utf-8");
|
|
}
|
|
|
|
export function nextSessionId() {
|
|
const nextId = readJson(nextSessionIdPath, 0);
|
|
writeFileSync(nextSessionIdPath, String(nextId + 1), "utf-8");
|
|
return nextId;
|
|
}
|
|
|
|
export function readSessions() {
|
|
return readJson<ServerSession[]>(sessionsPath, [])
|
|
}
|
|
|
|
export function writeSessions(sessions: ServerSession[]) {
|
|
writeFileSync(sessionsPath, JSON.stringify(sessions, undefined, "\t") + "\n", "utf-8");
|
|
}
|
|
|
|
export function nextAuthenticationMethodId() {
|
|
const nextId = readJson(nextAuthenticationMethodIdPath, 0);
|
|
writeFileSync(nextAuthenticationMethodIdPath, String(nextId + 1), "utf-8");
|
|
return nextId;
|
|
}
|
|
|
|
export function writeNextAuthenticationMethodId(nextId: number) {
|
|
writeFileSync(nextAuthenticationMethodIdPath, String(nextId), "utf-8");
|
|
}
|
|
|
|
export function readAuthenticationMethods() {
|
|
return readJson<ServerAuthenticationMethod[]>(authMethodPath, [])
|
|
}
|
|
|
|
export function writeAuthenticationMethods(authMethods: ServerAuthenticationMethod[]) {
|
|
writeFileSync(authMethodPath, JSON.stringify(authMethods, undefined, "\t") + "\n", "utf-8");
|
|
}
|
|
|
|
export function nextEventId() {
|
|
const nextId = readJson(nextEventIdPath, 0);
|
|
writeFileSync(nextEventIdPath, String(nextId + 1), "utf-8");
|
|
return nextId;
|
|
}
|
|
|
|
export function writeNextEventId(nextId: number) {
|
|
writeFileSync(nextEventIdPath, String(nextId), "utf-8");
|
|
}
|
|
|
|
export function readEvents() {
|
|
return readJson<ApiEvent[]>(eventsPath, [])
|
|
}
|
|
|
|
export function writeEvents(events: ApiEvent[]) {
|
|
writeFileSync(eventsPath, JSON.stringify(events, undefined, "\t") + "\n", "utf-8");
|
|
}
|