1.0.7 • Published 1 year ago

@dogonis/db v1.0.7

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

Hi there

This is a simple abstraction over indexedDB based on classes with opportunity to automate version migrations.

Table of content

Basics

Model

First, we need to create a data model for the DB:

import { DBModel, DBSTable } from '@dogonis/db'

const model = DBModel.create({
    items: new DBSTable<string>()
})

DBModel.create(tables) returns initial DB model based on tables it got. In the example above DBSTable is used as table type. DBSTable stands for Database Simple Table. Simple here means that it's just a key-value storage. The only generic argument here is a type of the table data, keys are always strings.

DB

So, we have Model now we can create DB:

import { DB } from '@dogonis/db'

const db = new DB('db-name', model);

Actual DB interface is implemented by DB class. It takes database name and model.

Basic operations

Lets try some simple operations like add, get and delete:

import { DBModel, DBSTable } from '@dogonis/db'

const model = DBModel.create({
    items: new DBSTable<string>()
})

const db = new DB('db-name', model);

const addItem = (key: string, value: string) => {
    return db.add("items", key, value);
}

const getItem = async (key: string) => {
    return db.get("items", key);
}

const getAllItems = async () => {
    return db.getAll("items");
}

const deleteItem = async (key: string) => {
    return db.delete("items", key);
}

Basically each method represents specific operation and takes table name as the first argument.

Migration

Unfortunately we can't just add new fields to the model or modify existing ones, we have to create a new model from the previous. DBModel provides 2 methods to do so: next() and extend().

Next

Simply creates new model from previous and provides migration function:

const model = DBModel
    .create({
        items: new DBSTable<string>()
    })
    .next(
        {
            entries: new DBSTable<{ name: string }>(),
        },
        (prevState) => ({
            entries: prevState.map((item) => ({ key: item.key, value: { name: item.value } }))
        })
    )

const db = new DB('db-name', model);

