Skip to content

Commit acef15a

Browse files
committed
Combine Database and Repository into a single class
1 parent dc7b487 commit acef15a

File tree

4 files changed

+133
-152
lines changed

4 files changed

+133
-152
lines changed

Pragmatic/static/application.js

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ const logger = new Logger('output');
2222
const schemas = {
2323
user: { keyPath: 'id', autoIncrement: true },
2424
};
25-
const db = await new Database('Example', 1, schemas);
26-
const repo = db.getStore('user');
25+
const db = await new Database('Example', { version: 1, schemas });
2726

2827
const actions = {
2928
add: async () => {
@@ -32,34 +31,34 @@ const actions = {
3231
const age = parseInt(prompt('Enter age:'), 10);
3332
if (!Number.isInteger(age)) return;
3433
const user = { name, age };
35-
await repo.insert(user);
34+
await db.insert('user', user);
3635
logger.log('Added:', user);
3736
},
3837

3938
get: async () => {
40-
const users = await repo.select();
39+
const users = await db.select('user');
4140
logger.log('Users:', users);
4241
},
4342

4443
update: async () => {
45-
const user = await repo.get({ id: 1 });
44+
const user = await db.get('user', { id: 1 });
4645
if (user) {
4746
user.age += 1;
48-
await repo.update(user);
47+
await db.update('user', user);
4948
logger.log('Updated:', user);
5049
} else {
5150
logger.log('User with id=1 not found');
5251
}
5352
},
5453

5554
delete: async () => {
56-
await repo.delete({ id: 2 });
55+
await db.delete('user', { id: 2 });
5756
logger.log('Deleted user with id=2');
5857
},
5958

6059
adults: async () => {
61-
const users = await repo.select({
62-
where: (user) => user.age >= 18,
60+
const users = await db.select('user', {
61+
filter: (user) => user.age >= 18,
6362
order: 'name asc',
6463
limit: 10,
6564
});

Pragmatic/static/storage.js

Lines changed: 103 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,135 +1,55 @@
1-
class Repository {
2-
constructor(store, db, schema) {
3-
this.store = store;
4-
this.db = db;
5-
this.schema = schema;
6-
}
7-
8-
insert(record) {
9-
const op = (store) => store.add(record);
10-
return this.db.execute(this.store, 'readwrite', op);
11-
}
12-
13-
async select({ where, limit, offset, order } = {}) {
14-
const op = (store) => {
15-
const results = [];
16-
let skipped = 0;
17-
return new Promise((resolve, reject) => {
18-
const cursor = store.openCursor();
19-
const done = () => resolve(Repository.#order(results, order));
20-
cursor.onerror = () => reject(cursor.error);
21-
cursor.onsuccess = (event) => {
22-
const cursor = event.target.result;
23-
if (!cursor) return void done();
24-
const record = cursor.value;
25-
if (!where || where(record)) {
26-
if (!offset || skipped >= offset) {
27-
results.push(record);
28-
if (limit && results.length >= limit) return void done();
29-
} else {
30-
skipped++;
31-
}
32-
}
33-
cursor.continue();
34-
};
35-
});
36-
};
37-
return this.db.execute(this.store, 'readonly', op);
38-
}
39-
40-
static #order(arr, order) {
41-
if (!order) return arr;
42-
const [field, dir = 'asc'] = order.split(' ');
43-
const sign = dir === 'desc' ? -1 : 1;
44-
return [...arr].sort((a, b) => {
45-
if (a[field] === b[field]) return 0;
46-
return a[field] > b[field] ? sign : -sign;
47-
});
48-
}
49-
50-
get({ id }) {
51-
return this.db.execute(this.store, 'readonly', (store) => {
52-
const req = store.get(id);
53-
return new Promise((resolve, reject) => {
54-
req.onerror = () => reject(req.error || new Error(`Can't get ${id}`));
55-
req.onsuccess = () => resolve(req.result);
56-
});
57-
});
58-
}
59-
60-
update(record) {
61-
const op = (store) => store.put(record);
62-
return this.db.execute(this.store, 'readwrite', op);
63-
}
64-
65-
delete({ id }) {
66-
const op = (store) => store.delete(id);
67-
return this.db.execute(this.store, 'readwrite', op);
68-
}
69-
}
70-
711
class Database {
722
#name;
73-
#version;
74-
#schemas;
75-
#instance;
3+
#version = 1;
4+
#schemas = null;
5+
#instance = null;
766
#active = false;
77-
#stores = new Map();
787

79-
constructor(name, version, schemas) {
8+
constructor(name, { version, schemas }) {
809
this.#name = name;
81-
this.#version = version;
82-
this.#schemas = schemas;
10+
if (version) this.#version = version;
11+
if (schemas) this.#schemas = schemas;
8312
return this.#open();
8413
}
8514

8615
async #open() {
87-
await this.#connect();
88-
await this.#init();
89-
return this;
90-
}
91-
92-
async #connect() {
93-
this.#instance = await new Promise((resolve, reject) => {
16+
await new Promise((resolve, reject) => {
9417
const request = indexedDB.open(this.#name, this.#version);
95-
request.onupgradeneeded = (event) => this.#upgrade(event);
18+
request.onupgradeneeded = (event) => this.#upgrade(event.target.result);
9619
request.onsuccess = (event) => {
9720
this.#active = true;
98-
resolve(event.target.result);
21+
this.#instance = event.target.result;
22+
resolve();
9923
};
10024
request.onerror = (event) => {
101-
reject(event.target.error || new Error('IndexedDB open error'));
25+
let { error } = event.target;
26+
if (!error) error = new Error(`IndexedDB: can't open ${this.#name}`);
27+
reject(error);
10228
};
10329
});
30+
return this;
10431
}
10532

106-
#init() {
107-
for (const [name, schema] of Object.entries(this.#schemas)) {
108-
const store = new Repository(name, this, schema);
109-
this.#stores.set(name, store);
110-
}
111-
}
112-
113-
#upgrade(event) {
114-
const db = event.target.result;
33+
#upgrade(db) {
11534
for (const [name, schema] of Object.entries(this.#schemas)) {
11635
if (!db.objectStoreNames.contains(name)) {
117-
db.createObjectStore(name, schema);
36+
const store = db.createObjectStore(name, schema);
37+
const indexes = schema.indexes || [];
38+
for (const index of Object.entries(indexes)) {
39+
store.createIndex(index.name, index.keyPath, index.options);
40+
}
11841
}
11942
}
12043
}
12144

122-
getStore(name) {
123-
return this.#stores.get(name);
124-
}
125-
126-
async execute(storeName, mode, operation) {
127-
if (!this.#active) throw new Error('Database not connected');
128-
const db = this.#instance;
45+
#exec(entity, operation, mode = 'readwrite') {
46+
if (!this.#active) {
47+
return Promise.reject(new Error('Database not connected'));
48+
}
12949
return new Promise((resolve, reject) => {
13050
try {
131-
const tx = db.transaction(storeName, mode);
132-
const store = tx.objectStore(storeName);
51+
const tx = this.#instance.transaction(entity, mode);
52+
const store = tx.objectStore(entity);
13353
const result = operation(store);
13454
tx.oncomplete = () => resolve(result);
13555
tx.onerror = () => reject(tx.error || new Error('Transaction error'));
@@ -138,6 +58,82 @@ class Database {
13858
}
13959
});
14060
}
61+
62+
async insert(entity, record) {
63+
return this.#exec(entity, (store) => store.add(record));
64+
}
65+
66+
async update(entity, record) {
67+
return this.#exec(entity, (store) => store.put(record));
68+
}
69+
70+
async delete(entity, { id }) {
71+
return this.#exec(entity, (store) => store.delete(id));
72+
}
73+
74+
async get(entity, { id }) {
75+
return this.#exec(
76+
entity,
77+
(store) => {
78+
const req = store.get(id);
79+
return new Promise((resolve, reject) => {
80+
req.onsuccess = () => resolve(req.result);
81+
req.onerror = () => reject(req.error || new Error(`Can't get ${id}`));
82+
});
83+
},
84+
'readonly',
85+
);
86+
}
87+
88+
async select(entity, { where, limit, offset, order, filter, sort } = {}) {
89+
return this.#exec(
90+
entity,
91+
(store) => {
92+
const results = [];
93+
let skipped = 0;
94+
return new Promise((resolve, reject) => {
95+
const req = store.openCursor();
96+
req.onerror = () => reject(req.error);
97+
req.onsuccess = (event) => {
98+
const cursor = event.target.result;
99+
if (!cursor) {
100+
const filtered = filter ? results.filter(filter) : results;
101+
const sorted = sort
102+
? filtered.toSorted(sort)
103+
: Database.#order(filtered, order);
104+
return void resolve(sorted);
105+
}
106+
const record = cursor.value;
107+
const match =
108+
!where ||
109+
Object.entries(where).every(([key, val]) => record[key] === val);
110+
if (match) {
111+
if (!offset || skipped >= offset) {
112+
results.push(record);
113+
if (limit && results.length >= limit) {
114+
return void resolve(results);
115+
}
116+
} else {
117+
skipped++;
118+
}
119+
}
120+
cursor.continue();
121+
};
122+
});
123+
},
124+
'readonly',
125+
);
126+
}
127+
128+
static #order(arr, order) {
129+
if (!order || typeof order !== 'object') return arr;
130+
const [[field, dir]] = Object.entries(order);
131+
const sign = dir === 'desc' ? -1 : 1;
132+
return [...arr].sort((a, b) => {
133+
if (a[field] === b[field]) return 0;
134+
return a[field] > b[field] ? sign : -sign;
135+
});
136+
}
141137
}
142138

143-
export { Repository, Database };
139+
export { Database };

Pragmatic/test/database.js

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,28 @@ import assert from 'node:assert/strict';
33
import 'fake-indexeddb/auto';
44
import { Database } from '../static/storage.js';
55

6-
test('Database connects and exposes repository', async () => {
7-
const entities = {
6+
test('Database basic CRUD', async () => {
7+
const schemas = {
88
user: { keyPath: 'id', autoIncrement: true },
99
};
10-
const db = await new Database('TestDatabase', 1, entities);
11-
const user = db.getStore('user');
12-
assert.ok(user);
10+
const db = await new Database('TestDatabase', { version: 1, schemas });
11+
12+
await db.insert('user', { name: 'Marcus', age: 30 });
13+
await db.insert('user', { name: 'Lucius', age: 20 });
14+
15+
const users = await db.select('user');
16+
assert.equal(users.length, 2);
17+
18+
const record = await db.get('user', { id: 1 });
19+
assert.equal(record.name, 'Marcus');
20+
21+
record.age++;
22+
await db.update('user', record);
23+
24+
const updated = await db.get('user', { id: 1 });
25+
assert.equal(updated.age, 31);
26+
27+
await db.delete('user', { id: 2 });
28+
const remaining = await db.select('user');
29+
assert.equal(remaining.length, 1);
1330
});

Pragmatic/test/repository.js

Lines changed: 0 additions & 31 deletions
This file was deleted.

0 commit comments

Comments
 (0)