Skip to content

Releases: D34DPlayer/w-orm

v0.5.0

20 Feb 01:27

Choose a tag to compare

Documentation
NPM
Full Changelog

Features

Arbitrary indices

Closes #36, closes #37, closes #12 and closes #13

Indexes are a feature of IndexedDB that allow for faster queries if used properly.

// 1st use case, multi-field ordering:
//  An index can be created with multiple fields, when using that index the results will be ordered by the fields in the index.

@Table({
  indexes: {
    nameAge: {
      fields: ['name', 'age'],
    },
  },
})
class Test extends Model {
  id!: number
  name!: string
  age!: number
}
*
// The results will be ordered by name and then by age
await Test.withIndex('nameAge').all()
*
// 2nd use case, unique fields:
//  An index can be created with the `unique` option, this will enforce that the field, or combination of fields is unique.
@Table({
  indexes: {
    uniqueName: { fields: ['firstName', 'lastName'], unique: true },
 }
})
class Test2 extends Model {
  id!: number
  firstName!: string
  lastName!: string
}
*
await Test2.create({ firstName: 'John', lastName: 'Doe', id: 1 })
// This will fail
await Test2.create({ firstName: 'John', lastName: 'Doe', id: 2 })
*
// 3rd use case, multi-field filtering:
//  An index with multiple fields can also be used to speed up filtering.
@Table({
  indexes: {
    nameAge: {
      fields: ['name', 'age'],
    },
  },
})
class Test3 extends Model {
 id!: number
 name!: string
 age!: number
}
// Looking for people named John that are 30 years old
await Test3.withIndex('nameAge', Between(['John', 30], ['John', 30])).all()
// The alternative will only use the name index and check the age for each result
await Test3.filter({ name: 'John', age: 30 }).all()
// "Between" can also be used for ranges, eg. looking for people named John that are between 30 and 40 years old
await Test3.withIndex('nameAge', Between(['John', 30], ['John', 40])).all()
*
// 4th use case, filter + ordering:
//  You can combine all this information and use the first field(s) for filtering and leaving the rest for ordering.

// Looking for people named John, ordered by age
await Test3.withIndex('nameAge', Between(['John', BetweenFilter.minKey], ['John', BetweenFilter.maxKey])).all()

The Table decorator

As you may have noticed in the previous section, an extra decorator has been added to provide table metadata.

Currently this allows the following:

  • Set the table name to something different than the class name
  • Set wether a table is abstract or not (and override the auto-inferrence)
  • Define extra indices

For non-decorator users, defineModel has been updated to allow for table metadata

defineModel<T extends Model>(
  modelClass: Constructor<T>,
  definition: TableDefinition,
  options: TableOptions = {},
)

BetweenFilter

Another feature paired with indices, they can be used to filter arbitrary indices, or used with the default single-field indices:

// Get all users with an age between 20 and 30
const query = User.filter({ age: new BetweenFilter(20, 30) })
// Get all users with an age between 20 and 30, excluding 20 and 30
const query = User.filter({ age: new BetweenFilter(20, 30, true, true) })

Changes to Transaction

The signature of Transaction has changed from:
Transaction(IDBMode, callback)
To the new:
Transaction(IDBMode, Table[], callback)

This is to make explicit what tables are being targeted by the transaction, as those will be blocked until done.

Export/Import

W-ORM now comes with utility functions to export/import the data in the database:

exportTable(table: string): Promise<unknown[]>

importTable(
  table: string,
  entries: unknown[],
  tx?: IDBTransaction,
): Promise<number>

exportDatabase(
  blacklist: string[] = [],
): Promise<Record<string, unknown[]>>

importDatabase(
  data: Record<string, unknown[]>,
): Promise<Record<string, number>>

exportDatabaseToBlob(blacklist?: string[]): Promise<Blob>

LenientModel

To improve the DX and safety Model doesn't allow anymore extra fields, however since this is something IDB allows LenientModel was created with the exact same functionality and the extra typing to allow for extra fields.

Bug-fixes

  • Auto-index shouldn't pick a field without an index
  • Shortcut init if called multiple times

