diff --git a/deno.json b/deno.json index 2f03587..27bee31 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@cross/kv", - "version": "0.0.8", + "version": "0.0.9", "exports": { ".": "./mod.ts" }, diff --git a/src/index.ts b/src/index.ts index 2cb68bc..70c513b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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))) diff --git a/src/key.test.ts b/src/key.test.ts new file mode 100644 index 0000000..fab0e04 --- /dev/null +++ b/src/key.test.ts @@ -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", + ); +}); diff --git a/src/key.ts b/src/key.ts index 309e8f5..92a0246 100644 --- a/src/key.ts +++ b/src/key.ts @@ -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; @@ -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', ); } } diff --git a/src/kv.test.ts b/src/kv.test.ts index e3b607e..518f656 100644 --- a/src/kv.test.ts +++ b/src/kv.test.ts @@ -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();