From 74b0cb2da3bce89f4169c6fc26de69fb44fc41cb Mon Sep 17 00:00:00 2001 From: Timur Shemsedinov Date: Tue, 1 Jul 2025 18:33:03 +0300 Subject: [PATCH 1/6] Optimize and improve user experience --- Pragmatic/static/storage.js | 54 ++++++++++------------ Pragmatic/test/database.js | 92 +++++++++++++++++++++++++++++-------- 2 files changed, 99 insertions(+), 47 deletions(-) diff --git a/Pragmatic/static/storage.js b/Pragmatic/static/storage.js index be5f884..e138771 100644 --- a/Pragmatic/static/storage.js +++ b/Pragmatic/static/storage.js @@ -1,14 +1,14 @@ class Database { #name; #version = 1; - #schemas = null; + #schemas = {}; #instance = null; #active = false; - constructor(name, { version, schemas }) { + constructor(name, { version = 1, schemas = {} } = {}) { this.#name = name; - if (version) this.#version = version; - if (schemas) this.#schemas = schemas; + this.#version = version; + this.#schemas = schemas; return this.#open(); } @@ -17,14 +17,15 @@ class Database { const request = indexedDB.open(this.#name, this.#version); request.onupgradeneeded = (event) => this.#upgrade(event.target.result); request.onsuccess = (event) => { - this.#active = true; this.#instance = event.target.result; + this.#active = true; resolve(); }; request.onerror = (event) => { - let { error } = event.target; - if (!error) error = new Error(`IndexedDB: can't open ${this.#name}`); - reject(error); + reject( + event.target.error ?? + new Error(`IndexedDB: can't open ${this.#name}`), + ); }; }); return this; @@ -33,9 +34,9 @@ class Database { #upgrade(db) { for (const [name, schema] of Object.entries(this.#schemas)) { if (!db.objectStoreNames.contains(name)) { - const store = db.createObjectStore(name, schema); - const indexes = schema.indexes || []; - for (const index of Object.entries(indexes)) { + const store = db.createObjectStore(name, schema.options ?? schema); + const indexes = schema.indexes ?? []; + for (const index of indexes) { store.createIndex(index.name, index.keyPath, index.options); } } @@ -52,40 +53,40 @@ class Database { const store = tx.objectStore(entity); const result = operation(store); tx.oncomplete = () => resolve(result); - tx.onerror = () => reject(tx.error || new Error('Transaction error')); + tx.onerror = () => reject(tx.error ?? new Error('Transaction error')); } catch (error) { reject(error); } }); } - async insert(entity, record) { + insert(entity, record) { return this.#exec(entity, (store) => store.add(record)); } - async update(entity, record) { + update(entity, record) { return this.#exec(entity, (store) => store.put(record)); } - async delete(entity, { id }) { + delete(entity, { id }) { return this.#exec(entity, (store) => store.delete(id)); } - async get(entity, { id }) { + 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}`)); + req.onerror = () => reject(req.error ?? new Error(`Can't get ${id}`)); }); }, 'readonly', ); } - async select(entity, { where, limit, offset, order, filter, sort } = {}) { + select(entity, { where, limit, offset, order, filter, sort } = {}) { return this.#exec( entity, (store) => { @@ -98,21 +99,16 @@ class Database { 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); + if (sort) return void resolve(filtered.toSorted(sort)); + return resolve(Database.#order(filtered, order)); } const record = cursor.value; - const match = - !where || - Object.entries(where).every(([key, val]) => record[key] === val); + const check = ([k, v]) => record[k] === v; + const match = !where || Object.entries(where).every(check); if (match) { if (!offset || skipped >= offset) { results.push(record); - if (limit && results.length >= limit) { - return void resolve(results); - } + if (limit && results.length >= limit) return resolve(results); } else { skipped++; } @@ -127,7 +123,7 @@ class Database { static #order(arr, order) { if (!order || typeof order !== 'object') return arr; - const [[field, dir]] = Object.entries(order); + const [[field, dir = 'asc']] = Object.entries(order); const sign = dir === 'desc' ? -1 : 1; return [...arr].sort((a, b) => { if (a[field] === b[field]) return 0; diff --git a/Pragmatic/test/database.js b/Pragmatic/test/database.js index 3abe1fd..16828a3 100644 --- a/Pragmatic/test/database.js +++ b/Pragmatic/test/database.js @@ -3,28 +3,84 @@ import assert from 'node:assert/strict'; import 'fake-indexeddb/auto'; import { Database } from '../static/storage.js'; -test('Database basic CRUD', async () => { - const schemas = { - user: { keyPath: 'id', autoIncrement: true }, - }; - const db = await new Database('TestDatabase', { version: 1, schemas }); +test('Database full CRUD + queries', async () => { + const db = await new Database('TestDB', { + version: 1, + schemas: { + user: { keyPath: 'id', autoIncrement: true }, + }, + }); - await db.insert('user', { name: 'Marcus', age: 30 }); - await db.insert('user', { name: 'Lucius', age: 20 }); + // Insert + await db.insert({ store: 'user', record: { name: 'Marcus', age: 20 } }); + await db.insert({ store: 'user', record: { name: 'Lucius', age: 20 } }); + await db.insert({ store: 'user', record: { name: 'Antoninus', age: 40 } }); - const users = await db.select('user'); - assert.equal(users.length, 2); + // Select all + const allUsers = await db.select({ store: 'user' }); + assert.equal(allUsers.length, 3); - const record = await db.get('user', { id: 1 }); - assert.equal(record.name, 'Marcus'); + // Get + const marcus = await db.get({ store: 'user', id: 1 }); + assert.equal(marcus.name, 'Marcus'); - record.age++; - await db.update('user', record); - - const updated = await db.get('user', { id: 1 }); + // Update + marcus.age = 31; + await db.update({ store: 'user', record: marcus }); + const updated = await db.get({ store: '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); + // Delete + await db.delete({ store: 'user', id: 2 }); + const afterDelete = await db.select({ store: 'user' }); + assert.equal(afterDelete.length, 2); + + // Select with where + const list = await db.select({ store: 'user', where: { name: 'Marcus' } }); + assert.equal(list.length, 1); + assert.equal(list[0].age, 31); + + // Select with filter + const adults = await db.select({ + store: 'user', + filter: (u) => u.age >= 30, + }); + assert.equal(adults.length, 2); + assert.equal(adults[0].name, 'Marcus'); + + // Select with order + const ordered = await db.select({ + store: 'user', + order: { age: 'desc' }, + }); + assert.equal(ordered[0].name, 'Antoninus'); + assert.equal(ordered[1].name, 'Marcus'); + + // Select with offset + const skipped = await db.select({ + store: 'user', + offset: 1, + order: { name: 'asc' }, + }); + assert.equal(skipped.length, 1); + assert.equal(skipped[0].name, 'Antoninus'); + + // Select with limit + const limited = await db.select({ + store: 'user', + limit: 1, + }); + assert.equal(limited.length, 1); +}); + +test('Database handles empty queries', async () => { + const db = await new Database('EmptyDB', { + version: 1, + schemas: { + log: { keyPath: 'id', autoIncrement: true }, + }, + }); + + const empty = await db.select({ store: 'log' }); + assert.deepEqual(empty, []); }); From 21e7ba36c42f3c7a8591bef4de89b42d9f573429 Mon Sep 17 00:00:00 2001 From: Timur Shemsedinov Date: Tue, 1 Jul 2025 19:06:04 +0300 Subject: [PATCH 2/6] Improve tests --- Pragmatic/static/application.js | 15 ++-- Pragmatic/static/storage.js | 117 ++++++++++++++------------------ 2 files changed, 59 insertions(+), 73 deletions(-) diff --git a/Pragmatic/static/application.js b/Pragmatic/static/application.js index c5e0372..3ee086b 100644 --- a/Pragmatic/static/application.js +++ b/Pragmatic/static/application.js @@ -31,20 +31,20 @@ const actions = { const age = parseInt(prompt('Enter age:'), 10); if (!Number.isInteger(age)) return; const user = { name, age }; - await db.insert('user', user); + await db.insert({ store: 'user', record: user }); logger.log('Added:', user); }, get: async () => { - const users = await db.select('user'); + const users = await db.select({ store: 'user' }); logger.log('Users:', users); }, update: async () => { - const user = await db.get('user', { id: 1 }); + const user = await db.get({ store: 'user', id: 1 }); if (user) { user.age += 1; - await db.update('user', user); + await db.update({ store: 'user', record: user }); logger.log('Updated:', user); } else { logger.log('User with id=1 not found'); @@ -52,14 +52,15 @@ const actions = { }, delete: async () => { - await db.delete('user', { id: 2 }); + await db.delete({ store: 'user', id: 2 }); logger.log('Deleted user with id=2'); }, adults: async () => { - const users = await db.select('user', { + const users = await db.select({ + store: 'user', filter: (user) => user.age >= 18, - order: 'name asc', + order: { name: 'asc' }, limit: 10, }); logger.log('Adults:', users); diff --git a/Pragmatic/static/storage.js b/Pragmatic/static/storage.js index e138771..8207c42 100644 --- a/Pragmatic/static/storage.js +++ b/Pragmatic/static/storage.js @@ -1,7 +1,7 @@ class Database { #name; - #version = 1; - #schemas = {}; + #version; + #schemas; #instance = null; #active = false; @@ -22,10 +22,7 @@ class Database { resolve(); }; request.onerror = (event) => { - reject( - event.target.error ?? - new Error(`IndexedDB: can't open ${this.#name}`), - ); + reject(event.target.error ?? new Error(`IndexedDB: can't open ${this.#name}`)); }; }); return this; @@ -34,24 +31,22 @@ class Database { #upgrade(db) { for (const [name, schema] of Object.entries(this.#schemas)) { if (!db.objectStoreNames.contains(name)) { - const store = db.createObjectStore(name, schema.options ?? schema); + const store = db.createObjectStore(name, schema); const indexes = schema.indexes ?? []; - for (const index of indexes) { - store.createIndex(index.name, index.keyPath, index.options); + for (const { name: idxName, keyPath, options } of indexes) { + store.createIndex(idxName, keyPath, options); } } } } - #exec(entity, operation, mode = 'readwrite') { - if (!this.#active) { - return Promise.reject(new Error('Database not connected')); - } + #exec(store, operation, mode = 'readwrite') { + if (!this.#active) return Promise.reject(new Error('Database not connected')); return new Promise((resolve, reject) => { try { - const tx = this.#instance.transaction(entity, mode); - const store = tx.objectStore(entity); - const result = operation(store); + const tx = this.#instance.transaction(store, mode); + const objectStore = tx.objectStore(store); + const result = operation(objectStore); tx.oncomplete = () => resolve(result); tx.onerror = () => reject(tx.error ?? new Error('Transaction error')); } catch (error) { @@ -60,65 +55,55 @@ class Database { }); } - insert(entity, record) { - return this.#exec(entity, (store) => store.add(record)); + insert({ store, record }) { + return this.#exec(store, (s) => s.add(record)); } - update(entity, record) { - return this.#exec(entity, (store) => store.put(record)); + update({ store, record }) { + return this.#exec(store, (s) => s.put(record)); } - delete(entity, { id }) { - return this.#exec(entity, (store) => store.delete(id)); + delete({ store, id }) { + return this.#exec(store, (s) => s.delete(id)); } - 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', - ); + get({ store, id }) { + return this.#exec(store, (s) => { + const req = s.get(id); + return new Promise((resolve, reject) => { + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error ?? new Error(`Can't get ${id}`)); + }); + }, 'readonly'); } - 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; - if (sort) return void resolve(filtered.toSorted(sort)); - return resolve(Database.#order(filtered, order)); - } - const record = cursor.value; - const check = ([k, v]) => record[k] === v; - const match = !where || Object.entries(where).every(check); - if (match) { - if (!offset || skipped >= offset) { - results.push(record); - if (limit && results.length >= limit) return resolve(results); - } else { - skipped++; - } + select({ store, where, limit, offset, order, filter, sort }) { + return this.#exec(store, (s) => { + const results = []; + let skipped = 0; + return new Promise((resolve, reject) => { + const req = s.openCursor(); + req.onerror = () => reject(req.error); + req.onsuccess = (event) => { + const cursor = event.target.result; + if (!cursor) { + const filtered = filter ? results.filter(filter) : results; + return resolve(sort ? filtered.toSorted(sort) : Database.#order(filtered, order)); + } + const record = cursor.value; + const match = !where || Object.entries(where).every(([k, v]) => record[k] === v); + if (match) { + if (!offset || skipped >= offset) { + results.push(record); + if (limit && results.length >= limit) return resolve(results); + } else { + skipped++; } - cursor.continue(); - }; - }); - }, - 'readonly', - ); + } + cursor.continue(); + }; + }); + }, 'readonly'); } static #order(arr, order) { From f1d5a9e2c4d8eab51c7edd82a8e80bb4d4da402e Mon Sep 17 00:00:00 2001 From: Timur Shemsedinov Date: Wed, 2 Jul 2025 00:43:12 +0300 Subject: [PATCH 3/6] Add indexedDB native API example --- Native/package.json | 3 ++ Native/static/404.html | 11 +++++ Native/static/application.js | 92 +++++++++++++++++++++++++++++++++++ Native/static/favicon.ico | Bin 0 -> 1150 bytes Native/static/index.html | 20 ++++++++ Native/static/styles.css | 37 ++++++++++++++ 6 files changed, 163 insertions(+) create mode 100644 Native/package.json create mode 100644 Native/static/404.html create mode 100644 Native/static/application.js create mode 100644 Native/static/favicon.ico create mode 100644 Native/static/index.html create mode 100644 Native/static/styles.css diff --git a/Native/package.json b/Native/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/Native/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/Native/static/404.html b/Native/static/404.html new file mode 100644 index 0000000..586632d --- /dev/null +++ b/Native/static/404.html @@ -0,0 +1,11 @@ + + + + + Error 404: File not found + + + +

File not found

+ + diff --git a/Native/static/application.js b/Native/static/application.js new file mode 100644 index 0000000..e8735bb --- /dev/null +++ b/Native/static/application.js @@ -0,0 +1,92 @@ +class Logger { + #output; + + constructor(outputId) { + this.#output = document.getElementById(outputId); + } + + log(...args) { + const lines = args.map(Logger.#serialize); + this.#output.textContent += lines.join(' ') + '\n'; + this.#output.scrollTop = this.#output.scrollHeight; + } + + static #serialize(x) { + return typeof x === 'object' ? JSON.stringify(x, null, 2) : x; + } +} + +const logger = new Logger('output'); + +const db = await new Promise((resolve, reject) => { + const request = indexedDB.open('Example', 1); + request.onupgradeneeded = () => { + const db = request.result; + if (!db.objectStoreNames.contains('user')) { + db.createObjectStore('user', { keyPath: 'id', autoIncrement: true }); + } + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); +}); + +document.getElementById('add').onclick = () => { + const name = prompt('Enter user name:'); + if (!name) return; + const age = parseInt(prompt('Enter age:'), 10); + if (!Number.isInteger(age)) return; + const tx = db.transaction('user', 'readwrite'); + tx.objectStore('user').add({ name, age }); + tx.oncomplete = () => logger.log('Added:', { name, age }); + tx.onerror = () => logger.log('Add failed'); +}; + +document.getElementById('get').onclick = () => { + const tx = db.transaction('user', 'readonly'); + const store = tx.objectStore('user'); + const req = store.getAll(); + req.onsuccess = () => logger.log('Users:', req.result); + req.onerror = () => logger.log('Get failed'); +}; + +document.getElementById('update').onclick = () => { + const tx = db.transaction('user', 'readwrite'); + const store = tx.objectStore('user'); + const req = store.get(1); + req.onsuccess = () => { + const user = req.result; + if (!user) { + logger.log('User with id=1 not found'); + return; + } + user.age += 1; + store.put(user); + tx.oncomplete = () => logger.log('Updatued:', user); + }; + req.onerror = () => logger.log('Update failed'); +}; + +document.getElementById('delete').onclick = () => { + const tx = db.transaction('user', 'readwrite'); + tx.objectStore('user').delete(2); + tx.oncomplete = () => logger.log('Deleted user with id=2'); + tx.onerror = () => logger.log('Delete failed'); +}; + +document.getElementById('adults').onclick = () => { + const tx = db.transaction('user', 'readonly'); + const store = tx.objectStore('user'); + const req = store.openCursor(); + const adults = []; + req.onsuccess = (event) => { + const cursor = event.target.result; + if (!cursor) { + logger.log('Adults:', adults); + return; + } + const user = cursor.value; + if (user.age >= 18) adults.push(user); + cursor.continue(); + }; + req.onerror = () => logger.log('Adult query failed'); +}; diff --git a/Native/static/favicon.ico b/Native/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1b758882a62e96a3b42f6e257cc4bc72e8a4da7a GIT binary patch literal 1150 zcma))ODIH97{~vjlno2nl7-mVC=0T&P?EYO@&NLeV2 zBuPn#rDknyWkFeRj_;f6%$>`4%=EwKp6~nr{^vV&fdu-erGcJVXiEYz0OXRWL>046 z3qZZ>d{dGAUk^jefUB(dFI(Kcq4mJfp3zt;NkLLdT;JECp%oDqCP{Qxicg+1!lZ() z_UPWw(uvuMaLlqgE8b&x&R|WD?6df4OW><{BnkNICC2InCL1M|T@n)w5~Fnj-WrL) zYJq_&&c4TjGd+6tvaUyb>{VKD3T(I~f=;WZdNWPjv+lCt))8=sD9i8b#4^9`7MP-0 z6wB)p&1L7yX&%e7qMI!9tRk zRq9*cjctIKO~{jnQ~xFS+{ + + + + IndexedDB Exampe + + + +

IndexedDB Repository Example

+
+ + + + + +
+

+  
+
+
diff --git a/Native/static/styles.css b/Native/static/styles.css
new file mode 100644
index 0000000..194793e
--- /dev/null
+++ b/Native/static/styles.css
@@ -0,0 +1,37 @@
+body {
+  font-family: system-ui, sans-serif;
+  padding: 1rem;
+  background: #f4f4f4;
+  color: #222;
+}
+
+h1 {
+  margin-bottom: 1rem;
+}
+
+.controls {
+  margin-bottom: 1rem;
+}
+
+button {
+  margin-right: 0.5rem;
+  padding: 0.5rem 1rem;
+  font-size: 1rem;
+  cursor: pointer;
+}
+
+pre#output {
+  position: fixed;
+  top: 10rem;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  overflow: auto;
+  margin: 0;
+  padding: 1rem;
+  background: #111;
+  color: #0f0;
+  font-family: monospace;
+  font-size: 0.9rem;
+  white-space: pre-wrap;
+}

From 08183460fb57d43022cc74b0658ef3e45c943ab2 Mon Sep 17 00:00:00 2001
From: Timur Shemsedinov 
Date: Wed, 2 Jul 2025 00:44:06 +0300
Subject: [PATCH 4/6] Add indexedDB enterprise API example

---
 Enterprise/package.json          |   3 ++
 Enterprise/static/404.html       |  11 +++++
 Enterprise/static/application.js |  68 +++++++++++++++++++++++++++++++
 Enterprise/static/core.js        |  50 +++++++++++++++++++++++
 Enterprise/static/database.js    |  46 +++++++++++++++++++++
 Enterprise/static/favicon.ico    | Bin 0 -> 1150 bytes
 Enterprise/static/index.html     |  20 +++++++++
 Enterprise/static/styles.css     |  37 +++++++++++++++++
 Enterprise/static/user.js        |  41 +++++++++++++++++++
 Enterprise/test/core.js          |  41 +++++++++++++++++++
 Enterprise/test/database.js      |  34 ++++++++++++++++
 Enterprise/test/user.js          |  42 +++++++++++++++++++
 12 files changed, 393 insertions(+)
 create mode 100644 Enterprise/package.json
 create mode 100644 Enterprise/static/404.html
 create mode 100644 Enterprise/static/application.js
 create mode 100644 Enterprise/static/core.js
 create mode 100644 Enterprise/static/database.js
 create mode 100644 Enterprise/static/favicon.ico
 create mode 100644 Enterprise/static/index.html
 create mode 100644 Enterprise/static/styles.css
 create mode 100644 Enterprise/static/user.js
 create mode 100644 Enterprise/test/core.js
 create mode 100644 Enterprise/test/database.js
 create mode 100644 Enterprise/test/user.js

diff --git a/Enterprise/package.json b/Enterprise/package.json
new file mode 100644
index 0000000..3dbc1ca
--- /dev/null
+++ b/Enterprise/package.json
@@ -0,0 +1,3 @@
+{
+  "type": "module"
+}
diff --git a/Enterprise/static/404.html b/Enterprise/static/404.html
new file mode 100644
index 0000000..586632d
--- /dev/null
+++ b/Enterprise/static/404.html
@@ -0,0 +1,11 @@
+
+
+  
+    
+    Error 404: File not found
+    
+  
+  
+    

File not found

+ + diff --git a/Enterprise/static/application.js b/Enterprise/static/application.js new file mode 100644 index 0000000..116e39c --- /dev/null +++ b/Enterprise/static/application.js @@ -0,0 +1,68 @@ +import { Database } from './database.js'; +import { UserRepository, UserService } from './user.js'; + +class Logger { + #output; + + constructor(outputId) { + this.#output = document.getElementById(outputId); + } + + log(...args) { + const lines = args.map(Logger.#serialize); + this.#output.textContent += lines.join(' ') + '\n'; + this.#output.scrollTop = this.#output.scrollHeight; + } + + static #serialize(x) { + return typeof x === 'object' ? JSON.stringify(x, null, 2) : x; + } +} + +const logger = new Logger('output'); + +const action = (id, handler) => { + const element = document.getElementById(id); + if (element) element.onclick = handler; +}; + +const db = new Database('EnterpriseApplication', 1, (db) => { + if (!db.objectStoreNames.contains('user')) { + db.createObjectStore('user', { keyPath: 'id', autoIncrement: true }); + } +}); +await db.connect(); +const userRepository = new UserRepository(db, 'user'); +const userService = new UserService(userRepository); + +action('add', async () => { + const name = prompt('Enter user name:'); + const age = parseInt(prompt('Enter age:'), 10); + if (!name || !Number.isInteger(age)) return; + const user = await userService.createUser(name, age); + logger.log('Added:', user); +}); + +action('get', async () => { + const users = await userRepository.getAll(); + logger.log('Users:', users); +}); + +action('update', async () => { + try { + const user = await userService.incrementAge(1); + logger.log('Updated:', user); + } catch (err) { + logger.log(err.message); + } +}); + +action('delete', async () => { + await userRepository.delete(2); + logger.log('Deleted user with id=2'); +}); + +action('adults', async () => { + const adults = await userService.findAdults(); + logger.log('Adults:', adults); +}); diff --git a/Enterprise/static/core.js b/Enterprise/static/core.js new file mode 100644 index 0000000..90bdce7 --- /dev/null +++ b/Enterprise/static/core.js @@ -0,0 +1,50 @@ +export class Repository { + constructor(database, storeName) { + this.db = database; + this.storeName = storeName; + } + + insert(record) { + return this.db.exec(this.storeName, 'readwrite', (store) => + store.add(record), + ); + } + + getAll() { + return this.db.exec(this.storeName, 'readonly', (store) => { + const req = store.getAll(); + return new Promise((resolve, reject) => { + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); + }); + } + + get(id) { + return this.db.exec(this.storeName, 'readonly', (store) => { + const req = store.get(id); + return new Promise((resolve, reject) => { + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); + }); + } + + update(record) { + return this.db.exec(this.storeName, 'readwrite', (store) => + store.put(record), + ); + } + + delete(id) { + return this.db.exec(this.storeName, 'readwrite', (store) => + store.delete(id), + ); + } +} + +export class Service { + constructor(repository) { + this.repository = repository; + } +} diff --git a/Enterprise/static/database.js b/Enterprise/static/database.js new file mode 100644 index 0000000..ebc1a6b --- /dev/null +++ b/Enterprise/static/database.js @@ -0,0 +1,46 @@ +export class Database { + #db; + + constructor(name, version = 1, upgradeCallback) { + this.name = name; + this.version = version; + this.upgradeCallback = upgradeCallback; + } + + async connect() { + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.name, this.version); + + request.onupgradeneeded = (event) => { + const db = event.target.result; + if (this.upgradeCallback) this.upgradeCallback(db); + }; + + request.onsuccess = () => { + this.#db = request.result; + resolve(); + }; + + request.onerror = () => reject(request.error); + }); + } + + transaction(storeName, mode = 'readonly') { + const tx = this.#db.transaction(storeName, mode); + return tx.objectStore(storeName); + } + + exec(storeName, mode, operation) { + return new Promise((resolve, reject) => { + try { + const tx = this.#db.transaction(storeName, mode); + const store = tx.objectStore(storeName); + const result = operation(store); + tx.oncomplete = () => resolve(result); + tx.onerror = () => reject(tx.error); + } catch (err) { + reject(err); + } + }); + } +} diff --git a/Enterprise/static/favicon.ico b/Enterprise/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1b758882a62e96a3b42f6e257cc4bc72e8a4da7a GIT binary patch literal 1150 zcma))ODIH97{~vjlno2nl7-mVC=0T&P?EYO@&NLeV2 zBuPn#rDknyWkFeRj_;f6%$>`4%=EwKp6~nr{^vV&fdu-erGcJVXiEYz0OXRWL>046 z3qZZ>d{dGAUk^jefUB(dFI(Kcq4mJfp3zt;NkLLdT;JECp%oDqCP{Qxicg+1!lZ() z_UPWw(uvuMaLlqgE8b&x&R|WD?6df4OW><{BnkNICC2InCL1M|T@n)w5~Fnj-WrL) zYJq_&&c4TjGd+6tvaUyb>{VKD3T(I~f=;WZdNWPjv+lCt))8=sD9i8b#4^9`7MP-0 z6wB)p&1L7yX&%e7qMI!9tRk zRq9*cjctIKO~{jnQ~xFS+{ + + + + IndexedDB Exampe + + + +

IndexedDB Repository Example

+
+ + + + + +
+

+  
+
+
diff --git a/Enterprise/static/styles.css b/Enterprise/static/styles.css
new file mode 100644
index 0000000..194793e
--- /dev/null
+++ b/Enterprise/static/styles.css
@@ -0,0 +1,37 @@
+body {
+  font-family: system-ui, sans-serif;
+  padding: 1rem;
+  background: #f4f4f4;
+  color: #222;
+}
+
+h1 {
+  margin-bottom: 1rem;
+}
+
+.controls {
+  margin-bottom: 1rem;
+}
+
+button {
+  margin-right: 0.5rem;
+  padding: 0.5rem 1rem;
+  font-size: 1rem;
+  cursor: pointer;
+}
+
+pre#output {
+  position: fixed;
+  top: 10rem;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  overflow: auto;
+  margin: 0;
+  padding: 1rem;
+  background: #111;
+  color: #0f0;
+  font-family: monospace;
+  font-size: 0.9rem;
+  white-space: pre-wrap;
+}
diff --git a/Enterprise/static/user.js b/Enterprise/static/user.js
new file mode 100644
index 0000000..5ef9126
--- /dev/null
+++ b/Enterprise/static/user.js
@@ -0,0 +1,41 @@
+import { Repository, Service } from './core.js';
+
+export class UserModel {
+  constructor(name, age) {
+    if (typeof name !== 'string' || name.trim().length === 0) {
+      throw new Error('Invalid name');
+    }
+    if (!Number.isInteger(age) || age < 0) {
+      throw new Error('Invalid age');
+    }
+    this.name = name.trim();
+    this.age = age;
+  }
+}
+
+export class UserRepository extends Repository {
+  constructor(database) {
+    super(database, 'user');
+  }
+}
+
+export class UserService extends Service {
+  async createUser(name, age) {
+    const user = new UserModel(name, age);
+    await this.repository.insert(user);
+    return user;
+  }
+
+  async incrementAge(id) {
+    const user = await this.repository.get(id);
+    if (!user) throw new Error('User with id=1 not found');
+    user.age += 1;
+    await this.repository.update(user);
+    return user;
+  }
+
+  async findAdults() {
+    const users = await this.repository.getAll();
+    return users.filter((user) => user.age >= 18);
+  }
+}
diff --git a/Enterprise/test/core.js b/Enterprise/test/core.js
new file mode 100644
index 0000000..c716dc6
--- /dev/null
+++ b/Enterprise/test/core.js
@@ -0,0 +1,41 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import 'fake-indexeddb/auto';
+import { Database } from '../static/database.js';
+import { Repository, Service } from '../static/core.js';
+
+test('Enterprise: Repository', async () => {
+  const db = new Database('RepoTestDB', 1, (db) => {
+    db.createObjectStore('items', { keyPath: 'id', autoIncrement: true });
+  });
+  await db.connect();
+
+  const repo = new Repository(db, 'items');
+
+  const item = { name: 'Item1' };
+  await repo.insert(item);
+
+  const items = await repo.getAll();
+  assert.equal(items.length, 1);
+  assert.equal(items[0].name, 'Item1');
+
+  const id = items[0].id;
+  const one = await repo.get(id);
+  assert.equal(one.name, 'Item1');
+
+  one.name = 'Item1Updated';
+  await repo.update(one);
+
+  const updated = await repo.get(id);
+  assert.equal(updated.name, 'Item1Updated');
+
+  await repo.delete(id);
+  const afterDelete = await repo.getAll();
+  assert.equal(afterDelete.length, 0);
+});
+
+test('Enterprise: Service', () => {
+  const fakeRepo = { insert: () => {}, get: () => {} };
+  const service = new Service(fakeRepo);
+  assert.equal(service.repository, fakeRepo);
+});
diff --git a/Enterprise/test/database.js b/Enterprise/test/database.js
new file mode 100644
index 0000000..c0e22e7
--- /dev/null
+++ b/Enterprise/test/database.js
@@ -0,0 +1,34 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import 'fake-indexeddb/auto';
+import { Database } from '../static/database.js';
+import { Repository, Service } from '../static/core.js';
+import { UserModel, UserRepository, UserService } from '../static/user.js';
+
+test('Enterprise: Database CRUD + queries', async () => {
+  const db = new Database('TestDB', 1, (db) => {
+    if (!db.objectStoreNames.contains('user')) {
+      db.createObjectStore('user', { keyPath: 'id', autoIncrement: true });
+    }
+  });
+  await db.connect();
+
+  const repo = new Repository(db, 'user');
+  const marcus = new UserModel('Marcus', 28);
+
+  await repo.insert(marcus);
+  const allUsers = await repo.getAll();
+  assert.equal(allUsers.length, 1);
+  assert.equal(allUsers[0].name, 'Marcus');
+
+  const user = await repo.get(1);
+  user.age = 29;
+  await repo.update(user);
+
+  const updated = await repo.get(1);
+  assert.equal(updated.age, 29);
+
+  await repo.delete(1);
+  const empty = await repo.getAll();
+  assert.equal(empty.length, 0);
+});
diff --git a/Enterprise/test/user.js b/Enterprise/test/user.js
new file mode 100644
index 0000000..bacdd92
--- /dev/null
+++ b/Enterprise/test/user.js
@@ -0,0 +1,42 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import 'fake-indexeddb/auto';
+import { Database } from '../static/database.js';
+import { Repository, Service } from '../static/core.js';
+import { UserModel, UserRepository, UserService } from '../static/user.js';
+
+test('Enterprise: UserModel validation', async () => {
+  assert.throws(() => new UserModel('', 25), /Invalid name/);
+  assert.throws(() => new UserModel('Faustina', -1), /Invalid age/);
+  assert.throws(() => new UserModel('Lucius', 3.14), /Invalid age/);
+
+  const user = new UserModel('Titus', 42);
+  assert.ok(user instanceof UserModel);
+  assert.strictEqual(user.name, 'Titus');
+  assert.strictEqual(user.age, 42);
+});
+
+test('Enterprise: UserService, UserRepository', async () => {
+  const db = new Database('ServiceTestDB', 1, (db) => {
+    if (!db.objectStoreNames.contains('user')) {
+      db.createObjectStore('user', { keyPath: 'id', autoIncrement: true });
+    }
+  });
+  await db.connect();
+
+  const userRepo = new UserRepository(db, 'user');
+  const userService = new UserService(userRepo);
+
+  await userService.createUser('Lucius', 17);
+  await userService.createUser('Antoninus', 33);
+  await userService.createUser('Faustina', 18);
+
+  const adults = await userService.findAdults();
+  assert.equal(adults.length, 2);
+  assert.ok(adults.some((user) => user.name === 'Antoninus'));
+
+  const updatedUser = await userService.incrementAge(2);
+  assert.equal(updatedUser.age, 34);
+
+  await assert.rejects(() => userService.incrementAge(999), /User with id=1 not found/);
+});

From a109bd84443ebb74d46d88871f9bdd67e106d3fd Mon Sep 17 00:00:00 2001
From: Timur Shemsedinov 
Date: Wed, 2 Jul 2025 00:45:44 +0300
Subject: [PATCH 5/6] Update pragmatic example

---
 Pragmatic/static/storage.js | 89 +++++++++++++++++++++++--------------
 Pragmatic/test/database.js  | 14 +-----
 2 files changed, 56 insertions(+), 47 deletions(-)

diff --git a/Pragmatic/static/storage.js b/Pragmatic/static/storage.js
index 8207c42..dc24382 100644
--- a/Pragmatic/static/storage.js
+++ b/Pragmatic/static/storage.js
@@ -22,7 +22,10 @@ class Database {
         resolve();
       };
       request.onerror = (event) => {
-        reject(event.target.error ?? new Error(`IndexedDB: can't open ${this.#name}`));
+        reject(
+          event.target.error ??
+            new Error(`IndexedDB: can't open ${this.#name}`),
+        );
       };
     });
     return this;
