Releases: D34DPlayer/w-orm
v0.5.0
Documentation
NPM
Full Changelog
Features
Arbitrary indices
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
initif called multiple times
Others
- Typing improvements
- Migrated to Vite for bundling the library
- Migrated to Vitest for library testing
v0.4.1
Documentation
NPM
Full Changelog
Bugfixes
migrations.tsis 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
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
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 definedQuery 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
initcall - 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
distwould break because theModelclass wasn't calledModelanymore - Documentation improved overall (closes #16)
- Improve test coverage (closes #19)
- UMD
distexposes the whole module underglobalThis.WORM, for the ESM version, you'll have toimport WORM from "https://unpkg.com/@d34d/[email protected]/dist/index.esm.js"
v0.3.0
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 libraryConnectionError: Whenever an operation is tried against a closed databaseModelError: 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
distbuilds are now smaller, with a minified version available (closes #24)
v0.2.1
Documentation
NPM
Full Changelog
Bug fixes
- Transaction weren't properly exposed in the index
Model.create()allowed not having avaluesparameter_HandleTableData()wasn't properly documented
v0.2.0
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
awaitcalls 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
Modelis now an abstract class
Other
- Target has been changed to ES2017, so that the final code is able to use native async/await