diff --git a/src/atomic_builder.ts b/src/atomic_builder.ts index 78bd7a6..c7fedb3 100644 --- a/src/atomic_builder.ts +++ b/src/atomic_builder.ts @@ -1,4 +1,5 @@ import type { Collection } from "./collection.ts" +import { ulid } from "./deps.ts" import { InvalidCollectionError } from "./errors.ts" import type { AtomicCheck, @@ -7,6 +8,7 @@ import type { CollectionOptions, CollectionSelector, EnqueueOptions, + HistoryEntry, KvId, KvObject, KvValue, @@ -20,7 +22,6 @@ import { allFulfilled, deleteIndices, extendKey, - getDocumentId, keyEq, prepareEnqueue, setIndices, @@ -63,9 +64,10 @@ export class AtomicBuilder< ) } - // Set kv and schema + // Set kv, schema and collection context this.kv = kv this.schema = schema + this.collection = collection // Initiate operations or set from given operations this.operations = operations ?? { @@ -74,9 +76,6 @@ export class AtomicBuilder< indexDeleteCollectionKeys: [], indexAddCollectionKeys: [], } - - // Set colection context - this.collection = collection } /** @@ -122,8 +121,7 @@ export class AtomicBuilder< * @returns Current AtomicBuilder instance. */ add(value: ParseInputType, options?: AtomicSetOptions) { - // Perform set operation with generated id. - return this.set(null, value, options) + return this.setDocument(null, value, options) } /** @@ -145,43 +143,11 @@ export class AtomicBuilder< * @returns Current AtomicBuilder instance. */ set( - id: KvId | null, + id: KvId, value: ParseInputType, options?: AtomicSetOptions, ) { - // Create id key from collection id key and id - const collection = this.collection - const parsed = collection._model.parse(value as TInput) - const docId = id ?? collection._idGenerator(parsed) - const idKey = extendKey(collection._keys.id, docId) - - // Add set operation - this.operations.atomic.check({ key: idKey, versionstamp: null }).set( - idKey, - parsed, - options, - ) - - if (collection._isIndexable) { - // Set data as KvObject type - const _data = parsed as KvObject - - // Add collection id key for collision detection - this.operations.indexAddCollectionKeys.push(collection._keys.base) - - // Add indexing operations - setIndices( - docId, - _data, - _data, - this.operations.atomic, - this.collection, - options, - ) - } - - // Return current AtomicBuilder - return this + return this.setDocument(id, value, options) } /** @@ -221,6 +187,18 @@ export class AtomicBuilder< }) } + // Set history entry if keeps history + if (this.collection._keepsHistory) { + const historyKey = extendKey(this.collection._keys.history, id, ulid()) + + const historyEntry: HistoryEntry = { + type: "delete", + timestamp: new Date(), + } + + this.operations.atomic.set(historyKey, historyEntry) + } + // Return current AtomicBuilder return this } @@ -276,13 +254,8 @@ export class AtomicBuilder< * @returns Current AtomicBuilder instance. */ sum(id: KvId, value: TOutput extends Deno.KvU64 ? bigint : never) { - // Create id key from id and collection id key const idKey = extendKey(this.collection._keys.id, id) - - // Add sum operation to atomic ops list this.operations.atomic.sum(idKey, value) - - // Return current AtomicBuilder return this } @@ -303,13 +276,8 @@ export class AtomicBuilder< * @returns Current AtomicBuilder instance. */ min(id: KvId, value: TOutput extends Deno.KvU64 ? bigint : never) { - // Create id key from id and collection id key const idKey = extendKey(this.collection._keys.id, id) - - // Add min operation to atomic ops list this.operations.atomic.min(idKey, value) - - // Return current AtomicBuilder return this } @@ -330,13 +298,8 @@ export class AtomicBuilder< * @returns Current AtomicBuilder instance. */ max(id: KvId, value: TOutput extends Deno.KvU64 ? bigint : never) { - // Create id key from id and collection id key const idKey = extendKey(this.collection._keys.id, id) - - // Add max operation to atomic ops list this.operations.atomic.max(idKey, value) - - // Return current AtomicBuilder return this } @@ -363,89 +326,28 @@ export class AtomicBuilder< * @param mutations - Atomic mutations to be performed. * @returns Current AtomicBuilder instance. */ - mutate(...mutations: AtomicMutation[]) { - // Get collection ref - const collection = this.collection - - // Map from atomic mutations to kv mutations - const kvMutations: Deno.KvMutation[] = mutations.map(({ id, ...rest }) => { - const idKey = extendKey(collection._keys.id, id) - - if (rest.type === "delete") { - return { - key: idKey, - ...rest, - } - } - - const { value: _, ...resTOutput } = rest - // deno-lint-ignore no-explicit-any - const parsed = collection._model.parse(rest.value as any) - - return { - key: idKey, - value: parsed, - ...resTOutput, - } as Deno.KvMutation - }) - - // Add mutation operation to atomic ops list - this.operations.atomic.mutate(...kvMutations) - - // Addtional checks - kvMutations.forEach((mut) => { - // If mutation type is "set", add check operation - if (mut.type === "set") { - this.operations.atomic.check({ - key: mut.key, - versionstamp: null, - }) - } - - // If collection is indexable, handle indexing - if (collection._isIndexable) { - // Get document id from mutation key - const id = getDocumentId(mut.key) - - // If id is undefined, continue to next mutation - if (typeof id === "undefined") { - return - } - - // If mutation type is "set", handle setting of indices - if (mut.type === "set") { - // Add collection key for collision detection - this.operations.indexAddCollectionKeys.push(collection._keys.base) - - // Add indexing operations to atomic ops list - setIndices( - id, - mut.value as KvObject, - mut.value as KvObject, - this.operations.atomic, - this.collection, - { - ...mut, - }, - ) - } - - // If mutation type is "delete", create and add delete preperation function - if (mut.type === "delete") { - // Add collection key for collision detection - this.operations.indexDeleteCollectionKeys.push( - collection._keys.base, - ) - - // Add delete preperation function to delete preperation functions list - this.operations.prepareDeleteFns.push(async (kv) => { - const doc = await kv.get(mut.key) - return { - id, - data: doc.value ?? {}, - } - }) - } + mutate(...mutations: AtomicMutation>[]) { + // Add each atomic mutation by case + mutations.forEach(({ id, ...rest }) => { + switch (rest.type) { + case "delete": + this.delete(id) + break + case "set": + this.set(id, rest.value, { expireIn: rest.expireIn }) + break + case "add": + this.add(rest.value, { expireIn: rest.expireIn }) + break + case "max": + this.max(id, rest.value as any) + break + case "min": + this.min(id, rest.value as any) + break + case "sum": + this.sum(id, rest.value as any) + break } }) @@ -555,4 +457,66 @@ export class AtomicBuilder< // Return commit result return commitResult } + + /***********************/ + /* */ + /* PRIVATE METHODS */ + /* */ + /***********************/ + + /** + * Set a new document entry + * + * @param id + * @param value + * @param options + * @returns + */ + private setDocument( + id: KvId | null, + value: ParseInputType, + options?: AtomicSetOptions, + ) { + // Create id key from collection id key and id + const collection = this.collection + const parsed = collection._model.parse(value as TInput) + const docId = id ?? collection._idGenerator(parsed) + const idKey = extendKey(collection._keys.id, docId) + + // Add set operation + this.operations.atomic + .check({ key: idKey, versionstamp: null }) + .set(idKey, parsed, options) + + if (collection._isIndexable) { + // Add collection id key for collision detection + this.operations.indexAddCollectionKeys.push(collection._keys.base) + + // Add indexing operations + setIndices( + docId, + parsed as KvObject, + parsed as KvObject, + this.operations.atomic, + this.collection, + options, + ) + } + + // Set history entry if keeps history + if (this.collection._keepsHistory) { + const historyKey = extendKey(this.collection._keys.history, docId, ulid()) + + const historyEntry: HistoryEntry = { + type: "write", + timestamp: new Date(), + value: parsed, + } + + this.operations.atomic.set(historyKey, historyEntry) + } + + // Return current AtomicBuilder + return this + } } diff --git a/src/types.ts b/src/types.ts index 783c116..d58205a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -189,7 +189,7 @@ export type AtomicCheck = { versionstamp: Document["versionstamp"] } -export type AtomicMutation = +export type AtomicMutation = & { id: KvId } @@ -199,17 +199,22 @@ export type AtomicMutation = value: T expireIn?: number } + | { + type: "add" + value: T + expireIn?: number + } | { type: "sum" - value: T extends Deno.KvU64 ? T : never + value: T extends Deno.KvU64 ? bigint : never } | { type: "min" - value: T extends Deno.KvU64 ? T : never + value: T extends Deno.KvU64 ? bigint : never } | { type: "max" - value: T extends Deno.KvU64 ? T : never + value: T extends Deno.KvU64 ? bigint : never } | { type: "delete" diff --git a/tests/db/atomic.test.ts b/tests/db/atomic.test.ts index a57476f..a1a6857 100644 --- a/tests/db/atomic.test.ts +++ b/tests/db/atomic.test.ts @@ -180,9 +180,10 @@ Deno.test("db - atomic", async (t) => { await t.step("Should perform mutation operations", async () => { await useDb(async (db) => { const initial = new Deno.KvU64(100n) - const value = new Deno.KvU64(200n) + const set = new Deno.KvU64(200n) + const add = new Deno.KvU64(300n) const id = "id" - const addition = new Deno.KvU64(100n) + const sum = new Deno.KvU64(100n) const min1 = new Deno.KvU64(10n) const min2 = new Deno.KvU64(200n) const max1 = new Deno.KvU64(200n) @@ -203,32 +204,37 @@ Deno.test("db - atomic", async (t) => { { id, type: "set", - value, + value: set, + }, + { + id, + type: "add", + value: add, }, { id: cr1.id, type: "sum", - value: addition, + value: sum.value, }, { id: cr2.id, type: "min", - value: min1, + value: min1.value, }, { id: cr3.id, type: "min", - value: min2, + value: min2.value, }, { id: cr4.id, type: "max", - value: max1, + value: max1.value, }, { id: cr5.id, type: "max", - value: max2, + value: max2.value, }, { id: cr6.id, @@ -237,7 +243,12 @@ Deno.test("db - atomic", async (t) => { ) .commit() - const docNew = await db.u64s.find(id) + const docSet = await db.u64s.find(id) + + const { result: [docAdd] } = await db.u64s.getMany({ + filter: (d) => d.value.value === 300n, + }) + const doc1 = await db.u64s.find(cr1.id) const doc2 = await db.u64s.find(cr2.id) const doc3 = await db.u64s.find(cr3.id) @@ -245,8 +256,9 @@ Deno.test("db - atomic", async (t) => { const doc5 = await db.u64s.find(cr5.id) const doc6 = await db.u64s.find(cr6.id) - assert(docNew?.value.value === value.value) - assert(doc1?.value.value === initial.value + addition.value) + assert(docSet?.value.value === set.value) + assert(docAdd?.value.value === add.value) + assert(doc1?.value.value === initial.value + sum.value) assert(doc2?.value.value === min1.value) assert(doc3?.value.value === initial.value) assert(doc4?.value.value === max1.value) @@ -355,4 +367,42 @@ Deno.test("db - atomic", async (t) => { assert(assertion3) }) }) + + await t.step("Should retain history in correct order", async () => { + await useKv(async (kv) => { + const db = kvdex(kv, { + numbers: collection(model(), { history: true }), + }) + + const id = "id" + + await db + .atomic((s) => s.numbers) + .add(100) + .set(id, 200) + .commit() + + await sleep(10) + + await db + .atomic((s) => s.numbers) + .delete(id) + .commit() + + const { result: [doc] } = await db.numbers.getMany({ + filter: (d) => d.value === 100, + }) + + const [h] = await db.numbers.findHistory(doc.id) + assert(h.type === "write") + assert(h.value === 100) + + const [h1, h2] = await db.numbers.findHistory(id) + + assert(h1.type === "write") + assert(h1.value === 200) + assert(h1.timestamp.valueOf() <= h2.timestamp.valueOf()) + assert(h2.type === "delete") + }) + }) })