@@ -41,7 +44,9 @@ class Database {
   }
 
   #exec(store, operation, mode = 'readwrite') {
-    if (!this.#active) return Promise.reject(new Error('Database not connected'));
+    if (!this.#active) {
+      return Promise.reject(new Error('Database not connected'));
+    }
     return new Promise((resolve, reject) => {
       try {
         const tx = this.#instance.transaction(store, mode);
@@ -68,42 +73,58 @@ class Database {
   }
 
   get({ store, id }) {
-    return this.#exec(store, (s) => {
-      const req = s.get(id);
-      return new Promise((resolve, reject) => {
-        req.onsuccess = () => resolve(req.result);
-        req.onerror = () => reject(req.error ?? new Error(`Can't get ${id}`));
-      });
-    }, 'readonly');
+    return this.#exec(
+      store,
+      (s) => {
+        const req = s.get(id);
+        return new Promise((resolve, reject) => {
+          req.onsuccess = () => resolve(req.result);
+          req.onerror = () => reject(req.error ?? new Error(`Can't get ${id}`));
+        });
+      },
+      'readonly',
+    );
   }
 
   select({ store, where, limit, offset, order, filter, sort }) {
-    return this.#exec(store, (s) => {
-      const results = [];
-      let skipped = 0;
-      return new Promise((resolve, reject) => {
-        const req = s.openCursor();
-        req.onerror = () => reject(req.error);
-        req.onsuccess = (event) => {
-          const cursor = event.target.result;
-          if (!cursor) {
-            const filtered = filter ? results.filter(filter) : results;
-            return resolve(sort ? filtered.toSorted(sort) : Database.#order(filtered, order));
-          }
-          const record = cursor.value;
-          const match = !where || Object.entries(where).every(([k, v]) => record[k] === v);
-          if (match) {
-            if (!offset || skipped >= offset) {
-              results.push(record);
-              if (limit && results.length >= limit) return resolve(results);
-            } else {
-              skipped++;
+    return this.#exec(
+      store,
+      (s) => {
+        const results = [];
+        let skipped = 0;
+        return new Promise((resolve, reject) => {
+          const req = s.openCursor();
+          req.onerror = () => reject(req.error);
+          req.onsuccess = (event) => {
+            const cursor = event.target.result;
+            if (!cursor) {
+              const filtered = filter ? results.filter(filter) : results;
+              return void resolve(
+                sort
+                  ? filtered.toSorted(sort)
+                  : Database.#order(filtered, order),
+              );
+            }
+            const record = cursor.value;
+            const match =
+              !where ||
+              Object.entries(where).every(([k, v]) => record[k] === v);
+            if (match) {
+              if (!offset || skipped >= offset) {
+                results.push(record);
+                if (limit && results.length >= limit) {
+                  return void resolve(results);
+                }
+              } else {
+                skipped++;
+              }
             }
-          }
-          cursor.continue();
-        };
-      });
-    }, 'readonly');
+            cursor.continue();
+          };
+        });
+      },
+      'readonly',
+    );
   }
 
   static #order(arr, order) {
