Move toju-app into own its folder

This commit is contained in:
2026-03-29 23:30:37 +02:00
parent 0467a7b612
commit 8162e0444a
287 changed files with 42 additions and 34 deletions

View File

@@ -0,0 +1,113 @@
# Persistence Infrastructure
Offline-first storage layer that keeps messages, users, rooms, reactions, bans, and attachments on the client. The rest of the app only ever talks to `DatabaseService`, which picks the right backend for the current platform at runtime.
## Files
```
persistence/
├── index.ts Barrel (exports DatabaseService)
├── database.service.ts Platform-agnostic facade
├── browser-database.service.ts IndexedDB backend (web)
└── electron-database.service.ts IPC/SQLite backend (desktop)
```
## Platform routing
```mermaid
graph TD
Consumer[Store effects / facades / components]
Consumer --> Facade[DatabaseService<br/>facade]
Facade -->|isBrowser?| Decision{Platform}
Decision -- Browser --> IDB[BrowserDatabaseService<br/>IndexedDB]
Decision -- Electron --> IPC[ElectronDatabaseService<br/>IPC to main process]
IPC --> Main[Electron main process<br/>TypeORM + SQLite]
click Facade "database.service.ts" "DatabaseService - platform-agnostic facade" _blank
click IDB "browser-database.service.ts" "IndexedDB backend for web" _blank
click IPC "electron-database.service.ts" "IPC client for Electron" _blank
```
`DatabaseService` is an `@Injectable({ providedIn: 'root' })` that injects both backends and delegates every call to whichever one matches the current platform. Consumers never import a backend directly.
## Object stores / tables
Both backends store the same entity types:
| Store | Key | Indexes | Description |
|---|---|---|---|
| `messages` | `id` | `roomId` | Chat messages, sorted by timestamp |
| `users` | `oderId` | | User profiles |
| `rooms` | `id` | | Server/room metadata |
| `reactions` | `oderId-emoji-messageId` | | Emoji reactions, deduplicated per user |
| `bans` | `oderId` | | Active bans per room |
| `attachments` | `id` | | File/image metadata tied to messages |
| `meta` | `key` | | Key-value pairs (e.g. `currentUserId`) |
The IndexedDB schema is at version 2.
## How the two backends differ
### Browser (IndexedDB)
All operations run inside IndexedDB transactions in the renderer thread. Queries like `getMessages` pull all messages for a room via the `roomId` index, sort them by timestamp in JS, then apply limit/offset. Deleted messages are normalised on read (content replaced with a sentinel string).
```mermaid
sequenceDiagram
participant Eff as NgRx Effect
participant DB as DatabaseService
participant BDB as BrowserDatabaseService
participant IDB as IndexedDB
Eff->>DB: getMessages(roomId, 50)
DB->>BDB: getMessages(roomId, 50)
BDB->>IDB: tx.objectStore("messages")<br/>.index("roomId").getAll(roomId)
IDB-->>BDB: Message[]
Note over BDB: Sort by timestamp, slice, normalise
BDB-->>DB: Message[]
DB-->>Eff: Message[]
```
### Electron (SQLite via IPC)
The renderer sends structured command/query objects through the Electron preload bridge. The main process handles them with TypeORM against a local SQLite file. No database logic runs in the renderer.
```mermaid
sequenceDiagram
participant Eff as NgRx Effect
participant DB as DatabaseService
participant EDB as ElectronDatabaseService
participant IPC as Preload Bridge
participant Main as Main Process<br/>TypeORM + SQLite
Eff->>DB: saveMessage(msg)
DB->>EDB: saveMessage(msg)
EDB->>IPC: api.command({type: "save-message", payload: {message}})
IPC->>Main: ipcRenderer.invoke
Main-->>IPC: void
IPC-->>EDB: Promise resolves
EDB-->>DB: void
DB-->>Eff: void
```
The Electron backend's `initialize()` is a no-op because the main process creates the database before the renderer window opens.
## API surface
Every method on `DatabaseService` maps 1:1 to both backends:
**Messages**: `saveMessage`, `getMessages`, `getMessageById`, `deleteMessage`, `updateMessage`, `clearRoomMessages`
**Reactions**: `saveReaction`, `removeReaction`, `getReactionsForMessage`
**Users**: `saveUser`, `getUser`, `getCurrentUser`, `setCurrentUserId`, `getUsersByRoom`, `updateUser`
**Rooms**: `saveRoom`, `getRoom`, `getAllRooms`, `deleteRoom`, `updateRoom`
**Bans**: `saveBan`, `removeBan`, `getBansForRoom`, `isUserBanned`
**Attachments**: `saveAttachment`, `getAttachmentsForMessage`, `getAllAttachments`, `deleteAttachmentsForMessage`
**Lifecycle**: `initialize`, `clearAllData`
The facade also exposes an `isReady` signal that flips to `true` after `initialize()` completes, so components can gate rendering until the DB is available.