As you can see we can(and it's recommended) just chain methods to change Database model, DB class will automatically evaluate state from previous versions using provided migration functions.

Extend

Does exactly what next() does, but takes previous model as basis:

const model = DBModel
    .create({
        items: new DBSTable<string>()
    })
    .next(
        {
            entries: new DBSTable<{ name: string }>(),
        },
        (prevState) => ({
            entries: prevState.map((item) => ({ key: item.key, value: { name: item.value } }))
        })
    )
    .extend(
        {
            categories: new DBSTable<string>(),
        },
        (prevState) => ({
            entries: prevState.entries,
            categories: [],
        })
    )

Here we just added new categories table, but it doesn't mean that we only can add, we also can override:

const model = DBModel
    .create({
        items: new DBSTable<string>()
    })
    .next(
        {
            entries: new DBSTable<{ name: string }>(),
        },
        (prevState) => ({
            entries: prevState.map((item) => ({ key: item.key, value: { name: item.value } }))
        })
    )
    .extend(
        {
            categories: new DBSTable<string>(),
        },
        (prevState) => ({
            entries: prevState.entries,
            categories: [],
        })
    )
    .extend(
        {
            entries: new DBSTable<number>(),
        },
        (prevState) => ({
            categories: prevState.categories,
            entries: [],
        })
    )

Disconnect

To disconnect the DB instance from indexedDB just call DB.disconnect()

const db = new DB('db-name', model);

// operations...

db.disconnect();

IndexedDB will wait until end of all existing operations and then just close the connection.

Advanced

DBSTable

Additionally DBSTable takes second autoKey parameter:

const model = DBModel.create({
    withNumbers: new DBSTable<string, true>(true),
})

Here we have to not only pass true to the constructor but also add second generic argument, because we need to save type data. If autoKey is provided then DB will change key type for this table from string to number and will set keys automatically, which also means that we cant use DB.put() method, because it only works with string keys.

const model = DBModel.create({
    withNumbers: new DBSTable<string, true>(true),
})

const db = new DB('db-name', model);

db.add("withNumbers", null, "test-string")

DBTable

Moving on from Simple table to Normal table or DBTable:

const model = DBModel.create({
    users: new DBTable({
        name: new DBColumn<string>(),
        surname: new DBColumn<string>(),
        age: new DBColumn<number>(),
    })
})

As it shown above DBTable takes some sort of table schema made of DBColumn. DBColumn takes column data type as generic argument. The main difference of DBTable from DBSTable is the opportunity to make indexes and unique fields. Indexes allows you to select items from table by specific field. To set column as indexable you need pass true to the constructor and generic argument:

const model = DBModel.create({
    users: new DBTable({
        nickname: new DBColumn<string, true>(true, true),
        name: new DBColumn<string>(),
        surname: new DBColumn<string, true>(true),
        age: new DBColumn<number>(),
    })
})

Above we added nickname column as new DBColumn<string, true>(true, true), second true means that this field is not only indexable, but also unique. Now we can use DB.getBy() and DB.getAllBy() to search data by nickname and surname:

const model = DBModel.create({
    characters: new DBTable({
        nickname: new DBColumn<string, true>(true, true),
        name: new DBColumn<string>(),
        surname: new DBColumn<string, true>(true),
        age: new DBColumn<number>(),
    })
})

const db = new DB('db-name', model);

const test = async () => {
    await db.add('characters', null, { nickname: '007', name: 'james', surname: 'bond', age: 32 });
    await db.add('characters', null, { nickname: 'fullmetal alchemist', name: 'edward', surname: 'elric', age: 18 });
    await db.add('characters', null, { nickname: 'armor', name: 'alphonse', surname: 'elric', age: 17 });

    const bond = await db.getBy('characters', 'nickname', '007');

    const elrics = await db.getAllBy('characters', 'surname', 'elric');
}

Listen for DB updates

Here we will describe methods to subscribe for DB updates.

DB subscription

DB class has subscribe() method which can be used to register listeners. The listener will receive table name that was updated, after add/put/update/delete/clear methods were called. Also DB.subscribe() returns subscription with unsubscribe() method.

Example:

import { DB, DBModel, DBSTable } from '@dogonis/db';

const db = new DB('db-name', new DBModel({
    a: new DBSTable<string>(),
    b: new DBSTable<number>(),
}))

db.subscribe((tableName) => console.log(`Table '${tableName}' updated`));

db.add("a", "foo", "bar"); // console:   Table 'a' updated
db.add("b", "foo", 3);     // console:   Table 'b' updated

DBObservable

DBObservable is a class that represents state of specified table. To create DBObservable you need to call DBObservable.create(db, table), this function takes DB it should listen to and table name.

import { DB, DBModel, DBTable, DBColumn, DBObservable } from '@dogonis/db';

const db = new DB('db-name', new DBModel({
    items: new DBTable({
        name: DBColumn<string, true>(true, true),
        count: DBColumn<number>(),
    }),
}))

const items$ = DBObservable.create(db, "items")

const subscription = items.subscribe((itemsValue) => {
    console.log(itemsValue)
})

await db.add("items", null, { name: "foo", count: 1 }); // console:  [{ name: "foo", count: 1 }]

subscription.unsubscribe();

As you can see above DBObservable also has subscribe() method, but this time the listener will receive the whole table.

Other methods:

  • getValue() - returns table value
  • destroy() - destroys the observable

IMPORTANT NOTE In sake of performance DBObservable stores current state of table, so it can cause memory problems if used with really large tables.

Additionally tou can pass third argument to DBObservable.create stateFactory function. This function will produce next state of the observable

import { DB, DBModel, DBTable, DBColumn, DBObservable } from '@dogonis/db';

const db = new DB('db-name', new DBModel({
    items: new DBTable({
        name: DBColumn<string, true>(true, true),
        category: DBColumn<string, true>(true),
    }),
}))

const food$ = DBObservable.create(db, "items", () => db.getAllBy("items", "category", "food"));

food$ observable will listen for updates in items table and fetch only items from "food" category.

Utility types

This section is about utility types

DBTablesName

Type of DB tables name:

import { DB, DBModel, DBSTable, DBTablesName } from '@dogonis/db';

const db = new DB('db-name', new DBModel({
    a: new DBSTable<string>(),
    b: new DBSTable<number>(),
}))

type Tables = DBTablesName<typeof db>; // "a" | "b"

DBTablesType

Type of DB tables:

import { DB, DBModel, DBSTable, DBTablesType } from '@dogonis/db';

const db = new DB('db-name', new DBModel({
    a: new DBSTable<string>(),
    b: new DBSTable<number>(),
}))

type Types = DBTablesType<typeof db>;

type AType = Types['a'] // string;
type BType = Types['b'] // number;

DBObservableType

Type of DBObservable

import { DB, DBModel, DBSTable, DBObservable, DBObservableType } from '@dogonis/db';

const db = new DB('db-name', new DBModel({
    names: new DBSTable<string>(),
    numbers: new DBSTable<number>(),
}))

const names = DBObservable.create(db, "names");

type NamesT = DBObservableType<typeof names>; // string[]
1.0.7

1 year ago

1.0.6

1 year ago

1.0.5

1 year ago

1.0.4

1 year ago

1.0.3

1 year ago

1.0.2

1 year ago

1.0.1

1 year ago

1.0.0

1 year ago

0.0.1

1 year ago