fastkv 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,13 +6,12 @@
6
6
 
7
7
  FastKV is a key-value store which offers the following features:
8
8
 
9
- - Supports temporary in-memory storage 🕒
10
- - Supports persistent storage with JSON files 📁
11
- - Lightweight with no dependencies ⚡
9
+ - Supports storage in memory and in JSON files
10
+ - Lightweight with zero dependencies
12
11
 
13
12
  ## Installation
14
13
 
15
- Get FastKV via npm using your favourite package manager:
14
+ Get FastKV via your package manager.
16
15
 
17
16
  ```sh
18
17
  npm install fastkv
@@ -26,25 +25,41 @@ bun add fastkv
26
25
 
27
26
  ## Example
28
27
 
29
- ```js
30
- import { KV } from 'fastkv';
28
+ ```ts
29
+ import { KV } from "fastkv";
31
30
 
32
- // For TypeScript users, the KV supports generics:
33
- // const example = new KV<string>();
34
- // Or: example.get<string>('my-key-name');
31
+ // Type safety (optional; for TypeScript)
32
+ interface User {
33
+ name: string;
34
+ age?: number;
35
+ settings?: { theme: string };
36
+ }
35
37
 
36
- const kv = new KV('./db.json'); // Save the data. Path resolves with process.cwd()
37
- const cache = new KV('::memory::'); // Keep the data in the system's memory.
38
+ const kv = new KV<User>({ path: "./db.json" }); // Persistent storage
39
+ const cache = new KV<User>(); // In-memory store (no path provided)
38
40
 
39
- // Set data
40
- kv.set('userSettings', { theme: 'dark' });
41
+ // Set or overwrite data
42
+ await kv.set("user1", { name: "Alice", age: 25 });
41
43
 
42
- // Retreive data by a key
43
- kv.get('userSettings'); // -> { theme: 'dark' }
44
+ // Get data by key
45
+ const user = await kv.get("user1"); // -> { __id: ..., key: 'user1', value: { name: 'Alice', age: 25 } }
44
46
 
45
- // Retreive all data
46
- kv.all(); // -> [{ key: 'userSettings', value: { theme: 'dark' } }]
47
+ // Get nested values
48
+ const theme = await kv.get("user1.settings.theme"); // -> 'dark'
47
49
 
48
- // Clear the store
49
- kv.clear();
50
+ // Upsert (update or create)
51
+ await kv.upsert({
52
+ where: { name: "Alice" }, // filter
53
+ create: { name: "Alice", age: 25, settings: { theme: "dark" } }, // if not exists
54
+ update: { age: 26 }, // partial update if exists
55
+ });
56
+
57
+ // Find entries by key or value
58
+ const usersNamedAlice = await kv.find({ where: "Alice", in: "value" });
59
+
60
+ // Get all entries
61
+ const allEntries = await kv.all();
62
+
63
+ // Clear all entries
64
+ await kv.clear();
50
65
  ```
