How to use IndexedDB

Here are some of the more important IndexedDB functionalities, wrapped in a simple Promise API.

export function openDatabase(
	dbName: string,
	version: number,
	init: (db: IDBDatabase) => void
): Promise<IDBDatabase> {
	return new Promise((resolve, reject) => {
		if (import.meta.client) {
			const request = indexedDB.open(dbName, version);

			request.onupgradeneeded = () => {
				const db = request.result;
				init(db);
			};

			request.onsuccess = () => {
				resolve(request.result);
			};

			request.onerror = () => {
				reject(request.error);
			};
		} else {
			reject(new Error("IndexedDB code does not run on server"));
		}
	});
}

export function saveRecord(
	db: IDBDatabase,
	store: string,
	record: any
): Promise<IDBValidKey> {
	return new Promise((resolve, reject) => {
		if (import.meta.client) {
			const transaction = db.transaction(store, "readwrite");
			const objectStore = transaction.objectStore(store);
			const request = objectStore.put(record);

			request.onsuccess = () => resolve(request.result);
			request.onerror = () => reject(request.error);
		} else {
			reject(new Error("IndexedDB code does not run on server"));
		}
	});
}

export function fetchAllRecords(
	db: IDBDatabase,
	store: string
): Promise<any[]> {
	return new Promise((resolve, reject) => {
		if (import.meta.client) {
			const transaction = db.transaction(store, "readwrite");
			const objectStore = transaction.objectStore(store);
			const request = objectStore.getAll();

			request.onsuccess = () => resolve(request.result);
			request.onerror = () => reject(request.error);
		} else {
			reject(new Error("IndexedDB code does not run on server"));
		}
	});
}

export function fetchRecordById(
	db: IDBDatabase,
	store: string,
	id: IDBValidKey
): Promise<any> {
	return new Promise((resolve, reject) => {
		if (import.meta.client) {
			const transaction = db.transaction(store, "readwrite");
			const objectStore = transaction.objectStore(store);
			const request = objectStore.get(id);

			request.onsuccess = () => resolve(request.result);
			request.onerror = () => reject(request.error);
		} else {
			reject(new Error("IndexedDB code does not run on server"));
		}
	});
}

export function deleteRecord(db: IDBDatabase, store: string, id: IDBValidKey) {
	return new Promise((resolve, reject) => {
		if (import.meta.client) {
			const transaction = db.transaction(store, "readwrite");
			const objectStore = transaction.objectStore(store);
			const request = objectStore.delete(id);

			request.onsuccess = () => resolve(request.result);
			request.onerror = () => reject(request.error);
		} else {
			reject(new Error("IndexedDB code does not run on server"));
		}
	});
}

And here is the same API wrapped in an object-oriented class:

export type DatabaseConfig = {
	name: string;
	version: number;
	stores?: { name: string; keyPath: string }[];
	init?: (db: IDBDatabase) => void;
};

class ObjectStore {
	#db: IDBDatabase;
	name: string;

	constructor(db: IDBDatabase, name: string) {
		this.#db = db;
		this.name = name;
	}

	save(record: any) {
		return saveRecord(this.#db, this.name, record);
	}

	fetch() {
		return fetchAllRecords(this.#db, this.name);
	}

	delete(id: IDBValidKey) {
		return deleteRecord(this.#db, this.name, id);
	}
}

// dummy for server
export class Database {
	#db?: IDBDatabase = undefined;
	onopen?: Function;

	constructor({ name, version, stores, init }: DatabaseConfig) {
		if (import.meta.client) {
			openDatabase(name, version, (db) => {
				if (stores) {
					for (const store of stores) {
						db.createObjectStore(store.name, {
							keyPath: store.keyPath,
						});
					}
				}
				if (init) {
					init(db);
				}
			})
				.then((result) => (this.#db = result))
				.then(() => {
					if (this.onopen) {
						this.onopen();
					}
				});
		}
	}

	save(store: string, record: any) {
		if (this.#db) {
			return saveRecord(this.#db, store, record);
		} else {
			throw new Error("database not initialized correctly");
		}
	}

	fetch(store: string) {
		if (this.#db) {
			return fetchAllRecords(this.#db, store);
		} else {
			throw new Error("database not initialized correctly");
		}
	}

	delete(store: string, id: IDBValidKey) {
		if (this.#db) {
			return deleteRecord(this.#db, store, id);
		} else {
			throw new Error("database not initialized correctly");
		}
	}

	getStore(store: string) {
		if (this.#db) {
			return new ObjectStore(this.#db, store);
		} else {
			throw new Error("database not initialized correctly");
		}
	}
}