Skip to content

A indexeddb database abstraction with LINQ query operators

License

Notifications You must be signed in to change notification settings

edgeworkscreative/smoke-database

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

smoke-database

A simple indexeddb database abstraction.

overview

smoke-database is a small abstraction over indexeddb. It aims to provide a simple interface for reading and writing store records to and from indexeddb and to offer a linq inspired query interface for querying records. smoke-database hides many of the details of indexeddb (database versioning, indices, etc) offering the user a straight forward insert, update, delete and query interface only.

building the project

creating a database

inserting records

updating records

deleting records

querying records

building the project

smoke-database is written in typescript, and is intended to be added to existing projects who need some form of data persistence in the browser. End users can build the project by running the tasks.js script included with this project.

node tasks install # installs build dependencies 
node tasks run     # starts http watch process on ./test/index.ts (port 5000) 

output is written to the ./target directory.

creating a database

The following will create a new database named app which contains a single store users. smoke-database will track versions based on the stores array and automatically add and remove stores based on the store names passed on the contructor. The database will be created if not exists, or loaded if it does.

import { Database } from "./smoke-database"

const database = new Database("app", ["users"])

inserting records

The following examples demonstrate adding records. Note that smoke mandates that the key property be set when adding records. The key is mandatory and analogous to id and thus must be unique. There is a convenience function createKey() available on the Database which is used to generate a unique key below, but any unique string value will work.

The following inserts a single record.

database.store("users").insert({
  key  : Database.createKey()
  name : "dave",
  email: "dave@domain.com"
})

The following inserts multiple records.

database.store("users").insert([{
  key  : Database.createKey()
  name : "dave",
  email: "dave@domain.com"
}, {
  key  : Database.createKey()
  name : "alice",
  email: "alice@domain.com"
}])

updating records

To update records, smoke-database requires you have a full copy of the object available. The reason is the update will overwrite the entirety of the record being updated. The easiest way to approach this is to request the record first.

The following gets the user dave and updates their email address. Note we use async functions here. both reading and writing is asynchronous in indexeddb and smoke-database reflects this, thus and async/await is leveraged for clarity in this example.

const f = async () => {
  // query record.
  const user = await database.store("users").query().where(n => n.name === "dave").first()

  // change email address.
  user.email = "dave.smith@domain.com"
  
  // update the record
  await database.store("users").update(user)
}

Alternatively, one can preform sweeping updates by passing a query to the store. The following queries all records and adds an additional address property to the record. Note select is analogous to javascripts map, and select is derived from LINQ.

const f = async () => {

  // remap all users
  const users = database.store("users").query().select(user => ({
    key    : user.key,
    name   : user.name,
    email  : email,
    address: "unknown" 
  })) // <-- IQueryable<T>

  // update users
  await database.store("users").update (user)
}

deleting records

Deleting records is similar to updating records where a copy of record is required first. Internally however, smoke-database only cares about the {key: ...} property. The following deletes user dave.

const f = async () => {
  await database.store("users").delete ( 
    database.store("users").query().where(n => n.name === "dave")
  )
}

Or all users if you prefer...

const f = async () => {
  await database.store("users").delete (
    database.store("users").query()
    )
}

querying records

A store's query method (as seen above) returns a type analogous to LINQ's IQueryable<T> interface. Internally smoke-database runs full linear scans across ALL store records, and these operations are applied lazily across each record emitted from indexeddb. Users should be mindful of this full linear scan, as stores with large record counts may result in a performance hit.

Unlike LINQ however, we have no waiting / block mechanism in JavaScript. smoke-database approaches this by having scalar results (aggregate, count, first, firstOrDefault etc) return promises which much be awaited, and for obtaining lists of results, the collect method must be used. The collect method returns a Promise<Array<T>> type.

operation 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.

licence

MIT

About

A indexeddb database abstraction with LINQ query operators

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 94.6%
  • JavaScript 5.1%
  • HTML 0.3%