From b2a9017d05b9ff6b8d960814cd7b2cf586e25ad0 Mon Sep 17 00:00:00 2001 From: oliver-oloughlin Date: Sun, 8 Oct 2023 00:33:36 +0200 Subject: [PATCH] refactored handleMany to be more reusable and handle errors --- src/collection.ts | 131 ++++++++++++++++-------------- src/indexable_collection.ts | 155 ++++++++++++------------------------ src/large_collection.ts | 48 ++++++++--- 3 files changed, 159 insertions(+), 175 deletions(-) diff --git a/src/collection.ts b/src/collection.ts index ffaf9e4..b3d7dac 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -39,6 +39,20 @@ import { import { Document } from "./document.ts" import { model } from "./model.ts" +/** + * Create a collection builder function. + * + * @example + * ```ts + * collection(model(), { + * idGenerator: () => ulid() + * }) + * ``` + * + * @param model - Model. + * @param options - Collection options. + * @returns A collection builder function. + */ export function collection( model: Model, options?: CollectionOptions, @@ -347,30 +361,12 @@ export class Collection< value: UpdateData, options?: UpdateManyOptions, ) { - // Initiate result and error list - const result: (CommitResult | Deno.KvCommitError)[] = [] - const errors: unknown[] = [] - // Update each document, add commit result to result list - const { cursor } = await this.handleMany(async (doc) => { - try { - const cr = await this.updateDocument(doc, value, options) - result.push(cr) - } catch (e) { - errors.push(e) - } - }, options) - - // Throw errors if caught - if (errors.length > 0) { - throw errors - } - - // Return result list and current iterator cursor - return { - result, - cursor, - } + return await this.handleMany( + this._keys.idKey, + (doc) => this.updateDocument(doc, value, options), + options, + ) } /** @@ -445,7 +441,13 @@ export class Collection< */ async deleteMany(options?: ListOptions) { // Execute delete operation for each document entry - return await this.handleMany((doc) => this.delete(doc.id), options) + const { cursor } = await this.handleMany( + this._keys.idKey, + (doc) => this.delete(doc.id), + options, + ) + + return { cursor } } /** @@ -468,20 +470,12 @@ export class Collection< * @returns A promise that resovles to an object containing a list of the retrieved documents and the iterator cursor */ async getMany(options?: ListOptions) { - // Initiate result list - const result: Document[] = [] - - // Add each document entry to result list - const { cursor } = await this.handleMany( - (doc) => result.push(doc), + // Get each document, return result list and current iterator cursor + return await this.handleMany( + this._keys.idKey, + (doc) => doc, options, ) - - // Return result list and current iterator cursor - return { - result, - cursor, - } } /** @@ -505,8 +499,14 @@ export class Collection< * @returns A promise that resovles to an object containing the iterator cursor */ async forEach(fn: (doc: Document) => void, options?: ListOptions) { - // Execute callback function for each document entry - return await this.handleMany((doc) => fn(doc), options) + // Execute callback function for each document entry and return cursor + const { cursor } = await this.handleMany( + this._keys.idKey, + (doc) => fn(doc), + options, + ) + + return { cursor } } /** @@ -531,24 +531,16 @@ export class Collection< * @param options - List options, optional. * @returns A promise that resovles to an object containing a list of the callback results and the iterator cursor */ - async map( - fn: (doc: Document) => TMapped, + async map( + fn: (doc: Document) => T, options?: ListOptions, ) { - // Initiate result list - const result: TMapped[] = [] - - // Execute callback function for each document entry, add to result list - const { cursor } = await this.handleMany( - (doc) => result.push(fn(doc)), + // Execute callback function for each document entry, return result and cursor + return await this.handleMany( + this._keys.idKey, + (doc) => fn(doc), options, ) - - // Return result list and current iterator cursor - return { - result, - cursor, - } } /** @@ -571,7 +563,7 @@ export class Collection< async count(options?: CountOptions) { // Initiate count variable, increment for each document entry, return result let result = 0 - await this.handleMany(() => result++, options) + await this.handleMany(this._keys.idKey, () => result++, options) return result } @@ -709,18 +701,24 @@ export class Collection< /** * Perform operations on lists of documents in the collection. * + * @param prefixKey - Prefix key for list selector. * @param fn - Callback function. * @param options - List options, optional. * @returns Promise that resolves to object with iterator cursor. */ - protected async handleMany( - fn: (doc: Document) => unknown, - options?: ListOptions, + protected async handleMany( + prefixKey: KvKey, + fn: (doc: Document) => T, + options: ListOptions | undefined, ) { - // Create list iterator with given options, initiate documents list - const selector = createListSelector(this._keys.idKey, options) + // Create list iterator with given options + const selector = createListSelector(prefixKey, options) const iter = this.kv.list(selector, options) + + // Initiate lists const docs: Document[] = [] + const result: Awaited[] = [] + const errors: unknown[] = [] // Loop over each document entry for await (const { key, value, versionstamp } of iter) { @@ -744,10 +742,23 @@ export class Collection< } // Execute callback function for each document - await allFulfilled(docs.map((doc) => fn(doc))) + await allFulfilled(docs.map(async (doc) => { + try { + const res = await fn(doc) + result.push(res) + } catch (e) { + errors.push(e) + } + })) - // Return current iterator cursor + // Throw any caught errors + if (errors.length > 0) { + throw errors + } + + // Return result and current iterator cursor return { + result, cursor: iter.cursor || undefined, } } diff --git a/src/indexable_collection.ts b/src/indexable_collection.ts index b8ac8d5..b9d0b3e 100644 --- a/src/indexable_collection.ts +++ b/src/indexable_collection.ts @@ -29,17 +29,33 @@ import type { import { allFulfilled, checkIndices, - createListSelector, deleteIndices, extendKey, - getDocumentId, setIndices, } from "./utils.ts" import { Document } from "./document.ts" +/** + * Create an indexable collection builder function. + * + * @example + * ```ts + * collection(model(), { + * idGenerator: () => ulid(), + * indices: { + * username: "primary" // unique + * age: "secondary" //non-unique + * } + * }) + * ``` + * + * @param model - Model. + * @param options - Indexable collection options. + * @returns An indexable collection builder function. + */ export function indexableCollection< - T1 extends KvObject, - T2 extends IndexableCollectionOptions, + const T1 extends KvObject, + const T2 extends IndexableCollectionOptions, >(model: Model, options: T2) { return ( kv: Deno.Kv, @@ -196,22 +212,19 @@ export class IndexableCollection< async findBySecondaryIndex< const K extends SecondaryIndexKeys, >(index: K, value: CheckKeyOf, options?: ListOptions) { - // Initiate result list - const result: Document[] = [] + // Create prefix key + const prefixKey = extendKey( + this._keys.secondaryIndexKey, + index as KvId, + value as KvId, + ) // Add documents to result list by secondary index - const { cursor } = await this.handleBySecondaryIndex( - index, - value, - (doc) => result.push(doc), + return await this.handleMany( + prefixKey, + (doc) => doc, options, ) - - // Return result and current iterator cursor - return { - result, - cursor, - } } async delete(...ids: KvId[]) { @@ -300,13 +313,21 @@ export class IndexableCollection< async deleteBySecondaryIndex< const K extends SecondaryIndexKeys, >(index: K, value: CheckKeyOf, options?: ListOptions) { + // Create prefix key + const prefixKey = extendKey( + this._keys.secondaryIndexKey, + index as KvId, + value as KvId, + ) + // Delete documents by secondary index, return iterator cursor - return await this.handleBySecondaryIndex( - index, - value, + const { cursor } = await this.handleMany( + prefixKey, (doc) => this.delete(doc.id), options, ) + + return { cursor } } /** @@ -379,35 +400,19 @@ export class IndexableCollection< data: UpdateData, options?: UpdateManyOptions, ) { - // Initiate result and error list - const result: (CommitResult | Deno.KvCommitError)[] = [] - const errors: unknown[] = [] + // Create prefix key + const prefixKey = extendKey( + this._keys.secondaryIndexKey, + index as KvId, + value as KvId, + ) // Update each document by secondary index, add commit result to result list - const { cursor } = await this.handleBySecondaryIndex( - index, - value, - async (doc) => { - try { - const cr = await this.updateDocument(doc, data, options) - result.push(cr) - } catch (e) { - errors.push(e) - } - }, + return await this.handleMany( + prefixKey, + (doc) => this.updateDocument(doc, data, options), options, ) - - // Throw any caught errors - if (errors.length > 0) { - throw errors - } - - // Return result list and current iterator cursor - return { - result, - cursor, - } } /** PROTECTED METHODS */ @@ -519,66 +524,4 @@ export class IndexableCollection< ok: false, } } - - /** - * Perform operations on lists of documents in the collection by secondary index. - * - * @param index - Index. - * @param value - Index value. - * @param fn - Callback function. - * @param options - List options or undefined. - * @returns - Promise that resolves to object containing iterator cursor. - */ - protected async handleBySecondaryIndex< - const K extends SecondaryIndexKeys, - >( - index: K, - value: CheckKeyOf, - fn: (doc: Document) => unknown, - options: ListOptions | undefined, - ) { - // Set prefix and create list selector - const prefix = extendKey( - this._keys.secondaryIndexKey, - index as KvId, - value as KvId, - ) - - const selector = createListSelector(prefix, options) - - // Create list iterator and initiate documents list - const iter = this.kv.list(selector, options) - const docs: Document[] = [] - - // Loop over document entries - for await (const { key, value, versionstamp } of iter) { - // Get document id - const id = getDocumentId(key) - - // If id is undefined, continue to next entry - if (typeof id === "undefined") { - continue - } - - // Create document - const doc = new Document(this._model, { - id, - versionstamp, - value, - }) - - // Filter document and add to documetns list - if (!options?.filter || options.filter(doc)) { - docs.push(doc) - } - } - - // Execute callback function for each document - await allFulfilled(docs.map((doc) => fn(doc))) - - // Return current iterator cursor - return { - cursor: iter.cursor || undefined, - } - } } diff --git a/src/large_collection.ts b/src/large_collection.ts index 5f7cdd5..4237228 100644 --- a/src/large_collection.ts +++ b/src/large_collection.ts @@ -32,7 +32,21 @@ import { import { Document } from "./document.ts" import { CorruptedDocumentDataError } from "./errors.ts" -export function largeCollection( +/** + * Create a large collection builder function. + * + * @example + * ```ts + * collection(model(), { + * idGenerator: () => ulid() + * }) + * ``` + * + * @param model - Model. + * @param options - Large collection options. + * @returns A large collection builder function. + */ +export function largeCollection( model: Model, options?: LargeCollectionOptions, ) { @@ -172,16 +186,19 @@ export class LargeCollection< /* PROTECTED METHODS */ - protected async handleMany( - fn: (doc: Document) => unknown, - options?: ListOptions, - ): Promise<{ cursor: string | undefined }> { + protected async handleMany( + prefixKey: KvKey, + fn: (doc: Document) => T, + options: ListOptions | undefined, + ) { // Create list iterator with given options - const selector = createListSelector(this._keys.idKey, options) + const selector = createListSelector(prefixKey, options) const iter = this.kv.list(selector, options) - // Initiate documents list + // Initiate lists const docs: Document[] = [] + const result: Awaited[] = [] + const errors: unknown[] = [] // Loop over each document entry for await (const { key } of iter) { @@ -206,10 +223,23 @@ export class LargeCollection< } // Execute callback function for each document - await allFulfilled(docs.map((doc) => fn(doc))) + await allFulfilled(docs.map(async (doc) => { + try { + const res = await fn(doc) + result.push(res) + } catch (e) { + errors.push(e) + } + })) + + // Throw any caught errors + if (errors.length > 0) { + throw errors + } - // Return current iterator cursor + // Return result and current iterator cursor return { + result, cursor: iter.cursor || undefined, } }