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
13 changes: 0 additions & 13 deletions src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,6 @@ export async function deleteDB(dbName: string): Promise<Event> {
})
}

/**
* Utility function to get an object store from the global database connection
* @param storeName The name of the object store
* @param mode The transaction mode
* @returns { IDBObjectStore } - The object store
* @internal
*/
export function _objectStore(storeName: string, mode: IDBTransactionMode = 'readonly'): IDBObjectStore {
if (!db.connected)
throw new Error('Database is not connected')
return db.session.transaction(storeName, mode).objectStore(storeName)
}

/**
* Closes the global database connection
*/
Expand Down
26 changes: 13 additions & 13 deletions src/models.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Filter, ModelFields, OrderBy } from './types'
import { TablesMetadata, getPrimaryKeys } from './metadata'
import { _objectStore } from './connection'
import { _objectStore } from './transaction'
import { Query } from './query'

/**
Expand Down Expand Up @@ -48,7 +48,7 @@ export abstract class Model {
* @param values - The values to initialize the model with
* @returns - The new model instance
*/
public static async create<T extends Model>(this: { new(): T }, values?: Partial<ModelFields<T>>): Promise<T> {
public static async create<T extends Model>(this: { new(): T }, values?: Partial<ModelFields<T>>, transaction?: IDBTransaction): Promise<T> {
const instance = new this()
Object.assign(instance, values)

Expand All @@ -75,7 +75,7 @@ export abstract class Model {
}

// Save instance to database
const store = _objectStore(this.name, 'readwrite')
const store = _objectStore(this.name, transaction || 'readwrite')

return new Promise((resolve, reject) => {
const request = store.add(instance)
Expand All @@ -93,8 +93,8 @@ export abstract class Model {
* @param key - The primary key of the model
* @returns - The model instance or null if not found
*/
public static async get<T extends Model>(this: { new(): T }, key: IDBValidKey): Promise<T | null> {
const store = _objectStore(this.name)
public static async get<T extends Model>(this: { new(): T }, key: IDBValidKey, transaction?: IDBTransaction): Promise<T | null> {
const store = _objectStore(this.name, transaction)
return new Promise((resolve, reject) => {
const request = store.get(Array.isArray(key) ? key : [key])
request.onerror = (_) => {
Expand All @@ -117,17 +117,17 @@ export abstract class Model {
* Get all model instances.
* @returns - The model instances
*/
public static async all<T extends Model>(this: { new(): T }): Promise<T[]> {
return (new Query(this)).all()
public static async all<T extends Model>(this: { new(): T }, transaction?: IDBTransaction): Promise<T[]> {
return (new Query(this)).all(transaction)
}

/**
* Get how many model instances there are.
* @returns - The number of model instances
*/
public static async count<T extends Model>(this: { new(): T }): Promise<number> {
public static async count<T extends Model>(this: { new(): T }, transaction?: IDBTransaction): Promise<number> {
return new Promise<number>((resolve, reject) => {
const store = _objectStore(this.name)
const store = _objectStore(this.name, transaction)
const request = store.count()
request.onerror = (_) => {
reject(request.error)
Expand Down Expand Up @@ -169,8 +169,8 @@ export abstract class Model {
/**
* Delete this instance from the database.
*/
public async delete(): Promise<void> {
const store = _objectStore(this.constructor.name, 'readwrite')
public async delete(transaction?: IDBTransaction): Promise<void> {
const store = _objectStore(this.constructor.name, transaction || 'readwrite')
const request = store.delete(this.keys)
return new Promise((resolve, reject) => {
request.onerror = (_) => {
Expand All @@ -185,9 +185,9 @@ export abstract class Model {
/**
* Save this instance's changes to the database.
*/
public async save(): Promise<void> {
public async save(transaction?: IDBTransaction): Promise<void> {
return new Promise((resolve, reject) => {
const store = _objectStore(this.constructor.name, 'readwrite')
const store = _objectStore(this.constructor.name, transaction || 'readwrite')
const request = store.put(this)

request.onerror = (_) => {
Expand Down
26 changes: 15 additions & 11 deletions src/query.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Filter, ModelFieldKey, OrderBy } from './types'
import type { Model } from './models'
import { _objectStore } from './connection'
import { _objectStore } from './transaction'

export class Query<T extends Model> {
/** The filters to apply to the query */
Expand All @@ -9,6 +9,10 @@ export class Query<T extends Model> {
private _orderBy?: ModelFieldKey<T>
/** Whether to reverse the order of the query */
private _reverse = false
/** Maximum amount of items to return */
private _limit?: number
/** Amount of items to skip */
private _skip?: number

/**
* A query builder for the models.
Expand Down Expand Up @@ -96,8 +100,8 @@ export class Query<T extends Model> {
* Utility function to get the cursor of the query.
* @returns
*/
private _getCursor(mode: IDBTransactionMode = 'readonly'): IDBRequest<IDBCursorWithValue | null> {
const store = _objectStore(this.TargetModel.name, mode)
private _getCursor(transactionOrMode: IDBTransactionMode | IDBTransaction = 'readonly'): IDBRequest<IDBCursorWithValue | null> {
const store = _objectStore(this.TargetModel.name, transactionOrMode)
if (this._orderBy) {
const index = store.index(this._orderBy)

Expand All @@ -112,8 +116,8 @@ export class Query<T extends Model> {
* Executes the query and returns the first result.
* @returns - The first result of the query, or null if no result was found
*/
async first(): Promise<T | null> {
const cursor = this._getCursor()
async first(transaction?: IDBTransaction): Promise<T | null> {
const cursor = this._getCursor(transaction)
return new Promise<T | null>((resolve, reject) => {
cursor.onsuccess = () => {
if (!cursor.result) {
Expand All @@ -138,8 +142,8 @@ export class Query<T extends Model> {
* Executes the query and returns all the results.
* @returns - All the results of the query
*/
async all(): Promise<T[]> {
const cursor = this._getCursor()
async all(transaction?: IDBTransaction): Promise<T[]> {
const cursor = this._getCursor(transaction)
const result: T[] = []
return new Promise<T[]>((resolve, reject) => {
cursor.onsuccess = () => {
Expand All @@ -165,8 +169,8 @@ export class Query<T extends Model> {
* Executes the query and returns the number of results.
* @returns - The amount of results of the query
*/
async count(): Promise<number> {
const cursor = this._getCursor()
async count(transaction?: IDBTransaction): Promise<number> {
const cursor = this._getCursor(transaction)
let count = 0
return new Promise<number>((resolve, reject) => {
cursor.onsuccess = () => {
Expand All @@ -190,8 +194,8 @@ export class Query<T extends Model> {
* Executes the query and deletes all the results.
* @returns - The amount of results deleted
*/
async delete(): Promise<number> {
const cursor = this._getCursor('readwrite')
async delete(transaction?: IDBTransaction): Promise<number> {
const cursor = this._getCursor(transaction || 'readwrite')
let amount = 0
return new Promise<number>((resolve, reject) => {
cursor.onsuccess = () => {
Expand Down
63 changes: 63 additions & 0 deletions src/transaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { TransactionCallback } from './types'
import { db } from './connection'

/**
* Utility function to get an object store from the global database connection
* @param storeName The name of the object store
* @param mode The transaction mode
* @returns { IDBObjectStore } - The object store
* @internal
*/
export function _objectStore(storeName: string, transaction?: IDBTransaction): IDBObjectStore
export function _objectStore(storeName: string, mode?: IDBTransactionMode): IDBObjectStore
export function _objectStore(storeName: string, modeOrTransaction?: IDBTransactionMode | IDBTransaction): IDBObjectStore
export function _objectStore(storeName: string, modeOrTransaction?: IDBTransactionMode | IDBTransaction): IDBObjectStore {
if (!db.connected)
throw new Error('Database is not connected')
if (modeOrTransaction instanceof IDBTransaction) {
return modeOrTransaction.objectStore(storeName)
}
else {
const mode = modeOrTransaction || 'readonly'
return db.session.transaction(storeName, mode).objectStore(storeName)
}
}

/**
* Starts a db transaction.
* Depending on the result of the transactionCallback, the transaction will be committed or aborted.
*
* @example
* ```ts
* await Transaction('readwrite', async (tx) => {
* const newUser = await User.create({ name: 'John Doe' }, tx)
* const getUser = await User.get(newUser.id, tx)
*
* // Any error thrown in the callback will abort the transaction, this will rollback any changes made
* throw new Error('rollback')
* // If no error is thrown, the transaction will be committed
* })
* ```
*
* @param mode - The transaction mode
* @param transactionCallback - The callback to execute in the transaction
*/
export async function Transaction(mode: IDBTransactionMode, transactionCallback: TransactionCallback): Promise<void> {
if (!db.connected)
throw new Error('Database is not connected')
const stores = Array.from(db.session.objectStoreNames)

const transaction = db.session.transaction(stores, mode)
const transactionPromise = new Promise<void>((resolve, reject) => {
transaction.oncomplete = () => resolve()
transaction.onerror = () => reject(transaction.error)
})
const callbackPromise = transactionCallback(transaction)
.then(() => transaction.commit())
.catch((error: Error) => {
transaction.abort()
throw error
})

await Promise.all([callbackPromise, transactionPromise])
}
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,5 @@ export type Filter<T extends Model> = {
}

export type OrderBy<T extends Model> = (ModelFieldKey<T>) | `-${ModelFieldKey<T>}`

export type TransactionCallback = (tx: IDBTransaction) => Promise<void>
3 changes: 2 additions & 1 deletion test/fields.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { assert } from 'chai'

import { _objectStore, db, init } from '../src/connection'
import { db, init } from '../src/connection'
import { Model } from '../src/models'
import { Field } from '../src/fields'
import { _objectStore } from '../src/transaction'

describe('Field options', () => {
it('should be able to set a field as primary key', async () => {
Expand Down
3 changes: 2 additions & 1 deletion test/models.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { assert } from 'chai'

import { Model } from '../src/models'
import { _objectStore, init } from '../src/connection'
import { init } from '../src/connection'
import { Field } from '../src/fields'
import { _objectStore } from '../src/transaction'

describe('Models', () => {
describe('create', () => {
Expand Down
70 changes: 70 additions & 0 deletions test/transaction.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { assert } from 'chai'

import { init } from '../src/connection'
import { Transaction } from '../src/transaction'
import { Field } from '../src/fields'
import { Model } from '../src/models'

describe('Transactions', () => {
it('should be able to create a transaction', async () => {
class Test extends Model {
@Field({ primaryKey: true })
id!: number

@Field()
balance!: number
}

await init('test', 1)

await Transaction('readwrite', async (tx) => {
const test1 = await Test.create({ id: 1, balance: 100 }, tx)
const test2 = await Test.create({ id: 2, balance: 50 }, tx)
const test3 = await Test.create({ id: 3, balance: 300 }, tx)

const obtainedTests = await Test.all(tx)
assert.sameDeepMembers(obtainedTests, [test2, test1, test3])
})
})
it('should commit by default', async () => {
class Test extends Model {
@Field({ primaryKey: true })
id!: number

@Field()
balance!: number
}

await init('test', 1)

await Transaction('readwrite', async (tx) => {
const test1 = await Test.create({ id: 1, balance: 100 }, tx)
})

const obtainedTests = await Test.all()
assert.lengthOf(obtainedTests, 1)
})
it('should be able to rollback', async () => {
class Test extends Model {
@Field({ primaryKey: true })
id!: number

@Field()
balance!: number
}

await init('test', 1)

await Transaction('readwrite', async (tx) => {
await Test.create({ id: 1, balance: 100 }, tx)
const obtainedTests = await Test.all(tx)
assert.lengthOf(obtainedTests, 1)

throw new Error('rollback')
}).then(() => assert.fail('Should have thrown an error'))
.catch((err: Error) => assert.match(err.message, /rollback/))

const obtainedTests = await Test.all()
assert.lengthOf(obtainedTests, 0)
})
})