Others

  • Typing improvements
  • Migrated to Vite for bundling the library
  • Migrated to Vitest for library testing

v0.4.1

24 Apr 11:07

Choose a tag to compare

Documentation
NPM
Full Changelog

Bugfixes

  • migrations.ts is now exported in index, which makes it visible in the documentation

Other

  • Package is now marked side-effect free, which should improve tree-shakiness

v0.4.0

21 Apr 23:00

Choose a tag to compare

Documentation
NPM
Full Changelog

Features

Disable indexing

Closes #28

Fields allow now to disable the automatic index creation (eg. for non indexable types like Blob)

class User extends Model {
  @Field({ index: false })
  profilePic: Blob
}

Migrations

Closes #1 and closes #31

Sometimes, changes to the way existing data is stored are required for an update, to cope with this W-ORM provides an intuitive migration system.

Migrations are defined as list of functions to be executed, depending on the current and target DB versions.
The key is the target version number, and the value is the migration callback.

Eg. { 2: (migration) => { ... } } will execute the migration callback when the current database version is smaller than 2.

A migration callback receives a MigrationContext object as its only argument.
This object contains a transaction to be used for the migration.

It is expected for the callback to create Model classes that represent the table's state in between these two versions.
The fields aren't actually used by W-ORM in this scenario, and only serve to improve the typing within the migration.

The Model methods can be then used to manipulate the data.
It is very important to use the transaction provided by the migration context, otherwise the migration will hang forever.

const migrations: MigrationList = {
  2: async (migration) => {
    class User extends Model {
      id!: number
      name!: string
    }

    const users = await User.all()
    for (const user of users) {
      user.name = `${user.id} name`
      await user.save(migration.tx)
    }
    // Or with the `forEach method`
    await User.forEach(async (instance, tx) => {
      instance.name = `${instance.id} name`
      await instance.save(tx)
    }, migration.tx)

    const specificUser = await User.get(69, migration.tx)
    await specificUser?.delete(migration.tx)
  },
}

Model now allows extra fields

The Typescript signature of Model has been changed so that you can provide and get extra, not defined fields from it. This is a feature supported by IndexedDB so it's only natural to support it here as well.

class User extends Model {
  id!: string
  name!: string 
}

const user = await User.create({
  id: "joe",
  name: "Joe",
  lastName: "Mama", // This would have caused an error before, it is allowed now.
})

const otherUser = await User.get("tom")
console.log(user.extraField) // This would have caused an error as well, you'll still need to check that it is defined

Query cloning

Closes #18

Queries can now be cloned, to allow branching and allow code deduplication.

// Regular pagination
const userPage = User.orderBy('id').offset(10).limit(10)

// Filtered pagination
const johnPage = userPage.clone().filter({ name: 'John' })
const janePage = userPage.clone().filter({ name: 'Jane' })

Model.keys()

Closes #17

We can now get a list with all the keys in a table (same as Model.all but with keys instead of instances)

const keys = await User.keys()
// equivalent to (even though more performant)
const keys = (await User.all()).map(u => u.keys)

Bug fixes

  • After an upgrade, changed indexes will now be recreated and unused indexes will be removed
  • Errors during an upgrade will now bubble up properly to the init call
  • Upgrades would fail silently because the automatic table creation would try to create already existing tables

