Skip to content

Commit ca4f3ee

Browse files
committed
(feat) Implement Query offset and limit
1 parent 1ab2d22 commit ca4f3ee

4 files changed

Lines changed: 164 additions & 72 deletions

File tree

src/query.ts

Lines changed: 92 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Filter, ModelFieldKey, OrderBy } from './types'
1+
import type { Filter, ModelFieldKey, OrderBy, TransactionOrMode } from './types'
22
import type { Model } from './models'
33
import { _objectStore } from './transaction'
44

@@ -74,6 +74,28 @@ export class Query<T extends Model> {
7474
return this
7575
}
7676

77+
/**
78+
* Add a limit to the query.
79+
* This means that the query will only return a maximum of `limit` items.
80+
* @param limit - The maximum amount of items to return
81+
* @returns - The query itself, to allow chaining
82+
*/
83+
public limit(limit: number): Query<T> {
84+
this._limit = limit
85+
return this
86+
}
87+
88+
/**
89+
* Add an offset to the query.
90+
* This means that the query will skip the first `offset` items.
91+
* @param offset - The amount of items to skip
92+
* @returns - The query itself, to allow chaining
93+
*/
94+
public offset(offset: number): Query<T> {
95+
this._skip = offset
96+
return this
97+
}
98+
7799
/**
78100
* Checks if the given instance fits the filters of the query.
79101
* @param instance - The instance to check
@@ -100,7 +122,7 @@ export class Query<T extends Model> {
100122
* Utility function to get the cursor of the query.
101123
* @returns
102124
*/
103-
private _getCursor(transactionOrMode: IDBTransactionMode | IDBTransaction = 'readonly'): IDBRequest<IDBCursorWithValue | null> {
125+
private _getCursor(transactionOrMode: TransactionOrMode = 'readonly'): IDBRequest<IDBCursorWithValue | null> {
104126
const store = _objectStore(this.TargetModel.name, transactionOrMode)
105127
if (this._orderBy) {
106128
const index = store.index(this._orderBy)
@@ -113,106 +135,107 @@ export class Query<T extends Model> {
113135
}
114136

115137
/**
116-
* Executes the query and returns the first result.
117-
* @returns - The first result of the query, or null if no result was found
138+
* Utility function to handle a cursor's cycle.
139+
* This implement the limit, offset and filter.
140+
* @param valueCallback - The callback to call for each value
141+
* @param transactionOrMode - The transaction or mode to use
142+
* @returns - A promise that resolves when the cursor is done
118143
*/
119-
async first(transaction?: IDBTransaction): Promise<T | null> {
120-
const cursor = this._getCursor(transaction)
121-
return new Promise<T | null>((resolve, reject) => {
122-
cursor.onsuccess = () => {
123-
if (!cursor.result) {
124-
resolve(null)
144+
private _cursorLogic(valueCallback: (value: IDBCursorWithValue) => void, transactionOrMode?: TransactionOrMode): Promise<void> {
145+
return new Promise<void>((resolve, reject) => {
146+
const request = this._getCursor(transactionOrMode)
147+
let matches = 0
148+
let skipped = 0
149+
150+
request.onsuccess = () => {
151+
if (!request.result) {
152+
resolve()
125153
}
126154
else {
127-
if (this._fitsFilters(cursor.result.value as T)) {
128-
const instance = new this.TargetModel()
129-
Object.assign(instance, cursor.result.value)
130-
resolve(instance)
155+
// If we have a limit, we check if we have reached it
156+
if ((matches - skipped) === this._limit) {
157+
resolve()
158+
return
159+
}
160+
// Apply the filters
161+
if (this._fitsFilters(request.result.value as T)) {
162+
// If we have an offset, we wait until we have reached it
163+
if (!this._skip || matches >= this._skip)
164+
valueCallback(request.result)
165+
else
166+
skipped += 1
167+
168+
matches += 1
131169
}
132-
else { cursor.result.continue() }
170+
171+
request.result.continue()
133172
}
134173
}
135-
cursor.onerror = (event) => {
174+
request.onerror = (event) => {
136175
reject(event)
137176
}
138177
})
139178
}
140179

180+
/**
181+
* Executes the query and returns the first result.
182+
* @returns - The first result of the query, or null if no result was found
183+
*/
184+
async first(transaction?: IDBTransaction): Promise<T | null> {
185+
let result: T | null = null
186+
this._limit = 1
187+
188+
await this._cursorLogic((cursor) => {
189+
const instance = new this.TargetModel()
190+
Object.assign(instance, cursor.value)
191+
result = instance
192+
}, transaction)
193+
194+
return result
195+
}
196+
141197
/**
142198
* Executes the query and returns all the results.
143199
* @returns - All the results of the query
144200
*/
145201
async all(transaction?: IDBTransaction): Promise<T[]> {
146-
const cursor = this._getCursor(transaction)
147202
const result: T[] = []
148-
return new Promise<T[]>((resolve, reject) => {
149-
cursor.onsuccess = () => {
150-
if (!cursor.result) {
151-
resolve(result)
152-
}
153-
else {
154-
if (this._fitsFilters(cursor.result.value as T)) {
155-
const instance = new this.TargetModel()
156-
Object.assign(instance, cursor.result.value)
157-
result.push(instance)
158-
}
159-
cursor.result.continue()
160-
}
161-
}
162-
cursor.onerror = (event) => {
163-
reject(event)
164-
}
165-
})
203+
204+
await this._cursorLogic((cursor) => {
205+
const instance = new this.TargetModel()
206+
Object.assign(instance, cursor.value)
207+
result.push(instance)
208+
}, transaction)
209+
210+
return result
166211
}
167212

168213
/**
169214
* Executes the query and returns the number of results.
170215
* @returns - The amount of results of the query
171216
*/
172217
async count(transaction?: IDBTransaction): Promise<number> {
173-
const cursor = this._getCursor(transaction)
174218
let count = 0
175-
return new Promise<number>((resolve, reject) => {
176-
cursor.onsuccess = () => {
177-
if (!cursor.result) {
178-
resolve(count)
179-
}
180-
else {
181-
if (this._fitsFilters(cursor.result.value as T))
182-
count++
183219

184-
cursor.result.continue()
185-
}
186-
}
187-
cursor.onerror = (event) => {
188-
reject(event)
189-
}
190-
})
220+
await this._cursorLogic(() => {
221+
count += 1
222+
}, transaction)
223+
224+
return count
191225
}
192226

193227
/**
194228
* Executes the query and deletes all the results.
195229
* @returns - The amount of results deleted
196230
*/
197231
async delete(transaction?: IDBTransaction): Promise<number> {
198-
const cursor = this._getCursor(transaction || 'readwrite')
199232
let amount = 0
200-
return new Promise<number>((resolve, reject) => {
201-
cursor.onsuccess = () => {
202-
if (!cursor.result) {
203-
resolve(amount)
204-
}
205-
else {
206-
if (this._fitsFilters(cursor.result.value as T)) {
207-
amount += 1
208-
cursor.result.delete()
209-
}
210-
cursor.result.continue()
211-
}
212-
}
213-
cursor.onerror = (event) => {
214-
reject(event)
215-
}
216-
})
233+
234+
await this._cursorLogic((cursor) => {
235+
cursor.delete()
236+
amount += 1
237+
}, transaction || 'readwrite')
238+
239+
return amount
217240
}
218241
}

src/transaction.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TransactionCallback } from './types'
1+
import type { TransactionCallback, TransactionOrMode } from './types'
22
import { db } from './connection'
33

44
/**
@@ -10,8 +10,8 @@ import { db } from './connection'
1010
*/
1111
export function _objectStore(storeName: string, transaction?: IDBTransaction): IDBObjectStore
1212
export function _objectStore(storeName: string, mode?: IDBTransactionMode): IDBObjectStore
13-
export function _objectStore(storeName: string, modeOrTransaction?: IDBTransactionMode | IDBTransaction): IDBObjectStore
14-
export function _objectStore(storeName: string, modeOrTransaction?: IDBTransactionMode | IDBTransaction): IDBObjectStore {
13+
export function _objectStore(storeName: string, modeOrTransaction?: TransactionOrMode): IDBObjectStore
14+
export function _objectStore(storeName: string, modeOrTransaction?: TransactionOrMode): IDBObjectStore {
1515
if (!db.connected)
1616
throw new Error('Database is not connected')
1717
if (modeOrTransaction instanceof IDBTransaction) {

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,5 @@ export type Filter<T extends Model> = {
5959
export type OrderBy<T extends Model> = (ModelFieldKey<T>) | `-${ModelFieldKey<T>}`
6060

6161
export type TransactionCallback = (tx: IDBTransaction) => Promise<void>
62+
63+
export type TransactionOrMode = IDBTransaction | IDBTransactionMode

test/query.spec.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,4 +276,71 @@ describe('Query builder', () => {
276276
const obtainedTests = await Test.all()
277277
assert.sameDeepMembers(obtainedTests, [test3])
278278
})
279+
it('should allow filtering with a limit', async () => {
280+
class Test extends Model {
281+
@Field({ primaryKey: true })
282+
id!: number
283+
284+
@Field()
285+
name!: string
286+
287+
@Field()
288+
age!: number
289+
}
290+
291+
await init('test', 1)
292+
293+
const test1 = await Test.create({ id: 1, name: 'test1', age: 10 })
294+
const test2 = await Test.create({ id: 2, name: 'test1', age: 20 })
295+
await Test.create({ id: 3, name: 'test3', age: 30 })
296+
await Test.create({ id: 4, name: 'test4', age: 10 })
297+
298+
const obtainedTests = await Test.filter({ age: t => t < 25 }).orderBy('id').limit(2).all()
299+
assert.sameDeepMembers(obtainedTests, [test1, test2])
300+
})
301+
it('should allow filtering with an offset', async () => {
302+
class Test extends Model {
303+
@Field({ primaryKey: true })
304+
id!: number
305+
306+
@Field()
307+
name!: string
308+
309+
@Field()
310+
age!: number
311+
}
312+
313+
await init('test', 1)
314+
315+
await Test.create({ id: 1, name: 'test1', age: 10 })
316+
const test2 = await Test.create({ id: 2, name: 'test1', age: 20 })
317+
await Test.create({ id: 3, name: 'test3', age: 30 })
318+
const test4 = await Test.create({ id: 4, name: 'test4', age: 10 })
319+
320+
const obtainedTests = await Test.filter({ age: t => t < 25 }).orderBy('id').offset(1).all()
321+
assert.sameDeepMembers(obtainedTests, [test2, test4])
322+
})
323+
it('should allow setting a limit and an offset', async () => {
324+
class Test extends Model {
325+
@Field({ primaryKey: true })
326+
id!: number
327+
328+
@Field()
329+
name!: string
330+
331+
@Field()
332+
age!: number
333+
}
334+
335+
await init('test', 1)
336+
337+
await Test.create({ id: 1, name: 'test1', age: 10 })
338+
await Test.create({ id: 2, name: 'test1', age: 20 })
339+
await Test.create({ id: 3, name: 'test3', age: 30 })
340+
const test4 = await Test.create({ id: 4, name: 'test4', age: 10 })
341+
await Test.create({ id: 5, name: 'test4', age: 10 })
342+
343+
const obtainedTests = await Test.orderBy('id').offset(3).limit(1).all()
344+
assert.sameDeepMembers(obtainedTests, [test4])
345+
})
279346
})

0 commit comments

Comments
 (0)