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 +34 -19
- package/dist/index.d.mts +5 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +71 -4
- package/dist/index.mjs +71 -4
- package/package.json +3 -3
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
|
-
-
|
|
10
|
-
-
|
|
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
|
|
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
|
-
```
|
|
30
|
-
import { KV } from
|
|
28
|
+
```ts
|
|
29
|
+
import { KV } from "fastkv";
|
|
31
30
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
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(
|
|
37
|
-
const cache = new KV(
|
|
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(
|
|
41
|
+
// Set or overwrite data
|
|
42
|
+
await kv.set("user1", { name: "Alice", age: 25 });
|
|
41
43
|
|
|
42
|
-
//
|
|
43
|
-
kv.get(
|
|
44
|
+
// Get data by key
|
|
45
|
+
const user = await kv.get("user1"); // -> { __id: ..., key: 'user1', value: { name: 'Alice', age: 25 } }
|
|
44
46
|
|
|
45
|
-
//
|
|
46
|
-
kv.
|
|
47
|
+
// Get nested values
|
|
48
|
+
const theme = await kv.get("user1.settings.theme"); // -> 'dark'
|
|
47
49
|
|
|
48
|
-
//
|
|
49
|
-
kv.
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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/
|
|
17
|
+
"url": "https://github.com/e60m5ss/fastkv"
|
|
18
18
|
},
|
|
19
19
|
"keywords": [
|
|
20
20
|
"kv",
|