Skip to content

Commit 5c79f2a

Browse files
committed
initial commit
0 parents  commit 5c79f2a

14 files changed

Lines changed: 689 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Node.js Package
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v3
14+
with:
15+
# Get all history for version calculation
16+
fetch-depth: 0
17+
- uses: actions/setup-node@v3
18+
with:
19+
node-version: lts/*
20+
registry-url: 'https://registry.npmjs.org'
21+
scope: '@dnvgl'
22+
- name: Install GitVersion
23+
uses: gittools/actions/gitversion/setup@v0.9.7
24+
with:
25+
versionSpec: '5.x'
26+
- name: Determine Version
27+
uses: gittools/actions/gitversion/execute@v0.9.7
28+
with:
29+
useConfigFile: true
30+
- run: yarn version --new-version $GITVERSION_SEMVER --no-git-tag-version
31+
- run: yarn install --frozen-lockfile
32+
- run: yarn build
33+
- run: yarn publish --access public
34+
env:
35+
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
dist
3+
sqljs-documentstore-*.tgz

.npmignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
tsconfig.json
2+
src

GitVersion.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
mode: Mainline
2+
branches: {}
3+
ignore:
4+
sha: []
5+
merge-message-formats: {}

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# @dnvgl-electricgrid/sqljs-documentstore
2+
3+
`@dnvgl-electricgrid/sqljs-documentstore` Minimal, encrypted, sql friendly typed document store, with support for indexed columns. Protects against transactional conflicts.
4+
The schema for each table is [id, json, ...indexedColumns]
5+
6+
7+
## Install
8+
`npm install --save @dnvgl-electricgrid/sqljs-documentstore`
9+
10+
## Usage
11+
12+
exported pieces:
13+
* `Db` abstract class, extend it and add `TypedDocumentStore`s for each table => class mapping
14+
* `TypedDocumentStore` is meant to map a class type to a table, and has read and write methods
15+
* `LockedDatabase` is consumed in TypedDocumentStore. This is a transactional wrapper around sql.js. It handles transactional scope for multiple programmatic writes and ensures locking so separate txns can't collide.
16+
* `sqlHelpers` has methods to load, save, etc
17+
18+
-----
19+
### Getting started
20+
21+
first, load your database
22+
recommend creating a wrapping "DocumentStores" class as shown below:
23+
```ts
24+
const database = await sqljsHelpers.load('mydb', await sqlite());
25+
const lockedDatabase = new LockedDatabase(database, () => sqljsHelpers.save('mydb', database));
26+
27+
public class DocumentStores extends Db {
28+
public CustomerData = new TypedDocumentStore(this.db, 'CustomerData', <CustomerData>{}, {
29+
name: x => x.name,
30+
orderCount: x => x.orders?.length
31+
}).asInterface;
32+
33+
//... more table declarations
34+
35+
36+
///////
37+
private static instance: DocumentStores;
38+
public static get Instance(): DocumentStores { return this.instance ?? (this.instance = new this()); }
39+
constructor() { super(lockedDatabase); }
40+
}
41+
```
42+
43+
see [examples.md] (https://github.com/dnvgl-opensource/sqljs-documentstore/docs/examples.md) for usage examples.

docs/examples.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
Example sqljs-documentstore usages
2+
3+
``` ts
4+
interface CustomerData {
5+
id: string;
6+
name: string;
7+
address: string;
8+
orders: any[];//OrderData[];
9+
}
10+
11+
// provide access to a CustomerData documentstore, with columns defined for name and orderCount
12+
let db: ILockedDatabase;
13+
const CustomerData = new TypedDocumentStore(() => db, 'CustomerData', <CustomerData>{}, {
14+
name: x => x.name,
15+
orderCount: x => x.orders?.length
16+
}).asInterface;
17+
18+
19+
// usage calling examples
20+
async function exampleUsage() {
21+
const allCustomers = await CustomerData.getAll();
22+
23+
const dave_id = 'abc123';
24+
const dave = await CustomerData.get(dave_id); //prefer use of 'get' if data is expected to exist, it will throw if it doesn't exist
25+
26+
const david_id = dave_id + 'id';
27+
const david = await CustomerData.tryGet(dave_id); //this will return a T? and you'll have to assert it's returned what you want
28+
29+
db.txn('update Dave', async txnId => {
30+
CustomerData.update(txnId, dave_id, x => {
31+
x.name += ' the great';
32+
x.address = 'cloud 9';
33+
x.orders = [];
34+
})
35+
});
36+
}
37+
```

package.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "@dnvgl-electricgrid/sqljs-documentstore",
3+
"version": "1.0.0",
4+
"description": "Minimal, encrypted, sql friendly typed document store, with support for indexed columns. Protects against transactional conflicts",
5+
"main": "dist/index.js",
6+
"types": "dist/index.d.ts",
7+
"files": [
8+
"/dist",
9+
"/src"
10+
],
11+
"scripts": {
12+
"build": "tsc"
13+
},
14+
"keywords": [
15+
"SQLite",
16+
"DocumentStore",
17+
"Document",
18+
"Store",
19+
"ORM",
20+
"Micro",
21+
"Typescript",
22+
"Generic",
23+
"ES7"
24+
],
25+
"license": "MIT",
26+
"dependencies": {
27+
"async-lock": "^1.4.1",
28+
"idb-keyval": "^6.2.2",
29+
"lodash": "^4.17.21",
30+
"sql.js": "^1.13.0"
31+
32+
},
33+
"devDependencies": {
34+
"@types/async-lock": "^1.4.2",
35+
"@types/lodash": "^4.17.17",
36+
"@types/sql.js": "^1.4.9",
37+
"typescript": "^5.8.3"
38+
},
39+
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
40+
}

src/Db.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { isTypedDocumentStore } from "./TypedDocumentStore";
2+
import { ILockedDatabase } from "./LockedDatabase";
3+
import { sqljsHelpers } from "./sqljsHelpers";
4+
5+
export abstract class Db {
6+
constructor(private database: () => ILockedDatabase) { }
7+
8+
public txn(description: string, actions: (txnId: string) => void): void { return this.database().txn(description, txnId => actions(txnId)); }
9+
public async txnAsync(description: string, actions: (txnId: string) => Promise<void>): Promise<void> { return this.database().txnAsync(description, txnId => actions(txnId)); }
10+
11+
public initted = false;
12+
public init() { Object.values(this).filter(value => isTypedDocumentStore(value)).forEach(store => store.init()); this.initted = true; }
13+
14+
/**
15+
* Used to query from console, or for advanced querying scenarios in code
16+
* @example //from console
17+
* console.table(await window.db.query('select * from FieldDefinitionData f join CalculatedFieldDefinitionData c on f.id = c.fieldId;'));
18+
* @example //from code - advanced query - join tables and union the data types:
19+
* await doc.query<FieldDefinitionData & CalculatedFieldDefinitionData, {fJson: string, cJson: string}>(
20+
* 'select f.json fJson, c.json cJson from FieldDefinitionData f join CalculatedFieldDefinitionData c on f.id = c.fieldId',
21+
* undefined, row => _.merge(<FieldDefinitionData>JSON.parse(row.fJson), <CalculatedFieldDefinitionData>JSON.parse(row.cJson)));
22+
* @example //from code - less type checking:
23+
* <(FieldDefinitionData & CalculatedFieldDefinitionData)[]> await doc.query(
24+
* 'select f.json fJson, c.json cJson from FieldDefinitionData f join CalculatedFieldDefinitionData c on f.id = c.fieldId',
25+
* undefined, row => _.merge(JSON.parse(row.fJson), JSON.parse(row.cJson)));
26+
*/
27+
public query<T = unknown, TRow = T>(sql: string, params?: unknown[], projection?: (row: TRow) => T): T[] {
28+
const result = sqljsHelpers.query<TRow>(this.database(), sql, params);
29+
if (result.length == 0) return [];
30+
if (!projection) return <T[]><any>result;
31+
return result.map(row => projection(row));
32+
}
33+
}