View File

@@ -0,0 +1,441 @@
/* eslint-disable, @typescript-eslint/no-non-null-assertion */
import { Injectable } from '@angular/core';
import {
DELETED_MESSAGE_CONTENT,
Message,
User,
Room,
Reaction,
BanEntry
} from '../../shared-kernel';
import type { ChatAttachmentMeta } from '../../shared-kernel';
/** IndexedDB database name for the MetoYou application. */
const DATABASE_NAME = 'metoyou';
/** IndexedDB schema version - bump when adding/changing object stores. */
const DATABASE_VERSION = 2;
/** Names of every object store used by the application. */
const STORE_MESSAGES = 'messages';
const STORE_USERS = 'users';
const STORE_ROOMS = 'rooms';
const STORE_REACTIONS = 'reactions';
const STORE_BANS = 'bans';
const STORE_META = 'meta';
const STORE_ATTACHMENTS = 'attachments';
/** All object store names, used when clearing the entire database. */
const ALL_STORE_NAMES: string[] = [
STORE_MESSAGES,
STORE_USERS,
STORE_ROOMS,
STORE_REACTIONS,
STORE_BANS,
STORE_ATTACHMENTS,
STORE_META
];
/**
* IndexedDB-backed database service used when the app runs in a
* plain browser (i.e. without Electron).
*
* Every public method mirrors the {@link DatabaseService} API so the
* facade can delegate transparently.
*/
@Injectable({ providedIn: 'root' })
export class BrowserDatabaseService {
/** Handle to the opened IndexedDB database, or `null` before {@link initialize}. */
private database: IDBDatabase | null = null;
/** Open (or create) the IndexedDB database. Safe to call multiple times. */
async initialize(): Promise<void> {
if (this.database)
return;
this.database = await this.openDatabase();
}
/** Persist a single message. */
async saveMessage(message: Message): Promise<void> {
await this.put(STORE_MESSAGES, message);
}
/**
* Retrieve messages for a room, sorted oldest-first.
* @param roomId - Target room.
* @param limit - Maximum number of messages to return.
* @param offset - Number of messages to skip (for pagination).
*/
async getMessages(roomId: string, limit = 100, offset = 0): Promise<Message[]> {
const allRoomMessages = await this.getAllFromIndex<Message>(
STORE_MESSAGES, 'roomId', roomId
);
return allRoomMessages
.sort((first, second) => first.timestamp - second.timestamp)
.slice(offset, offset + limit)
.map((message) => this.normaliseMessage(message));
}
/** Delete a message by its ID. */
async deleteMessage(messageId: string): Promise<void> {
await this.deleteRecord(STORE_MESSAGES, messageId);
}
/** Apply partial updates to an existing message. */
async updateMessage(messageId: string, updates: Partial<Message>): Promise<void> {
const existing = await this.get<Message>(STORE_MESSAGES, messageId);
if (existing) {
await this.put(STORE_MESSAGES, { ...existing,
...updates });
}
}
/** Retrieve a single message by ID, or `null` if not found. */
async getMessageById(messageId: string): Promise<Message | null> {
const message = await this.get<Message>(STORE_MESSAGES, messageId);
return message ? this.normaliseMessage(message) : null;
}
/** Remove every message belonging to a room. */
async clearRoomMessages(roomId: string): Promise<void> {
const messages = await this.getAllFromIndex<Message>(
STORE_MESSAGES, 'roomId', roomId
);
const transaction = this.createTransaction(STORE_MESSAGES, 'readwrite');
for (const message of messages) {
transaction.objectStore(STORE_MESSAGES).delete(message.id);
}
await this.awaitTransaction(transaction);
}
/**
* Persist a reaction, ignoring duplicates (same user + same emoji on
* the same message).
*/
async saveReaction(reaction: Reaction): Promise<void> {
const existing = await this.getAllFromIndex<Reaction>(
STORE_REACTIONS, 'messageId', reaction.messageId
);
const isDuplicate = existing.some(
(entry) => entry.userId === reaction.userId && entry.emoji === reaction.emoji
);
if (!isDuplicate) {
await this.put(STORE_REACTIONS, reaction);
}
}
/** Remove a specific reaction (identified by user + emoji + message). */
async removeReaction(messageId: string, userId: string, emoji: string): Promise<void> {
const reactions = await this.getAllFromIndex<Reaction>(
STORE_REACTIONS, 'messageId', messageId
);
const target = reactions.find(
(entry) => entry.userId === userId && entry.emoji === emoji
);
if (target) {
await this.deleteRecord(STORE_REACTIONS, target.id);
}
}
/** Return all reactions for a given message. */
async getReactionsForMessage(messageId: string): Promise<Reaction[]> {
return this.getAllFromIndex<Reaction>(STORE_REACTIONS, 'messageId', messageId);
}
/** Persist a user record. */
async saveUser(user: User): Promise<void> {
await this.put(STORE_USERS, user);
}
/** Retrieve a user by ID, or `null` if not found. */
async getUser(userId: string): Promise<User | null> {
return (await this.get<User>(STORE_USERS, userId)) ?? null;
}
/** Retrieve the last-authenticated ("current") user, or `null`. */
async getCurrentUser(): Promise<User | null> {
const meta = await this.get<{ id: string; value: string }>(
STORE_META, 'currentUserId'
);
if (!meta)
return null;
return this.getUser(meta.value);
}
/** Store which user ID is considered "current" (logged-in). */
async setCurrentUserId(userId: string): Promise<void> {
await this.put(STORE_META, { id: 'currentUserId',
value: userId });
}
/**
* Retrieve all known users.
* @param _roomId - Accepted for API parity but currently unused.
*/
async getUsersByRoom(_roomId: string): Promise<User[]> {
return this.getAll<User>(STORE_USERS);
}
/** Apply partial updates to an existing user. */
async updateUser(userId: string, updates: Partial<User>): Promise<void> {
const existing = await this.get<User>(STORE_USERS, userId);
if (existing) {
await this.put(STORE_USERS, { ...existing,
...updates });
}
}
/** Persist a room record. */
async saveRoom(room: Room): Promise<void> {
await this.put(STORE_ROOMS, room);
}
/** Retrieve a room by ID, or `null` if not found. */
async getRoom(roomId: string): Promise<Room | null> {
return (await this.get<Room>(STORE_ROOMS, roomId)) ?? null;
}
/** Return every persisted room. */
async getAllRooms(): Promise<Room[]> {
return this.getAll<Room>(STORE_ROOMS);
}
/** Delete a room and all of its messages. */
async deleteRoom(roomId: string): Promise<void> {
await this.deleteRecord(STORE_ROOMS, roomId);
await this.clearRoomMessages(roomId);
}
/** Apply partial updates to an existing room. */
async updateRoom(roomId: string, updates: Partial<Room>): Promise<void> {
const existing = await this.get<Room>(STORE_ROOMS, roomId);
if (existing) {
await this.put(STORE_ROOMS, { ...existing,
...updates });
}
}
/** Persist a ban entry. */
async saveBan(ban: BanEntry): Promise<void> {
await this.put(STORE_BANS, ban);
}
/** Remove a ban by the banned user's `oderId`. */
async removeBan(oderId: string): Promise<void> {
const allBans = await this.getAll<BanEntry>(STORE_BANS);
const match = allBans.find((ban) => ban.oderId === oderId);
if (match) {
await this.deleteRecord(STORE_BANS, match.oderId);
}
}
/**
* Return active (non-expired) bans for a room.
*
* @param roomId - Room to query.
*/
async getBansForRoom(roomId: string): Promise<BanEntry[]> {
const allBans = await this.getAllFromIndex<BanEntry>(
STORE_BANS, 'roomId', roomId
);
const now = Date.now();
return allBans.filter(
(ban) => !ban.expiresAt || ban.expiresAt > now
);
}
/** Check whether a specific user is currently banned from a room. */
async isUserBanned(userId: string, roomId: string): Promise<boolean> {
const activeBans = await this.getBansForRoom(roomId);
return activeBans.some((ban) => ban.oderId === userId);
}
/** Persist attachment metadata associated with a chat message. */
async saveAttachment(attachment: ChatAttachmentMeta): Promise<void> {
await this.put(STORE_ATTACHMENTS, attachment);
}
/** Return all attachment records associated with a message. */
async getAttachmentsForMessage(messageId: string): Promise<ChatAttachmentMeta[]> {
return this.getAllFromIndex<ChatAttachmentMeta>(STORE_ATTACHMENTS, 'messageId', messageId);
}
/** Return every persisted attachment record. */
async getAllAttachments(): Promise<ChatAttachmentMeta[]> {
return this.getAll<ChatAttachmentMeta>(STORE_ATTACHMENTS);
}
/** Delete every attachment record for a specific message. */
async deleteAttachmentsForMessage(messageId: string): Promise<void> {
const attachments = await this.getAllFromIndex<ChatAttachmentMeta>(
STORE_ATTACHMENTS, 'messageId', messageId
);
const transaction = this.createTransaction(STORE_ATTACHMENTS, 'readwrite');
for (const attachment of attachments) {
transaction.objectStore(STORE_ATTACHMENTS).delete(attachment.id);
}
await this.awaitTransaction(transaction);
}
/** Wipe all persisted data in every object store. */
async clearAllData(): Promise<void> {
const transaction = this.createTransaction(ALL_STORE_NAMES, 'readwrite');
for (const storeName of ALL_STORE_NAMES) {
transaction.objectStore(storeName).clear();
}
await this.awaitTransaction(transaction);
}
private openDatabase(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DATABASE_NAME, DATABASE_VERSION);
request.onerror = () => reject(request.error);
request.onupgradeneeded = () => this.setupSchema(request.result);
request.onsuccess = () => resolve(request.result);
});
}
private setupSchema(database: IDBDatabase): void {
const messagesStore = this.ensureStore(database, STORE_MESSAGES, { keyPath: 'id' });
this.ensureIndex(messagesStore, 'roomId', 'roomId');
this.ensureIndex(messagesStore, 'timestamp', 'timestamp');
this.ensureStore(database, STORE_USERS, { keyPath: 'id' });
const roomsStore = this.ensureStore(database, STORE_ROOMS, { keyPath: 'id' });
this.ensureIndex(roomsStore, 'timestamp', 'timestamp');
const reactionsStore = this.ensureStore(database, STORE_REACTIONS, { keyPath: 'id' });
this.ensureIndex(reactionsStore, 'messageId', 'messageId');
this.ensureIndex(reactionsStore, 'userId', 'userId');
const bansStore = this.ensureStore(database, STORE_BANS, { keyPath: 'oderId' });
this.ensureIndex(bansStore, 'roomId', 'roomId');
this.ensureIndex(bansStore, 'expiresAt', 'expiresAt');
this.ensureStore(database, STORE_META, { keyPath: 'id' });
const attachmentsStore = this.ensureStore(database, STORE_ATTACHMENTS, { keyPath: 'id' });
this.ensureIndex(attachmentsStore, 'messageId', 'messageId');
}
private ensureStore(
database: IDBDatabase,
name: string,
options?: IDBObjectStoreParameters
): IDBObjectStore {
if (database.objectStoreNames.contains(name)) {
return (database.transaction(name, 'readonly') as IDBTransaction).objectStore(name);
}
return database.createObjectStore(name, options);
}
private ensureIndex(store: IDBObjectStore, name: string, keyPath: string): void {
if (!store.indexNames.contains(name)) {
store.createIndex(name, keyPath, { unique: false });
}
}
private createTransaction(
storeNames: string | string[],
mode: IDBTransactionMode
): IDBTransaction {
if (!this.database) {
throw new Error('Database has not been initialized');
}
return this.database.transaction(storeNames, mode);
}
private awaitTransaction(transaction: IDBTransaction): Promise<void> {
return new Promise((resolve, reject) => {
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
transaction.onabort = () => reject(transaction.error);
});
}
private async put(storeName: string, value: unknown): Promise<void> {
const transaction = this.createTransaction(storeName, 'readwrite');
transaction.objectStore(storeName).put(value);
await this.awaitTransaction(transaction);
}
private async get<T>(storeName: string, key: IDBValidKey): Promise<T | undefined> {
const transaction = this.createTransaction(storeName, 'readonly');
const request = transaction.objectStore(storeName).get(key);
return new Promise<T | undefined>((resolve, reject) => {
request.onsuccess = () => resolve(request.result as T | undefined);
request.onerror = () => reject(request.error);
});
}
private async getAll<T>(storeName: string): Promise<T[]> {
const transaction = this.createTransaction(storeName, 'readonly');
const request = transaction.objectStore(storeName).getAll();
return new Promise<T[]>((resolve, reject) => {
request.onsuccess = () => resolve((request.result as T[]) ?? []);
request.onerror = () => reject(request.error);
});
}
private async getAllFromIndex<T>(
storeName: string,
indexName: string,
query: IDBValidKey | IDBKeyRange
): Promise<T[]> {
const transaction = this.createTransaction(storeName, 'readonly');
const request = transaction.objectStore(storeName)
.index(indexName)
.getAll(query);
return new Promise<T[]>((resolve, reject) => {
request.onsuccess = () => resolve((request.result as T[]) ?? []);
request.onerror = () => reject(request.error);
});
}
private async deleteRecord(storeName: string, key: IDBValidKey): Promise<void> {
const transaction = this.createTransaction(storeName, 'readwrite');
transaction.objectStore(storeName).delete(key);
await this.awaitTransaction(transaction);
}
private normaliseMessage(message: Message): Message {
if (message.content === DELETED_MESSAGE_CONTENT) {
return { ...message,
reactions: [] };
}
return message;
}
}

