/smoke-db

IndexedDB + LINQ

Primary LanguageTypeScriptOtherNOASSERTION

smoke-db

Abstraction over indexeddb with LINQ style data queries and projection.

// create database.
const database = new smokedb.Database("myapp")

// insert records into "users" store.
await database.store("users")
  .insert({name: "dave",  email: "dave@domain.com" })
  .insert({name: "alice", email: "alice@domain.com"})
  .insert({name: "jones"  email: "jones@domain.com"})
  .insert({name: "mary",  email: "mary@domain.com" })
  .insert({name: "tim",   email: "tim@domain.com"  })
  .submit()

// read records from "users" store.
let user = await database.store ("users")
  .where (record  => record.value.email === "jones@domain.com")
  .select(record  => record.value)
  .collect()

overview

smoke-db is a proof of concept database abstraction over indexeddb. smoke-db seeks to provide a simple insert/update/delete/query interface against stores managed in indexeddb, and ultimately simplify
indexeddb development for small projects.

smoke-db is a work in progress.

creating a database

The following code will create a database that attaches to a indexeddb database named my-crm.

> If the database does not already exist, smoke will automatically create the database on first request to read / write data.

const database = new smokedb.Database("my-crm")

insert records

The following code will insert records into the object store users. If the store does not exist, it will be automatically created.

> All stores created by smoke are set to have auto incremented key values. These keys are numeric, and managed by indexeddb. Callers can obtain the keys when querying records. (see record type)

database
  .store("users")
  .insert({name: "dave",  age: 32})
  .insert({name: "alice", age: 29})
  .insert({name: "bob",   age: 42})
  .insert({name: "jones", age: 25})
  .submit()

update records

To update a record, the caller must first query the record. The following preforms a query to read back the user dave. The code then increments the users age, and updates.

let user = await database.store("users")
  .where(record => record.value.name === "dave")
  .first()

user.value.age += 1;

await database.store("users")
  .update(user)
  .submit()

deleting records

Deleting records works in a similar fashion to updating. First we read the record, followed by a call to delete.

> It is possible to prevent the initial read by storing the key for the record. Calling delete({key: 123}) works equally well.

let user = await database.store("users")
  .where(record => record.value.name === "alice")
  .first()

await database.store("users")
  .delete(user)
  .submit()

the record type

All read / query operations return the type Record<T>. The record type looks as follows.

interface Record<T> {
  key   : number 
  value : T         
}

Callers need to be mindful when filtering and mapping records, that the actual data for the record is housed under the value property, with the auto-generated key available on the key property of the record.

querying records

Records can be queried from stores. Smoke-DB provides a query interface fashioned on a asynchronous version of .NET IQueryable<T> interface. In fact, the store type implements a version of IQueryable<Record<T>>, meaning query functions are available on the store directly. Like IQueryable<T>, reading does not begin until the caller requests a result, allowing operations to be chained and deferred.

It is important to note that currently, ALL queries require a complete linear scan of the store, making stores with high record counts somewhat impractical. This aspect may be addressed in future.

The following table outlines the full list of queries available on stores.

method description
aggregate<U>(func: (acc: U, value: T, index: number) => U, initial: U): Promise<U> Applies an accumulator function over a sequence.
all(func: (value: T, index: number) => boolean): Promise<boolean> Determines whether all the elements of a sequence satisfy a condition.
any(func: (value: T, index: number) => boolean): Promise<boolean> Determines whether a sequence contains any elements that meet this criteria.
average(func: (value: T, index: number) => number): Promise<number> Computes the average of a sequence of numeric values.
cast<U>(): IQueryable<U> preforms a type cast from the source type T to U. Only useful to typescript.
collect(): Promise<Array<T>> Collects the results of this queryable into a array.
concat(queryable: IQueryable<T>): IQueryable<T> Concatenates two queryable sequences returning a new queryable that enumerates the first, then the second.
count(): Promise<number> Returns the number of elements in a sequence.
distinct(): IQueryable<T> Returns distinct elements from a sequence by using the default equality comparer to compare values.
each(func: (value: T, index: number) => void): Promise<any> Enumerates each element in this sequence.
elementAt(index: number): Promise<T> Returns the element at the specified index, if no element exists, reject.
elementAtOrDefault(index: number): Promise<T> Returns the element at the specified index, if no element exists, resolve undefined.
first(): Promise<T> Returns the first element. if no element exists, reject.
firstOrDefault(): Promise<T> Returns the first element. if no element exists, resolve undefined.
intersect(queryable: IQueryable<T>): IQueryable<T> Produces the set intersection of two sequences by using the default equality comparer to compare values.
last(): Promise<T> Returns the last element in this sequence. if empty, reject.
lastOrDefault(): Promise<T> Returns the last element in this sequence. if empty, resolve undefined.
orderBy<U>(func: (value: T) => U): IQueryable<T> Sorts the elements of a sequence in ascending order according to a key.
orderByDescending<U>(func: (value: T) => U): IQueryable<T> Sorts the elements of a sequence in descending order according to a key.
reverse(): IQueryable<T> Inverts the order of the elements in a sequence.
select<U>(func: (value: T, index: number) => U): IQueryable<U> Projects each element of a sequence into a new form.
selectMany<U>(func: (value: T, index: number) => Array<U>): IQueryable<U> Projects each element of a sequence to an IEnumerable<T> and combines the resulting sequences into one sequence.
single(func: (value: T, index: number) => boolean): Promise<T> Returns the only element of a sequence that satisfies a specified condition.
singleOrDefault(func: (value: T, index: number) => boolean): Promise<T> Returns the only element of a sequence that satisfies a specified condition or null if no such element exists.
skip(count: number): IQueryable<T> Bypasses a specified number of elements in a sequence and then returns the remaining elements.
sum(func: (value: T, index: number) => number): Promise<number> Computes the sum of the sequence of numeric values.
take(count: number): IQueryable<T> Returns a specified number of contiguous elements from the start of a sequence.
where(func: (value: T, index: number) => boolean): IQueryable<T> Filters a sequence of values based on a predicate.

examples

count records in store.

let count = await database.store("users").count()

map records to customers and collect the result as a array.

let customers = await database.store("users")
  .select(record => record.value)
  .collect()

order customers by lastname

let ordered = await database
  .store  ("users")
  .select (record => record.value)
  .orderBy(customer => customer.lastname)
  .collect()

compute the average age of customers

let average = await database
  .store   ("users")
  .select  (record => record.value.age)
  .average (age => age)

license

MIT