polystore 0.15.11 → 0.15.13
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/package.json +2 -1
- package/readme.md +133 -16
- package/src/clients/api.js +5 -1
- package/src/server.js +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polystore",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.13",
|
|
4
4
|
"description": "A small compatibility layer for many popular KV stores like localStorage, Redis, FileSystem, etc.",
|
|
5
5
|
"homepage": "https://polystore.dev/",
|
|
6
6
|
"repository": "https://github.com/franciscop/polystore.git",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
}
|
|
57
57
|
},
|
|
58
58
|
"jest": {
|
|
59
|
+
"testTimeout": 15000,
|
|
59
60
|
"testEnvironment": "jsdom",
|
|
60
61
|
"setupFiles": [
|
|
61
62
|
"./test/setup.js"
|
package/readme.md
CHANGED
|
@@ -36,8 +36,8 @@ Available clients for the KV store:
|
|
|
36
36
|
- [**Cookies** `"cookie"`](#cookies) (fe): persist the data using cookies
|
|
37
37
|
- [**LocalForage** `localForage`](#local-forage) (fe): persist the data on IndexedDB
|
|
38
38
|
- [**Fetch API** `"https://..."`](#fetch-api) (fe+be): call an API to save/retrieve the data
|
|
39
|
-
- [**File** `
|
|
40
|
-
- [**Folder** `
|
|
39
|
+
- [**File** `"file:///[...].json"`](#file) (be): store the data in a single JSON file in your FS
|
|
40
|
+
- [**Folder** `"file:///[...]/"`](#folder) (be): store each key in a folder as json files
|
|
41
41
|
- [**Redis Client** `redisClient`](#redis-client) (be): use the Redis instance that you connect to
|
|
42
42
|
- [**Cloudflare KV** `env.KV_NAMESPACE`](#cloudflare-kv) (be): use Cloudflare's KV store
|
|
43
43
|
- [**Level** `new Level('example', { valueEncoding: 'json' })`](#level) (fe+be): support the whole Level ecosystem
|
|
@@ -435,8 +435,8 @@ const store = kv(new Map());
|
|
|
435
435
|
const session = kv(new Map());
|
|
436
436
|
|
|
437
437
|
// Two file-stores
|
|
438
|
-
const users = kv(
|
|
439
|
-
const books = kv(
|
|
438
|
+
const users = kv(`file://${import.meta.dirname}/users.json`);
|
|
439
|
+
const books = kv(`file://${import.meta.dirname}/books.json`);
|
|
440
440
|
```
|
|
441
441
|
|
|
442
442
|
The main reason this is not stable is because [_some_ store engines don't allow for atomic deletion of keys given a prefix](https://stackoverflow.com/q/4006324/938236). While we do still clear them internally in those cases, that is a non-atomic operation and it could have some trouble if some other thread is reading/writing the data _at the same time_.
|
|
@@ -646,32 +646,39 @@ console.log(await store.get("key1"));
|
|
|
646
646
|
|
|
647
647
|
> Note: the API client expire resolution is in the seconds, so times shorter than 1 second like `expires: 0.02` (20 ms) don't make sense for this storage method and won't properly save them.
|
|
648
648
|
|
|
649
|
-
> Note: see the reference implementation in src/server.js
|
|
649
|
+
> Note: see the [reference implementation in src/server.js](https://github.com/franciscop/polystore/blob/master/src/server.js)
|
|
650
650
|
|
|
651
651
|
|
|
652
652
|
### File
|
|
653
653
|
|
|
654
|
-
Treat a JSON file in your filesystem as the source for the KV store:
|
|
654
|
+
Treat a JSON file in your filesystem as the source for the KV store. Pass it an absolute `file://` url or a `new URL('file://...')` instance:
|
|
655
655
|
|
|
656
656
|
```js
|
|
657
657
|
import kv from "polystore";
|
|
658
658
|
|
|
659
|
-
|
|
659
|
+
// Path is "/Users/me/project/cache.json"
|
|
660
|
+
const store = kv("file:///Users/me/project/cache.json");
|
|
660
661
|
|
|
661
662
|
await store.set("key1", "Hello world", { expires: "1h" });
|
|
662
663
|
console.log(await store.get("key1"));
|
|
663
664
|
// "Hello world"
|
|
664
665
|
```
|
|
665
666
|
|
|
666
|
-
> Note: an extension is needed, to disambiguate with "folder"
|
|
667
|
+
> Note: an extension is needed, to disambiguate with ["folder"](#folder)
|
|
667
668
|
|
|
668
669
|
You can also create multiple stores:
|
|
669
670
|
|
|
670
671
|
```js
|
|
671
672
|
// Paths need to be absolute, but you can use process.cwd() to make
|
|
672
673
|
// it relative to the current process:
|
|
674
|
+
const store1 = kv(`file://${process.cwd()}/cache.json`);
|
|
675
|
+
const store2 = kv(`file://${import.meta.dirname}/data.json`);
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
You can also pass a `URL` instance:
|
|
679
|
+
|
|
680
|
+
```js
|
|
673
681
|
const store1 = kv(new URL(`file://${process.cwd()}/cache.json`));
|
|
674
|
-
const store2 = kv(new URL(`file://${import.meta.dirname}/data.json`));
|
|
675
682
|
```
|
|
676
683
|
|
|
677
684
|
<details>
|
|
@@ -705,7 +712,7 @@ Treat a single folder in your filesystem as the store, where each key is a file:
|
|
|
705
712
|
```js
|
|
706
713
|
import kv from "polystore";
|
|
707
714
|
|
|
708
|
-
const store = kv(
|
|
715
|
+
const store = kv("file:///Users/me/project/data/");
|
|
709
716
|
|
|
710
717
|
await store.set("key1", "Hello world", { expires: "1h" });
|
|
711
718
|
// Writes "./data/key1.json"
|
|
@@ -713,19 +720,26 @@ console.log(await store.get("key1"));
|
|
|
713
720
|
// "Hello world"
|
|
714
721
|
```
|
|
715
722
|
|
|
716
|
-
> Note: the ending slash `/` is needed, to disambiguate with "file"
|
|
723
|
+
> Note: the ending slash `/` is needed, to disambiguate with ["file"](#file)
|
|
717
724
|
|
|
718
725
|
You can also create multiple stores:
|
|
719
726
|
|
|
720
727
|
```js
|
|
721
728
|
// Paths need to be absolute, but you can use `process.cwd()` to make
|
|
722
729
|
// it relative to the current process, or `import.meta.dirname`:
|
|
723
|
-
const store1 = kv(
|
|
724
|
-
const store2 = kv(
|
|
730
|
+
const store1 = kv(`file://${process.cwd()}/cache/`);
|
|
731
|
+
const store2 = kv(`file://${import.meta.dirname}/data/`);
|
|
725
732
|
```
|
|
726
733
|
|
|
727
734
|
The folder is created if it doesn't exist. When a key is deleted, the corresponding file is also deleted. The data is serialized as JSON, with a meta wrapper to store the expiration date.
|
|
728
735
|
|
|
736
|
+
|
|
737
|
+
You can also pass a `URL` instance:
|
|
738
|
+
|
|
739
|
+
```js
|
|
740
|
+
const store1 = kv(new URL(`file://${process.cwd()}/cache/`));
|
|
741
|
+
```
|
|
742
|
+
|
|
729
743
|
<details>
|
|
730
744
|
<summary>Why use polystore with a folder?</summary>
|
|
731
745
|
<p>These benefits are for wrapping a folder with polystore:</p>
|
|
@@ -767,6 +781,8 @@ export default {
|
|
|
767
781
|
};
|
|
768
782
|
```
|
|
769
783
|
|
|
784
|
+
It expects that you pass the namespace from Cloudflare straight as a `kv()` argument. This is unfortunately not available outside of the `fetch()` method.
|
|
785
|
+
|
|
770
786
|
<details>
|
|
771
787
|
<summary>Why use polystore with Cloudflare's KV?</summary>
|
|
772
788
|
<p>These benefits are for wrapping Cloudflare's KV with polystore:</p>
|
|
@@ -804,6 +820,8 @@ console.log(await store.get("key1"));
|
|
|
804
820
|
// "Hello world"
|
|
805
821
|
```
|
|
806
822
|
|
|
823
|
+
You will need to set the `valueEncoding` to `"json"` for the store to work as expected.
|
|
824
|
+
|
|
807
825
|
<details>
|
|
808
826
|
<summary>Why use polystore with Level?</summary>
|
|
809
827
|
<p>These benefits are for wrapping Level with polystore:</p>
|
|
@@ -835,6 +853,8 @@ console.log(await store.get("key1"));
|
|
|
835
853
|
// "Hello world"
|
|
836
854
|
```
|
|
837
855
|
|
|
856
|
+
You'll need to be running the etcd store for this to work as expected.
|
|
857
|
+
|
|
838
858
|
<details>
|
|
839
859
|
<summary>Why use polystore with Etcd?</summary>
|
|
840
860
|
<p>These benefits are for wrapping Etcd with polystore:</p>
|
|
@@ -938,7 +958,7 @@ class MyClient {
|
|
|
938
958
|
|
|
939
959
|
Note that this is NOT the public API, it's the internal **client** API. It's simpler than the public API since we do some of the heavy lifting as an intermediate layer (e.g. for the client, the `expires` will always be a `null` or `number`, never `undefined` or a `string`), but also it differs from polystore's public API, like `.add()` has a different signature, and the group methods all take a explicit prefix.
|
|
940
960
|
|
|
941
|
-
**Expires**: if you set the `EXPIRES = true`, then you are indicating that the client WILL manage the lifecycle of the data. This includes all methods, for example if an item is expired, then its key should not be returned in `.keys()`, it's value should not be returned in `.values()`, and the method `.has()` will return `false`. The good news is that you will always receive the option `expires`, which is either `null` (no expiration) or a `number` indicating the
|
|
961
|
+
**Expires**: if you set the `EXPIRES = true`, then you are indicating that the client WILL manage the lifecycle of the data. This includes all methods, for example if an item is expired, then its key should not be returned in `.keys()`, it's value should not be returned in `.values()`, and the method `.has()` will return `false`. The good news is that you will always receive the option `expires`, which is either `null` (no expiration) or a `number` indicating the **seconds** for the key/value to will expire.
|
|
942
962
|
|
|
943
963
|
**Prefix**: we manage the `prefix` as an invisible layer on top, you only need to be aware of it in the `.add()` method, as well as in the group methods:
|
|
944
964
|
|
|
@@ -966,7 +986,7 @@ client.keys = (prefix) => {
|
|
|
966
986
|
|
|
967
987
|
While the signatures are different, you can check each entries on the output of Polystore API to see what is expected for the methods of the client to do, e.g. `.clear()` will remove all of the items that match the prefix (or everything if there's no prefix).
|
|
968
988
|
|
|
969
|
-
|
|
989
|
+
### Example: Plain Object client
|
|
970
990
|
|
|
971
991
|
This is a good example of how simple a store can be, however do not use it literally since it behaves the same as the already-supported `new Map()`, only use it as the base for your own clients:
|
|
972
992
|
|
|
@@ -996,7 +1016,7 @@ class MyClient {
|
|
|
996
1016
|
|
|
997
1017
|
We don't set `EXPIRES` to true since plain objects do NOT support expiration natively. So by not adding the `EXPIRES` property, it's the same as setting it to `false`, and polystore will manage all the expirations as a layer on top of the data. We could be more explicit and set it to `EXPIRES = false`, but it's not needed in this case.
|
|
998
1018
|
|
|
999
|
-
|
|
1019
|
+
### Example: custom ID generation
|
|
1000
1020
|
|
|
1001
1021
|
You might want to provide your custom key generation algorithm, which I'm going to call `customId()` for example purposes. The only place where `polystore` generates IDs is in `add`, so you can provide your client with a custom generator:
|
|
1002
1022
|
|
|
@@ -1028,3 +1048,100 @@ const id = await store.add({ hello: "world" });
|
|
|
1028
1048
|
const id2 = await store.prefix("hello:").add({ hello: "world" });
|
|
1029
1049
|
// this is `hello:{your own custom id}`
|
|
1030
1050
|
```
|
|
1051
|
+
|
|
1052
|
+
### Example: serializing the data
|
|
1053
|
+
|
|
1054
|
+
If you need to serialize the data before storing it, you can do it within your custom client. Here's an example of how you can handle data serialization when setting values:
|
|
1055
|
+
|
|
1056
|
+
```js
|
|
1057
|
+
class MyClient {
|
|
1058
|
+
get(key) {
|
|
1059
|
+
const data = dataSource[key];
|
|
1060
|
+
return data ? JSON.parse(data) : null;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
set(key, value) {
|
|
1064
|
+
dataSource[key] = JSON.stringify(value);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
*iterate(prefix) {
|
|
1068
|
+
for (const [key, value] of Object.entries(dataSource)) {
|
|
1069
|
+
if (key.startsWith(prefix) && value) {
|
|
1070
|
+
yield [key, JSON.parse(value)];
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
```
|
|
1076
|
+
|
|
1077
|
+
### Example: Cloudflare API calls
|
|
1078
|
+
|
|
1079
|
+
In this example on one of my projects, I needed to use Cloudflare's REST API since I didn't have access to any KV store I was happy with on Netlify's Edge Functions. So I created it like this:
|
|
1080
|
+
|
|
1081
|
+
> Warning: this code snippet is an experimental example and hasn't gone through rigurous testing as the rest of the library, so please treat with caution.
|
|
1082
|
+
|
|
1083
|
+
```js
|
|
1084
|
+
const {
|
|
1085
|
+
CLOUDFLARE_ACCOUNT,
|
|
1086
|
+
CLOUDFLARE_NAMESPACE,
|
|
1087
|
+
CLOUDFLARE_EMAIL,
|
|
1088
|
+
CLOUDFLARE_API_KEY,
|
|
1089
|
+
} = process.env;
|
|
1090
|
+
|
|
1091
|
+
const baseUrl = `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT}/storage/kv/namespaces/${CLOUDFLARE_NAMESPACE}`;
|
|
1092
|
+
const headers = {
|
|
1093
|
+
"X-Auth-Email": CLOUDFLARE_EMAIL,
|
|
1094
|
+
"X-Auth-Key": CLOUDFLARE_API_KEY,
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
class CloudflareCustom {
|
|
1098
|
+
EXPIRES = true;
|
|
1099
|
+
|
|
1100
|
+
async get(key) {
|
|
1101
|
+
const res = await fetch(`${baseUrl}/values/${key}`, { headers });
|
|
1102
|
+
if (res.status === 404) return null; // It does not exist
|
|
1103
|
+
const data = await (res.headers.get("content-type").includes("json")
|
|
1104
|
+
? res.json()
|
|
1105
|
+
: res.text());
|
|
1106
|
+
if (!data) return null;
|
|
1107
|
+
return JSON.parse(data);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
async set(key, body, { expires }) {
|
|
1111
|
+
const expiration = expires ? `expiration_ttl=${expires}&` : "";
|
|
1112
|
+
await fetch(`${baseUrl}/values/${key}?${expiration}`, {
|
|
1113
|
+
method: "PUT",
|
|
1114
|
+
headers,
|
|
1115
|
+
body: JSON.stringify(body),
|
|
1116
|
+
});
|
|
1117
|
+
return key;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
async keys(prefix) {
|
|
1121
|
+
const res = await fetch(`${baseUrl}/keys`, { headers });
|
|
1122
|
+
const data = await res.json();
|
|
1123
|
+
return data.result
|
|
1124
|
+
.map((it) => it.name)
|
|
1125
|
+
.filter((key) => key.startsWith(prefix));
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
async *iterate(prefix) {
|
|
1129
|
+
const keys = await this.keys(prefix);
|
|
1130
|
+
|
|
1131
|
+
// A list of promises. Requests them all in parallel, but will start
|
|
1132
|
+
// yielding them as soon as they are available (in order)
|
|
1133
|
+
const pairs = keys.map(async (key) => [key, await this.get(key)]);
|
|
1134
|
+
for (let prom of pairs) {
|
|
1135
|
+
const pair = await prom;
|
|
1136
|
+
// Some values could have been nullified from reading of the keys to
|
|
1137
|
+
// reading of the value
|
|
1138
|
+
if (!pair[1]) continue;
|
|
1139
|
+
yield await pair;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const store = kv(CloudflareCustom);
|
|
1145
|
+
````
|
|
1146
|
+
|
|
1147
|
+
It's lacking few things, so make sure to adapt to your needs, but it worked for my very simple cache needs.
|
package/src/clients/api.js
CHANGED
|
@@ -13,7 +13,11 @@ export default class Api extends Client {
|
|
|
13
13
|
const headers = { accept: "application/json" };
|
|
14
14
|
if (body) headers["content-type"] = "application/json";
|
|
15
15
|
const res = await fetch(url, { method, headers, body });
|
|
16
|
-
|
|
16
|
+
if (!res.ok) return null;
|
|
17
|
+
if (res.headers.get("content-type")?.includes("application/json")) {
|
|
18
|
+
return res.json();
|
|
19
|
+
}
|
|
20
|
+
return res.text();
|
|
17
21
|
};
|
|
18
22
|
|
|
19
23
|
get = (key) => this.#api(key);
|
package/src/server.js
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
import http from "node:http";
|
|
3
3
|
import kv from "./index.js";
|
|
4
4
|
|
|
5
|
+
// Add/remove the key whether you want the API to be behind a key
|
|
6
|
+
const key = null;
|
|
7
|
+
// const key = 'MY-SECRET-KEY';
|
|
8
|
+
|
|
5
9
|
// Modify this to use any sub-store as desired. It's nice
|
|
6
10
|
// to use polystore itself for the polystore server library!'
|
|
7
11
|
const store = kv(new Map());
|
|
@@ -51,6 +55,9 @@ async function fetch({ method, url, body }) {
|
|
|
51
55
|
|
|
52
56
|
// http or express server-like handler:
|
|
53
57
|
async function server(req, res) {
|
|
58
|
+
// Secure it behind a key (optional)
|
|
59
|
+
if (key && req.headers.get("x-api-key") !== key) return res.send(401);
|
|
60
|
+
|
|
54
61
|
const url = new URL(req.url, "http://localhost:3000/").href;
|
|
55
62
|
const reply = await fetch({ ...req, url });
|
|
56
63
|
res.writeHead(reply.status, null, reply.headers || {});
|