Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,170 @@
---

**[Documentation](https://6xgate.github.io/indesely/)** — **[GitHub](https://github.com/6XGate/indesely)** — **[Package](https://www.npmjs.com/package/indesely)**

## Getting Started

To use Indesely, your application must run on a modern web browser, or similar application framework, that supports IndexedDB. Indesely is provided as a ECMAScript library via NPM. It may be used with any bundler that supports ESM or used directly by your web application.

## Installing

To install and use Indesely via NPM, you must have node.js and your favorite package manager installed.

<details>
<summary>npm</summary>

```sh [npm]
npm install indesely
```

</details>

<details>
<summary>yarn</summary>

```sh [yarn]
yarn add indesely
```

</details>

<details>
<summary>pnpm</summary>

```sh [pnpm]
pnpm install indesely
```

</details>

## Quick Example

To use the type-safe facilities of Indesely, you will need to defined a model and provide information about your database's object stores. Then define a schema for your database based on the names of your object stores.

### Models, Keys, and Indices

First, you'll need the models for your object stores.

```ts [models.ts]
interface Employee {
id: number;
name: string;
address?: string | undefined;
cell?: string | undefined;
departmentId: number;
}

interface Department {
id: number;
name: string;
}
```

Next, you'll need to provide the basic information about your object store's keys an indices.

```ts [models.ts]
interface Employee {
/* ... */
}

// Employee's key is `id` and we want to index `departmentId` with the `department` index.
type Employees = Store<Employee, 'id', { department: 'departmentId' }>;

interface Department {
/* ... */
}

// Department's key is `id`.
type Departments = Store<Department, 'id'>;
```

### Database Schema

Once your models and object store information is defined, you can then define your database schema. This will tell Indesely what the names of your object stores are and what structure they have.

```ts [employment.ts]
interface Employment {
employees: Employees;
departments: Departments;
}
```

With that, you can then defined a factory for your database, with some basic migrations to set up the structure.

```ts [employment.ts]
interface Employment {
/* ... */
}

const useEmploymentDatabase = defineDatabase<Employment>({
name: 'employment',
migrations: [
// Sets up version 1 of the data.
(trx) => {
trx.createStore('employees', 'id').createIndex('department', 'departmentId');
trx.createStore('departments', 'id');
},
],
});
```

### Connection

Once you've defined the database. You need to connect to it, in a sense. IndexedDB's idea of a connection isn't the same as most RDBMSs, it's really a handle or descriptor to the database like SQLite.

```ts [seed.ts]
const db = useEmploymentDatabase();
```

### Writing Data

Now that you have a database, it needs some data to be useful. Adding objects to it is almost as easy as using local storage. You just have to start a read/write transaction. With IndexedDB, you have to tell it to which stores you want to write, and you must with Indesely too.

Indesely will even give you a type-safe check when you start an operations to ensure you request a change or read from those stores.

```ts [seed.ts]
// ...

await db.change(['employees', 'departments'], async (trx) => {
await trx.insertInfo('departments').add({ id: 1, name: 'DevOps' });
await trx.insertInfo('departments').add({ id: 2, name: 'Product' });
await trx.insertInto('employees').add({ id: 1, name: 'Fred', departmentId: 1 });
await trx.insertInto('employees').add({ id: 2, name: 'Jane', departmentId: 1 });
await trx.insertInto('employees').add({ id: 3, name: 'Bob', departmentId: 2 });
await trx.insertInto('employees').add({ id: 4, name: 'Terra', departmentId: 2 });
});
```

### Reading Data

Now that you've put data in your database stores, you can query that data. IndexedDB, while powerful, doesn't support a very complex query language. It only has a few operations on keys and indices and the ability to define a range or value to query.

Say you want everyone in the Product departments.

```ts [read.ts]
const productFolks = await db.read(['employees'], async (trx) => {
return await trx.selectFrom('employees').where('department', '=', 2).getAll();
});

console.log(productFolks);
// Writes the following to the console.
// [
// { id: 3, name: 'Bob', departmentId: 2 },
// { id: 4, name: 'Terra', departmentId: 2 },
// ]
```

## Learning More

Now that you've seen the basic concepts of Indesely, you can learn more in depth information about the it and its API.

For more about the core concepts;

- To better understand defining models, the keys of models, the indices of an object store, and the schema of a database; read the guide for [Defining the Schema](https://6xgate.github.io/indesely/guide/defining-the-schema).
- To learn how write migrations to upgrade your database with each new version, read about [Migrations](https://6xgate.github.io/indesely/guide/migrations).
- To learn all about reading data from object store, or writing data to them; read about [Reading and Writing Data](https://6xgate.github.io/indesely/guide/reading-and-writing-data).

Once you understand the core concepts;

- You can see in depth documentation about the [Indesely API](https://6xgate.github.io/indesely/reference/management).
- You can even manage your databases [Managing Databases](https://6xgate.github.io/indesely/guide/managing-databases).
60 changes: 43 additions & 17 deletions docs/guide/defining-the-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ outline: deep

# Defining the Schema

Defining a schema comes in a few parts. The first and most important part is the [model of the objects](#models), also called records, in your object stores. The next is how you will reference, or key, you objects, called the [primary key](#defining-a-key). And finally, how you want to [index the data](#defining-indices).

## Models

The model defines the shape of the record objects that will be stored in your object stores. This is usually an object with a defined set of required and optional fields. Record models should be fully supported by the [structured clone algorithm](https://developer.mozilla.org/docs/Web/API/Web_Workers_API/Structured_clone_algorithm).

```ts
interface Employee {
id: number;
Expand All @@ -16,40 +20,55 @@ interface Employee {
}
```

Models only define the type information about the objects. Currently, IndexedDB, and Indesely by proxy at the moment, does not support validation of a schema. For schema validation, we recommend using [zod](https://zod.dev/) for the best development experience. But keep in mind that such validation will carry a performance cost.

## Store Schema

The store schema defines not just the model for an object store, but also tell Indesely the primary key and indices of that store. To define a store, use the [Store](/reference/schema#store) type.

### Basic Store

The most basic store is one that has manually defined keys for each record of any type valid in IndexedDB and no indices. These stores will alway require a key parameter when add, updating, and replacing models. They will also have no ability to use index operations.

```ts
// Store with a model
type Employees = Store<Employee>;
```

### Defining a Key

You can also define the key for a store. One way is to define a path where IndexedDB will automatically get the key from the record object itself, also called a key path. You can also define the type of manually specified keys and auto incrementing keys for a store. See [Store Keys](#store-keys) later for more information.

```ts
// Store with a model and path to a store key in the model.
type Employees = Store<Employee, 'id'>;
```

### Defining Indices

Indices are defined as an interface or type that maps an index name to a path to the index value from the record. Indies may not have manual or auto increment keys, but must come from the model itself. Indices can be used as other ways to sort and query records rather than the primary key. See [Store Indices](#store-indices) for more information.

```ts
// Store with a model, a key, and indices with paths to indexed value.
type Employees = Store<Employee, 'id', { department: 'departmentId' }>;
```

## Store Keys

The following examples will show each of the means of defining a store key and what that means for your object store.

### From a Model Field

Using the name of one of the fields of the model results in the key of the store always being based on the value of that field in a record. It also means that each record must have a unique value for that field. Any attempt to put a new record in the store with the same value in that field will either result in an error or replacing the existing record depending on whether using [`add`](/reference/update-query-builder#add) or [`put`](../reference/update-query-builder#put) respectively.

```ts
// Store with a model and path to a store key in the model.
type Employees = Store<Employee, 'id'>;
```

### Manually Defined

Using manually defined key, specified using [`ManualKey`](/reference/schema#manualkey), requires that each addition or update to the store be given a value for the primary key. Reusing an existing key will either result in an error or replacing the existing record depending on whether using [`add`](/reference/update-query-builder#add) or [`put`](/reference/update-query-builder#put) respectively. [`ManualKey`](/reference/schema#manualkey) is the default key specification for [`Store`](/reference/schema#store), so can be omitted.

```ts
interface Employee {
// id: string;
Expand All @@ -59,35 +78,36 @@ interface Employee {

// Store with a model and manual key.
type Employees = Store<Employee>;
```

```ts
// Also, a store with a model and manual key.
type Employees = Store<Employee, ManualKey>;
```

But, using manual keys by default means any valid IndexedDB key value may be used as a key for your store. To specify the type of the key, you can give that type to [`ManualKey`](/reference/schema#manualkey).

```ts
// Also, a store with a model and manual key of a specific type.
type Employees = Store<Employee, ManualKey<number>>;
```

### Auto Incrementing

Using auto-incrementing keys, specified using [`AutoIncrement`](/reference/schema#autoincrement), means that each newly added records will have its own numeric key that is one more than the prior added record. Replacing record requires using the same key with [`put`](/reference/update-query-builder#put) as the record you want to replace.

```ts
interface Employee {
// id: string;
name: string;
// ...
}
```

```ts
// Store with a model and auto-increment key.
type Employees = Store<Employee, AutoIncrement>;
```

### From a Model Path

Must like model fields, model path just uses the value from deep within the record based on the path.

```ts
// The key can reference values deep in the object, for example:
type Employees = Store<Employee, 'ids.number'>;
Expand All @@ -97,23 +117,27 @@ type Employees = Store<Employee, 'ids.dln'>;

## Store Indices

### From a Model Field
Object store indices are defined using an object type or interface that maps the index's name to its field or path. Much like primary keys using paths, the value of keys in an index is taken from record itself. Depending on whether the index is unique or not, value may not or may be shared with the same field in multiple records.

```ts
// Store with a model, a key, and indices with paths to indexed value.
type Employees = Store<Employee, 'id', { department: 'departmentId' }>;
```
- **From a Model Field**

### From a Model Path
```ts
// Store with a model, a key, and indices with paths to indexed value.
type Employees = Store<Employee, 'id', { department: 'departmentId' }>;
```

```ts
// Indices can reference values deep in the object, for example:
type Employees = Store<Employee, 'id', { department: 'departments.mainId' }>;
type Employees = Store<Employee, 'id', { department: 'departments.secondaryId' }>;
```
- **From a Model Path**

```ts
// Indices can reference values deep in the object, for example:
type Employees = Store<Employee, 'id', { department: 'departments.mainId' }>;
type Employees = Store<Employee, 'id', { department: 'departments.secondaryId' }>;
```

## Database Schema

Once your stores are define. You will then want to define the schema of your database. This is simply just an [object type or interface](/reference/database) that maps a stores name to its object store schema.

```ts
type Employees = Store<...>;
type Departments = Store<...>l
Expand All @@ -125,6 +149,8 @@ interface Employment {

## Database Factory

Once a schema defined define, all that remains is creating a [`DatbaseFactory`](../reference/database#usedatabase) using [`defineDatabase`](/reference/management#definedatabase). When defining a database, you will want to give it the options for a name and an array of [migrations](../guide/migrations).

```ts
const useEmploymentDatabase = defineDatabase<Employment>({
name: 'employment',
Expand Down
6 changes: 3 additions & 3 deletions docs/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ To install and use Indesely via NPM, you must have node.js and your favorite pac
::: code-group

```sh [npm]
npm install --save-dev indesely
npm install indesely
```

```sh [yarn]
yarn add --dev indesely
yarn add indesely
```

```sh [pnpm]
pnpm install --dev indesely
pnpm install indesely
```

:::
Expand Down
Loading