diff --git a/Pragmatic/test/database.js b/Pragmatic/test/database.js
index 16828a3..396e534 100644
--- a/Pragmatic/test/database.js
+++ b/Pragmatic/test/database.js
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
 import 'fake-indexeddb/auto';
 import { Database } from '../static/storage.js';
 
-test('Database full CRUD + queries', async () => {
+test('Pragmatic: Database CRUD + DSL', async () => {
   const db = await new Database('TestDB', {
     version: 1,
     schemas: {
@@ -72,15 +72,3 @@ test('Database full CRUD + queries', async () => {
   });
   assert.equal(limited.length, 1);
 });
-
-test('Database handles empty queries', async () => {
-  const db = await new Database('EmptyDB', {
-    version: 1,
-    schemas: {
-      log: { keyPath: 'id', autoIncrement: true },
-    },
-  });
-
-  const empty = await db.select({ store: 'log' });
-  assert.deepEqual(empty, []);
-});

From b354a6fc0447a404995e34a13d230cff7556b1f1 Mon Sep 17 00:00:00 2001
From: Timur Shemsedinov 
Date: Wed, 2 Jul 2025 00:46:11 +0300
Subject: [PATCH 6/6] Update eslint config

---
 eslint.config.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/eslint.config.js b/eslint.config.js
index 4275f34..e970e96 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -6,7 +6,7 @@ module.exports = init;
 module.exports = [
   ...init,
   {
-    files: ['Pragmatic/**/*.js'],
+    files: ['Enterprise/**/*.js', 'Native/**/*.js', 'Pragmatic/**/*.js'],
     languageOptions: {
       sourceType: 'module',
       globals: {