View File

@@ -0,0 +1,135 @@
/* eslint-disable @typescript-eslint/member-ordering, */
import {
inject,
Injectable,
signal
} from '@angular/core';
import {
Message,
User,
Room,
Reaction,
BanEntry
} from '../../shared-kernel';
import type { ChatAttachmentMeta } from '../../shared-kernel';
import { PlatformService } from '../../core/platform';
import { BrowserDatabaseService } from './browser-database.service';
import { ElectronDatabaseService } from './electron-database.service';
/**
* Facade database service that transparently delegates to the correct
* storage backend based on the runtime platform.
*
* - Electron -> SQLite via {@link ElectronDatabaseService} (IPC to main process).
* - Browser -> IndexedDB via {@link BrowserDatabaseService}.
*
* All consumers inject `DatabaseService`; the underlying storage engine
* is selected automatically.
*/
@Injectable({ providedIn: 'root' })
export class DatabaseService {
private readonly platform = inject(PlatformService);
private readonly browserDb = inject(BrowserDatabaseService);
private readonly electronDb = inject(ElectronDatabaseService);
/** Reactive flag: `true` once {@link initialize} has completed. */
isReady = signal(false);
/** The active storage backend for the current platform. */
private get backend() {
return this.platform.isBrowser ? this.browserDb : this.electronDb;
}
/** Initialise the platform-specific database. */
async initialize(): Promise<void> {
await this.backend.initialize();
this.isReady.set(true);
}
/** Persist a single chat message. */
saveMessage(message: Message) { return this.backend.saveMessage(message); }
/** Retrieve messages for a room with optional pagination. */
getMessages(roomId: string, limit = 100, offset = 0) { return this.backend.getMessages(roomId, limit, offset); }
/** Permanently delete a message by ID. */
deleteMessage(messageId: string) { return this.backend.deleteMessage(messageId); }
/** Apply partial updates to an existing message. */
updateMessage(messageId: string, updates: Partial<Message>) { return this.backend.updateMessage(messageId, updates); }
/** Retrieve a single message by ID. */
getMessageById(messageId: string) { return this.backend.getMessageById(messageId); }
/** Remove every message belonging to a room. */
clearRoomMessages(roomId: string) { return this.backend.clearRoomMessages(roomId); }
/** Persist a reaction. */
saveReaction(reaction: Reaction) { return this.backend.saveReaction(reaction); }
/** Remove a specific reaction (user + emoji + message). */
removeReaction(messageId: string, userId: string, emoji: string) { return this.backend.removeReaction(messageId, userId, emoji); }
/** Return all reactions for a given message. */
getReactionsForMessage(messageId: string) { return this.backend.getReactionsForMessage(messageId); }
/** Persist a user record. */
saveUser(user: User) { return this.backend.saveUser(user); }
/** Retrieve a user by ID. */
getUser(userId: string) { return this.backend.getUser(userId); }
/** Retrieve the current (logged-in) user. */
getCurrentUser() { return this.backend.getCurrentUser(); }
/** Store the current user ID. */
setCurrentUserId(userId: string) { return this.backend.setCurrentUserId(userId); }
/** Retrieve users in a room. */
getUsersByRoom(roomId: string) { return this.backend.getUsersByRoom(roomId); }
/** Apply partial updates to an existing user. */
updateUser(userId: string, updates: Partial<User>) { return this.backend.updateUser(userId, updates); }
/** Persist a room record. */
saveRoom(room: Room) { return this.backend.saveRoom(room); }
/** Retrieve a room by ID. */
getRoom(roomId: string) { return this.backend.getRoom(roomId); }
/** Return every persisted room. */
getAllRooms() { return this.backend.getAllRooms(); }
/** Delete a room and its associated messages. */
deleteRoom(roomId: string) { return this.backend.deleteRoom(roomId); }
/** Apply partial updates to an existing room. */
updateRoom(roomId: string, updates: Partial<Room>) { return this.backend.updateRoom(roomId, updates); }
/** Persist a ban entry. */
saveBan(ban: BanEntry) { return this.backend.saveBan(ban); }
/** Remove a ban by oderId. */
removeBan(oderId: string) { return this.backend.removeBan(oderId); }
/** Return active bans for a room. */
getBansForRoom(roomId: string) { return this.backend.getBansForRoom(roomId); }
/** Check whether a user is currently banned from a room. */
isUserBanned(userId: string, roomId: string) { return this.backend.isUserBanned(userId, roomId); }
/** Persist attachment metadata. */
saveAttachment(attachment: ChatAttachmentMeta) { return this.backend.saveAttachment(attachment); }
/** Return all attachment records for a message. */
getAttachmentsForMessage(messageId: string) { return this.backend.getAttachmentsForMessage(messageId); }
/** Return every persisted attachment record. */
getAllAttachments() { return this.backend.getAllAttachments(); }
/** Delete all attachment records for a message. */
deleteAttachmentsForMessage(messageId: string) { return this.backend.deleteAttachmentsForMessage(messageId); }
/** Wipe all persisted data. */
clearAllData() { return this.backend.clearAllData(); }
}

