Skip to content

Commit 41ed3f9

Browse files
feat: added upsert by id/index + passing tests
1 parent 5b9b284 commit 41ed3f9

File tree

6 files changed

+334
-2
lines changed

6 files changed

+334
-2
lines changed

src/collection.ts

+55
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
HistoryEntry,
1212
IdempotentListener,
1313
IdGenerator,
14+
IdUpsertInput,
1415
IndexDataEntry,
1516
KvId,
1617
KvKey,
@@ -22,6 +23,7 @@ import type {
2223
ParseInputType,
2324
PossibleCollectionOptions,
2425
PrimaryIndexKeys,
26+
PrimaryIndexUpsertInput,
2527
QueueHandlers,
2628
QueueListenerOptions,
2729
QueueMessageHandler,
@@ -33,6 +35,8 @@ import type {
3335
UpdateData,
3436
UpdateManyOptions,
3537
UpdateOptions,
38+
UpsertInput,
39+
UpsertOptions,
3640
WatchOptions,
3741
} from "./types.ts"
3842
import {
@@ -830,6 +834,57 @@ export class Collection<
830834
)
831835
}
832836

837+
async upsert<
838+
const TIndex extends PrimaryIndexKeys<TOutput, TOptions>,
839+
>(
840+
input: UpsertInput<TInput, TOutput, TIndex>,
841+
options?: UpsertOptions,
842+
) {
843+
// Check if is id or primary index upsert
844+
if ((input as any).index !== undefined) {
845+
const inp = input as PrimaryIndexUpsertInput<TInput, TOutput, TIndex>
846+
847+
// First attempt update
848+
const updateCr = await this.updateByPrimaryIndex(
849+
...inp.index,
850+
inp.update,
851+
options,
852+
)
853+
854+
if (updateCr.ok) {
855+
return updateCr
856+
}
857+
858+
// If id is present, set new entry with given id
859+
if (inp.id) {
860+
return await this.set(inp.id, inp.set, {
861+
...options,
862+
overwrite: false,
863+
})
864+
}
865+
866+
// If no id, set new entry with generated id
867+
return await this.add(inp.set, {
868+
...options,
869+
overwrite: false,
870+
})
871+
} else {
872+
// First attempt update
873+
const id = (input as IdUpsertInput<TInput, TOutput>).id
874+
const updateCr = await this.update(id, input.update, options)
875+
876+
if (updateCr.ok) {
877+
return updateCr
878+
}
879+
880+
// Set new entry with given id
881+
return await this.set(id, input.set, {
882+
...options,
883+
overwrite: false,
884+
})
885+
}
886+
}
887+
833888
/**
834889
* Update the value of multiple existing documents in the collection.
835890
*

src/types.ts

+29-2
Original file line numberDiff line numberDiff line change
@@ -351,9 +351,9 @@ export type SetOptions = NonNullable<Parameters<Deno.Kv["set"]>["2"]> & {
351351

352352
export type ListOptions<T> = Deno.KvListOptions & {
353353
/**
354-
* Filter documents based on predicate.
354+
* Filter based on predicate.
355355
*
356-
* @param doc - Document.
356+
* @param value - Input value.
357357
* @returns true or false.
358358
*/
359359
filter?: (value: T) => boolean
@@ -415,6 +415,33 @@ export type QueueListenerOptions = {
415415

416416
export type WatchOptions = NonNullable<Parameters<Deno.Kv["watch"]>[1]>
417417

418+
export type IdUpsertInput<TInput, TOutput extends KvValue> = {
419+
id: KvId
420+
set: ParseInputType<TInput, TOutput>
421+
update: UpdateData<TOutput>
422+
}
423+
424+
export type PrimaryIndexUpsertInput<
425+
TInput,
426+
TOutput extends KvValue,
427+
TIndex,
428+
> = {
429+
id?: KvId
430+
index: [TIndex, CheckKeyOf<TIndex, TOutput>]
431+
set: ParseInputType<TInput, TOutput>
432+
update: UpdateData<TOutput>
433+
}
434+
435+
export type UpsertInput<
436+
TInput,
437+
TOutput extends KvValue,
438+
TIndex,
439+
> =
440+
| IdUpsertInput<TInput, TOutput>
441+
| PrimaryIndexUpsertInput<TInput, TOutput, TIndex>
442+
443+
export type UpsertOptions = UpdateOptions
444+
418445
/********************/
419446
/* */
420447
/* SCHEMA TYPES */

tests/collection/upsert.test.ts

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { assert } from "../deps.ts"
2+
import { mockUser1, mockUser2, mockUser3 } from "../mocks.ts"
3+
import { useDb } from "../utils.ts"
4+
5+
Deno.test("collection - upsert", async (t) => {
6+
await t.step("Should set new doucment entry by id", async () => {
7+
await useDb(async (db) => {
8+
const id = "id"
9+
10+
const cr = await db.users.upsert({
11+
id: id,
12+
set: mockUser2,
13+
update: mockUser3,
14+
})
15+
16+
assert(cr.ok)
17+
18+
const doc = await db.users.find(id)
19+
assert(doc !== null)
20+
assert(doc.value.username === mockUser2.username)
21+
})
22+
})
23+
24+
await t.step("Should update existing document entry by id", async () => {
25+
await useDb(async (db) => {
26+
const id = "id"
27+
28+
const cr1 = await db.users.set(id, mockUser1)
29+
assert(cr1.ok)
30+
31+
const cr2 = await db.users.upsert({
32+
id: id,
33+
set: mockUser2,
34+
update: mockUser3,
35+
})
36+
37+
assert(cr2.ok)
38+
39+
const doc = await db.users.find(id)
40+
assert(doc !== null)
41+
assert(doc.value.username === mockUser3.username)
42+
})
43+
})
44+
})
+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { assert } from "../deps.ts"
2+
import { mockUser1, mockUser2, mockUser3 } from "../mocks.ts"
3+
import { useDb } from "../utils.ts"
4+
5+
Deno.test("indexable_collection - upsert", async (t) => {
6+
await t.step("Should set new doucment entry by id", async () => {
7+
await useDb(async (db) => {
8+
const id = "id"
9+
10+
const cr = await db.i_users.upsert({
11+
id: id,
12+
set: mockUser2,
13+
update: mockUser3,
14+
})
15+
16+
assert(cr.ok)
17+
18+
const doc = await db.i_users.find(id)
19+
assert(doc !== null)
20+
assert(doc.value.username === mockUser2.username)
21+
})
22+
})
23+
24+
await t.step("Should set new doucment entry by index", async () => {
25+
await useDb(async (db) => {
26+
const cr = await db.i_users.upsert({
27+
index: ["username", mockUser1.username],
28+
set: mockUser2,
29+
update: mockUser3,
30+
})
31+
32+
assert(cr.ok)
33+
34+
const doc = await db.i_users.find(cr.id)
35+
assert(doc !== null)
36+
assert(doc.value.username === mockUser2.username)
37+
})
38+
})
39+
40+
await t.step("Should update existing document entry by id", async () => {
41+
await useDb(async (db) => {
42+
const id = "id"
43+
44+
const cr1 = await db.i_users.set(id, mockUser1)
45+
assert(cr1.ok)
46+
47+
const cr2 = await db.i_users.upsert({
48+
id: id,
49+
set: mockUser2,
50+
update: mockUser3,
51+
})
52+
53+
assert(cr2.ok)
54+
55+
const doc = await db.i_users.find(id)
56+
assert(doc !== null)
57+
assert(doc.value.username === mockUser3.username)
58+
})
59+
})
60+
61+
await t.step("Should update existing document entry by index", async () => {
62+
await useDb(async (db) => {
63+
const id = "id"
64+
65+
const cr1 = await db.i_users.set(id, mockUser1)
66+
assert(cr1.ok)
67+
68+
const cr2 = await db.i_users.upsert({
69+
index: ["username", mockUser1.username],
70+
set: mockUser2,
71+
update: mockUser3,
72+
})
73+
74+
assert(cr2.ok)
75+
76+
const doc = await db.i_users.find(id)
77+
assert(doc !== null)
78+
assert(doc.value.username === mockUser3.username)
79+
})
80+
})
81+
})
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { assert } from "../deps.ts"
2+
import { mockUser1, mockUser2, mockUser3 } from "../mocks.ts"
3+
import { useDb } from "../utils.ts"
4+
5+
Deno.test("serialized_collection - upsert", async (t) => {
6+
await t.step("Should set new doucment entry by id", async () => {
7+
await useDb(async (db) => {
8+
const id = "id"
9+
10+
const cr = await db.s_users.upsert({
11+
id: id,
12+
set: mockUser2,
13+
update: mockUser3,
14+
})
15+
16+
assert(cr.ok)
17+
18+
const doc = await db.s_users.find(id)
19+
assert(doc !== null)
20+
assert(doc.value.username === mockUser2.username)
21+
})
22+
})
23+
24+
await t.step("Should update existing document entry by id", async () => {
25+
await useDb(async (db) => {
26+
const id = "id"
27+
28+
const cr1 = await db.s_users.set(id, mockUser1)
29+
assert(cr1.ok)
30+
31+
const cr2 = await db.s_users.upsert({
32+
id: id,
33+
set: mockUser2,
34+
update: mockUser3,
35+
})
36+
37+
assert(cr2.ok)
38+
39+
const doc = await db.s_users.find(id)
40+
assert(doc !== null)
41+
assert(doc.value.username === mockUser3.username)
42+
})
43+
})
44+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { assert } from "../deps.ts"
2+
import { mockUser1, mockUser2, mockUser3 } from "../mocks.ts"
3+
import { useDb } from "../utils.ts"
4+
5+
Deno.test("serialized_indexable_collection - upsert", async (t) => {
6+
await t.step("Should set new doucment entry by id", async () => {
7+
await useDb(async (db) => {
8+
const id = "id"
9+
10+
const cr = await db.is_users.upsert({
11+
id: id,
12+
set: mockUser2,
13+
update: mockUser3,
14+
})
15+
16+
assert(cr.ok)
17+
18+
const doc = await db.is_users.find(id)
19+
assert(doc !== null)
20+
assert(doc.value.username === mockUser2.username)
21+
})
22+
})
23+
24+
await t.step("Should set new doucment entry by index", async () => {
25+
await useDb(async (db) => {
26+
const cr = await db.is_users.upsert({
27+
index: ["username", mockUser1.username],
28+
set: mockUser2,
29+
update: mockUser3,
30+
})
31+
32+
assert(cr.ok)
33+
34+
const doc = await db.is_users.find(cr.id)
35+
assert(doc !== null)
36+
assert(doc.value.username === mockUser2.username)
37+
})
38+
})
39+
40+
await t.step("Should update existing document entry by id", async () => {
41+
await useDb(async (db) => {
42+
const id = "id"
43+
44+
const cr1 = await db.is_users.set(id, mockUser1)
45+
assert(cr1.ok)
46+
47+
const cr2 = await db.is_users.upsert({
48+
id: id,
49+
set: mockUser2,
50+
update: mockUser3,
51+
})
52+
53+
assert(cr2.ok)
54+
55+
const doc = await db.is_users.find(id)
56+
assert(doc !== null)
57+
assert(doc.value.username === mockUser3.username)
58+
})
59+
})
60+
61+
await t.step("Should update existing document entry by index", async () => {
62+
await useDb(async (db) => {
63+
const id = "id"
64+
65+
const cr1 = await db.is_users.set(id, mockUser1)
66+
assert(cr1.ok)
67+
68+
const cr2 = await db.is_users.upsert({
69+
index: ["username", mockUser1.username],
70+
set: mockUser2,
71+
update: mockUser3,
72+
})
73+
74+
assert(cr2.ok)
75+
76+
const doc = await db.is_users.find(id)
77+
assert(doc !== null)
78+
assert(doc.value.username === mockUser3.username)
79+
})
80+
})
81+
})

0 commit comments

Comments
 (0)