Skip to content

Commit

Permalink
Support partial and empty ranges.
Browse files Browse the repository at this point in the history
  • Loading branch information
Hexagon committed May 8, 2024
1 parent 0ed482b commit 1fd34ec
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 7 deletions.
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cross/kv",
"version": "0.0.8",
"version": "0.0.9",
"exports": {
".": "./mod.ts"
},
Expand Down
10 changes: 7 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,21 @@ export class KVIndex {
recurse(childNode, keyIndex + 1);
}
} else if (
typeof keyPart === "object" &&
(keyPart.from !== undefined || keyPart.to !== undefined)
typeof keyPart === "object"
) {
// Key range
const range = keyPart as KVKeyRange;

// Key range
for (const [index, childNode] of node.children.entries()) {
// Iterate over children, comparing the index to the range
if (
// Shortcut for empty key = all
(range.from === undefined && range.to === undefined) ||
// String comparison
(typeof index === "string" &&
(range.from === undefined || index >= (range.from as string)) &&
(range.to === undefined || index <= (range.to as string))) ||
// Number comparison
(typeof index === "number" &&
(range.from === undefined || index >= (range.from as number)) &&
(range.to === undefined || index <= (range.to as number)))
Expand Down
75 changes: 75 additions & 0 deletions src/key.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { assertEquals, assertThrows } from "@std/assert";
import { test } from "@cross/test";
import { KVKey /* ... */ } from "./key.ts";

test("KVKey: constructs with valid string key", () => {
const key = new KVKey(["users", "user123"]);
assertEquals(key.get(), ["users", "user123"]);
});

test("KVKey: throws with invalid string key (colon)", () => {
assertThrows(
() => new KVKey(["users", ":lol"]),
TypeError,
"String elements in the key can only contain",
);
});

test("KVKey: throws with invalid string key (space)", () => {
assertThrows(
() => new KVKey(["users", "l ol"]),
TypeError,
"String elements in the key can only contain",
);
});

test("KVKey: constructs with valid number key", () => {
const key = new KVKey(["data", 42]);
assertEquals(key.get(), ["data", 42]);
});

test("KVKey: returns correct string representation", () => {
const key = new KVKey(["users", "data", "user123"]);
assertEquals(key.getKeyRepresentation(), "users.data.user123");
});

test("KVKey: constructs with valid range", async () => {
const key = new KVKey(["users", { from: "user001", to: "user999" }], true);
assertEquals(key.get(), ["users", { from: "user001", to: "user999" }]);
});

test("KVKey: constructs with valid range (only from)", () => {
const key = new KVKey(["users", { from: "user001" }], true);
assertEquals(key.get(), ["users", { from: "user001" }]);
});

test("KVKey: constructs with valid range (only to)", () => {
const key = new KVKey(["users", { to: "user001" }], true);
assertEquals(key.get(), ["users", { to: "user001" }]);
});

test("KVKey: constructs with valid range (all)", () => {
const key = new KVKey(["users", {}], true);
assertEquals(key.get(), ["users", {}]);
});

test("KVKey: constructs with invalid range (extra property)", () => {
assertThrows(
// @ts-expect-error test unknown property
() => new KVKey(["users", { test: 1 }], true),
TypeError,
"Ranges must have only",
);
});

test("KVKey: throws on empty key", () => {
assertThrows(() => new KVKey([]), TypeError, "Key cannot be empty");
});

test("KVKey: only allows string keys as first entry", () => {
assertThrows(
() => new KVKey([123121]),
TypeError,
"First index of the key must be a string",
);
});
15 changes: 12 additions & 3 deletions src/key.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const KV_KEY_ALLOWED_CHARS = /^[a-zA-Z0-9\-_]+$/;
export const KV_KEY_ALLOWED_CHARS = /^[a-zA-Z0-9\-_@]+$/;

export interface KVKeyRange {
from?: string | number;
Expand Down Expand Up @@ -39,9 +39,18 @@ export class KVKey {
}

if (typeof element === "object") {
if (!(element.from || element.to)) {
const allowedKeys = ["from", "to"];
const elementKeys = Object.keys(element);

// Check for empty object
if (elementKeys.length === 0) {
return; // Allow an empty object
}

// Check for additional keys
if (!elementKeys.every((key) => allowedKeys.includes(key))) {
throw new TypeError(
'Ranges must have one or both of "from" and "to"',
'Ranges must have only "from" and/or "to" keys',
);
}
}
Expand Down
45 changes: 45 additions & 0 deletions src/kv.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,51 @@ test("KV: supports numeric key ranges", async () => {
assertEquals(rangeResults[2].data, "Value 9");
});

test("KV: supports additional levels after numeric key ranges", async () => {
const tempFilePrefix = await tempfile();
const kvStore = new KV();
await kvStore.open(tempFilePrefix);

// Set some values within a range
for (let i = 5; i <= 10; i++) {
await kvStore.set(["data", i, "doc1"], `Value ${i} in doc1`);
await kvStore.set(["data", i, "doc2"], `Value ${i} in doc2`);
}

// Test if the 'get' function returns the expected values
const rangeResults = await kvStore.getMany([
"data",
{ from: 7, to: 9 },
"doc1",
]);
assertEquals(rangeResults.length, 3);
assertEquals(rangeResults[0].data, "Value 7 in doc1");
assertEquals(rangeResults[1].data, "Value 8 in doc1");
assertEquals(rangeResults[2].data, "Value 9 in doc1");
});

test("KV: supports empty numeric key ranges to get all", async () => {
const tempFilePrefix = await tempfile();
const kvStore = new KV();
await kvStore.open(tempFilePrefix);

// Set some values within a range
for (let i = 5; i <= 10; i++) {
await kvStore.set(["data", i, "doc1"], `Value ${i} in doc1`);
await kvStore.set(["data", i, "doc2"], `Value ${i} in doc2`);
}

// Test if the 'get' function returns the expected values
const rangeResults = await kvStore.getMany(["data", {}, "doc1"]);
assertEquals(rangeResults.length, 6);
const rangeResults2 = await kvStore.getMany(["data", {}, "doc2"]);
assertEquals(rangeResults2.length, 6);
const rangeResults3 = await kvStore.getMany(["data", {}]);
assertEquals(rangeResults3.length, 12);
const rangeResults4 = await kvStore.getMany(["data"]);
assertEquals(rangeResults4.length, 12);
});

test("KV: supports string key ranges", async () => {
const tempFilePrefix = await tempfile();
const kvStore = new KV();
Expand Down

0 comments on commit 1fd34ec

Please sign in to comment.