View File

@@ -0,0 +1,189 @@
import { Injectable, inject } from '@angular/core';
import {
Message,
User,
Room,
Reaction,
BanEntry
} from '../../shared-kernel';
import type { ElectronApi } from '../../core/platform/electron/electron-api.models';
import { ElectronBridgeService } from '../../core/platform/electron/electron-bridge.service';
/**
* Database service for the Electron (desktop) runtime.
*
* The SQLite database is managed by TypeORM in the Electron main process.
* This service is a thin CQRS IPC client that dispatches structured
* command/query objects through the unified preload channels.
*/
@Injectable({ providedIn: 'root' })
export class ElectronDatabaseService {
private readonly electronBridge = inject(ElectronBridgeService);
/** Shorthand accessor for the preload-exposed CQRS API. */
private get api(): ElectronApi {
return this.electronBridge.requireApi();
}
/**
* No-op: the database is initialised in the main process before the
* renderer window opens and requires no explicit bootstrap call here.
*/
async initialize(): Promise<void> { /* no-op */ }
/** Persist a single chat message. */
saveMessage(message: Message): Promise<void> {
return this.api.command({ type: 'save-message', payload: { message } });
}
/**
* Retrieve messages for a room, sorted oldest-first.
*
* @param roomId - Target room.
* @param limit - Maximum number of messages to return.
* @param offset - Number of messages to skip (for pagination).
*/
getMessages(roomId: string, limit = 100, offset = 0): Promise<Message[]> {
return this.api.query<Message[]>({ type: 'get-messages', payload: { roomId, limit, offset } });
}
/** Permanently delete a message by ID. */
deleteMessage(messageId: string): Promise<void> {
return this.api.command({ type: 'delete-message', payload: { messageId } });
}
/** Apply partial updates to an existing message. */
updateMessage(messageId: string, updates: Partial<Message>): Promise<void> {
return this.api.command({ type: 'update-message', payload: { messageId, updates } });
}
/** Retrieve a single message by ID, or `null` if not found. */
getMessageById(messageId: string): Promise<Message | null> {
return this.api.query<Message | null>({ type: 'get-message-by-id', payload: { messageId } });
}
/** Remove every message belonging to a room. */
clearRoomMessages(roomId: string): Promise<void> {
return this.api.command({ type: 'clear-room-messages', payload: { roomId } });
}
/** Persist a reaction (deduplication is handled main-process side). */
saveReaction(reaction: Reaction): Promise<void> {
return this.api.command({ type: 'save-reaction', payload: { reaction } });
}
/** Remove a specific reaction (user + emoji + message). */
removeReaction(messageId: string, userId: string, emoji: string): Promise<void> {
return this.api.command({ type: 'remove-reaction', payload: { messageId, userId, emoji } });
}
/** Return all reactions for a given message. */
getReactionsForMessage(messageId: string): Promise<Reaction[]> {
return this.api.query<Reaction[]>({ type: 'get-reactions-for-message', payload: { messageId } });
}
/** Persist a user record. */
saveUser(user: User): Promise<void> {
return this.api.command({ type: 'save-user', payload: { user } });
}
/** Retrieve a user by ID, or `null` if not found. */
getUser(userId: string): Promise<User | null> {
return this.api.query<User | null>({ type: 'get-user', payload: { userId } });
}
/** Retrieve the last-authenticated ("current") user, or `null`. */
getCurrentUser(): Promise<User | null> {
return this.api.query<User | null>({ type: 'get-current-user', payload: {} });
}
/** Store which user ID is considered "current" (logged-in). */
setCurrentUserId(userId: string): Promise<void> {
return this.api.command({ type: 'set-current-user-id', payload: { userId } });
}
/** Retrieve users associated with a room. */
getUsersByRoom(roomId: string): Promise<User[]> {
return this.api.query<User[]>({ type: 'get-users-by-room', payload: { roomId } });
}
/** Apply partial updates to an existing user. */
updateUser(userId: string, updates: Partial<User>): Promise<void> {
return this.api.command({ type: 'update-user', payload: { userId, updates } });
}
/** Persist a room record. */
saveRoom(room: Room): Promise<void> {
return this.api.command({ type: 'save-room', payload: { room } });
}
/** Retrieve a room by ID, or `null` if not found. */
getRoom(roomId: string): Promise<Room | null> {
return this.api.query<Room | null>({ type: 'get-room', payload: { roomId } });
}
/** Return every persisted room. */
getAllRooms(): Promise<Room[]> {
return this.api.query<Room[]>({ type: 'get-all-rooms', payload: {} });
}
/** Delete a room by ID (also removes its messages). */
deleteRoom(roomId: string): Promise<void> {
return this.api.command({ type: 'delete-room', payload: { roomId } });
}
/** Apply partial updates to an existing room. */
updateRoom(roomId: string, updates: Partial<Room>): Promise<void> {
return this.api.command({ type: 'update-room', payload: { roomId, updates } });
}
/** Persist a ban entry. */
saveBan(ban: BanEntry): Promise<void> {
return this.api.command({ type: 'save-ban', payload: { ban } });
}
/** Remove a ban by the banned user's `oderId`. */
removeBan(oderId: string): Promise<void> {
return this.api.command({ type: 'remove-ban', payload: { oderId } });
}
/** Return active bans for a room. */
getBansForRoom(roomId: string): Promise<BanEntry[]> {
return this.api.query<BanEntry[]>({ type: 'get-bans-for-room', payload: { roomId } });
}
/** Check whether a user is currently banned from a room. */
isUserBanned(userId: string, roomId: string): Promise<boolean> {
return this.api.query<boolean>({ type: 'is-user-banned', payload: { userId, roomId } });
}
/** Persist attachment metadata. */
// eslint-disable-next-line
saveAttachment(attachment: any): Promise<void> {
return this.api.command({ type: 'save-attachment', payload: { attachment } });
}
/** Return all attachment records for a message. */
// eslint-disable-next-line
getAttachmentsForMessage(messageId: string): Promise<any[]> {
// eslint-disable-next-line
return this.api.query<any[]>({ type: 'get-attachments-for-message', payload: { messageId } });
}
/** Return every persisted attachment record. */
// eslint-disable-next-line
getAllAttachments(): Promise<any[]> {
// eslint-disable-next-line
return this.api.query<any[]>({ type: 'get-all-attachments', payload: {} });
}
/** Delete all attachment records for a message. */
deleteAttachmentsForMessage(messageId: string): Promise<void> {
return this.api.command({ type: 'delete-attachments-for-message', payload: { messageId } });
}
/** Wipe every table, removing all persisted data. */
clearAllData(): Promise<void> {
return this.api.command({ type: 'clear-all-data', payload: {} });
}
}

View File

@@ -0,0 +1 @@
export * from './database.service';