Skip to content

Combine Database and Repository into a single class #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions Pragmatic/static/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ const logger = new Logger('output');
const schemas = {
user: { keyPath: 'id', autoIncrement: true },
};
const db = await new Database('Example', 1, schemas);
const repo = db.getStore('user');
const db = await new Database('Example', { version: 1, schemas });

const actions = {
add: async () => {
Expand All @@ -32,34 +31,34 @@ const actions = {
const age = parseInt(prompt('Enter age:'), 10);
if (!Number.isInteger(age)) return;
const user = { name, age };
await repo.insert(user);
await db.insert('user', user);
logger.log('Added:', user);
},

get: async () => {
const users = await repo.select();
const users = await db.select('user');
logger.log('Users:', users);
},

update: async () => {
const user = await repo.get({ id: 1 });
const user = await db.get('user', { id: 1 });
if (user) {
user.age += 1;
await repo.update(user);
await db.update('user', user);
logger.log('Updated:', user);
} else {
logger.log('User with id=1 not found');
}
},

delete: async () => {
await repo.delete({ id: 2 });
await db.delete('user', { id: 2 });
logger.log('Deleted user with id=2');
},

adults: async () => {
const users = await repo.select({
where: (user) => user.age >= 18,
const users = await db.select('user', {
filter: (user) => user.age >= 18,
order: 'name asc',
limit: 10,
});
Expand Down
210 changes: 103 additions & 107 deletions Pragmatic/static/storage.js
Original file line number Diff line number Diff line change
@@ -1,135 +1,55 @@
class Repository {
constructor(store, db, schema) {
this.store = store;
this.db = db;
this.schema = schema;
}

insert(record) {
const op = (store) => store.add(record);
return this.db.execute(this.store, 'readwrite', op);
}

async select({ where, limit, offset, order } = {}) {
const op = (store) => {
const results = [];
let skipped = 0;
return new Promise((resolve, reject) => {
const cursor = store.openCursor();
const done = () => resolve(Repository.#order(results, order));
cursor.onerror = () => reject(cursor.error);
cursor.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor) return void done();
const record = cursor.value;
if (!where || where(record)) {
if (!offset || skipped >= offset) {
results.push(record);
if (limit && results.length >= limit) return void done();
} else {
skipped++;
}
}
cursor.continue();
};
});
};
return this.db.execute(this.store, 'readonly', op);
}

static #order(arr, order) {
if (!order) return arr;
const [field, dir = 'asc'] = order.split(' ');
const sign = dir === 'desc' ? -1 : 1;
return [...arr].sort((a, b) => {
if (a[field] === b[field]) return 0;
return a[field] > b[field] ? sign : -sign;
});
}

get({ id }) {
return this.db.execute(this.store, 'readonly', (store) => {
const req = store.get(id);
return new Promise((resolve, reject) => {
req.onerror = () => reject(req.error || new Error(`Can't get ${id}`));
req.onsuccess = () => resolve(req.result);
});
});
}

update(record) {
const op = (store) => store.put(record);
return this.db.execute(this.store, 'readwrite', op);
}

delete({ id }) {
const op = (store) => store.delete(id);
return this.db.execute(this.store, 'readwrite', op);
}
}

class Database {
#name;
#version;
#schemas;
#instance;
#version = 1;
#schemas = null;
#instance = null;
#active = false;
#stores = new Map();

constructor(name, version, schemas) {
constructor(name, { version, schemas }) {
this.#name = name;
this.#version = version;
this.#schemas = schemas;
if (version) this.#version = version;
if (schemas) this.#schemas = schemas;
return this.#open();
}

async #open() {
await this.#connect();
await this.#init();
return this;
}

async #connect() {
this.#instance = await new Promise((resolve, reject) => {
await new Promise((resolve, reject) => {
const request = indexedDB.open(this.#name, this.#version);
request.onupgradeneeded = (event) => this.#upgrade(event);
request.onupgradeneeded = (event) => this.#upgrade(event.target.result);
request.onsuccess = (event) => {
this.#active = true;
resolve(event.target.result);
this.#instance = event.target.result;
resolve();
};
request.onerror = (event) => {
reject(event.target.error || new Error('IndexedDB open error'));
let { error } = event.target;
if (!error) error = new Error(`IndexedDB: can't open ${this.#name}`);
reject(error);
};
});
return this;
}

#init() {
for (const [name, schema] of Object.entries(this.#schemas)) {
const store = new Repository(name, this, schema);
this.#stores.set(name, store);
}
}

#upgrade(event) {
const db = event.target.result;
#upgrade(db) {
for (const [name, schema] of Object.entries(this.#schemas)) {
if (!db.objectStoreNames.contains(name)) {
db.createObjectStore(name, schema);
const store = db.createObjectStore(name, schema);
const indexes = schema.indexes || [];
for (const index of Object.entries(indexes)) {
store.createIndex(index.name, index.keyPath, index.options);
}
}
}
}

getStore(name) {
return this.#stores.get(name);
}

async execute(storeName, mode, operation) {
if (!this.#active) throw new Error('Database not connected');
const db = this.#instance;
#exec(entity, operation, mode = 'readwrite') {
if (!this.#active) {
return Promise.reject(new Error('Database not connected'));
}
return new Promise((resolve, reject) => {
try {
const tx = db.transaction(storeName, mode);
const store = tx.objectStore(storeName);
const tx = this.#instance.transaction(entity, mode);
const store = tx.objectStore(entity);
const result = operation(store);
tx.oncomplete = () => resolve(result);
tx.onerror = () => reject(tx.error || new Error('Transaction error'));
Expand All @@ -138,6 +58,82 @@ class Database {
}
});
}

async insert(entity, record) {
return this.#exec(entity, (store) => store.add(record));
}

async update(entity, record) {
return this.#exec(entity, (store) => store.put(record));
}

async delete(entity, { id }) {
return this.#exec(entity, (store) => store.delete(id));
}

async get(entity, { id }) {
return this.#exec(
entity,
(store) => {
const req = store.get(id);
return new Promise((resolve, reject) => {
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error || new Error(`Can't get ${id}`));
});
},
'readonly',
);
}

async select(entity, { where, limit, offset, order, filter, sort } = {}) {
return this.#exec(
entity,
(store) => {
const results = [];
let skipped = 0;
return new Promise((resolve, reject) => {
const req = store.openCursor();
req.onerror = () => reject(req.error);
req.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor) {
const filtered = filter ? results.filter(filter) : results;
const sorted = sort
? filtered.toSorted(sort)
: Database.#order(filtered, order);
return void resolve(sorted);
}
const record = cursor.value;
const match =
!where ||
Object.entries(where).every(([key, val]) => record[key] === val);
if (match) {
if (!offset || skipped >= offset) {
results.push(record);
if (limit && results.length >= limit) {
return void resolve(results);
}
} else {
skipped++;
}
}
cursor.continue();
};
});
},
'readonly',
);
}

static #order(arr, order) {
if (!order || typeof order !== 'object') return arr;
const [[field, dir]] = Object.entries(order);
const sign = dir === 'desc' ? -1 : 1;
return [...arr].sort((a, b) => {
if (a[field] === b[field]) return 0;
return a[field] > b[field] ? sign : -sign;
});
}
}

export { Repository, Database };
export { Database };
27 changes: 22 additions & 5 deletions Pragmatic/test/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,28 @@ import assert from 'node:assert/strict';
import 'fake-indexeddb/auto';
import { Database } from '../static/storage.js';

test('Database connects and exposes repository', async () => {
const entities = {
test('Database basic CRUD', async () => {
const schemas = {
user: { keyPath: 'id', autoIncrement: true },
};
const db = await new Database('TestDatabase', 1, entities);
const user = db.getStore('user');
assert.ok(user);
const db = await new Database('TestDatabase', { version: 1, schemas });

await db.insert('user', { name: 'Marcus', age: 30 });
await db.insert('user', { name: 'Lucius', age: 20 });

const users = await db.select('user');
assert.equal(users.length, 2);

const record = await db.get('user', { id: 1 });
assert.equal(record.name, 'Marcus');

record.age++;
await db.update('user', record);

const updated = await db.get('user', { id: 1 });
assert.equal(updated.age, 31);

await db.delete('user', { id: 2 });
const remaining = await db.select('user');
assert.equal(remaining.length, 1);
});
31 changes: 0 additions & 31 deletions Pragmatic/test/repository.js

This file was deleted.