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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polystore",
3
- "version": "0.15.11",
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** `new URL('file:///...')`](#file) (be): store the data in a single JSON file in your FS
40
- - [**Folder** `new URL('file:///...')`](#folder) (be): store each key in a folder as json files
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(new URL(`file://${import.meta.dirname}/users.json`));
439
- const books = kv(new URL(`file://${import.meta.dirname}/books.json`));
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
- const store = kv(new URL("file:///Users/me/project/cache.json"));
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(new URL("file:///Users/me/project/data/"));
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(new URL(`file://${process.cwd()}/cache/`));
724
- const store2 = kv(new URL(`file://${import.meta.dirname}/data/`));
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 time when it will expire.
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
- **Example: Plain Object client**
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
- **Example: custom ID generation**
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.
@@ -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
- return res.ok ? res.json() : null;
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 || {});