diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..70aae34 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: ci + +on: + push: + branches: + - main + pull_request: + branches: + - main + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Deno + uses: denoland/setup-deno@v1 + + - name: Verify formatting + run: deno fmt --check + + - name: Run linter + run: deno task lint + + - name: Run TypeScript checking + run: deno task check + + - name: Run tests + run: deno task test + + - name: Publish dry run + run: deno publish --dry-run diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..586fe74 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,27 @@ +name: Publish + +on: + workflow_run: + workflows: [ci] + types: [completed] + branches: [main] + +jobs: + publish: + runs-on: ubuntu-latest + + if: ${{ github.event.workflow_run.conclusion == 'success' }} + + permissions: + contents: read + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Deno + uses: denoland/setup-deno@v1 + + - name: Publish package + run: deno publish diff --git a/.vscode/settings.json b/.vscode/settings.json index 4ccf370..5518269 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,6 @@ { "deno.enable": true, "deno.lint": true, - "deno.unstable": true, - "deno.importMap": "./_test/import_map.web_storage.json", "editor.defaultFormatter": "denoland.vscode-deno", "editor.tabSize": 2 } diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d2824e8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0] + +- Initial release on JSR diff --git a/LICENSE b/LICENSE index 6c0a19b..d816bdc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Mark Gibson +Copyright (c) 2024 Mark Gibson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 10c4be8..a28df74 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,9 @@ -# Pluggable Storage Modules for Deno +# Pluggable Storage Modules -NOTE: This is an experimental module. +NOTE: This is an experimental library. Each module here provides the same interface to a hierarchical key -> value storage mechanism. So they can be imported directly or as swappable interface -via an import map, for example: +via an import map. -```json -{ - "imports": { - "$store": "https://deno.land/x/storage_modules/deno_fs.ts" - } -} -``` - -## Functions - -Each function takes a hierarchical key as an array of strings. It's up to the -storage module how those are translated to the underlying storage. But it may be -best to assume the first level key to be a grouping level, eg. a database name. - -Any JSON serializable value can be stored. - -See the [types](./types.ts) for a description of the module interface. - -## Modules - -### web_storage.ts - -This uses `localStorage` of the standard -[Web Storage](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API) -API. - -The parts of the key are joined with a `/` to form a single key string for use -with the `localStorage` API. - -Import mapping: `"$store": "https://deno.land/x/storage_modules/web_storage.ts"` - -### deno_fs.ts - -This stores values in individual files under a directory hierarchy via -[Deno fs](https://deno.land/api?s=Deno.readTextFile) calls. By default this is -under a `.store` dir under the current working dir. This can be overridden via -the environment var `STORE_FS_ROOT`. - -Each level of the key becomes a directory up to the last segment which becomes a -JSON file. - -eg: `["one", "two", "three"]` -> `.store/one/two/three.json` - -Import mapping: `"$store": "https://deno.land/x/storage_modules/deno_fs.ts"` - -### deno_kv.ts - -Use the [Deno KV](https://deno.land/manual/runtime/kv) API for storage. - -Import mapping: `"$store": "https://deno.land/x/storage_modules/deno_kv.ts"` - -### deno_kv_fs.ts - -Combination of a readonly `deno_fs.ts` and `deno_kv.ts`, allowing fallback or -immutable storage in the filesystem, and mutable storage via the KV store. - -By default the filesystem takes priority, and cannot be overridden by KV values, -unless the env var `STORE_PRIMARY` is set to `kv`, in which case the KV always -overrides filesystem values. +See [./store/README.md] for more details. diff --git a/_test/import_map.deno_fs.json b/_test/import_map.deno_fs.json deleted file mode 100644 index 086dff2..0000000 --- a/_test/import_map.deno_fs.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "imports": { - "$store": "../deno_fs.ts" - } -} diff --git a/_test/import_map.deno_kv.json b/_test/import_map.deno_kv.json deleted file mode 100644 index c730aa1..0000000 --- a/_test/import_map.deno_kv.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "imports": { - "$store": "../deno_kv.ts" - } -} diff --git a/_test/import_map.deno_kv_fs.json b/_test/import_map.deno_kv_fs.json deleted file mode 100644 index 85dfb7a..0000000 --- a/_test/import_map.deno_kv_fs.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "imports": { - "$store": "../deno_kv_fs.ts" - } -} diff --git a/_test/import_map.web_storage.json b/_test/import_map.web_storage.json deleted file mode 100644 index 92e1608..0000000 --- a/_test/import_map.web_storage.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "imports": { - "$store": "../web_storage.ts" - } -} diff --git a/deno.json b/deno.json index 897ec15..6a0c2ee 100644 --- a/deno.json +++ b/deno.json @@ -1,10 +1,28 @@ { + "unstable": [ + "kv" + ], "tasks": { - "test:web_storage": "deno test --check --import-map=./_test/import_map.web_storage.json", - "test:deno_fs": "deno test --check --allow-read --allow-write --allow-env --import-map=./_test/import_map.deno_fs.json", - "test:deno_kv": "deno test --check --unstable --allow-env --import-map=./_test/import_map.deno_kv.json", - "test:deno_kv_fs": "deno test --check --unstable --allow-read --allow-env --import-map=./_test/import_map.deno_kv_fs.json", - "test": "deno task test:web_storage; deno task test:deno_fs; deno task test:deno_kv; deno task test:deno_kv_fs", - "check": "deno fmt && deno lint && deno task test" + "test": "deno test --allow-env --allow-read --allow-write --allow-net", + "reload": "deno cache --reload **/*.ts jsr:@check/deps", + "check": "deno check **/*.ts", + "lint": "deno lint", + "ok": "deno fmt && deno task lint && deno task check && deno task test && deno publish --dry-run --allow-dirty", + "outdated": "deno run --allow-read=. --allow-net=jsr.io,registry.npmjs.org jsr:@check/deps", + "lock": "rm -f deno.lock && deno task check" + }, + "workspaces": [ + "./store", + "./store-common", + "./store-deno-fs", + "./store-deno-kv", + "./store-deno-kv-fs", + "./store-web-storage" + ], + "imports": { + "@std/assert": "jsr:@std/assert@^1.0.0-rc.3", + "@std/fs": "jsr:@std/fs@^1.0.0-rc.3", + "@std/path": "jsr:@std/path@^1.0.0-rc.3", + "@std/url": "jsr:@std/url@^1.0.0-rc.2" } } diff --git a/deno.lock b/deno.lock index 5d91fb0..bd308e8 100644 --- a/deno.lock +++ b/deno.lock @@ -1,97 +1,49 @@ { "version": "3", - "remote": { - "https://deno.land/std@0.208.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9", - "https://deno.land/std@0.208.0/assert/_diff.ts": "58e1461cc61d8eb1eacbf2a010932bf6a05b79344b02ca38095f9b805795dc48", - "https://deno.land/std@0.208.0/assert/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", - "https://deno.land/std@0.208.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", - "https://deno.land/std@0.208.0/assert/assert_array_includes.ts": "6856d7f2c3544bc6e62fb4646dfefa3d1df5ff14744d1bca19f0cbaf3b0d66c9", - "https://deno.land/std@0.208.0/assert/assert_equals.ts": "d8ec8a22447fbaf2fc9d7c3ed2e66790fdb74beae3e482855d75782218d68227", - "https://deno.land/std@0.208.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", - "https://deno.land/std@0.208.0/assert/equal.ts": "9f1a46d5993966d2596c44e5858eec821859b45f783a5ee2f7a695dfc12d8ece", - "https://deno.land/std@0.208.0/fmt/colors.ts": "34b3f77432925eb72cf0bfb351616949746768620b8e5ead66da532f93d10ba2", - "https://deno.land/std@0.208.0/fs/_util.ts": "fbf57dcdc9f7bc8128d60301eece608246971a7836a3bb1e78da75314f08b978", - "https://deno.land/std@0.208.0/fs/ensure_dir.ts": "dc64c4c75c64721d4e3fb681f1382f803ff3d2868f08563ff923fdd20d071c40", - "https://deno.land/std@0.208.0/fs/exists.ts": "cb59a853d84871d87acab0e7936a4dac11282957f8e195102c5a7acb42546bb8", - "https://deno.land/std@0.208.0/fs/walk.ts": "c1e6b43f72a46e89b630140308bd51a4795d416a416b4cfb7cd4bd1e25946723", - "https://deno.land/std@0.208.0/path/_common/assert_path.ts": "061e4d093d4ba5aebceb2c4da3318bfe3289e868570e9d3a8e327d91c2958946", - "https://deno.land/std@0.208.0/path/_common/basename.ts": "0d978ff818f339cd3b1d09dc914881f4d15617432ae519c1b8fdc09ff8d3789a", - "https://deno.land/std@0.208.0/path/_common/common.ts": "9e4233b2eeb50f8b2ae10ecc2108f58583aea6fd3e8907827020282dc2b76143", - "https://deno.land/std@0.208.0/path/_common/constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", - "https://deno.land/std@0.208.0/path/_common/dirname.ts": "2ba7fb4cc9fafb0f38028f434179579ce61d4d9e51296fad22b701c3d3cd7397", - "https://deno.land/std@0.208.0/path/_common/format.ts": "11aa62e316dfbf22c126917f5e03ea5fe2ee707386555a8f513d27ad5756cf96", - "https://deno.land/std@0.208.0/path/_common/from_file_url.ts": "ef1bf3197d2efbf0297a2bdbf3a61d804b18f2bcce45548ae112313ec5be3c22", - "https://deno.land/std@0.208.0/path/_common/glob_to_reg_exp.ts": "5c3c2b79fc2294ec803d102bd9855c451c150021f452046312819fbb6d4dc156", - "https://deno.land/std@0.208.0/path/_common/normalize.ts": "2ba7fb4cc9fafb0f38028f434179579ce61d4d9e51296fad22b701c3d3cd7397", - "https://deno.land/std@0.208.0/path/_common/normalize_string.ts": "88c472f28ae49525f9fe82de8c8816d93442d46a30d6bb5063b07ff8a89ff589", - "https://deno.land/std@0.208.0/path/_common/relative.ts": "1af19d787a2a84b8c534cc487424fe101f614982ae4851382c978ab2216186b4", - "https://deno.land/std@0.208.0/path/_common/strip_trailing_separators.ts": "7ffc7c287e97bdeeee31b155828686967f222cd73f9e5780bfe7dfb1b58c6c65", - "https://deno.land/std@0.208.0/path/_common/to_file_url.ts": "a8cdd1633bc9175b7eebd3613266d7c0b6ae0fb0cff24120b6092ac31662f9ae", - "https://deno.land/std@0.208.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", - "https://deno.land/std@0.208.0/path/_os.ts": "30b0c2875f360c9296dbe6b7f2d528f0f9c741cecad2e97f803f5219e91b40a2", - "https://deno.land/std@0.208.0/path/basename.ts": "04bb5ef3e86bba8a35603b8f3b69537112cdd19ce64b77f2522006da2977a5f3", - "https://deno.land/std@0.208.0/path/common.ts": "f4d061c7d0b95a65c2a1a52439edec393e906b40f1caf4604c389fae7caa80f5", - "https://deno.land/std@0.208.0/path/dirname.ts": "88a0a71c21debafc4da7a4cd44fd32e899462df458fbca152390887d41c40361", - "https://deno.land/std@0.208.0/path/extname.ts": "2da4e2490f3b48b7121d19fb4c91681a5e11bd6bd99df4f6f47d7a71bb6ecdf2", - "https://deno.land/std@0.208.0/path/format.ts": "3457530cc85d1b4bab175f9ae73998b34fd456c830d01883169af0681b8894fb", - "https://deno.land/std@0.208.0/path/from_file_url.ts": "e7fa233ea1dff9641e8d566153a24d95010110185a6f418dd2e32320926043f8", - "https://deno.land/std@0.208.0/path/glob_to_regexp.ts": "74d7448c471e293d03f05ccb968df4365fed6aaa508506b6325a8efdc01d8271", - "https://deno.land/std@0.208.0/path/is_absolute.ts": "67232b41b860571c5b7537f4954c88d86ae2ba45e883ee37d3dec27b74909d13", - "https://deno.land/std@0.208.0/path/is_glob.ts": "567dce5c6656bdedfc6b3ee6c0833e1e4db2b8dff6e62148e94a917f289c06ad", - "https://deno.land/std@0.208.0/path/join.ts": "98d3d76c819af4a11a81d5ba2dbb319f1ce9d63fc2b615597d4bcfddd4a89a09", - "https://deno.land/std@0.208.0/path/join_globs.ts": "9b84d5103b63d3dbed4b2cf8b12477b2ad415c7d343f1488505162dc0e5f4db8", - "https://deno.land/std@0.208.0/path/mod.ts": "3defabebc98279e62b392fee7a6937adc932a8f4dcd2471441e36c15b97b00e0", - "https://deno.land/std@0.208.0/path/normalize.ts": "aa95be9a92c7bd4f9dc0ba51e942a1973e2b93d266cd74f5ca751c136d520b66", - "https://deno.land/std@0.208.0/path/normalize_glob.ts": "674baa82e1c00b6cb153bbca36e06f8e0337cb8062db6d905ab5de16076ca46b", - "https://deno.land/std@0.208.0/path/parse.ts": "d87ff0deef3fb495bc0d862278ff96da5a06acf0625ca27769fc52ac0d3d6ece", - "https://deno.land/std@0.208.0/path/posix/_util.ts": "ecf49560fedd7dd376c6156cc5565cad97c1abe9824f4417adebc7acc36c93e5", - "https://deno.land/std@0.208.0/path/posix/basename.ts": "a630aeb8fd8e27356b1823b9dedd505e30085015407caa3396332752f6b8406a", - "https://deno.land/std@0.208.0/path/posix/common.ts": "e781d395dc76f6282e3f7dd8de13194abb8b04a82d109593141abc6e95755c8b", - "https://deno.land/std@0.208.0/path/posix/dirname.ts": "f48c9c42cc670803b505478b7ef162c7cfa9d8e751b59d278b2ec59470531472", - "https://deno.land/std@0.208.0/path/posix/extname.ts": "ee7f6571a9c0a37f9218fbf510c440d1685a7c13082c348d701396cc795e0be0", - "https://deno.land/std@0.208.0/path/posix/format.ts": "b94876f77e61bfe1f147d5ccb46a920636cd3cef8be43df330f0052b03875968", - "https://deno.land/std@0.208.0/path/posix/from_file_url.ts": "b97287a83e6407ac27bdf3ab621db3fccbf1c27df0a1b1f20e1e1b5acf38a379", - "https://deno.land/std@0.208.0/path/posix/glob_to_regexp.ts": "6ed00c71fbfe0ccc35977c35444f94e82200b721905a60bd1278b1b768d68b1a", - "https://deno.land/std@0.208.0/path/posix/is_absolute.ts": "159900a3422d11069d48395568217eb7fc105ceda2683d03d9b7c0f0769e01b8", - "https://deno.land/std@0.208.0/path/posix/is_glob.ts": "ec4fbc604b9db8487f7b56ab0e759b24a971ab6a45f7b0b698bc39b8b9f9680f", - "https://deno.land/std@0.208.0/path/posix/join.ts": "0c0d84bdc344876930126640011ec1b888e6facf74153ffad9ef26813aa2a076", - "https://deno.land/std@0.208.0/path/posix/join_globs.ts": "f4838d54b1f60a34a40625a3293f6e583135348be1b2974341ac04743cb26121", - "https://deno.land/std@0.208.0/path/posix/mod.ts": "f1b08a7f64294b7de87fc37190d63b6ce5b02889af9290c9703afe01951360ae", - "https://deno.land/std@0.208.0/path/posix/normalize.ts": "11de90a94ab7148cc46e5a288f7d732aade1d616bc8c862f5560fa18ff987b4b", - "https://deno.land/std@0.208.0/path/posix/normalize_glob.ts": "10a1840c628ebbab679254d5fa1c20e59106102354fb648a1765aed72eb9f3f9", - "https://deno.land/std@0.208.0/path/posix/parse.ts": "199208f373dd93a792e9c585352bfc73a6293411bed6da6d3bc4f4ef90b04c8e", - "https://deno.land/std@0.208.0/path/posix/relative.ts": "e2f230608b0f083e6deaa06e063943e5accb3320c28aef8d87528fbb7fe6504c", - "https://deno.land/std@0.208.0/path/posix/resolve.ts": "51579d83159d5c719518c9ae50812a63959bbcb7561d79acbdb2c3682236e285", - "https://deno.land/std@0.208.0/path/posix/separator.ts": "0b6573b5f3269a3164d8edc9cefc33a02dd51003731c561008c8bb60220ebac1", - "https://deno.land/std@0.208.0/path/posix/to_file_url.ts": "08d43ea839ee75e9b8b1538376cfe95911070a655cd312bc9a00f88ef14967b6", - "https://deno.land/std@0.208.0/path/posix/to_namespaced_path.ts": "c9228a0e74fd37e76622cd7b142b8416663a9b87db643302fa0926b5a5c83bdc", - "https://deno.land/std@0.208.0/path/relative.ts": "23d45ede8b7ac464a8299663a43488aad6b561414e7cbbe4790775590db6349c", - "https://deno.land/std@0.208.0/path/resolve.ts": "5b184efc87155a0af9fa305ff68a109e28de9aee81fc3e77cd01380f19daf867", - "https://deno.land/std@0.208.0/path/separator.ts": "40a3e9a4ad10bef23bc2cd6c610291b6c502a06237c2c4cd034a15ca78dedc1f", - "https://deno.land/std@0.208.0/path/to_file_url.ts": "edaafa089e0bce386e1b2d47afe7c72e379ff93b28a5829a5885e4b6c626d864", - "https://deno.land/std@0.208.0/path/to_namespaced_path.ts": "cf8734848aac3c7527d1689d2adf82132b1618eff3cc523a775068847416b22a", - "https://deno.land/std@0.208.0/path/windows/_util.ts": "f32b9444554c8863b9b4814025c700492a2b57ff2369d015360970a1b1099d54", - "https://deno.land/std@0.208.0/path/windows/basename.ts": "8a9dbf7353d50afbc5b221af36c02a72c2d1b2b5b9f7c65bf6a5a2a0baf88ad3", - "https://deno.land/std@0.208.0/path/windows/common.ts": "e781d395dc76f6282e3f7dd8de13194abb8b04a82d109593141abc6e95755c8b", - "https://deno.land/std@0.208.0/path/windows/dirname.ts": "5c2aa541384bf0bd9aca821275d2a8690e8238fa846198ef5c7515ce31a01a94", - "https://deno.land/std@0.208.0/path/windows/extname.ts": "07f4fa1b40d06a827446b3e3bcc8d619c5546b079b8ed0c77040bbef716c7614", - "https://deno.land/std@0.208.0/path/windows/format.ts": "343019130d78f172a5c49fdc7e64686a7faf41553268961e7b6c92a6d6548edf", - "https://deno.land/std@0.208.0/path/windows/from_file_url.ts": "d53335c12b0725893d768be3ac6bf0112cc5b639d2deb0171b35988493b46199", - "https://deno.land/std@0.208.0/path/windows/glob_to_regexp.ts": "290755e18ec6c1a4f4d711c3390537358e8e3179581e66261a0cf348b1a13395", - "https://deno.land/std@0.208.0/path/windows/is_absolute.ts": "245b56b5f355ede8664bd7f080c910a97e2169972d23075554ae14d73722c53c", - "https://deno.land/std@0.208.0/path/windows/is_glob.ts": "ec4fbc604b9db8487f7b56ab0e759b24a971ab6a45f7b0b698bc39b8b9f9680f", - "https://deno.land/std@0.208.0/path/windows/join.ts": "e6600bf88edeeef4e2276e155b8de1d5dec0435fd526ba2dc4d37986b2882f16", - "https://deno.land/std@0.208.0/path/windows/join_globs.ts": "f4838d54b1f60a34a40625a3293f6e583135348be1b2974341ac04743cb26121", - "https://deno.land/std@0.208.0/path/windows/mod.ts": "d7040f461465c2c21c1c68fc988ef0bdddd499912138cde3abf6ad60c7fb3814", - "https://deno.land/std@0.208.0/path/windows/normalize.ts": "9deebbf40c81ef540b7b945d4ccd7a6a2c5a5992f791e6d3377043031e164e69", - "https://deno.land/std@0.208.0/path/windows/normalize_glob.ts": "344ff5ed45430495b9a3d695567291e50e00b1b3b04ea56712a2acf07ab5c128", - "https://deno.land/std@0.208.0/path/windows/parse.ts": "120faf778fe1f22056f33ded069b68e12447668fcfa19540c0129561428d3ae5", - "https://deno.land/std@0.208.0/path/windows/relative.ts": "026855cd2c36c8f28f1df3c6fbd8f2449a2aa21f48797a74700c5d872b86d649", - "https://deno.land/std@0.208.0/path/windows/resolve.ts": "5ff441ab18a2346abadf778121128ee71bda4d0898513d4639a6ca04edca366b", - "https://deno.land/std@0.208.0/path/windows/separator.ts": "ae21f27015f10510ed1ac4a0ba9c4c9c967cbdd9d9e776a3e4967553c397bd5d", - "https://deno.land/std@0.208.0/path/windows/to_file_url.ts": "8e9ea9e1ff364aa06fa72999204229952d0a279dbb876b7b838b2b2fea55cce3", - "https://deno.land/std@0.208.0/path/windows/to_namespaced_path.ts": "e0f4d4a5e77f28a5708c1a33ff24360f35637ba6d8f103d19661255ef7bfd50d", - "https://deno.land/std@0.208.0/url/_strip.ts": "86f852d266b86e5867f50ac5d453bedea7b7e7a1919669df93d66a0b59b00e5b", - "https://deno.land/std@0.208.0/url/basename.ts": "1257643f9934b65696d8af3ad993b3269d55231e6258ac13fba3d4fe193f30be" + "packages": { + "specifiers": { + "jsr:@std/assert@^1.0.0-rc.3": "jsr:@std/assert@1.0.0-rc.3", + "jsr:@std/fs@^1.0.0-rc.3": "jsr:@std/fs@1.0.0-rc.3", + "jsr:@std/internal@^1.0.1": "jsr:@std/internal@1.0.1", + "jsr:@std/path@1.0.0-rc.3": "jsr:@std/path@1.0.0-rc.3", + "jsr:@std/path@^1.0.0-rc.3": "jsr:@std/path@1.0.0-rc.3" + }, + "jsr": { + "@std/assert@1.0.0-rc.3": { + "integrity": "27fb4b1da846ea3f7f0504f9b2acf7c83a763387a88c497e94a62ca168c9a24e", + "dependencies": [ + "jsr:@std/internal@^1.0.1" + ] + }, + "@std/fs@1.0.0-rc.3": { + "integrity": "50a0a4366e7e2bd444ed0831757e3d9dd39b1d24612764bbb9ac7725169903f4", + "dependencies": [ + "jsr:@std/path@1.0.0-rc.3" + ] + }, + "@std/internal@1.0.1": { + "integrity": "6f8c7544d06a11dd256c8d6ba54b11ed870aac6c5aeafff499892662c57673e6" + }, + "@std/path@1.0.0-rc.3": { + "integrity": "672b8f88b3b58b6e932052ae412f40fc063940fcdb228d12f80353bc6aff38c1" + } + } + }, + "remote": {}, + "workspace": { + "dependencies": [ + "jsr:@std/assert@^1.0.0-rc.3", + "jsr:@std/fs@^1.0.0-rc.3", + "jsr:@std/path@^1.0.0-rc.3", + "jsr:@std/url@^1.0.0-rc.2" + ], + "members": { + "@jollytoad/store": {}, + "@jollytoad/store-common": {}, + "@jollytoad/store-deno-fs": {}, + "@jollytoad/store-deno-kv": {}, + "@jollytoad/store-deno-kv-fs": {}, + "@jollytoad/store-web-storage": {} + } } } diff --git a/store-common/README.md b/store-common/README.md new file mode 100644 index 0000000..586c24a --- /dev/null +++ b/store-common/README.md @@ -0,0 +1,10 @@ +# Common types and utilities for Pluggable Storage Modules + +See [@jollytoad/store](https://jsr.io/@jollytoad/store) for the bigger picture. + +This package provides the storage module interface definition and some reusable +utility function for use by implementations. + +- [the interface](./types.ts) +- [key conversion utils](./key-utils.ts) +- [test utils](./test-storage-module.ts) diff --git a/store-common/deno.json b/store-common/deno.json new file mode 100644 index 0000000..1d11366 --- /dev/null +++ b/store-common/deno.json @@ -0,0 +1,9 @@ +{ + "name": "@jollytoad/store-common", + "version": "0.1.0", + "exports": { + "./key-utils": "./key-utils.ts", + "./test-storage-module": "./test-storage-module.ts", + "./types": "./types.ts" + } +} diff --git a/_key_util.ts b/store-common/key-utils.ts similarity index 80% rename from _key_util.ts rename to store-common/key-utils.ts index 2891f48..19c8002 100644 --- a/_key_util.ts +++ b/store-common/key-utils.ts @@ -1,5 +1,8 @@ import { StorageKey } from "./types.ts"; +/** + * Convert a {@linkcode StorageKey} to an array of strings + */ export function toStrKey(key: StorageKey = []): string[] { return key.map((k) => typeof k === "string" @@ -10,6 +13,9 @@ export function toStrKey(key: StorageKey = []): string[] { ); } +/** + * Convert an array of strings to a {@linkcode StorageKey} + */ export function fromStrKey(strKey: string[]): StorageKey { return strKey.map((k) => { if (k === "true") return true; diff --git a/_test/storage_test.ts b/store-common/test-storage-module.ts similarity index 59% rename from _test/storage_test.ts rename to store-common/test-storage-module.ts index a8aa79a..6ab5a07 100644 --- a/_test/storage_test.ts +++ b/store-common/test-storage-module.ts @@ -1,27 +1,27 @@ import { - clearItems, - close, - getItem, - hasItem, - listItems, - removeItem, - setItem, -} from "$store"; -import * as $store from "$store"; -import { exists } from "https://deno.land/std@0.208.0/fs/exists.ts"; -import { basename } from "https://deno.land/std@0.208.0/url/basename.ts"; -import { assert } from "https://deno.land/std@0.208.0/assert/assert.ts"; -import { assertArrayIncludes } from "https://deno.land/std@0.208.0/assert/assert_array_includes.ts"; -import { assertEquals } from "https://deno.land/std@0.208.0/assert/assert_equals.ts"; -import type { StorageModule } from "../types.ts"; - -const storage_module = basename(import.meta.resolve("$store")); - -$store satisfies StorageModule; - -await Deno.test(`storage module: ${storage_module}`, async (t) => { - await open(); + assert, + assertArrayIncludes, + assertEquals, + assertStringIncludes, +} from "@std/assert"; +import type { StorageModule } from "./types.ts"; + +export async function testUrl( + t: Deno.TestContext, + { url }: StorageModule, + includes: string, +) { + await t.step(`url contains "${includes}"`, async () => { + const actualUrl = await url(); + console.log("StorageModule URL:", actualUrl); + assertStringIncludes(actualUrl, includes); + }); +} +export async function testSetItem( + t: Deno.TestContext, + { setItem }: StorageModule, +) { await t.step("setItem", async () => { await setItem(["store", "number"], 100); await setItem(["store", "string"], "string"); @@ -33,7 +33,12 @@ await Deno.test(`storage module: ${storage_module}`, async (t) => { await setItem(["store", true], "true key"); await setItem(["store", false], "false key"); }); +} +export async function testHasItem( + t: Deno.TestContext, + { hasItem }: StorageModule, +) { await t.step("hasItem", async () => { assert(await hasItem(["store", "number"])); assert(await hasItem(["store", "string"])); @@ -45,7 +50,12 @@ await Deno.test(`storage module: ${storage_module}`, async (t) => { assert(await hasItem(["store", true])); assert(await hasItem(["store", false])); }); +} +export async function testGetItem( + t: Deno.TestContext, + { getItem }: StorageModule, +) { await t.step("getItem", async () => { assertEquals(await getItem(["store", "number"]), 100); assertEquals(await getItem(["store", "string"]), "string"); @@ -57,7 +67,12 @@ await Deno.test(`storage module: ${storage_module}`, async (t) => { assertEquals(await getItem(["store", true]), "true key"); assertEquals(await getItem(["store", false]), "false key"); }); +} +export async function testListItems( + t: Deno.TestContext, + { listItems }: StorageModule, +) { await t.step("listItems", async () => { const list = [ 100, @@ -75,7 +90,12 @@ await Deno.test(`storage module: ${storage_module}`, async (t) => { assertArrayIncludes(list, [value]); } }); +} +export async function testRemoveItem( + t: Deno.TestContext, + { removeItem, listItems, setItem, hasItem }: StorageModule, +) { await t.step("removeItem", async () => { await removeItem(["store", "number"]); await removeItem(["store", "string"]); @@ -103,7 +123,12 @@ await Deno.test(`storage module: ${storage_module}`, async (t) => { assert(await hasItem(["store", "nested", "item"])); }); +} +export async function testClearItems( + t: Deno.TestContext, + { clearItems, setItem, hasItem, listItems }: StorageModule, +) { await t.step("clearItems", async () => { await setItem(["store", "nested", "number"], 100); await setItem(["store", "string"], "string"); @@ -126,64 +151,59 @@ await Deno.test(`storage module: ${storage_module}`, async (t) => { } assertEquals(count, 0, "Expected no items to be found"); - - switch (storage_module) { - case "deno_fs.ts": - assert( - !await exists(".store/store"), - "Expected .store/store folder to no longer exist", - ); - } }); +} - if (storage_module === "deno_fs.ts") { - await t.step("empty folders are deleted from fs", async () => { - await setItem(["store", "deeply", "nested", "item"], true); - - assert( - await exists(".store/store/deeply/nested"), - "Expected .store/store/deeply/nested folder to exist", - ); - - await removeItem(["store", "deeply", "nested", "item"]); +// if (storage_module === "deno_fs.ts") { +// await t.step("empty folders are deleted from fs", async () => { +// await setItem(["store", "deeply", "nested", "item"], true); + +// assert( +// await exists(".store/store/deeply/nested"), +// "Expected .store/store/deeply/nested folder to exist", +// ); + +// await removeItem(["store", "deeply", "nested", "item"]); + +// assert( +// !await exists(".store/store"), +// "Expected .store/store folder to no longer exist", +// ); +// }); +// } + +export async function testOrdering( + t: Deno.TestContext, + { setItem, listItems, removeItem }: StorageModule, +) { + await t.step("ordered items", async (t) => { + await t.step("populate list", async () => { + await setItem(["store_list", 2], 2); + await setItem(["store_list", 4], 4); + await setItem(["store_list", 3], 3); + await setItem(["store_list", 1], 1); + }); - assert( - !await exists(".store/store"), - "Expected .store/store folder to no longer exist", - ); + await t.step("in natural order", async () => { + const values = await gatherValues(listItems(["store_list"])); + assertEquals(values, [1, 2, 3, 4]); }); - } - // Only KN & Web Storage are guaranteed to be ordered - if (storage_module === "deno_kv.ts" || storage_module === "web_storage.ts") { - await t.step("ordered items", async (t) => { - await t.step("populate list", async () => { - await setItem(["store_list", 2], 2); - await setItem(["store_list", 4], 4); - await setItem(["store_list", 3], 3); - await setItem(["store_list", 1], 1); - }); - - await t.step("in natural order", async () => { - const values = await gatherValues(listItems(["store_list"])); - assertEquals(values, [1, 2, 3, 4]); - }); - - await t.step("in reverse order", async () => { - const values = await gatherValues(listItems(["store_list"], true)); - assertEquals(values, [4, 3, 2, 1]); - }); - - await t.step("delete list", async () => { - await removeItem(["store_list"]); - }); + await t.step("in reverse order", async () => { + const values = await gatherValues(listItems(["store_list"], true)); + assertEquals(values, [4, 3, 2, 1]); }); - } - await close(); -}); + await t.step("delete list", async () => { + await removeItem(["store_list"]); + }); + }); +} -async function open() { +export async function open( + _t: Deno.TestContext, + { hasItem, clearItems }: StorageModule, +) { // Just ensure that the underlying storage has been opened and is empty await hasItem(["store"]); await clearItems([]); diff --git a/types.ts b/store-common/types.ts similarity index 93% rename from types.ts rename to store-common/types.ts index 71bbdf5..204dd61 100644 --- a/types.ts +++ b/store-common/types.ts @@ -48,4 +48,9 @@ export interface StorageModule { * Close all associated resources. */ close(): Promise; + + /** + * Returns the `import.meta.url` of the module. + */ + url(): Promise; } diff --git a/store-deno-fs/README.md b/store-deno-fs/README.md new file mode 100644 index 0000000..9aca40d --- /dev/null +++ b/store-deno-fs/README.md @@ -0,0 +1,17 @@ +# Deno Filesystem Storage Module + +See [@jollytoad/store](https://jsr.io/@jollytoad/store) for the bigger picture. + +This package provides an implementation of the storage module interface. + +This stores values in individual files under a directory hierarchy via +[Deno fs](https://deno.land/api?s=Deno.readTextFile) calls. By default this is +under a `.store` dir under the current working dir. This can be overridden via +the environment var `STORE_FS_ROOT`. + +Each level of the key becomes a directory up to the last segment which becomes a +JSON file. + +eg: `["one", "two", "three"]` -> `.store/one/two/three.json` + +Import mapping: `"$store": "jsr:@jollytoad/store-deno-fs"` diff --git a/store-deno-fs/deno.json b/store-deno-fs/deno.json new file mode 100644 index 0000000..effca5a --- /dev/null +++ b/store-deno-fs/deno.json @@ -0,0 +1,7 @@ +{ + "name": "@jollytoad/store-deno-fs", + "version": "0.1.0", + "exports": { + ".": "./mod.ts" + } +} diff --git a/deno_fs.ts b/store-deno-fs/mod.ts similarity index 79% rename from deno_fs.ts rename to store-deno-fs/mod.ts index 5b143ee..db20953 100644 --- a/deno_fs.ts +++ b/store-deno-fs/mod.ts @@ -1,12 +1,12 @@ -import { dirname } from "https://deno.land/std@0.208.0/path/dirname.ts"; -import { relative } from "https://deno.land/std@0.208.0/path/relative.ts"; -import { resolve } from "https://deno.land/std@0.208.0/path/resolve.ts"; -import { SEP_PATTERN } from "https://deno.land/std@0.208.0/path/separator.ts"; -import { ensureDir } from "https://deno.land/std@0.208.0/fs/ensure_dir.ts"; -import { exists } from "https://deno.land/std@0.208.0/fs/exists.ts"; -import { walk } from "https://deno.land/std@0.208.0/fs/walk.ts"; -import type { StorageKey, StorageModule } from "./types.ts"; -import { fromStrKey, toStrKey } from "./_key_util.ts"; +import { dirname } from "@std/path/dirname"; +import { relative } from "@std/path/relative"; +import { resolve } from "@std/path/resolve"; +import { SEPARATOR_PATTERN } from "@std/path/constants"; +import { ensureDir } from "@std/fs/ensure-dir"; +import { exists } from "@std/fs/exists"; +import { walk } from "@std/fs/walk"; +import type { StorageKey, StorageModule } from "@jollytoad/store-common/types"; +import { fromStrKey, toStrKey } from "@jollytoad/store-common/key-utils"; export type { StorageKey, StorageModule }; @@ -19,8 +19,13 @@ export type { StorageKey, StorageModule }; listItems, clearItems, close, + url, }) satisfies StorageModule; +export function url(): Promise { + return Promise.resolve(import.meta.url); +} + export async function isWritable(key: StorageKey = []): Promise { if (Deno.env.get("DENO_DEPLOYMENT_ID")) { return false; @@ -83,7 +88,7 @@ export async function* listItems( for await (const entry of walk(path)) { if (entry.isFile && entry.name.endsWith(".json")) { const key = relative(root, entry.path.slice(0, -5)).split( - SEP_PATTERN, + SEPARATOR_PATTERN, ); const item = await getItem(key); if (item) { @@ -113,7 +118,7 @@ export async function clearItems(keyPrefix: StorageKey): Promise { } } -export function close() { +export function close(): Promise { return Promise.resolve(); } diff --git a/store-deno-fs/store.test.ts b/store-deno-fs/store.test.ts new file mode 100644 index 0000000..07fca3c --- /dev/null +++ b/store-deno-fs/store.test.ts @@ -0,0 +1,53 @@ +import { assert } from "@std/assert"; +import { + open, + testClearItems, + testGetItem, + testHasItem, + testListItems, + testRemoveItem, + testSetItem, + testUrl, +} from "../store-common/test-storage-module.ts"; +import * as store from "./mod.ts"; +import { StorageModule } from "./mod.ts"; +import { exists } from "@std/fs/exists"; + +Deno.test("store-deno-fs", async (t) => { + try { + await open(t, store); + await testUrl(t, store, "store-deno-fs"); + await testSetItem(t, store); + await testHasItem(t, store); + await testGetItem(t, store); + await testListItems(t, store); + await testRemoveItem(t, store); + await testClearItems(t, store); + await testDirectoryPurge(t, store); + // Ordering is not currently supported on FS + // await testOrdering(t, store); + } finally { + await store.close(); + } +}); + +export async function testDirectoryPurge( + t: Deno.TestContext, + { setItem, removeItem }: StorageModule, +) { + await t.step("empty folders are deleted from fs", async () => { + await setItem(["store", "deeply", "nested", "item"], true); + + assert( + await exists(".store/store/deeply/nested"), + "Expected .store/store/deeply/nested folder to exist", + ); + + await removeItem(["store", "deeply", "nested", "item"]); + + assert( + !await exists(".store/store"), + "Expected .store/store folder to no longer exist", + ); + }); +} diff --git a/store-deno-kv-fs/README.md b/store-deno-kv-fs/README.md new file mode 100644 index 0000000..b1e2c2c --- /dev/null +++ b/store-deno-kv-fs/README.md @@ -0,0 +1,14 @@ +# Deno KV+Filesystem Storage Module + +See [@jollytoad/store](https://jsr.io/@jollytoad/store) for the bigger picture. + +This package provides an implementation of the storage module interface. + +Combination of a readonly `deno-fs` and writeable `deno-kv`, allowing fallback +or immutable storage in the filesystem, and mutable storage via the KV store. + +By default the filesystem takes priority, and cannot be overridden by KV values, +unless the env var `STORE_PRIMARY` is set to `kv`, in which case the KV always +overrides filesystem values. + +Import mapping: `"$store": "jsr:@jollytoad/store-deno-kv-fs"` diff --git a/store-deno-kv-fs/deno.json b/store-deno-kv-fs/deno.json new file mode 100644 index 0000000..a1c23ac --- /dev/null +++ b/store-deno-kv-fs/deno.json @@ -0,0 +1,7 @@ +{ + "name": "@jollytoad/store-deno-kv-fs", + "version": "0.1.0", + "exports": { + ".": "./mod.ts" + } +} diff --git a/deno_kv_fs.ts b/store-deno-kv-fs/mod.ts similarity index 84% rename from deno_kv_fs.ts rename to store-deno-kv-fs/mod.ts index 587811d..e1c4014 100644 --- a/deno_kv_fs.ts +++ b/store-deno-kv-fs/mod.ts @@ -1,6 +1,6 @@ -import * as kv from "./deno_kv.ts"; -import * as fs from "./deno_fs.ts"; -import type { StorageKey, StorageModule } from "./types.ts"; +import * as kv from "@jollytoad/store-deno-kv"; +import * as fs from "@jollytoad/store-deno-fs"; +import type { StorageKey, StorageModule } from "@jollytoad/store-common/types"; export type { StorageKey, StorageModule }; @@ -13,8 +13,13 @@ export type { StorageKey, StorageModule }; listItems, clearItems, close, + url, }) satisfies StorageModule; +export function url(): Promise { + return Promise.resolve(import.meta.url); +} + export async function isWritable(key: StorageKey = []): Promise { if (key.length && isFsPrimary() && await fs.hasItem(key)) { return false; @@ -65,7 +70,7 @@ export async function clearItems(prefix: StorageKey) { await kv.clearItems(prefix); } -export function close() { +export function close(): Promise { return kv.close(); } diff --git a/store-deno-kv-fs/store.test.ts b/store-deno-kv-fs/store.test.ts new file mode 100644 index 0000000..108bcef --- /dev/null +++ b/store-deno-kv-fs/store.test.ts @@ -0,0 +1,28 @@ +import { + open, + testClearItems, + testGetItem, + testHasItem, + testListItems, + testOrdering, + testRemoveItem, + testSetItem, + testUrl, +} from "../store-common/test-storage-module.ts"; +import * as store from "./mod.ts"; + +Deno.test("store-deno-kv-fs", async (t) => { + try { + await open(t, store); + await testUrl(t, store, "store-deno-kv-fs"); + await testSetItem(t, store); + await testHasItem(t, store); + await testGetItem(t, store); + await testListItems(t, store); + await testRemoveItem(t, store); + await testClearItems(t, store); + await testOrdering(t, store); + } finally { + await store.close(); + } +}); diff --git a/store-deno-kv/README.md b/store-deno-kv/README.md new file mode 100644 index 0000000..0a0b9d8 --- /dev/null +++ b/store-deno-kv/README.md @@ -0,0 +1,9 @@ +# Deno KV Storage Module + +See [@jollytoad/store](https://jsr.io/@jollytoad/store) for the bigger picture. + +This package provides an implementation of the storage module interface. + +Uses the [Deno KV](https://deno.land/manual/runtime/kv) API for storage. + +Import mapping: `"$store": "jsr:@jollytoad/store-deno-kv"` diff --git a/store-deno-kv/deno.json b/store-deno-kv/deno.json new file mode 100644 index 0000000..bbd1189 --- /dev/null +++ b/store-deno-kv/deno.json @@ -0,0 +1,7 @@ +{ + "name": "@jollytoad/store-deno-kv", + "version": "0.1.0", + "exports": { + ".": "./mod.ts" + } +} diff --git a/deno_kv.ts b/store-deno-kv/mod.ts similarity index 90% rename from deno_kv.ts rename to store-deno-kv/mod.ts index 70d5964..6de4631 100644 --- a/deno_kv.ts +++ b/store-deno-kv/mod.ts @@ -1,4 +1,4 @@ -import type { StorageKey, StorageModule } from "./types.ts"; +import type { StorageKey, StorageModule } from "@jollytoad/store-common/types"; export type { StorageKey, StorageModule }; @@ -13,8 +13,13 @@ const consistency: Deno.KvConsistencyLevel = "eventual"; listItems, clearItems, close, + url, }) satisfies StorageModule; +export function url(): Promise { + return Promise.resolve(import.meta.url); +} + export function isWritable(_key?: StorageKey): Promise { return Promise.resolve(true); } @@ -71,7 +76,7 @@ export async function clearItems(prefix: StorageKey): Promise { await op.commit(); } -export async function close() { +export async function close(): Promise { const kvs = [...kvCache.values()]; kvCache.clear(); await Promise.all(kvs.map((kv) => kv.close())); diff --git a/store-deno-kv/store.test.ts b/store-deno-kv/store.test.ts new file mode 100644 index 0000000..6baa790 --- /dev/null +++ b/store-deno-kv/store.test.ts @@ -0,0 +1,28 @@ +import { + open, + testClearItems, + testGetItem, + testHasItem, + testListItems, + testOrdering, + testRemoveItem, + testSetItem, + testUrl, +} from "../store-common/test-storage-module.ts"; +import * as store from "./mod.ts"; + +Deno.test("store-deno-kv", async (t) => { + try { + await open(t, store); + await testUrl(t, store, "store-deno-kv"); + await testSetItem(t, store); + await testHasItem(t, store); + await testGetItem(t, store); + await testListItems(t, store); + await testRemoveItem(t, store); + await testClearItems(t, store); + await testOrdering(t, store); + } finally { + await store.close(); + } +}); diff --git a/store-web-storage/README.md b/store-web-storage/README.md new file mode 100644 index 0000000..2426e5b --- /dev/null +++ b/store-web-storage/README.md @@ -0,0 +1,14 @@ +# Web Storage Module + +See [@jollytoad/store](https://jsr.io/@jollytoad/store) for the bigger picture. + +This package provides an implementation of the storage module interface. + +This uses `localStorage` of the standard +[Web Storage](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API) +API. + +The parts of the key are joined with a `/` to form a single key string for use +with the `localStorage` API. + +Import mapping: `"$store": "jsr:@jollytoad/store-web-storage"` diff --git a/store-web-storage/deno.json b/store-web-storage/deno.json new file mode 100644 index 0000000..af2ff50 --- /dev/null +++ b/store-web-storage/deno.json @@ -0,0 +1,7 @@ +{ + "name": "@jollytoad/store-web-storage", + "version": "0.1.0", + "exports": { + ".": "./mod.ts" + } +} diff --git a/web_storage.ts b/store-web-storage/mod.ts similarity index 88% rename from web_storage.ts rename to store-web-storage/mod.ts index 7ba48cf..84d6148 100644 --- a/web_storage.ts +++ b/store-web-storage/mod.ts @@ -1,5 +1,5 @@ -import { fromStrKey, toStrKey } from "./_key_util.ts"; -import type { StorageKey, StorageModule } from "./types.ts"; +import { fromStrKey, toStrKey } from "@jollytoad/store-common/key-utils"; +import type { StorageKey, StorageModule } from "@jollytoad/store-common/types"; export type { StorageKey, StorageModule }; @@ -12,10 +12,15 @@ export type { StorageKey, StorageModule }; listItems, clearItems, close, + url, }) satisfies StorageModule; const SEP = "/"; +export function url(): Promise { + return Promise.resolve(import.meta.url); +} + export function isWritable(_key?: StorageKey): Promise { return Promise.resolve(true); } @@ -83,7 +88,7 @@ export function clearItems(keyPrefix: StorageKey): Promise { return Promise.resolve(); } -export function close() { +export function close(): Promise { return Promise.resolve(); } diff --git a/store-web-storage/store.test.ts b/store-web-storage/store.test.ts new file mode 100644 index 0000000..29e9d4f --- /dev/null +++ b/store-web-storage/store.test.ts @@ -0,0 +1,28 @@ +import { + open, + testClearItems, + testGetItem, + testHasItem, + testListItems, + testOrdering, + testRemoveItem, + testSetItem, + testUrl, +} from "../store-common/test-storage-module.ts"; +import * as store from "./mod.ts"; + +Deno.test("store-web-storage", async (t) => { + try { + await open(t, store); + await testUrl(t, store, "store-web-storage"); + await testSetItem(t, store); + await testHasItem(t, store); + await testGetItem(t, store); + await testListItems(t, store); + await testRemoveItem(t, store); + await testClearItems(t, store); + await testOrdering(t, store); + } finally { + await store.close(); + } +}); diff --git a/store/README.md b/store/README.md new file mode 100644 index 0000000..71a2fdc --- /dev/null +++ b/store/README.md @@ -0,0 +1,160 @@ +# Pluggable Storage Modules + +This package provides a pluggable hierarchical key -> value storage mechanism. + +Supported underlying mechanisms: + +- Filesystem (via Deno functions) +- Web Storage (`localStorage`) +- Deno KV + +You can use this particular package to allow dynamic switching of the +implementation, either via function call at the start of your app or via a +environment variable. + +Alternatively you can use one of the implementation packages directly, or +aliased via your import map. + +## Via import map + +Map one of the implementations to a bare module specifier: + +```json +{ + "imports": { + "$store": "jsr:@jollytoad/store-deno-fs" + } +} +``` + +and then just import it: + +```ts +import { setItem } from "$store"; + +await setItem(["store", "hello"], "world"); +``` + +You can switch your implementation simply by changing what `$store` maps to in +the import map. + +(btw, you can choose whatever alias you like, it doesn't have to be `$store`) + +## Via initialization function + +```ts +import { setStore } from "jsr:@jollytoad/store"; +import * as store from "jsr:@jollytoad/store-deno-fs"; + +setStore(store); +``` + +or even via dynamic import (no need to await the import): + +```ts +import { setStore } from "jsr:@jollytoad/store"; + +setStore(import("jsr:@jollytoad/store-deno-fs")); +``` + +and then use the functions from `jsr:@jollytoad/store`: + +```ts +import { setItem } from "jsr:@jollytoad/store"; + +await setItem(["store", "hello"], "world"); +``` + +## Via environment variable + +Set the `STORAGE_MODULE` environment variable to the URL of the preferred +storage module. + +_(TODO: Test whether `jsr:` specifiers work for the env var)_ + +```sh +STORAGE_MODULE=jsr:@jollytoad/store-deno-fs deno run --allow-env --allow-net ... +``` + +and then use the functions from `jsr:@jollytoad/store`: + +```ts +import { setItem } from "jsr:@jollytoad/store"; + +await setItem(["store", "hello"], "world"); +``` + +## Why a Module and not a Class? + +Exposing the storage functions via a module allows you to just import it and use +it wherever you need it, there is no need for any dependency injection +mechanism. + +In fact, you could consider the import map to be the dependency injection. + +## Functions + +Each function takes a hierarchical key as an array of strings. It's up to the +storage module how those are translated to the underlying storage. But it may be +best to assume the first level key to be a grouping level, eg. a database name. + +Any JSON serializable value can be stored. + +See the [types](./store-common/types.ts) for a description of the module +interface. + +## Modules + +### [web-storage](https://jsr.io/@jollytoad/store-web-storage) + +This uses `localStorage` of the standard +[Web Storage](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API) +API. + +The parts of the key are joined with a `/` to form a single key string for use +with the `localStorage` API. + +Import mapping: `"$store": "jsr:@jollytoad/store-web-storage"` + +### [deno-fs](https://jsr.io/@jollytoad/store-deno-fs) + +This stores values in individual files under a directory hierarchy via +[Deno fs](https://deno.land/api?s=Deno.readTextFile) calls. By default this is +under a `.store` dir under the current working dir. This can be overridden via +the environment var `STORE_FS_ROOT`. + +Each level of the key becomes a directory up to the last segment which becomes a +JSON file. + +eg: `["one", "two", "three"]` -> `.store/one/two/three.json` + +Import mapping: `"$store": "jsr:@jollytoad/store-deno-fs"` + +### [deno-kv](https://jsr.io/@jollytoad/store-deno-kv) + +Uses the [Deno KV](https://deno.land/manual/runtime/kv) API for storage. + +Import mapping: `"$store": "jsr:@jollytoad/store-deno-kv"` + +### [deno-kv-fs](https://jsr.io/@jollytoad/store-deno-kv-fs) + +Combination of a readonly `deno-fs` and writeable `deno-kv`, allowing fallback +or immutable storage in the filesystem, and mutable storage via the KV store. + +By default the filesystem takes priority, and cannot be overridden by KV values, +unless the env var `STORE_PRIMARY` is set to `kv`, in which case the KV always +overrides filesystem values. + +Import mapping: `"$store": "jsr:@jollytoad/store-deno-kv-fs"` + +### Bring your own + +If these don't fulfil your needs then you can implement your own storage module +based on the [interface](./store-common/types.ts), and switch to it. + +See the existing implementations for inspiration... + +- [web-storage](./store-web-storage/mod.ts) +- [deno-fs](./store-deno-fs/mod.ts) +- [deno-kv](./store-deno-kv/mod.ts) +- [deno-kv-fs](./store-deno-kv-fs/mod.ts) diff --git a/store/deno.json b/store/deno.json new file mode 100644 index 0000000..7b2a0ae --- /dev/null +++ b/store/deno.json @@ -0,0 +1,7 @@ +{ + "name": "@jollytoad/store", + "version": "0.1.0", + "exports": { + ".": "./mod.ts" + } +} diff --git a/store/mod.ts b/store/mod.ts new file mode 100644 index 0000000..b081422 --- /dev/null +++ b/store/mod.ts @@ -0,0 +1,81 @@ +import type { StorageKey, StorageModule } from "@jollytoad/store-common/types"; + +export type { StorageKey, StorageModule }; + +({ + isWritable, + hasItem, + getItem, + setItem, + removeItem, + listItems, + clearItems, + close, + url, +}) satisfies StorageModule; + +let store: Promise | undefined; + +export function setStore( + storageModule?: StorageModule | Promise, +) { + store = storageModule ? Promise.resolve(storageModule) : undefined; +} + +export async function getStore(): Promise { + if (!store) { + const moduleSpecifier = Deno.env.get("STORAGE_MODULE"); + if (moduleSpecifier) { + store = import(import.meta.resolve(moduleSpecifier)); + } else { + throw new Error( + "A StorageModule was not selected, either via `setStore()`, or the `STORAGE_MODULE` env var", + ); + } + } + return await store; +} + +export async function url(): Promise { + return (await getStore()).url(); +} + +export async function isWritable(key: StorageKey = []): Promise { + return (await getStore()).isWritable(key); +} + +export async function hasItem(key: StorageKey): Promise { + return (await getStore()).hasItem(key); +} + +export async function getItem(key: StorageKey): Promise { + return (await getStore()).getItem(key) as Promise; +} + +export async function setItem(key: StorageKey, value: T): Promise { + return (await getStore()).setItem(key, value); +} + +export async function removeItem(key: StorageKey): Promise { + return (await getStore()).removeItem(key); +} + +export async function* listItems( + prefix: StorageKey = [], + reverse = false, +): AsyncIterable<[StorageKey, T]> { + yield* (await getStore()).listItems(prefix, reverse) as AsyncIterable< + [StorageKey, T] + >; +} + +export async function clearItems(prefix: StorageKey): Promise { + await getStore(); + return (await getStore()).clearItems(prefix); +} + +export async function close(): Promise { + if (store) { + (await store)?.close(); + } +} diff --git a/store/store.test.ts b/store/store.test.ts new file mode 100644 index 0000000..331509c --- /dev/null +++ b/store/store.test.ts @@ -0,0 +1,51 @@ +import { + open, + testClearItems, + testGetItem, + testHasItem, + testListItems, + testRemoveItem, + testSetItem, + testUrl, +} from "../store-common/test-storage-module.ts"; +import * as store from "./mod.ts"; + +Deno.test("store - via STORAGE_MODULE", async (t) => { + try { + Deno.env.set( + "STORAGE_MODULE", + import.meta.resolve("../store-web-storage/mod.ts"), + ); + + // clear the store implementation, so it gets picked up by the env var + store.setStore(); + + await open(t, store); + await testUrl(t, store, "store-web-storage"); + await testSetItem(t, store); + await testHasItem(t, store); + await testGetItem(t, store); + await testListItems(t, store); + await testRemoveItem(t, store); + await testClearItems(t, store); + } finally { + await store.close(); + } +}); + +Deno.test("store - via setStore()", async (t) => { + try { + store.setStore(import("../store-deno-kv/mod.ts")); + + await open(t, store); + await testUrl(t, store, "store-deno-kv"); + await testSetItem(t, store); + await testHasItem(t, store); + await testGetItem(t, store); + await testListItems(t, store); + await testRemoveItem(t, store); + await testClearItems(t, store); + } finally { + await store.close(); + } +});