Other

  • Errors are now displayed in their own section in the documentation
  • Minified dist would break because the Model class wasn't called Model anymore
  • Documentation improved overall (closes #16)
  • Improve test coverage (closes #19)
  • UMD dist exposes the whole module under globalThis.WORM, for the ESM version, you'll have to import WORM from "https://unpkg.com/@d34d/[email protected]/dist/index.esm.js"

v0.3.0

13 Apr 21:25
117cac1

Choose a tag to compare

Documentation
NPM
Full Changelog

Features

Query.update

Bulk update the instances in a query. The provided fields will be changed on every matched item. (filters, limits and offsets apply)

await Test.filter({ age: t => t < 25 }).update({ name: 'test2' })

Model.update

Allows updating fields on an instance with type hints, note that saving still needs to happen in a separate step.

const test = Test.get(1)
test.update({
  name: "Joe",
})
await test.save()

Query.forEach

Closes #9

Allows doing complex operations in a query, if the other methods aren't enough. Actually they could all be implemented with this system and already share most of the logic.
The callback receives two arguments, the current instance and the ongoing transaction. The transaction needs to be used if extra changes to the database need to be done in the loop.
If at some point you want to end the loop early, returning true will do it. No return or a false return will keep it going until there's no more items.

// Simple usage
const results = []
await Test.orderBy('-age').forEach(async (instance) => {
  results.push(instance.age)
})
// Complex usage
await Test.filter({ age: t => t < 25 }).forEach(async (t, tx) => {
  // If true is returned, the loop is stopped
  if (t.name == 'test2') return true

  t.update({ name: 'test2' })
  await t.save(tx)
}, 'readwrite')

New Error types

Closes #8

Three new error types have been added:

  • WormError: Base error for all the errors produced by this library
  • ConnectionError: Whenever an operation is tried against a closed database
  • ModelError: Any Model validation error (eg. not nullable field set to null)

Decorator-less definitions

Closes #20

Besides the "decorator" way of using this library, there's now a new way to define a tables fields:

class Test extends Model {
  id!: number
  unique!: string
}
defineModel(Test, {
  id: { primaryKey: true },
  unique: { unique: true },
})

This will allow using this library in a non-TS environment.

Global namespace

Now all the module's exports can be found in globalThis.WORM.

"Script" usage

Closes #21

This library can now be used without any compiler, by simply including a script tag in the HTML:

<script src="https://unpkg.com/@d34d/w-orm@latest/dist/index.umd.min.js"></script>
<script>
    class Test extends WORM.Model { }
    WORM.defineModel(Test, {
      id: { primaryKey: true },
    })

    WORM.init('test1', 1).then(() => console.log(WORM.db))
</script>

Other

  • dist builds are now smaller, with a minified version available (closes #24)

v0.2.1

06 Apr 11:17

Choose a tag to compare

Documentation
NPM
Full Changelog

Bug fixes

  • Transaction weren't properly exposed in the index
  • Model.create() allowed not having a values parameter
  • _HandleTableData() wasn't properly documented

v0.2.0

05 Apr 20:37

Choose a tag to compare

Documentation
NPM
Full Changelog

Features

Table inheritance

Now abstract tables can be used to group up reused fields

abstract class BaseModel extends Model {
  @Field({ primaryKey: true, default: () => crypto.randomUUID() })
  id!: string
}

class User extends BaseModel {
  @Field()
  username!: string
}

The only limitation is that wether a table is created or not depends simply on how many children tables it has.

Transactions

Users can now create and handle transactions themselves, those can be used for any DB interaction.
The main benefits are performance and atomicity.

let result: Test[]
await Transaction('readwrite', async (tx) => {
  await Test.create({ id: 1, balance: 100 }, tx)
  await Test.create({ id: 2, balance: 50 }, tx)
  await Test.create({ id: 3, balance: 300 }, tx)
})
  result = Test.all()

A transaction will commit by default if the provided callback succeeds and if an error is thrown inside, it'll be rolled back.

Important notes:

  • The transaction object (tx) should be provided to all the DB operations within the transaction.
  • The only await calls that are supported are DB operations, if anything other is called the transaction may auto-commit in between and thus break any following operations. This is an IndexedDB limitation.
  • The first parameter is the transaction mode, this can either be "readwrite" or "readonly", choose wisely depending on the operations that will happen within the transaction.

Query limit and offset

Queries can now be configured with a limit and an offset.
Those will be taken into account for all the operations (count, all, delete...)
Only exception being limit and first() as the second one is equivalent to having limit(1)

// This will skip the first result, and stop once 3 items have been found
const results = await Test.orderBy('balance').limit(3).offset(1).all()

Bug fixes

  • Model is now an abstract class

Other

  • Target has been changed to ES2017, so that the final code is able to use native async/await

v0.1.0

02 Apr 16:50
8d246e0

Choose a tag to compare