Files
palace/src/db.ts

262 lines
7.5 KiB
TypeScript

interface Note {
id: string
x: number
y: number
content: string
}
interface Space {
id: string
notes: Note[]
}
type StorageMode = 'indexeddb' | 'database'
interface StorageAdapter {
init(): Promise<void>
getSpace(id: string): Promise<Space | undefined>
saveSpace(space: Space): Promise<void>
createSpace(id: string): Promise<Space>
addNoteToSpace(spaceId: string, note: Note): Promise<void>
updateNoteInSpace(spaceId: string, noteId: string, updates: Partial<Note>): Promise<void>
deleteNoteFromSpace(spaceId: string, noteId: string): Promise<void>
getAllSpaceIds(): Promise<string[]>
deleteSpace(id: string): Promise<void>
}
class IndexedDBAdapter implements StorageAdapter {
private db: IDBDatabase | null = null
async init() {
return new Promise<void>((resolve, reject) => {
const request = indexedDB.open('palace-db', 1)
request.onerror = () => reject(request.error)
request.onsuccess = () => {
this.db = request.result
resolve()
}
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result
if (!db.objectStoreNames.contains('spaces')) {
db.createObjectStore('spaces', { keyPath: 'id' })
}
}
})
}
async getSpace(id: string): Promise<Space | undefined> {
if (!this.db) {
await this.init()
}
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(['spaces'], 'readonly')
const store = transaction.objectStore('spaces')
const request = store.get(id)
request.onerror = () => reject(request.error)
request.onsuccess = () => resolve(request.result)
})
}
async saveSpace(space: Space): Promise<void> {
if (!this.db) {
await this.init()
}
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(['spaces'], 'readwrite')
const store = transaction.objectStore('spaces')
const request = store.put(space)
request.onerror = () => reject(request.error)
request.onsuccess = () => resolve()
})
}
async createSpace(id: string): Promise<Space> {
const space: Space = {
id,
notes: []
}
await this.saveSpace(space)
return space
}
async addNoteToSpace(spaceId: string, note: Note): Promise<void> {
const space = await this.getSpace(spaceId)
if (!space) {
throw new Error(`Space ${spaceId} not found`)
}
space.notes.push(note)
await this.saveSpace(space)
}
async updateNoteInSpace(spaceId: string, noteId: string, updates: Partial<Note>): Promise<void> {
const space = await this.getSpace(spaceId)
if (!space) {
throw new Error(`Space ${spaceId} not found`)
}
const noteIndex = space.notes.findIndex(n => n.id === noteId)
if (noteIndex === -1) {
throw new Error(`Note ${noteId} not found in space ${spaceId}`)
}
space.notes[noteIndex] = { ...space.notes[noteIndex]!, ...updates } as Note
await this.saveSpace(space)
}
async deleteNoteFromSpace(spaceId: string, noteId: string): Promise<void> {
const space = await this.getSpace(spaceId)
if (!space) {
throw new Error(`Space ${spaceId} not found`)
}
space.notes = space.notes.filter(n => n.id !== noteId)
await this.saveSpace(space)
}
async getAllSpaceIds(): Promise<string[]> {
if (!this.db) {
await this.init()
}
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(['spaces'], 'readonly')
const store = transaction.objectStore('spaces')
const request = store.getAllKeys()
request.onerror = () => reject(request.error)
request.onsuccess = () => resolve(request.result as string[])
})
}
async deleteSpace(id: string): Promise<void> {
if (!this.db) {
await this.init()
}
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction(['spaces'], 'readwrite')
const store = transaction.objectStore('spaces')
const request = store.delete(id)
request.onerror = () => reject(request.error)
request.onsuccess = () => resolve()
})
}
}
class DatabaseAdapter implements StorageAdapter {
private readonly apiUrl: string
constructor(apiUrl?: string) {
this.apiUrl = apiUrl || this.getApiUrl()
}
private getApiUrl(): string {
if (typeof window !== 'undefined' && window.location.hostname === 'palace.raphaelforment.fr') {
return 'https://palace.raphaelforment.fr:3001/api'
}
return '/api'
}
async init(): Promise<void> {
// Database connection is handled per request
}
async getSpace(id: string): Promise<Space | undefined> {
const response = await fetch(`${this.apiUrl}/spaces/${id}`)
if (response.status === 404) {
return undefined
}
if (!response.ok) {
throw new Error(`Failed to get space: ${response.statusText}`)
}
return await response.json()
}
async saveSpace(space: Space): Promise<void> {
const response = await fetch(`${this.apiUrl}/spaces/${space.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(space)
})
if (!response.ok) {
throw new Error(`Failed to save space: ${response.statusText}`)
}
}
async createSpace(id: string): Promise<Space> {
const space: Space = { id, notes: [] }
await this.saveSpace(space)
return space
}
async addNoteToSpace(spaceId: string, note: Note): Promise<void> {
const response = await fetch(`${this.apiUrl}/spaces/${spaceId}/notes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(note)
})
if (!response.ok) {
throw new Error(`Failed to add note: ${response.statusText}`)
}
}
async updateNoteInSpace(spaceId: string, noteId: string, updates: Partial<Note>): Promise<void> {
const response = await fetch(`${this.apiUrl}/spaces/${spaceId}/notes/${noteId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
})
if (!response.ok) {
throw new Error(`Failed to update note: ${response.statusText}`)
}
}
async deleteNoteFromSpace(spaceId: string, noteId: string): Promise<void> {
const response = await fetch(`${this.apiUrl}/spaces/${spaceId}/notes/${noteId}`, {
method: 'DELETE'
})
if (!response.ok) {
throw new Error(`Failed to delete note: ${response.statusText}`)
}
}
async getAllSpaceIds(): Promise<string[]> {
const response = await fetch(`${this.apiUrl}/spaces`)
if (!response.ok) {
throw new Error(`Failed to get spaces: ${response.statusText}`)
}
const spaces = await response.json()
return spaces.map((space: Space) => space.id)
}
async deleteSpace(id: string): Promise<void> {
const response = await fetch(`${this.apiUrl}/spaces/${id}`, {
method: 'DELETE'
})
if (!response.ok) {
throw new Error(`Failed to delete space: ${response.statusText}`)
}
}
}
function getStorageMode(): StorageMode {
return (localStorage.getItem('palace-storage-mode') as StorageMode) || 'indexeddb'
}
function setStorageMode(mode: StorageMode): void {
localStorage.setItem('palace-storage-mode', mode)
}
function createStorageAdapter(): StorageAdapter {
const mode = getStorageMode()
switch (mode) {
case 'database':
return new DatabaseAdapter()
case 'indexeddb':
default:
return new IndexedDBAdapter()
}
}
export const db = createStorageAdapter()
export { type Note, type Space, type StorageMode, getStorageMode, setStorageMode }