src/LockedDatabase.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import {BindParams, QueryExecResult, type Database} from 'sql.js';
2+
import AsyncLock from "async-lock";
3+
4+
export class LockedDatabase implements ILockedDatabase {
5+
lock = new AsyncLock();
6+
config: configType = {};
7+
state: {queued: queuedItemType[]} = { queued: [] };
8+
txnId?: string;
9+
10+
constructor(private db: Pick<Database, 'exec'|'run'>, private flush: () => void) {}
11+
12+
run(txnId: string, sql: string, params: BindParams = []): void {
13+
if (txnId !== this.txnId) throw new Error(`run: transaction doesn't match, txn in progress: ${this.txnId}, attempted txn: ${this.txnId}`);
14+
this.db.run(sql, params);
15+
}
16+
17+
exec(sql: string, params: BindParams = []): QueryExecResult[] { return this.db.exec(sql, params); }
18+
19+
/**
20+
* all write operations must be wrapped in a transaction
21+
* txns are locked using async-lock library to avoid bleeding across session/txn
22+
*/
23+
async txnAsync(description: string, actions: (txnId: string) => Promise<void>): Promise<void> {
24+
const txnId = `${description} ${Math.floor(Math.random() * 4294967296).toString(16)}`; //generate a unique but identifiable txnId
25+
const queuedItem = { txnId, description, timing: { waitMs: 0, actionMs: 0, flushMs: 0 } };
26+
this.state.queued.push(queuedItem);
27+
var ms = performance.now();
28+
await this.lock.acquire('txn_lock', async () => {
29+
try {
30+
queuedItem.timing.waitMs = performance.now() - ms; ms = performance.now();
31+
this.txnId = txnId;
32+
this.run(txnId, 'BEGIN TRANSACTION;');
33+
await actions(txnId);
34+
queuedItem.timing.actionMs = performance.now() - ms; ms = performance.now();
35+
this.run(txnId, 'COMMIT TRANSACTION;');
36+
this.flush();
37+
queuedItem.timing.flushMs = performance.now() - ms;
38+
} catch (error) {
39+
this.run(txnId, 'ROLLBACK TRANSACTION;');
40+
throw error;
41+
} finally {
42+
this.txnId = undefined;
43+
this.state.queued.pop();
44+
this.config.loggingHook?.(queuedItem);
45+
}
46+
});
47+
}
48+
49+
txn(description: string, actions: (txnId: string) => void): void {
50+
const txnId = `${description} ${Math.floor(Math.random() * 4294967296).toString(16)}`; //generate a unique but identifiable txnId
51+
const queuedItem = { txnId, description, timing: { waitMs: 0, actionMs: 0, flushMs: 0 } };
52+
this.state.queued.push(queuedItem);
53+
var ms = performance.now();
54+
if (this.lock.isBusy('txn_lock')) throw new Error(`_txnSync: transaction already in progress, txn in progress: ${this.txnId}, attempted txn: ${txnId}`);
55+
56+
try {
57+
queuedItem.timing.waitMs = performance.now() - ms; ms = performance.now();
58+
this.txnId = txnId;
59+
this.run(txnId, 'BEGIN TRANSACTION;');
60+
actions(txnId);
61+
queuedItem.timing.actionMs = performance.now() - ms; ms = performance.now();
62+
this.run(txnId, 'COMMIT TRANSACTION;');
63+
this.flush();
64+
queuedItem.timing.flushMs = performance.now() - ms;
65+
} catch (error) {
66+
this.run(txnId, 'ROLLBACK TRANSACTION;');
67+
throw error;
68+
} finally {
69+
this.txnId = undefined;
70+
this.state.queued.pop();
71+
this.config.loggingHook?.(queuedItem);
72+
}
73+
}
74+
}
75+
76+
export interface ILockedDatabase extends LockedDatabase{};
77+
export interface configType { loggingHook?: (q: queuedItemType) => Promise<void> };
78+
export interface queuedItemType {txnId: string, description: string, timing: { waitMs: number, actionMs: number, flushMs: number}};

0 commit comments

Comments
 (0)