From 6158c1fa5249d3882cf95f8a525396a612bf3fe6 Mon Sep 17 00:00:00 2001 From: oliver-oloughlin Date: Sat, 13 Jan 2024 12:36:35 +0100 Subject: [PATCH] feat: added getOneBySecondaryIndex & updateOneBySecondaryIndex --- README.md | 58 +++- src/collection.ts | 170 ++++++++-- src/utils.ts | 51 ++- tests/collection/getOne.test.ts | 20 ++ tests/collection/updateOne.test.ts | 299 ++++++++++++++++++ tests/deps.ts | 1 + tests/indexable_collection/getOne.test.ts | 20 ++ .../getOneBySecondaryIndex.test.ts | 26 ++ tests/indexable_collection/updateOne.test.ts | 172 ++++++++++ .../updateOneBySecondaryIndex.test.ts | 195 ++++++++++++ tests/serialized_collection/getOne.test.ts | 20 ++ tests/serialized_collection/updateOne.test.ts | 299 ++++++++++++++++++ .../getOne.test.ts | 20 ++ .../getOneBySecondaryIndex.test.ts | 26 ++ .../updateOne.test.ts | 174 ++++++++++ .../updateOneBySecondaryIndex.test.ts | 195 ++++++++++++ 16 files changed, 1698 insertions(+), 48 deletions(-) create mode 100644 tests/collection/getOne.test.ts create mode 100644 tests/collection/updateOne.test.ts create mode 100644 tests/indexable_collection/getOne.test.ts create mode 100644 tests/indexable_collection/getOneBySecondaryIndex.test.ts create mode 100644 tests/indexable_collection/updateOne.test.ts create mode 100644 tests/indexable_collection/updateOneBySecondaryIndex.test.ts create mode 100644 tests/serialized_collection/getOne.test.ts create mode 100644 tests/serialized_collection/updateOne.test.ts create mode 100644 tests/serialized_indexable_collection/getOne.test.ts create mode 100644 tests/serialized_indexable_collection/getOneBySecondaryIndex.test.ts create mode 100644 tests/serialized_indexable_collection/updateOne.test.ts create mode 100644 tests/serialized_indexable_collection/updateOneBySecondaryIndex.test.ts diff --git a/README.md b/README.md index fa95510..5ba4dce 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ _Supported Deno verisons:_ **^1.38.5** - [updateByPrimaryIndex()](#updatebyprimaryindex) - [updateBySecondaryIndex()](#updatebysecondaryindex) - [updateMany()](#updatemany) + - [updateOne()](#updateone) + - [updateOneBySecondaryIndex()](#updateonebysecondaryindex) - [upsert()](#upsert) - [delete()](#delete) - [deleteByPrimaryIndex()](#deletebyprimaryindex) @@ -50,6 +52,8 @@ _Supported Deno verisons:_ **^1.38.5** - [deleteHistory()](#deletehistory) - [deleteUndelivered()](#deleteundelivered) - [getMany()](#getmany) + - [getOne()](#getone) + - [getOneBySecondaryIndex()](#getonebysecondaryindex) - [forEach()](#foreach) - [forEachBySecondaryIndex()](#foreachbysecondaryindex) - [map()](#map) @@ -417,18 +421,43 @@ same `options` argument as `updateMany()`. If no options are given, `updateOne()` will update the first document in the collection. ```ts -// Updates the first user document and sets name = 67 -await db.users.updateOne({ age: 67 }) +// Updates the first user document and sets age = 67 +const result = await db.users.updateOne({ age: 67 }) ``` ```ts // Updates the first user where age > 20, using shallow merge -await db.users.updateOne({ age: 67 }, { +const result = await db.users.updateOne({ age: 67 }, { filter: (doc) => doc.value.age > 20, strategy: "merge-shallow", }) ``` +### updateOneBySecondaryIndex() + +Update the first matching document from the KV store by a secondary index. It +optionally takes the same `options` argument as `updateMany()`. If no options +are given, `updateOneBySecondaryIndex()` will update the first document in the +collection by the given index value. + +```ts +// Updates the first user document where age = 20 and sets age = 67 +const result = await db.users.updateOneBySecondaryIndex("age", 20, { age: 67 }) +``` + +```ts +// Updates the first user where age = 20 and username starts with "a", using shallow merge +const result = await db.users.updateOneBySecondaryIndex( + "age", + 20, + { age: 67 }, + { + filter: (doc) => doc.value.username.startsWith("a"), + strategy: "merge-shallow", + }, +) +``` + ### upsert() Update an existing document by either id or primary index, or set a new document @@ -586,14 +615,31 @@ retrieve the first document in the collection. ```ts // Retrieves the first user document -const { result } = await db.users.getOne() +const user = await db.users.getOne() -// Retrieves the first document where the user's age is above or equal to 18 -const { result } = await db.users.getOne({ +// Retrieves the first user where the user's age is above or equal to 18 +const user = await db.users.getOne({ filter: (doc) => doc.value.age > 18, }) ``` +### getOneBySecondaryIndex() + +Retrieve the first matching document from the KV store by a secondary index. It +optionally takes the same `options` argument as `getMany()`. If no options are +given, `getOneBySecondaryIndex()` will retrieve the first document in the +collection by the given index value. + +```ts +// Retrieves the first user document where age = 20 +const user = await db.users.getOneBySecondaryIndex("age", 20) + +// Retrieves the first user where age = 20 and username starts with "a" +const user = await db.users.getOneBySecondaryIndex("age", 20, { + filter: (doc) => doc.value.username.startsWith("a"), +}) +``` + ### forEach() Execute a callback function for multiple documents in the KV store. Takes an diff --git a/src/collection.ts b/src/collection.ts index 1395fd4..d2eaa08 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -46,6 +46,7 @@ import { createHandlerId, createListOptions, createListSelector, + createSecondaryIndexKeyPrefix, decompress, deleteIndices, denoCoreDeserialize, @@ -994,39 +995,95 @@ export class Collection< } /** - * Update the value of one existing documents in the collection. + * Update the value of one existing document in the collection. * * @example * ```ts * // Updates the first user document and sets name = 67 - * await db.users.updateMany({ age: 67 }) + * const result = await db.users.updateOne({ age: 67 }) * ``` * * @example * ```ts * // Updates the first user where age > 20, using shallow merge - * await db.users.updateOne({ age: 67 }, { + * const result = await db.users.updateOne({ age: 67 }, { * filter: (doc) => doc.value.age > 20, * strategy: "merge-shallow" * }) * ``` * - * @param value - Updated value to be inserted into documents. + * @param data - Updated data to be inserted into documents. * @param options - Update many options, optional. - * @returns Promise resolving to an object containining result. + * @returns Promise resolving to either a commit result or commit error object. */ - - async updateOne( - value: UpdateData, - options?: UpdateManyOptions>, + async updateOne< + const T extends UpdateManyOptions>, + >( + data: UpdateData, + options?: T, ) { - const doc = await this.getOne(options) + // Update a single document + const { result } = await this.handleMany( + this._keys.id, + (doc) => this.updateDocument(doc, data, options), + { ...options, resultLimit: 1 }, + ) - if (doc) { - return await this.updateDocument(doc, value, options) + // Return first result, or commit error object if not present + return result.at(0) ?? { + ok: false, } + } - return { + /** + * Update the value of one existing document in the collection by a secondary index. + * + * @example + * ```ts + * // Updates the first user with age = 20 and sets age = 67 + * const result = await db.users.updateOneBySecondaryIndex("age", 20, { age: 67 }) + * ``` + * + * @example + * ```ts + * // Updates the first user where age = 20 and username starts with "a", using shallow merge + * const result = await db.users.updateOne("age", 20, { age: 67 }, { + * filter: (doc) => doc.value.username.startsWith("a"), + * strategy: "merge-shallow" + * }) + * ``` + * + * @param index - Index. + * @param value - Index value. + * @param data - Updated data to be inserted into documents. + * @param options - Update many options, optional. + * @returns Promise resolving to either a commit result or commit error object. + */ + async updateOneBySecondaryIndex< + const T extends UpdateManyOptions>, + const K extends SecondaryIndexKeys, + >( + index: K, + value: CheckKeyOf, + data: UpdateData, + options?: T, + ) { + // Create prefix key + const prefixKey = createSecondaryIndexKeyPrefix( + index, + value as KvValue, + this, + ) + + // Update a single document + const { result } = await this.handleMany( + prefixKey, + (doc) => this.updateDocument(doc, data, options), + { ...options, resultLimit: 1 }, + ) + + // Return first result, or commit error object if not present + return result.at(0) ?? { ok: false, } } @@ -1197,10 +1254,13 @@ export class Collection< * @example * ```ts * // Get the first user - * const { result } = await db.users.getOne() + * const user = await db.users.getOne() + * ``` * + * @example + * ```ts * // Get the first user with username that starts with "a" - * const { result } = await db.users.getOne({ + * const user = await db.users.getOne({ * filter: doc => doc.value.username.startsWith("a") * }) * ``` @@ -1209,13 +1269,63 @@ export class Collection< * @returns A promise that resovles to the retreived document */ async getOne(options?: ListOptions>) { - // Get and return one document + // Get result list with limit of one item const { result } = await this.handleMany( this._keys.id, (doc) => doc, { ...options, resultLimit: 1 }, ) + // Return first result item, or null if not present + return result.at(0) ?? null + } + + /** + * Retrieves one document from the KV store by a secondary index and according to the given options. + * + * If no options are given, the first document in the collection by the given index is retreived. + * + * @example + * ```ts + * // Get the first user with age = 69 + * const user = await db.users.getOneBySecondaryIndex("age", 69) + * ``` + * + * @example + * ```ts + * // Get the first user with age = 40 and username that starts with "a" + * const user = await db.users.getOneBySecondaryIndex("age", 40, { + * filter: doc => doc.value.username.startsWith("a") + * }) + * ``` + * + * @param index - Index. + * @param value - Index value. + * @param options - List options, optional. + * @returns A promise resolving to either a document or null. + */ + async getOneBySecondaryIndex< + const K extends SecondaryIndexKeys, + >( + index: K, + value: CheckKeyOf, + options?: ListOptions>, + ) { + // Create prefix key + const prefixKey = createSecondaryIndexKeyPrefix( + index, + value as KvValue, + this, + ) + + // Get result list with limit of one item + const { result } = await this.handleMany( + prefixKey, + (doc) => doc, + { ...options, resultLimit: 1 }, + ) + + // Return first result item, or null if not present return result.at(0) ?? null } @@ -1283,15 +1393,11 @@ export class Collection< fn: (doc: Document) => unknown, options?: UpdateManyOptions>, ) { - // Serialize and compress index value - const serialized = this._serializer.serialize(value) - const compressed = this._serializer.compress(serialized) - // Create prefix key - const prefixKey = extendKey( - this._keys.secondaryIndex, - index as KvId, - compressed, + const prefixKey = createSecondaryIndexKeyPrefix( + index, + value as KvValue, + this, ) // Execute callback function for each document entry @@ -2032,25 +2138,26 @@ export class Collection< const resultLimit = options?.resultLimit // Loop over each document entry - let count = 0 + let count = -1 const offset = options?.offset ?? 0 for await (const entry of iter) { - // Skip by offset + // Increment count count++ - if (count <= offset) { + // Skip by offset + if (count < offset) { continue } - // Check if result limit reached - if (resultLimit && result.length >= resultLimit) { + // Check if result limit is reached + if (resultLimit && docs.length >= resultLimit) { break } // Construct document from entry const doc = await this.constructDocument(entry) - // Continue if document not constructed + // Continue if document is not constructed if (!doc) { continue } @@ -2065,8 +2172,7 @@ export class Collection< // Execute callback function for each document await allFulfilled(docs.map(async (doc) => { try { - const res = await fn(doc) - result.push(res) + result.push(await fn(doc)) } catch (e) { errors.push(e) } diff --git a/src/utils.ts b/src/utils.ts index 6760a87..60dc004 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -37,6 +37,16 @@ export function generateId() { return ulid() } +/** + * Get a document id from a document key. + * + * @param key - A document key. + * @returns A document id, or undefined if key is empty. + */ +export function getDocumentId(key: Deno.KvKey) { + return key.at(-1) +} + /** * Extend a kv key with key parts. * @@ -48,16 +58,6 @@ export function extendKey(key: KvKey, ...keyParts: KvKey) { return [...key, ...keyParts] as KvKey } -/** - * Get a document id from a document key. - * - * @param key - A document key. - * @returns A document id, or undefined if key is empty. - */ -export function getDocumentId(key: Deno.KvKey) { - return key.at(-1) -} - /** * Compare two kv keys for equality. * @@ -69,6 +69,31 @@ export function keyEq(k1: KvKey, k2: KvKey) { return JSON.stringify(k1) === JSON.stringify(k2) } +/** + * Create a secondary index key prefix. + * + * @param index - Index. + * @param value - Index value. + * @param collection - Collection. + * @returns A key prefix from a secondary index. + */ +export function createSecondaryIndexKeyPrefix( + index: string | number | symbol, + value: KvValue, + collection: Collection, +) { + // Serialize and compress index value + const serialized = collection._serializer.serialize(value) + const compressed = collection._serializer.compress(serialized) + + // Create prefix key + return extendKey( + collection._keys.secondaryIndex, + index as KvId, + compressed, + ) +} + /** * Determine whether a kv value is an instance of KvObject. * @@ -466,6 +491,12 @@ export function createListSelector( } } +/** + * Create kv list options from given options. + * + * @param options + * @returns + */ export function createListOptions(options: ListOptions | undefined) { const limit = options?.limit && options.limit + (options.offset ?? 0) return { diff --git a/tests/collection/getOne.test.ts b/tests/collection/getOne.test.ts new file mode 100644 index 0000000..313a634 --- /dev/null +++ b/tests/collection/getOne.test.ts @@ -0,0 +1,20 @@ +import { assert } from "../deps.ts" +import { sleep, useDb } from "../utils.ts" +import { mockUser1, mockUser2 } from "../mocks.ts" + +Deno.test("collection - getOne", async (t) => { + await t.step("Should get only one document", async () => { + await useDb(async (db) => { + const cr1 = await db.users.add(mockUser1) + await sleep(10) + const cr2 = await db.users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const doc = await db.users.getOne() + assert(doc !== null) + assert(doc.value.username === mockUser1.username) + }) + }) +}) diff --git a/tests/collection/updateOne.test.ts b/tests/collection/updateOne.test.ts new file mode 100644 index 0000000..38c943d --- /dev/null +++ b/tests/collection/updateOne.test.ts @@ -0,0 +1,299 @@ +import { collection, kvdex, model } from "../../mod.ts" +import { assert, assertEquals, assertNotEquals } from "../deps.ts" +import { mockUser3 } from "../mocks.ts" +import { mockUser2 } from "../mocks.ts" +import { mockUser1, mockUserInvalid } from "../mocks.ts" +import { sleep } from "../utils.ts" +import { generateNumbers, generateUsers, useDb, useKv } from "../utils.ts" + +Deno.test("collection - updateOne", async (t) => { + await t.step( + "Should update only one document of KvObject type using shallow merge", + async () => { + await useDb(async (db) => { + const cr1 = await db.users.add(mockUser1) + await sleep(10) + const cr2 = await db.users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const updateData = { + address: { + country: "Ireland", + city: "Dublin", + houseNr: null, + }, + } + + const updateCr = await db.users.updateOne(updateData, { + strategy: "merge-shallow", + }) + + assert(updateCr.ok) + + const doc1 = await db.users.find(cr1.id) + const doc2 = await db.users.find(cr2.id) + + assert(doc1) + assert(doc2) + + assert(doc1.value.address.country === updateData.address.country) + assert(doc1.value.address.city === updateData.address.city) + assert(doc1.value.address.houseNr === updateData.address.houseNr) + assert(doc1.value.address.street === undefined) + + assert(doc2.value.address.country === mockUser2.address.country) + assert(doc2.value.address.city === mockUser2.address.city) + assert(doc2.value.address.houseNr === mockUser2.address.houseNr) + assert(doc2.value.address.street === mockUser2.address.street) + }) + }, + ) + + await t.step( + "Should update only one document of KvObject type using deep merge", + async () => { + await useDb(async (db) => { + const cr1 = await db.users.add(mockUser1) + await sleep(10) + const cr2 = await db.users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const updateData = { + address: { + country: "Ireland", + city: "Dublin", + houseNr: null, + }, + } + + const updateCr = await db.users.updateOne(updateData, { + strategy: "merge", + }) + + assert(updateCr.ok) + + const doc1 = await db.users.find(cr1.id) + const doc2 = await db.users.find(cr2.id) + + assert(doc1) + assert(doc2) + + assert(doc1.value.address.country === updateData.address.country) + assert(doc1.value.address.city === updateData.address.city) + assert(doc1.value.address.houseNr === updateData.address.houseNr) + assert(doc1.value.address.street === mockUser1.address.street) + + assert(doc2.value.address.country === mockUser2.address.country) + assert(doc2.value.address.city === mockUser2.address.city) + assert(doc2.value.address.houseNr === mockUser2.address.houseNr) + assert(doc2.value.address.street === mockUser2.address.street) + }) + }, + ) + + await t.step( + "Should update only one document of KvObject type using replace", + async () => { + await useDb(async (db) => { + const cr1 = await db.users.add(mockUser1) + await sleep(10) + const cr2 = await db.users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const updateData = mockUser3 + + const updateCr = await db.users.updateOne(updateData, { + strategy: "replace", + }) + + assert(updateCr.ok) + + const doc1 = await db.users.find(cr1.id) + const doc2 = await db.users.find(cr2.id) + + assert(doc1) + assert(doc2) + + assert(doc1.value.username === updateData.username) + assert(doc1.value.age === updateData.age) + assert(doc1.value.address.country === updateData.address.country) + assert(doc1.value.address.city === updateData.address.city) + assert(doc1.value.address.houseNr === updateData.address.houseNr) + assert(doc1.value.address.street === undefined) + + assert(doc2.value.username === mockUser2.username) + assert(doc2.value.age === mockUser2.age) + assert(doc2.value.address.country === mockUser2.address.country) + assert(doc2.value.address.city === mockUser2.address.city) + assert(doc2.value.address.houseNr === mockUser2.address.houseNr) + assert(doc2.value.address.street === mockUser2.address.street) + }) + }, + ) + + await t.step( + "Should update only one document of type Array, Set and Map using merge", + async () => { + await useKv(async (kv) => { + const db = kvdex(kv, { + arrays: collection(model()), + sets: collection(model>()), + maps: collection(model>()), + }) + + const val1 = [1, 2, 4] + const setEntries = [1, 2, 4] + const val2 = new Set(setEntries) + const mapEntries = [["1", 1], ["2", 2], ["4", 4]] as const + const val3 = new Map(mapEntries) + + const vals1: number[][] = [] + const vals2: Set[] = [] + const vals3: Map[] = [] + for (let i = 0; i < 1_000; i++) { + vals1.push(val1) + vals2.push(val2) + vals3.push(val3) + } + + const cr1 = await db.arrays.addMany(vals1) + const cr2 = await db.sets.addMany(vals2) + const cr3 = await db.maps.addMany(vals3) + + assert(cr1.ok) + assert(cr2.ok) + assert(cr3.ok) + + const u1 = [1, 3, 5] + const uSetEntries = [1, 3, 5] + const u2 = new Set(uSetEntries) + const uMapEntries = [["1", 1], ["3", 3], ["5", 5]] as const + const u3 = new Map(uMapEntries) + + const updateCr1 = await db.arrays.updateOne(u1, { + strategy: "merge", + }) + + const updateCr2 = await db.sets.updateOne(u2, { + strategy: "merge", + }) + + const updateCr3 = await db.maps.updateOne(u3, { + strategy: "merge", + }) + + assert(updateCr1.ok) + assert(updateCr2.ok) + assert(updateCr3.ok) + + const { result: [d1, ...docs1] } = await db.arrays.getMany() + const { result: [d2, ...docs2] } = await db.sets.getMany() + const { result: [d3, ...docs3] } = await db.maps.getMany() + + assertEquals(d1.value, [...val1, ...u1]) + assertEquals(d2.value, new Set([...setEntries, ...uSetEntries])) + assertEquals(d3.value, new Map([...mapEntries, ...uMapEntries])) + + docs1.forEach((doc) => assertNotEquals(doc.value, [...val1, ...u1])) + docs2.forEach((doc) => + assertNotEquals(doc.value, new Set([...setEntries, ...uSetEntries])) + ) + docs3.forEach((doc) => + assertNotEquals(doc.value, new Map([...mapEntries, ...uMapEntries])) + ) + }) + }, + ) + + await t.step( + "Should update only one document of types primitive and built-in object using replace", + async () => { + await useKv(async (kv) => { + const db = kvdex(kv, { + numbers: collection(model()), + strings: collection(model()), + dates: collection(model()), + }) + + const numbers = generateNumbers(1_000) + + const strings: string[] = [] + for (let i = 0; i < 1_000; i++) { + strings.push(Math.random().toString()) + } + + const dates: Date[] = [] + for (let i = 0; i < 1_000; i++) { + dates.push(new Date("2000-01-01")) + } + + const cr1 = await db.numbers.addMany(numbers) + const cr2 = await db.strings.addMany(strings) + const cr3 = await db.dates.addMany(dates) + + assert(cr1.ok) + assert(cr2.ok) + assert(cr3.ok) + + const val1 = -100 + const val2 = "updated" + const val3 = new Date("2016-01-01") + + const updateCr1 = await db.numbers.updateOne(val1) + const updateCr2 = await db.strings.updateOne(val2) + const updateCr3 = await db.dates.updateOne(val3) + + assert(updateCr1.ok) + assert(updateCr2.ok) + assert(updateCr3.ok) + + const { result: [d1, ...ds1] } = await db.numbers.getMany() + const { result: [d2, ...ds2] } = await db.strings.getMany() + const { result: [d3, ...ds3] } = await db.dates.getMany() + + assertEquals(d1.value, val1) + ds1.forEach((doc) => assertNotEquals(doc.value, val1)) + + assertEquals(d2.value, val2) + ds2.forEach((doc) => assertNotEquals(doc.value, val2)) + + assertEquals(d3.value, val3) + ds3.forEach((doc) => assertNotEquals(doc.value, val3)) + }) + }, + ) + + await t.step("Should successfully parse and update", async () => { + await useDb(async (db) => { + const users = generateUsers(10) + let assertion = true + + const cr = await db.z_users.addMany(users) + assert(cr.ok) + + await db.z_users.updateOne(mockUser1).catch(() => assertion = false) + + assert(assertion) + }) + }) + + await t.step("Should fail to parse and update document", async () => { + await useDb(async (db) => { + const users = generateUsers(10) + let assertion = false + + const cr = await db.z_users.addMany(users) + assert(cr.ok) + + await db.z_users.updateOne(mockUserInvalid).catch(() => assertion = true) + + assert(assertion) + }) + }) +}) diff --git a/tests/deps.ts b/tests/deps.ts index 2e39135..82a92d7 100644 --- a/tests/deps.ts +++ b/tests/deps.ts @@ -1,4 +1,5 @@ export { assert } from "https://deno.land/std@0.195.0/assert/assert.ts" export { assertEquals } from "https://deno.land/std@0.195.0/assert/assert_equals.ts" +export { assertNotEquals } from "https://deno.land/std@0.195.0/assert/assert_not_equals.ts" export { assertThrows } from "https://deno.land/std@0.195.0/assert/assert_throws.ts" export { z } from "https://deno.land/x/zod@v3.22.4/mod.ts" diff --git a/tests/indexable_collection/getOne.test.ts b/tests/indexable_collection/getOne.test.ts new file mode 100644 index 0000000..7df6133 --- /dev/null +++ b/tests/indexable_collection/getOne.test.ts @@ -0,0 +1,20 @@ +import { assert } from "../deps.ts" +import { sleep, useDb } from "../utils.ts" +import { mockUser1, mockUser2 } from "../mocks.ts" + +Deno.test("indexable_collection - getOne", async (t) => { + await t.step("Should get only one document", async () => { + await useDb(async (db) => { + const cr1 = await db.i_users.add(mockUser1) + await sleep(10) + const cr2 = await db.i_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const doc = await db.i_users.getOne() + assert(doc !== null) + assert(doc.value.username === mockUser1.username) + }) + }) +}) diff --git a/tests/indexable_collection/getOneBySecondaryIndex.test.ts b/tests/indexable_collection/getOneBySecondaryIndex.test.ts new file mode 100644 index 0000000..f9c8af1 --- /dev/null +++ b/tests/indexable_collection/getOneBySecondaryIndex.test.ts @@ -0,0 +1,26 @@ +import { assert } from "../deps.ts" +import { sleep, useDb } from "../utils.ts" +import { mockUser1, mockUser2 } from "../mocks.ts" + +Deno.test("indexable_collection - getOneBySecondaryIndex", async (t) => { + await t.step( + "Should get only one document by a secondary index", + async () => { + await useDb(async (db) => { + const cr1 = await db.i_users.add(mockUser1) + await sleep(10) + const cr2 = await db.i_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const doc = await db.i_users.getOneBySecondaryIndex( + "age", + mockUser2.age, + ) + assert(doc !== null) + assert(doc.value.username === mockUser1.username) + }) + }, + ) +}) diff --git a/tests/indexable_collection/updateOne.test.ts b/tests/indexable_collection/updateOne.test.ts new file mode 100644 index 0000000..0708999 --- /dev/null +++ b/tests/indexable_collection/updateOne.test.ts @@ -0,0 +1,172 @@ +import { assert } from "../deps.ts" +import { mockUser3 } from "../mocks.ts" +import { mockUser2 } from "../mocks.ts" +import { mockUser1, mockUserInvalid } from "../mocks.ts" +import { sleep } from "../utils.ts" +import { useDb } from "../utils.ts" + +Deno.test("indexable_collection - updateOne", async (t) => { + await t.step( + "Should update only one document of KvObject type using shallow merge", + async () => { + await useDb(async (db) => { + const cr1 = await db.i_users.add(mockUser1) + await sleep(10) + const cr2 = await db.i_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const updateData = { + address: { + country: "Ireland", + city: "Dublin", + houseNr: null, + }, + } + + const updateCr = await db.i_users.updateOne(updateData, { + strategy: "merge-shallow", + }) + + assert(updateCr.ok) + + const doc1 = await db.i_users.find(cr1.id) + const doc2 = await db.i_users.find(cr2.id) + + assert(doc1) + assert(doc2) + + assert(doc1.value.address.country === updateData.address.country) + assert(doc1.value.address.city === updateData.address.city) + assert(doc1.value.address.houseNr === updateData.address.houseNr) + assert(doc1.value.address.street === undefined) + + assert(doc2.value.address.country === mockUser2.address.country) + assert(doc2.value.address.city === mockUser2.address.city) + assert(doc2.value.address.houseNr === mockUser2.address.houseNr) + assert(doc2.value.address.street === mockUser2.address.street) + }) + }, + ) + + await t.step( + "Should update only one document of KvObject type using deep merge", + async () => { + await useDb(async (db) => { + const cr1 = await db.i_users.add(mockUser1) + await sleep(10) + const cr2 = await db.i_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const updateData = { + address: { + country: "Ireland", + city: "Dublin", + houseNr: null, + }, + } + + const updateCr = await db.i_users.updateOne(updateData, { + strategy: "merge", + }) + + assert(updateCr.ok) + + const doc1 = await db.i_users.find(cr1.id) + const doc2 = await db.i_users.find(cr2.id) + + assert(doc1) + assert(doc2) + + assert(doc1.value.address.country === updateData.address.country) + assert(doc1.value.address.city === updateData.address.city) + assert(doc1.value.address.houseNr === updateData.address.houseNr) + assert(doc1.value.address.street === mockUser1.address.street) + + assert(doc2.value.address.country === mockUser2.address.country) + assert(doc2.value.address.city === mockUser2.address.city) + assert(doc2.value.address.houseNr === mockUser2.address.houseNr) + assert(doc2.value.address.street === mockUser2.address.street) + }) + }, + ) + + await t.step( + "Should update only one document of KvObject type using replace", + async () => { + await useDb(async (db) => { + const cr1 = await db.i_users.add(mockUser1) + await sleep(10) + const cr2 = await db.i_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const updateData = mockUser3 + + const updateCr = await db.i_users.updateOne(updateData, { + strategy: "replace", + }) + + assert(updateCr.ok) + + const doc1 = await db.i_users.find(cr1.id) + const doc2 = await db.i_users.find(cr2.id) + + assert(doc1) + assert(doc2) + + assert(doc1.value.username === updateData.username) + assert(doc1.value.age === updateData.age) + assert(doc1.value.address.country === updateData.address.country) + assert(doc1.value.address.city === updateData.address.city) + assert(doc1.value.address.houseNr === updateData.address.houseNr) + assert(doc1.value.address.street === undefined) + + assert(doc2.value.username === mockUser2.username) + assert(doc2.value.age === mockUser2.age) + assert(doc2.value.address.country === mockUser2.address.country) + assert(doc2.value.address.city === mockUser2.address.city) + assert(doc2.value.address.houseNr === mockUser2.address.houseNr) + assert(doc2.value.address.street === mockUser2.address.street) + }) + }, + ) + + await t.step("Should successfully parse and update", async () => { + await useDb(async (db) => { + let assertion = true + + const cr1 = await db.zi_users.add(mockUser1) + await sleep(10) + const cr2 = await db.zi_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + await db.zi_users.updateOne(mockUser1).catch(() => assertion = false) + + assert(assertion) + }) + }) + + await t.step("Should fail to parse and update document", async () => { + await useDb(async (db) => { + let assertion = false + + const cr1 = await db.zi_users.add(mockUser1) + await sleep(10) + const cr2 = await db.zi_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + await db.zi_users.updateOne(mockUserInvalid).catch(() => assertion = true) + + assert(assertion) + }) + }) +}) diff --git a/tests/indexable_collection/updateOneBySecondaryIndex.test.ts b/tests/indexable_collection/updateOneBySecondaryIndex.test.ts new file mode 100644 index 0000000..21a7a23 --- /dev/null +++ b/tests/indexable_collection/updateOneBySecondaryIndex.test.ts @@ -0,0 +1,195 @@ +import { assert } from "../deps.ts" +import { mockUser3 } from "../mocks.ts" +import { mockUser2 } from "../mocks.ts" +import { mockUser1, mockUserInvalid } from "../mocks.ts" +import { sleep } from "../utils.ts" +import { useDb } from "../utils.ts" + +Deno.test("indexable_collection - updateOneBySecondaryIndex", async (t) => { + await t.step( + "Should update only one document of KvObject type using shallow merge", + async () => { + await useDb(async (db) => { + const cr1 = await db.i_users.add(mockUser1) + await sleep(10) + const cr2 = await db.i_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const updateData = { + address: { + country: "Ireland", + city: "Dublin", + houseNr: null, + }, + } + + const updateCr = await db.i_users.updateOneBySecondaryIndex( + "age", + mockUser2.age, + updateData, + { + strategy: "merge-shallow", + }, + ) + + assert(updateCr.ok) + + const doc1 = await db.i_users.find(cr1.id) + const doc2 = await db.i_users.find(cr2.id) + + assert(doc1) + assert(doc2) + + assert(doc1.value.address.country === updateData.address.country) + assert(doc1.value.address.city === updateData.address.city) + assert(doc1.value.address.houseNr === updateData.address.houseNr) + assert(doc1.value.address.street === undefined) + + assert(doc2.value.address.country === mockUser2.address.country) + assert(doc2.value.address.city === mockUser2.address.city) + assert(doc2.value.address.houseNr === mockUser2.address.houseNr) + assert(doc2.value.address.street === mockUser2.address.street) + }) + }, + ) + + await t.step( + "Should update only one document of KvObject type using deep merge", + async () => { + await useDb(async (db) => { + const cr1 = await db.i_users.add(mockUser1) + await sleep(10) + const cr2 = await db.i_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const updateData = { + address: { + country: "Ireland", + city: "Dublin", + houseNr: null, + }, + } + + const updateCr = await db.i_users.updateOneBySecondaryIndex( + "age", + mockUser2.age, + updateData, + { + strategy: "merge", + }, + ) + + assert(updateCr.ok) + + const doc1 = await db.i_users.find(cr1.id) + const doc2 = await db.i_users.find(cr2.id) + + assert(doc1) + assert(doc2) + + assert(doc1.value.address.country === updateData.address.country) + assert(doc1.value.address.city === updateData.address.city) + assert(doc1.value.address.houseNr === updateData.address.houseNr) + assert(doc1.value.address.street === mockUser1.address.street) + + assert(doc2.value.address.country === mockUser2.address.country) + assert(doc2.value.address.city === mockUser2.address.city) + assert(doc2.value.address.houseNr === mockUser2.address.houseNr) + assert(doc2.value.address.street === mockUser2.address.street) + }) + }, + ) + + await t.step( + "Should update only one document of KvObject type using replace", + async () => { + await useDb(async (db) => { + const cr1 = await db.i_users.add(mockUser1) + await sleep(10) + const cr2 = await db.i_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const updateData = mockUser3 + + const updateCr = await db.i_users.updateOneBySecondaryIndex( + "age", + mockUser2.age, + updateData, + { + strategy: "replace", + }, + ) + + assert(updateCr.ok) + + const doc1 = await db.i_users.find(cr1.id) + const doc2 = await db.i_users.find(cr2.id) + + assert(doc1) + assert(doc2) + + assert(doc1.value.username === updateData.username) + assert(doc1.value.age === updateData.age) + assert(doc1.value.address.country === updateData.address.country) + assert(doc1.value.address.city === updateData.address.city) + assert(doc1.value.address.houseNr === updateData.address.houseNr) + assert(doc1.value.address.street === undefined) + + assert(doc2.value.username === mockUser2.username) + assert(doc2.value.age === mockUser2.age) + assert(doc2.value.address.country === mockUser2.address.country) + assert(doc2.value.address.city === mockUser2.address.city) + assert(doc2.value.address.houseNr === mockUser2.address.houseNr) + assert(doc2.value.address.street === mockUser2.address.street) + }) + }, + ) + + await t.step("Should successfully parse and update", async () => { + await useDb(async (db) => { + let assertion = true + + const cr1 = await db.zi_users.add(mockUser1) + await sleep(10) + const cr2 = await db.zi_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + await db.zi_users.updateOneBySecondaryIndex( + "age", + mockUser2.age, + mockUser1, + ).catch(() => assertion = false) + + assert(assertion) + }) + }) + + await t.step("Should fail to parse and update document", async () => { + await useDb(async (db) => { + let assertion = false + + const cr1 = await db.zi_users.add(mockUser1) + await sleep(10) + const cr2 = await db.zi_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + await db.zi_users.updateOneBySecondaryIndex( + "age", + mockUser2.age, + mockUserInvalid, + ).catch(() => assertion = true) + + assert(assertion) + }) + }) +}) diff --git a/tests/serialized_collection/getOne.test.ts b/tests/serialized_collection/getOne.test.ts new file mode 100644 index 0000000..ed09081 --- /dev/null +++ b/tests/serialized_collection/getOne.test.ts @@ -0,0 +1,20 @@ +import { assert } from "../deps.ts" +import { sleep, useDb } from "../utils.ts" +import { mockUser1, mockUser2 } from "../mocks.ts" + +Deno.test("serialized_collection - getOne", async (t) => { + await t.step("Should get only one document", async () => { + await useDb(async (db) => { + const cr1 = await db.s_users.add(mockUser1) + await sleep(10) + const cr2 = await db.s_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const doc = await db.s_users.getOne() + assert(doc !== null) + assert(doc.value.username === mockUser1.username) + }) + }) +}) diff --git a/tests/serialized_collection/updateOne.test.ts b/tests/serialized_collection/updateOne.test.ts new file mode 100644 index 0000000..4491113 --- /dev/null +++ b/tests/serialized_collection/updateOne.test.ts @@ -0,0 +1,299 @@ +import { collection, kvdex, model } from "../../mod.ts" +import { assert, assertEquals, assertNotEquals } from "../deps.ts" +import { mockUser3 } from "../mocks.ts" +import { mockUser2 } from "../mocks.ts" +import { mockUser1, mockUserInvalid } from "../mocks.ts" +import { sleep } from "../utils.ts" +import { generateNumbers, generateUsers, useDb, useKv } from "../utils.ts" + +Deno.test("serialized_collection - updateOne", async (t) => { + await t.step( + "Should update only one document of KvObject type using shallow merge", + async () => { + await useDb(async (db) => { + const cr1 = await db.s_users.add(mockUser1) + await sleep(10) + const cr2 = await db.s_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const updateData = { + address: { + country: "Ireland", + city: "Dublin", + houseNr: null, + }, + } + + const updateCr = await db.s_users.updateOne(updateData, { + strategy: "merge-shallow", + }) + + assert(updateCr.ok) + + const doc1 = await db.s_users.find(cr1.id) + const doc2 = await db.s_users.find(cr2.id) + + assert(doc1) + assert(doc2) + + assert(doc1.value.address.country === updateData.address.country) + assert(doc1.value.address.city === updateData.address.city) + assert(doc1.value.address.houseNr === updateData.address.houseNr) + assert(doc1.value.address.street === undefined) + + assert(doc2.value.address.country === mockUser2.address.country) + assert(doc2.value.address.city === mockUser2.address.city) + assert(doc2.value.address.houseNr === mockUser2.address.houseNr) + assert(doc2.value.address.street === mockUser2.address.street) + }) + }, + ) + + await t.step( + "Should update only one document of KvObject type using deep merge", + async () => { + await useDb(async (db) => { + const cr1 = await db.s_users.add(mockUser1) + await sleep(10) + const cr2 = await db.s_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const updateData = { + address: { + country: "Ireland", + city: "Dublin", + houseNr: null, + }, + } + + const updateCr = await db.s_users.updateOne(updateData, { + strategy: "merge", + }) + + assert(updateCr.ok) + + const doc1 = await db.s_users.find(cr1.id) + const doc2 = await db.s_users.find(cr2.id) + + assert(doc1) + assert(doc2) + + assert(doc1.value.address.country === updateData.address.country) + assert(doc1.value.address.city === updateData.address.city) + assert(doc1.value.address.houseNr === updateData.address.houseNr) + assert(doc1.value.address.street === mockUser1.address.street) + + assert(doc2.value.address.country === mockUser2.address.country) + assert(doc2.value.address.city === mockUser2.address.city) + assert(doc2.value.address.houseNr === mockUser2.address.houseNr) + assert(doc2.value.address.street === mockUser2.address.street) + }) + }, + ) + + await t.step( + "Should update only one document of KvObject type using replace", + async () => { + await useDb(async (db) => { + const cr1 = await db.s_users.add(mockUser1) + await sleep(10) + const cr2 = await db.s_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const updateData = mockUser3 + + const updateCr = await db.s_users.updateOne(updateData, { + strategy: "replace", + }) + + assert(updateCr.ok) + + const doc1 = await db.s_users.find(cr1.id) + const doc2 = await db.s_users.find(cr2.id) + + assert(doc1) + assert(doc2) + + assert(doc1.value.username === updateData.username) + assert(doc1.value.age === updateData.age) + assert(doc1.value.address.country === updateData.address.country) + assert(doc1.value.address.city === updateData.address.city) + assert(doc1.value.address.houseNr === updateData.address.houseNr) + assert(doc1.value.address.street === undefined) + + assert(doc2.value.username === mockUser2.username) + assert(doc2.value.age === mockUser2.age) + assert(doc2.value.address.country === mockUser2.address.country) + assert(doc2.value.address.city === mockUser2.address.city) + assert(doc2.value.address.houseNr === mockUser2.address.houseNr) + assert(doc2.value.address.street === mockUser2.address.street) + }) + }, + ) + + await t.step( + "Should update only one document of type Array, Set and Map using merge", + async () => { + await useKv(async (kv) => { + const db = kvdex(kv, { + arrays: collection(model()), + sets: collection(model>()), + maps: collection(model>()), + }) + + const val1 = [1, 2, 4] + const setEntries = [1, 2, 4] + const val2 = new Set(setEntries) + const mapEntries = [["1", 1], ["2", 2], ["4", 4]] as const + const val3 = new Map(mapEntries) + + const vals1: number[][] = [] + const vals2: Set[] = [] + const vals3: Map[] = [] + for (let i = 0; i < 1_000; i++) { + vals1.push(val1) + vals2.push(val2) + vals3.push(val3) + } + + const cr1 = await db.arrays.addMany(vals1) + const cr2 = await db.sets.addMany(vals2) + const cr3 = await db.maps.addMany(vals3) + + assert(cr1.ok) + assert(cr2.ok) + assert(cr3.ok) + + const u1 = [1, 3, 5] + const uSetEntries = [1, 3, 5] + const u2 = new Set(uSetEntries) + const uMapEntries = [["1", 1], ["3", 3], ["5", 5]] as const + const u3 = new Map(uMapEntries) + + const updateCr1 = await db.arrays.updateOne(u1, { + strategy: "merge", + }) + + const updateCr2 = await db.sets.updateOne(u2, { + strategy: "merge", + }) + + const updateCr3 = await db.maps.updateOne(u3, { + strategy: "merge", + }) + + assert(updateCr1.ok) + assert(updateCr2.ok) + assert(updateCr3.ok) + + const { result: [d1, ...docs1] } = await db.arrays.getMany() + const { result: [d2, ...docs2] } = await db.sets.getMany() + const { result: [d3, ...docs3] } = await db.maps.getMany() + + assertEquals(d1.value, [...val1, ...u1]) + assertEquals(d2.value, new Set([...setEntries, ...uSetEntries])) + assertEquals(d3.value, new Map([...mapEntries, ...uMapEntries])) + + docs1.forEach((doc) => assertNotEquals(doc.value, [...val1, ...u1])) + docs2.forEach((doc) => + assertNotEquals(doc.value, new Set([...setEntries, ...uSetEntries])) + ) + docs3.forEach((doc) => + assertNotEquals(doc.value, new Map([...mapEntries, ...uMapEntries])) + ) + }) + }, + ) + + await t.step( + "Should update only one document of types primitive and built-in object using replace", + async () => { + await useKv(async (kv) => { + const db = kvdex(kv, { + numbers: collection(model()), + strings: collection(model()), + dates: collection(model()), + }) + + const numbers = generateNumbers(1_000) + + const strings: string[] = [] + for (let i = 0; i < 1_000; i++) { + strings.push(Math.random().toString()) + } + + const dates: Date[] = [] + for (let i = 0; i < 1_000; i++) { + dates.push(new Date("2000-01-01")) + } + + const cr1 = await db.numbers.addMany(numbers) + const cr2 = await db.strings.addMany(strings) + const cr3 = await db.dates.addMany(dates) + + assert(cr1.ok) + assert(cr2.ok) + assert(cr3.ok) + + const val1 = -100 + const val2 = "updated" + const val3 = new Date("2016-01-01") + + const updateCr1 = await db.numbers.updateOne(val1) + const updateCr2 = await db.strings.updateOne(val2) + const updateCr3 = await db.dates.updateOne(val3) + + assert(updateCr1.ok) + assert(updateCr2.ok) + assert(updateCr3.ok) + + const { result: [d1, ...ds1] } = await db.numbers.getMany() + const { result: [d2, ...ds2] } = await db.strings.getMany() + const { result: [d3, ...ds3] } = await db.dates.getMany() + + assertEquals(d1.value, val1) + ds1.forEach((doc) => assertNotEquals(doc.value, val1)) + + assertEquals(d2.value, val2) + ds2.forEach((doc) => assertNotEquals(doc.value, val2)) + + assertEquals(d3.value, val3) + ds3.forEach((doc) => assertNotEquals(doc.value, val3)) + }) + }, + ) + + await t.step("Should successfully parse and update", async () => { + await useDb(async (db) => { + const users = generateUsers(10) + let assertion = true + + const cr = await db.zs_users.addMany(users) + assert(cr.ok) + + await db.zs_users.updateOne(mockUser1).catch(() => assertion = false) + + assert(assertion) + }) + }) + + await t.step("Should fail to parse and update document", async () => { + await useDb(async (db) => { + const users = generateUsers(10) + let assertion = false + + const cr = await db.zs_users.addMany(users) + assert(cr.ok) + + await db.zs_users.updateOne(mockUserInvalid).catch(() => assertion = true) + + assert(assertion) + }) + }) +}) diff --git a/tests/serialized_indexable_collection/getOne.test.ts b/tests/serialized_indexable_collection/getOne.test.ts new file mode 100644 index 0000000..60d6026 --- /dev/null +++ b/tests/serialized_indexable_collection/getOne.test.ts @@ -0,0 +1,20 @@ +import { assert } from "../deps.ts" +import { sleep, useDb } from "../utils.ts" +import { mockUser1, mockUser2 } from "../mocks.ts" + +Deno.test("serialized_indexable_collection - getOne", async (t) => { + await t.step("Should get only one document", async () => { + await useDb(async (db) => { + const cr1 = await db.is_users.add(mockUser1) + await sleep(10) + const cr2 = await db.is_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const doc = await db.is_users.getOne() + assert(doc !== null) + assert(doc.value.username === mockUser1.username) + }) + }) +}) diff --git a/tests/serialized_indexable_collection/getOneBySecondaryIndex.test.ts b/tests/serialized_indexable_collection/getOneBySecondaryIndex.test.ts new file mode 100644 index 0000000..a861cd5 --- /dev/null +++ b/tests/serialized_indexable_collection/getOneBySecondaryIndex.test.ts @@ -0,0 +1,26 @@ +import { assert } from "../deps.ts" +import { sleep, useDb } from "../utils.ts" +import { mockUser1, mockUser2 } from "../mocks.ts" + +Deno.test("serialized_indexable_collection - getOneBySecondaryIndex", async (t) => { + await t.step( + "Should get only one document by a secondary index", + async () => { + await useDb(async (db) => { + const cr1 = await db.is_users.add(mockUser1) + await sleep(10) + const cr2 = await db.is_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const doc = await db.is_users.getOneBySecondaryIndex( + "age", + mockUser2.age, + ) + assert(doc !== null) + assert(doc.value.username === mockUser1.username) + }) + }, + ) +}) diff --git a/tests/serialized_indexable_collection/updateOne.test.ts b/tests/serialized_indexable_collection/updateOne.test.ts new file mode 100644 index 0000000..97ec0ee --- /dev/null +++ b/tests/serialized_indexable_collection/updateOne.test.ts @@ -0,0 +1,174 @@ +import { assert } from "../deps.ts" +import { mockUser3 } from "../mocks.ts" +import { mockUser2 } from "../mocks.ts" +import { mockUser1, mockUserInvalid } from "../mocks.ts" +import { sleep } from "../utils.ts" +import { useDb } from "../utils.ts" + +Deno.test("serialized_indexable_collection - updateOne", async (t) => { + await t.step( + "Should update only one document of KvObject type using shallow merge", + async () => { + await useDb(async (db) => { + const cr1 = await db.is_users.add(mockUser1) + await sleep(10) + const cr2 = await db.is_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const updateData = { + address: { + country: "Ireland", + city: "Dublin", + houseNr: null, + }, + } + + const updateCr = await db.is_users.updateOne(updateData, { + strategy: "merge-shallow", + }) + + assert(updateCr.ok) + + const doc1 = await db.is_users.find(cr1.id) + const doc2 = await db.is_users.find(cr2.id) + + assert(doc1) + assert(doc2) + + assert(doc1.value.address.country === updateData.address.country) + assert(doc1.value.address.city === updateData.address.city) + assert(doc1.value.address.houseNr === updateData.address.houseNr) + assert(doc1.value.address.street === undefined) + + assert(doc2.value.address.country === mockUser2.address.country) + assert(doc2.value.address.city === mockUser2.address.city) + assert(doc2.value.address.houseNr === mockUser2.address.houseNr) + assert(doc2.value.address.street === mockUser2.address.street) + }) + }, + ) + + await t.step( + "Should update only one document of KvObject type using deep merge", + async () => { + await useDb(async (db) => { + const cr1 = await db.is_users.add(mockUser1) + await sleep(10) + const cr2 = await db.is_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const updateData = { + address: { + country: "Ireland", + city: "Dublin", + houseNr: null, + }, + } + + const updateCr = await db.is_users.updateOne(updateData, { + strategy: "merge", + }) + + assert(updateCr.ok) + + const doc1 = await db.is_users.find(cr1.id) + const doc2 = await db.is_users.find(cr2.id) + + assert(doc1) + assert(doc2) + + assert(doc1.value.address.country === updateData.address.country) + assert(doc1.value.address.city === updateData.address.city) + assert(doc1.value.address.houseNr === updateData.address.houseNr) + assert(doc1.value.address.street === mockUser1.address.street) + + assert(doc2.value.address.country === mockUser2.address.country) + assert(doc2.value.address.city === mockUser2.address.city) + assert(doc2.value.address.houseNr === mockUser2.address.houseNr) + assert(doc2.value.address.street === mockUser2.address.street) + }) + }, + ) + + await t.step( + "Should update only one document of KvObject type using replace", + async () => { + await useDb(async (db) => { + const cr1 = await db.is_users.add(mockUser1) + await sleep(10) + const cr2 = await db.is_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const updateData = mockUser3 + + const updateCr = await db.is_users.updateOne(updateData, { + strategy: "replace", + }) + + assert(updateCr.ok) + + const doc1 = await db.is_users.find(cr1.id) + const doc2 = await db.is_users.find(cr2.id) + + assert(doc1) + assert(doc2) + + assert(doc1.value.username === updateData.username) + assert(doc1.value.age === updateData.age) + assert(doc1.value.address.country === updateData.address.country) + assert(doc1.value.address.city === updateData.address.city) + assert(doc1.value.address.houseNr === updateData.address.houseNr) + assert(doc1.value.address.street === undefined) + + assert(doc2.value.username === mockUser2.username) + assert(doc2.value.age === mockUser2.age) + assert(doc2.value.address.country === mockUser2.address.country) + assert(doc2.value.address.city === mockUser2.address.city) + assert(doc2.value.address.houseNr === mockUser2.address.houseNr) + assert(doc2.value.address.street === mockUser2.address.street) + }) + }, + ) + + await t.step("Should successfully parse and update", async () => { + await useDb(async (db) => { + let assertion = true + + const cr1 = await db.zis_users.add(mockUser1) + await sleep(10) + const cr2 = await db.zis_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + await db.zis_users.updateOne(mockUser1).catch(() => assertion = false) + + assert(assertion) + }) + }) + + await t.step("Should fail to parse and update document", async () => { + await useDb(async (db) => { + let assertion = false + + const cr1 = await db.zis_users.add(mockUser1) + await sleep(10) + const cr2 = await db.zis_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + await db.zis_users.updateOne(mockUserInvalid).catch(() => + assertion = true + ) + + assert(assertion) + }) + }) +}) diff --git a/tests/serialized_indexable_collection/updateOneBySecondaryIndex.test.ts b/tests/serialized_indexable_collection/updateOneBySecondaryIndex.test.ts new file mode 100644 index 0000000..a0add5e --- /dev/null +++ b/tests/serialized_indexable_collection/updateOneBySecondaryIndex.test.ts @@ -0,0 +1,195 @@ +import { assert } from "../deps.ts" +import { mockUser3 } from "../mocks.ts" +import { mockUser2 } from "../mocks.ts" +import { mockUser1, mockUserInvalid } from "../mocks.ts" +import { sleep } from "../utils.ts" +import { useDb } from "../utils.ts" + +Deno.test("serialized_indexable_collection - updateOneBySecondaryIndex", async (t) => { + await t.step( + "Should update only one document of KvObject type using shallow merge", + async () => { + await useDb(async (db) => { + const cr1 = await db.is_users.add(mockUser1) + await sleep(10) + const cr2 = await db.is_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const updateData = { + address: { + country: "Ireland", + city: "Dublin", + houseNr: null, + }, + } + + const updateCr = await db.is_users.updateOneBySecondaryIndex( + "age", + mockUser2.age, + updateData, + { + strategy: "merge-shallow", + }, + ) + + assert(updateCr.ok) + + const doc1 = await db.is_users.find(cr1.id) + const doc2 = await db.is_users.find(cr2.id) + + assert(doc1) + assert(doc2) + + assert(doc1.value.address.country === updateData.address.country) + assert(doc1.value.address.city === updateData.address.city) + assert(doc1.value.address.houseNr === updateData.address.houseNr) + assert(doc1.value.address.street === undefined) + + assert(doc2.value.address.country === mockUser2.address.country) + assert(doc2.value.address.city === mockUser2.address.city) + assert(doc2.value.address.houseNr === mockUser2.address.houseNr) + assert(doc2.value.address.street === mockUser2.address.street) + }) + }, + ) + + await t.step( + "Should update only one document of KvObject type using deep merge", + async () => { + await useDb(async (db) => { + const cr1 = await db.is_users.add(mockUser1) + await sleep(10) + const cr2 = await db.is_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const updateData = { + address: { + country: "Ireland", + city: "Dublin", + houseNr: null, + }, + } + + const updateCr = await db.is_users.updateOneBySecondaryIndex( + "age", + mockUser2.age, + updateData, + { + strategy: "merge", + }, + ) + + assert(updateCr.ok) + + const doc1 = await db.is_users.find(cr1.id) + const doc2 = await db.is_users.find(cr2.id) + + assert(doc1) + assert(doc2) + + assert(doc1.value.address.country === updateData.address.country) + assert(doc1.value.address.city === updateData.address.city) + assert(doc1.value.address.houseNr === updateData.address.houseNr) + assert(doc1.value.address.street === mockUser1.address.street) + + assert(doc2.value.address.country === mockUser2.address.country) + assert(doc2.value.address.city === mockUser2.address.city) + assert(doc2.value.address.houseNr === mockUser2.address.houseNr) + assert(doc2.value.address.street === mockUser2.address.street) + }) + }, + ) + + await t.step( + "Should update only one document of KvObject type using replace", + async () => { + await useDb(async (db) => { + const cr1 = await db.is_users.add(mockUser1) + await sleep(10) + const cr2 = await db.is_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + const updateData = mockUser3 + + const updateCr = await db.is_users.updateOneBySecondaryIndex( + "age", + mockUser2.age, + updateData, + { + strategy: "replace", + }, + ) + + assert(updateCr.ok) + + const doc1 = await db.is_users.find(cr1.id) + const doc2 = await db.is_users.find(cr2.id) + + assert(doc1) + assert(doc2) + + assert(doc1.value.username === updateData.username) + assert(doc1.value.age === updateData.age) + assert(doc1.value.address.country === updateData.address.country) + assert(doc1.value.address.city === updateData.address.city) + assert(doc1.value.address.houseNr === updateData.address.houseNr) + assert(doc1.value.address.street === undefined) + + assert(doc2.value.username === mockUser2.username) + assert(doc2.value.age === mockUser2.age) + assert(doc2.value.address.country === mockUser2.address.country) + assert(doc2.value.address.city === mockUser2.address.city) + assert(doc2.value.address.houseNr === mockUser2.address.houseNr) + assert(doc2.value.address.street === mockUser2.address.street) + }) + }, + ) + + await t.step("Should successfully parse and update", async () => { + await useDb(async (db) => { + let assertion = true + + const cr1 = await db.zis_users.add(mockUser1) + await sleep(10) + const cr2 = await db.zis_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + await db.zis_users.updateOneBySecondaryIndex( + "age", + mockUser2.age, + mockUser1, + ).catch(() => assertion = false) + + assert(assertion) + }) + }) + + await t.step("Should fail to parse and update document", async () => { + await useDb(async (db) => { + let assertion = false + + const cr1 = await db.zis_users.add(mockUser1) + await sleep(10) + const cr2 = await db.zis_users.add(mockUser2) + + assert(cr1.ok) + assert(cr2.ok) + + await db.zis_users.updateOneBySecondaryIndex( + "age", + mockUser2.age, + mockUserInvalid, + ).catch(() => assertion = true) + + assert(assertion) + }) + }) +})