Skip to content

Commit

Permalink
Merge pull request #37 from oliver-oloughlin/feature/select
Browse files Browse the repository at this point in the history
Feature/select
  • Loading branch information
oliver-oloughlin authored Jul 19, 2023
2 parents 7ae0915 + 8970cba commit dd9425d
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 6 deletions.
37 changes: 32 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Optional and nullable properties are allowed. If you wish to use Zod, you can
create your Zod object schema and use its type as your model.

```ts
import type { Model } from "https://deno.land/x/[email protected].2/mod.ts"
import type { Model } from "https://deno.land/x/[email protected].3/mod.ts"

interface User extends Model {
username: string
Expand All @@ -35,7 +35,7 @@ The "createDb" function is used for creating a new database instance. It takes a
Deno KV instance and a schema builder function as arguments.

```ts
import { createDb } from "https://deno.land/x/[email protected].2/mod.ts"
import { createDb } from "https://deno.land/x/[email protected].3/mod.ts"

const kv = await Deno.openKv()

Expand Down Expand Up @@ -247,8 +247,8 @@ const { result } = await db.users.getMany({

The "forEach" method is used for executing a callback function for multiple
documents in the KV store. It takes an optional options argument that can be
used for filtering of documents and pagination. If no options are given, "forEach" will execute
the callback function for all documents in the collection.
used for filtering of documents and pagination. If no options are given,
the callback function will be executed for all documents in the collection.

```ts
// Log the username of every user document
Expand All @@ -271,6 +271,33 @@ await db.users.forEach((doc) => console.log(doc.value.username), {
})
```

### Map

The "map" method is used for executing a callback function for multiple documents in the KV store, and retrieving the results.
It takes an optional options argument that can be used for filtering of documents and pagination.
If no options are given, the callback function will be executed for all documents in the collection.

```ts
// Get a list of all the ids of the user documents
const { result } = await db.users.map((doc) => doc.id)

// Get a list of all usernames of users with age > 20
const { result } = await db.users.map((doc) => doc.value.username, {
filter: (doc) => doc.value.age > 20,
})

// Get a list of the usernames of the first 10 users in the KV store
const { result } = await db.users.forEach((doc) => doc.value.username, {
limit: 10,
})

// Get a list of the usernames of the last 10 users in the KV store
const { result } = await db.users.forEach((doc) => doc.value.username, {
limit: 10,
reverse: true
})
```

### Count

The "count" method is used to count the number of documents in a collection.
Expand Down Expand Up @@ -440,7 +467,7 @@ result will be an object containing: id, versionstamp and all the entries in the
document value.

```ts
import { flatten } from "https://deno.land/x/[email protected].2/mod.ts"
import { flatten } from "https://deno.land/x/[email protected].3/mod.ts"

// We assume the document exists in the KV store
const doc = await db.users.find(123n)
Expand Down
57 changes: 56 additions & 1 deletion src/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export class Collection<const T extends KvValue> {

for (const { key, versionstamp, value } of entries) {
const id = getDocumentId(key)

if (
typeof id === "undefined" || versionstamp === null || value === null
) continue
Expand Down Expand Up @@ -325,6 +326,7 @@ export class Collection<const T extends KvValue> {
await this.kv.delete(entry.key)
}
}

return {
cursor: iter.cursor || undefined,
}
Expand Down Expand Up @@ -363,7 +365,9 @@ export class Collection<const T extends KvValue> {
value: entry.value,
}

if (!options?.filter || options.filter(doc)) result.push(doc)
if (!options?.filter || options.filter(doc)) {
result.push(doc)
}
}

return {
Expand Down Expand Up @@ -407,11 +411,62 @@ export class Collection<const T extends KvValue> {

if (!options?.filter || options.filter(doc)) fn(doc)
}

return {
cursor: iter.cursor || undefined,
}
}

/**
* Executes a callback function for every document according to the given options.
*
* If no options are given, the callback function is executed for all documents in the collection.
*
* The results from the callback function are returned as a list.
*
* **Example**
* ```ts
* // Maps from all user documents to a list of all user document ids
* const { result } = await db.users.map(doc => doc.id)
*
* // Maps from users with age > 20 to a list of usernames
* const { result } = await db.users.map(doc => doc.value.username, {
* filter: doc => doc.value.age > 20
* })
* ```
*
* @param fn - Callback function.
* @param options
* @returns A promise that resovles to an object containing a list of the callback results and the iterator cursor
*/
async map<const TMapped>(
fn: (doc: Document<T>) => TMapped,
options?: ListOptions<T>,
) {
const iter = this.kv.list<T>({ prefix: this.keys.idKey }, options)
const result: TMapped[] = []

for await (const entry of iter) {
const id = getDocumentId(entry.key)
if (typeof id === "undefined") continue

const doc: Document<T> = {
id,
versionstamp: entry.versionstamp,
value: entry.value,
}

if (!options?.filter || options.filter(doc)) {
result.push(fn(doc))
}
}

return {
result,
cursor: iter.cursor || undefined,
}
}

/**
* Counts the number of documents in the collection.
*
Expand Down
39 changes: 39 additions & 0 deletions test/tests/collection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,45 @@ Deno.test({
})
})

await t.step("map", async (t) => {
await t.step(
"Should map from all documents to document ids",
async () => {
await reset()

const crs = await db.values.numbers.addMany(1, 2, 3, 4, 5)
assert(crs.every((cr) => cr.ok))

const ids = await db.values.numbers.map((doc) => doc.id)
assert(ids.result.length === 5)
assert(ids.result.every((id) => typeof id === "string"))

const allNums = await db.values.numbers.findMany(ids.result)
assert(allNums.length === 5)
},
)

await t.step(
"Should map from all filtered documents to document ids",
async () => {
await reset()

const crs = await db.values.numbers.addMany(1, 2, 3, 4, 5)
assert(crs.every((cr) => cr.ok))

const ids = await db.values.numbers.map((doc) => doc.id, {
filter: (doc) => doc.value < 3,
})

assert(ids.result.length === 2)
assert(ids.result.every((id) => typeof id === "string"))

const allNums = await db.values.numbers.findMany(ids.result)
assert(allNums.length === 2)
},
)
})

// Perform last reset
await t.step("RESET", async () => await reset())
},
Expand Down
50 changes: 50 additions & 0 deletions test/tests/indexable_collection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,56 @@ Deno.test("indexable_collection", async (t1) => {
})
})

await t1.step("map", async (t) => {
await t.step(
"Should map from all documents to document fields",
async () => {
await reset()

const crs = await db.indexablePeople.addMany(testPerson, testPerson2)
assert(crs.every((cr) => cr.ok))

const names = await db.indexablePeople.map((doc) => doc.value.name)
assert(names.result.length === 2)
assert(names.result.every((id) => typeof id === "string"))

const docsByName = await Promise.all(
names.result.map((name) =>
db.indexablePeople.findByPrimaryIndex("name", name)
),
)
assert(docsByName.length === 2)
assert(docsByName.every((doc) => doc !== null))
},
)

await t.step(
"Should map from all filtered documents to document fields",
async () => {
await reset()

const crs = await db.indexablePeople.addMany(testPerson, testPerson2)
assert(crs.every((cr) => cr.ok))

const names = await db.indexablePeople.map((doc) => doc.value.name, {
filter: (doc) => doc.value.name === testPerson.name,
})

assert(names.result.length === 1)
assert(names.result.every((id) => typeof id === "string"))

const docsByName = await Promise.all(
names.result.map((name) =>
db.indexablePeople.findByPrimaryIndex("name", name)
),
)

assert(docsByName.length === 1)
assert(docsByName.every((doc) => doc !== null))
},
)
})

// Perform last reset
await t1.step("RESET", async () => await reset())
})

0 comments on commit dd9425d

Please sign in to comment.