package/dist/index.d.mts CHANGED
@@ -1,3 +1,7 @@
1
+ interface KVOptions {
2
+ path?: string;
3
+ prettify?: boolean;
4
+ }
1
5
  type Entry<T = any> = {
2
6
  __id: string;
3
7
  key: string;
@@ -9,10 +13,7 @@ type DeepPartial<T> = {
9
13
  declare class KV<T = any> {
10
14
  #private;
11
15
  path?: string;
12
- constructor(options?: {
13
- path?: string;
14
- prettify?: boolean;
15
- });
16
+ constructor(options?: KVOptions);
16
17
  set(key: string, value: T): Promise<Entry<T>>;
17
18
  upsert(options: {
18
19
  where: DeepPartial<T> | string;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,7 @@
1
+ interface KVOptions {
2
+ path?: string;
3
+ prettify?: boolean;
4
+ }
1
5
  type Entry<T = any> = {
2
6
  __id: string;
3
7
  key: string;
@@ -9,10 +13,7 @@ type DeepPartial<T> = {
9
13
  declare class KV<T = any> {
10
14
  #private;
11
15
  path?: string;
12
- constructor(options?: {
13
- path?: string;
14
- prettify?: boolean;
15
- });
16
+ constructor(options?: KVOptions);
16
17
  set(key: string, value: T): Promise<Entry<T>>;
17
18
  upsert(options: {
18
19
  where: DeepPartial<T> | string;
package/dist/index.js CHANGED
@@ -42,6 +42,10 @@ var KV = class {
42
42
  #data = [];
43
43
  #ready;
44
44
  #prettify;
45
+ /**
46
+ * Create a new KV instance
47
+ * @param options Configuration options
48
+ */
45
49
  constructor(options) {
46
50
  if (options?.prettify) {
47
51
  this.#prettify = options?.prettify;
@@ -55,15 +59,25 @@ var KV = class {
55
59
  this.#ready = Promise.resolve();
56
60
  }
57
61
  }
62
+ /**
63
+ * Load stored data from disk into memory, create a new empty store if file doesn't exist
64
+ */
58
65
  async #loadFromFile() {
59
66
  try {
60
67
  const content = await import_promises.default.readFile(this.path, "utf8");
61
68
  this.#data = JSON.parse(content);
62
- } catch {
63
- this.#data = [];
64
- await this.#saveToFile();
69
+ } catch (error) {
70
+ if (error.code === "ENOENT") {
71
+ this.#data = [];
72
+ await this.#saveToFile();
73
+ } else {
74
+ throw new Error("Failed to load database file: " + error.message);
75
+ }
65
76
  }
66
77
  }
78
+ /**
79
+ * Save in-memory data to disk if file path is provided
80
+ */
67
81
  async #saveToFile() {
68
82
  if (!this.path) return;
69
83
  await import_promises.default.writeFile(
@@ -72,17 +86,37 @@ var KV = class {
72
86
  "utf8"
73
87
  );
74
88
  }
89
+ /**
90
+ * Resolves dot-notation paths within objects
91
+ * @param object Base object
92
+ * @param path Dot-separated path string
93
+ * @returns Resolved value, or undefined
94
+ */
75
95
  #resolvePath(object, path2) {
76
96
  return path2.split(".").reduce((acc, part) => acc ? acc[part] : void 0, object);
77
97
  }
98
+ /**
99
+ * Compare a value against a partial filter object
100
+ * @param value Actual stored value
101
+ * @param where Partial value to match against
102
+ * @returns
103
+ */
78
104
  #matches(value, where) {
105
+ if (where === null) return value === null;
79
106
  if (typeof value !== typeof where) return false;
80
107
  if (typeof value === "object" && value !== null && where !== null) {
81
108
  return Object.entries(where).every(([k, v]) => this.#matches(value[k], v));
82
109
  }
83
110
  return value === where;
84
111
  }
112
+ /**
113
+ * Set a value by key, creates a new entry or overrides existing one
114
+ * @param key Unique identifier
115
+ * @param value Data to store
116
+ * @returns
117
+ */
85
118
  async set(key, value) {
119
+ if (!key) throw new Error("Key cannot be empty");
86
120
  await this.#ready;
87
121
  const entry = this.#data.find((entry2) => entry2.key === key);
88
122
  if (entry) {
@@ -95,7 +129,17 @@ var KV = class {
95
129
  await this.#saveToFile();
96
130
  return newEntry;
97
131
  }
132
+ /**
133
+ * Update an existing entry or create a new one if not found
134
+ * @param options Upserting options
135
+ */
98
136
  async upsert(options) {
137
+ if (!options.create && !options.update) {
138
+ throw new Error("Upsert requires at least create or update to be provided");
139
+ }
140
+ if (typeof options.where === "string" && !options.where) {
141
+ throw new Error("Upsert key cannot be empty");
142
+ }
99
143
  await this.#ready;
100
144
  let entry;
101
145
  if (typeof options.where === "string") {
@@ -109,13 +153,25 @@ var KV = class {
109
153
  }
110
154
  } else {
111
155
  const key = typeof options.where === "string" ? options.where : (0, import_node_crypto.randomUUID)();
112
- entry = { __id: (0, import_node_crypto.randomUUID)(), key, value: { ...options.create ?? {} } };
156
+ const baseValue = options.create ?? options.update ?? {};
157
+ entry = {
158
+ __id: (0, import_node_crypto.randomUUID)(),
159
+ key,
160
+ value: { ...baseValue }
161
+ };
113
162
  this.#data.push(entry);
114
163
  }
115
164
  await this.#saveToFile();
116
165
  return entry;
117
166
  }
167
+ /**
168
+ * Find entries by matching key or value
169
+ * @param options Search options
170
+ */
118
171
  async find(options) {
172
+ if (options.in && options.in !== "key" && options.in !== "value") {
173
+ throw new Error(`Invalid search field: ${options.in}`);
174
+ }
119
175
  await this.#ready;
120
176
  const field = options.in ?? "value";
121
177
  const where = options.where;
@@ -126,7 +182,12 @@ var KV = class {
126
182
  return this.#matches(entry.value, where);
127
183
  });
128
184
  }
185
+ /**
186
+ * Get an entry, supports nested values with dot notation
187
+ * @param query Key or path via dot notation
188
+ */
129
189
  async get(query) {
190
+ if (!query) throw new Error("Query string cannot be empty");
130
191
  await this.#ready;
131
192
  const [baseKey, ...rest] = query.split(".");
132
193
  const entry = this.#data.find((entry2) => entry2.key === baseKey);
@@ -134,10 +195,16 @@ var KV = class {
134
195
  if (rest.length === 0) return entry;
135
196
  return this.#resolvePath({ value: entry.value }, rest.join(".")) ?? null;
136
197
  }
198
+ /**
199
+ * Get all stored entries
200
+ */
137
201
  async all() {
138
202
  await this.#ready;
139
203
  return [...this.#data];
140
204
  }
205
+ /**
206
+ * Remove all entries
207
+ */
141
208
  async clear() {
142
209
  await this.#ready;
143
210
  this.#data = [];
package/dist/index.mjs CHANGED
@@ -7,6 +7,10 @@ var KV = class {
7
7
  #data = [];
8
8
  #ready;
9
9
  #prettify;
10
+ /**
11
+ * Create a new KV instance
12
+ * @param options Configuration options
13
+ */
10
14
  constructor(options) {
11
15
  if (options?.prettify) {
12
16
  this.#prettify = options?.prettify;
@@ -20,15 +24,25 @@ var KV = class {
20
24
  this.#ready = Promise.resolve();
21
25
  }
22
26
  }
27
+ /**
28
+ * Load stored data from disk into memory, create a new empty store if file doesn't exist
29
+ */
23
30
  async #loadFromFile() {
24
31
  try {
25
32
  const content = await fs.readFile(this.path, "utf8");
26
33
  this.#data = JSON.parse(content);
27
- } catch {
28
- this.#data = [];
29
- await this.#saveToFile();
34
+ } catch (error) {
35
+ if (error.code === "ENOENT") {
36
+ this.#data = [];
37
+ await this.#saveToFile();
38
+ } else {
39
+ throw new Error("Failed to load database file: " + error.message);
40
+ }
30
41
  }
31
42
  }
43
+ /**
44
+ * Save in-memory data to disk if file path is provided
45
+ */
32
46
  async #saveToFile() {
33
47
  if (!this.path) return;
34
48
  await fs.writeFile(
@@ -37,17 +51,37 @@ var KV = class {
37
51
  "utf8"
38
52
  );
39
53
  }
54
+ /**
55
+ * Resolves dot-notation paths within objects
56
+ * @param object Base object
57
+ * @param path Dot-separated path string
58
+ * @returns Resolved value, or undefined
59
+ */
40
60
  #resolvePath(object, path2) {
41
61
  return path2.split(".").reduce((acc, part) => acc ? acc[part] : void 0, object);
42
62
  }
63
+ /**
64
+ * Compare a value against a partial filter object
65
+ * @param value Actual stored value
66
+ * @param where Partial value to match against
67
+ * @returns
68
+ */
43
69
  #matches(value, where) {
70
+ if (where === null) return value === null;
44
71
  if (typeof value !== typeof where) return false;
45
72
  if (typeof value === "object" && value !== null && where !== null) {
46
73
  return Object.entries(where).every(([k, v]) => this.#matches(value[k], v));
47
74
  }
48
75
  return value === where;
49
76
  }
77
+ /**
78
+ * Set a value by key, creates a new entry or overrides existing one
79
+ * @param key Unique identifier
80
+ * @param value Data to store
81
+ * @returns
82
+ */
50
83
  async set(key, value) {
84
+ if (!key) throw new Error("Key cannot be empty");
51
85
  await this.#ready;
52
86
  const entry = this.#data.find((entry2) => entry2.key === key);
53
87
  if (entry) {
@@ -60,7 +94,17 @@ var KV = class {
60
94
  await this.#saveToFile();
61
95
  return newEntry;
62
96
  }
97
+ /**
98
+ * Update an existing entry or create a new one if not found
99
+ * @param options Upserting options
100
+ */
63
101
  async upsert(options) {
102
+ if (!options.create && !options.update) {
103
+ throw new Error("Upsert requires at least create or update to be provided");
104
+ }
105
+ if (typeof options.where === "string" && !options.where) {
106
+ throw new Error("Upsert key cannot be empty");
107
+ }
64
108
  await this.#ready;
65
109
  let entry;
66
110
  if (typeof options.where === "string") {
@@ -74,13 +118,25 @@ var KV = class {
74
118
  }
75
119
  } else {
76
120
  const key = typeof options.where === "string" ? options.where : randomUUID();
77
- entry = { __id: randomUUID(), key, value: { ...options.create ?? {} } };
121
+ const baseValue = options.create ?? options.update ?? {};
122
+ entry = {
123
+ __id: randomUUID(),
124
+ key,
125
+ value: { ...baseValue }
126
+ };
78
127
  this.#data.push(entry);
79
128
  }
80
129
  await this.#saveToFile();
81
130
  return entry;
82
131
  }
132
+ /**
133
+ * Find entries by matching key or value
134
+ * @param options Search options
135
+ */
83
136
  async find(options) {
137
+ if (options.in && options.in !== "key" && options.in !== "value") {
138
+ throw new Error(`Invalid search field: ${options.in}`);
139
+ }
84
140
  await this.#ready;
85
141
  const field = options.in ?? "value";
86
142
  const where = options.where;
@@ -91,7 +147,12 @@ var KV = class {
91
147
  return this.#matches(entry.value, where);
92
148
  });
93
149
  }
150
+ /**
151
+ * Get an entry, supports nested values with dot notation
152
+ * @param query Key or path via dot notation
153
+ */
94
154
  async get(query) {
155
+ if (!query) throw new Error("Query string cannot be empty");
95
156
  await this.#ready;
96
157
  const [baseKey, ...rest] = query.split(".");
97
158
  const entry = this.#data.find((entry2) => entry2.key === baseKey);
@@ -99,10 +160,16 @@ var KV = class {
99
160
  if (rest.length === 0) return entry;
100
161
  return this.#resolvePath({ value: entry.value }, rest.join(".")) ?? null;
101
162
  }
163
+ /**
164
+ * Get all stored entries
165
+ */
102
166
  async all() {
103
167
  await this.#ready;
104
168
  return [...this.#data];
105
169
  }
170
+ /**
171
+ * Remove all entries
172
+ */
106
173
  async clear() {
107
174
  await this.#ready;
108
175
  this.#data = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastkv",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -9,12 +9,12 @@
9
9
  "package.json",
10
10
  "README.md"
11
11
  ],
12
- "description": "A key-value store, helpful for caches in apps.",
12
+ "description": "A simple key-value store with optional JSON file storing",
13
13
  "main": "./dist/index.js",
14
14
  "module": "./dist/index.mjs",
15
15
  "types": "./dist/index.d.ts",
16
16
  "repository": {
17
- "url": "https://github.com/m1-dev/fastkv"
17
+ "url": "https://github.com/e60m5ss/fastkv"
18
18
  },
19
19
  "keywords": [
20
20
  "kv",