From e7138d377c4f6ea242cea645cda4d26eb7d7d377 Mon Sep 17 00:00:00 2001 From: Niels Klomp Date: Fri, 19 May 2023 14:00:12 +0200 Subject: [PATCH] feat(kv-store): add key-value store based on a typescript port of the keyv package (#1150) --- authors.md | 1 + packages/cli/src/dev.ts | 2 +- packages/kv-store/CHANGELOG.md | 0 packages/kv-store/LICENSE | 201 +++++++++++ packages/kv-store/README.md | 140 ++++++++ packages/kv-store/api-extractor.json | 18 + packages/kv-store/package.json | 54 +++ packages/kv-store/src/__tests__/keyv.test.ts | 184 ++++++++++ .../kv-store/src/__tests__/kvstore.test.ts | 92 +++++ packages/kv-store/src/index.ts | 10 + packages/kv-store/src/key-value-store.ts | 123 +++++++ packages/kv-store/src/key-value-types.ts | 132 +++++++ packages/kv-store/src/keyv/keyv-types.ts | 84 +++++ packages/kv-store/src/keyv/keyv.ts | 319 +++++++++++++++++ packages/kv-store/src/store-adapters/index.ts | 2 + .../src/store-adapters/tiered/index.ts | 175 ++++++++++ .../src/store-adapters/tiered/types.ts | 16 + .../typeorm/entities/keyValueStoreEntity.ts | 20 ++ .../src/store-adapters/typeorm/index.ts | 131 +++++++ .../typeorm/migrations/1.createKVDatabase.ts | 50 +++ .../typeorm/migrations/index.ts | 10 + .../src/store-adapters/typeorm/types.ts | 25 ++ packages/kv-store/tsconfig.json | 13 + packages/tsconfig.json | 1 + pnpm-lock.yaml | 327 +++++++++++++----- 25 files changed, 2051 insertions(+), 79 deletions(-) create mode 100644 packages/kv-store/CHANGELOG.md create mode 100644 packages/kv-store/LICENSE create mode 100644 packages/kv-store/README.md create mode 100644 packages/kv-store/api-extractor.json create mode 100644 packages/kv-store/package.json create mode 100644 packages/kv-store/src/__tests__/keyv.test.ts create mode 100644 packages/kv-store/src/__tests__/kvstore.test.ts create mode 100644 packages/kv-store/src/index.ts create mode 100644 packages/kv-store/src/key-value-store.ts create mode 100644 packages/kv-store/src/key-value-types.ts create mode 100644 packages/kv-store/src/keyv/keyv-types.ts create mode 100644 packages/kv-store/src/keyv/keyv.ts create mode 100644 packages/kv-store/src/store-adapters/index.ts create mode 100644 packages/kv-store/src/store-adapters/tiered/index.ts create mode 100644 packages/kv-store/src/store-adapters/tiered/types.ts create mode 100644 packages/kv-store/src/store-adapters/typeorm/entities/keyValueStoreEntity.ts create mode 100644 packages/kv-store/src/store-adapters/typeorm/index.ts create mode 100644 packages/kv-store/src/store-adapters/typeorm/migrations/1.createKVDatabase.ts create mode 100644 packages/kv-store/src/store-adapters/typeorm/migrations/index.ts create mode 100644 packages/kv-store/src/store-adapters/typeorm/types.ts create mode 100644 packages/kv-store/tsconfig.json diff --git a/authors.md b/authors.md index f80265a59..a65663bad 100644 --- a/authors.md +++ b/authors.md @@ -34,3 +34,4 @@ Special thanks to the following extraordinary individuals, for their contributio - [Devraj Singh Rawat](https://github.com/devrajsinghrawat) - [Eseoghene Mentie](https://github.com/Eseoghene) - [Ilan Olkies](https://github.com/ilanolkies) +- [Niels Klomp](https://github.com/nklomp) diff --git a/packages/cli/src/dev.ts b/packages/cli/src/dev.ts index eb36713c1..3e11c1f6a 100644 --- a/packages/cli/src/dev.ts +++ b/packages/cli/src/dev.ts @@ -31,7 +31,7 @@ function createSchema(generator: TJS.SchemaGenerator, symbol: string) { return { components: { schemas: {} } } } - let fixedSymbol = symbol.replace('Array<', '').replace('>', '') + let fixedSymbol = symbol.replace(/Array\<(.*)\>/gm, '$1') const schema = generator.createSchema(fixedSymbol) diff --git a/packages/kv-store/CHANGELOG.md b/packages/kv-store/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/packages/kv-store/LICENSE b/packages/kv-store/LICENSE new file mode 100644 index 000000000..fd815d7f8 --- /dev/null +++ b/packages/kv-store/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 Consensys AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/kv-store/README.md b/packages/kv-store/README.md new file mode 100644 index 000000000..d34252813 --- /dev/null +++ b/packages/kv-store/README.md @@ -0,0 +1,140 @@ +# Veramo Key Value store + +A simple typed Key Value store with out of the box support for in memory/maps, sqlite and typeorm implementations for +usage in browser, NodeJS and React-Native. +It includes a tiered local/remote implementation that support all environments. + +# Usage in your application or module + +## setup + +Veramo provides 3 store adapters out of the box. The first is an in memory Map based store, the second is a TypeORM +based store, meaning we support multiple databases. Lastly there is a tiered store, which allows you to use a local and +remote store together. + +You should also be able to use store adapters from the [keyv](https://github.com/jaredwray/keyv) project. Be aware that +Veramo uses an internal fork of that project, as we have several constraints not present in the upstream project. As a +result the adapters are not guaranteed to work in your environment. +See [Keyv project relationship](#keyv-project-relationship) for more information. + +### Map based in memory store + +This is the simplest implementation where a Map is being used to store the Keys and Values + +````typescript +import { IKeyValueStore, IKeyValueStoreOptions, KeyValueStore } from '@veramo/key-value-store' + +const options: IKeyValueStoreOptions = { + namespace: 'example', + store: new Map() +} +const kvStore: IKeyValueStore = new KeyValueStore({ options }) +```` + +### TypeORM store + +This implementation uses TypeORM using a simple entity to store the namespace prefixed key, an expiration value and the +value itself. It supports multiple database backends. + +````typescript +import { + IKeyValueStore, + IKeyValueStoreOptions, + KeyValueStore, + KeyValueStoreEntity, + KeyValueTypeORMStoreAdapter +} from '@veramo/key-value-store' +import { DataSource } from 'typeorm' + +const dbConnection: DataSource = await new DataSource({ + type: 'sqlite', + database: ':memory:', + logging: 'all', + migrationsRun: true, + synchronize: false, + entities: [KeyValueStoreEntity], +}).initialize() + +const options: IKeyValueStoreOptions = { + namespace: 'example', + store: new KeyValueTypeORMStoreAdapter({ dbConnection }) +} +const kvStore: IKeyValueStore = new KeyValueStore({ options }) +```` + +### Tiered store + +The tiered store expects a local store and a remote store. Obviously it makes most sense to have a memory based local +store and potentially more expensive/slower remote store + +````typescript +import { + kvStoreMigrations, + IKeyValueStore, + IKeyValueStoreOptions, + KeyValueStore, + KeyValueStoreEntity, + KeyValueTieredStoreAdapter, + KeyValueTypeORMStoreAdapter +} from '@veramo/key-value-store' +import { DataSource } from 'typeorm' + +dbConnection = await new DataSource({ + type: 'sqlite', + database: ':memory:', + logging: 'all', + migrationsRun: true, + synchronize: false, + migrations: [...kvStoreMigrations], + entities: [KeyValueStoreEntity], +}).initialize() + +const local: Map = new Map() +const remote = new KeyValueTypeORMStoreAdapter({ dbConnection }) + +const options: IKeyValueStoreOptions = { + namespace: 'example', + store: new KeyValueTieredStoreAdapter({ local, remote }) +} +const kvStore: IKeyValueStore = new KeyValueStore({ options }) +```` + +## Usage + +After you have setup the Key Value Store as described above in your agent, it will be available in your agent. + +````typescript + +kvStore.set('hello', { example: world }) // Stores the value object by key untill deleted +kvStore.set('expiring', { example: 'expiration' }, 1000) // Stores the value object by key for 1 second +const value = await kvStore.get('hello') // { example: world } +const values = await kvStore.getMany(['hello', 'expiring']) // [{ example: world }, {example: expiring}] + +if (await kvStore.has('hello')) { + await kvStore.delete('hello') // return a boolean for success. You can also call it on non existing keys, which will return false +} + +await kvStore.clear() // Removes all keys/values from the store +kvStore.disconnect() // disconnects the store from it backing adapter. You cannot reuse the store afterwards unlesss you initialize a new store. + +```` + +# Keyv project relationship + +Please note that a large portion of the Veramo Key Value Store code is a port of +the [keyv](https://github.com/jaredwray/keyv) project, adding support for Typescript and ESM, so it can be used Veramo + +The ported code should support the storage plugins available for the keyv project, although testing has been limited +Veramo itself has a requirement to support NodeJS, Browser and React-Native environments. The port and the store +adapters included in Veramo run in these environments. +Please be aware that these requirements probably aren't true for any keyv storage plugins. + +One of the big changes compared to the upstream project is that this port does not have dynamic loading of adapters +based on URIs. We believe that any Veramo Key Value store should use explicit defined adapters. + +The keyv port is part of the Veramo Key Value Store module code itself, as we do not want to make an official maintained port at this point. + +Veramo exposes its own API/interfaces for the Key Value store, meaning we could also support any other implementation in +the future + +We welcome any new storage modules diff --git a/packages/kv-store/api-extractor.json b/packages/kv-store/api-extractor.json new file mode 100644 index 000000000..409d7f16c --- /dev/null +++ b/packages/kv-store/api-extractor.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "apiReport": { + "enabled": true, + "reportFolder": "./api", + "reportTempFolder": "./api" + }, + + "docModel": { + "enabled": true, + "apiJsonFilePath": "./api/.api.json" + }, + + "dtsRollup": { + "enabled": false + }, + "mainEntryPointFilePath": "/build/index.d.ts" +} diff --git a/packages/kv-store/package.json b/packages/kv-store/package.json new file mode 100644 index 000000000..9ce64a008 --- /dev/null +++ b/packages/kv-store/package.json @@ -0,0 +1,54 @@ +{ + "name": "@veramo/kv-store", + "description": "Veramo Key Value Store plugin", + "version": "5.1.2", + "main": "build/index.js", + "exports": "./build/index.js", + "types": "build/index.d.ts", + "scripts": { + "build": "tsc", + "test": "jest --config=../../jest.config.mjs", + "test:ci": "jest --config=../../jest.config.mjs", + "extract-api": "node ../cli/bin/veramo.js dev extract-api" + }, + "dependencies": { + "@veramo/core-types": "workspace:*", + "@veramo/utils": "workspace:*", + "events": "^3.3.0", + "debug": "^4.3.4", + "json-buffer": "^3.0.1", + "typeorm": "^0.3.10", + "uint8arrays": "^3.1.1" + }, + "devDependencies": { + "@types/json-buffer": "^3.0.0", + "@types/debug": "^4.1.7", + "keyv": "^4.5.2", + "@keyv/test-suite": "^1.9.2", + "@keyv/sqlite": "^3.6.5", + "@keyv/compress-brotli": "^1.1.3", + "@keyv/compress-gzip": "^1.2.3", + "eslint": "^8.33.0", + "eslint-plugin-promise": "^6.1.1", + "timekeeper": "^2.2.0", + "typescript": "^4.9.5" + }, + "files": [ + "build/**/*", + "src/**/*", + "README.md", + "LICENSE" + ], + "publishConfig": { + "access": "public" + }, + "repository": "git@github.com:uport-project/veramo.git", + "author": "Niels Klomp ", + "license": "Apache-2.0", + "keywords": ["Veramo", "Key Value Store", "keyv"], + "type": "module", + "moduleDirectories": [ + "node_modules", + "src" + ] +} diff --git a/packages/kv-store/src/__tests__/keyv.test.ts b/packages/kv-store/src/__tests__/keyv.test.ts new file mode 100644 index 000000000..244d8d41a --- /dev/null +++ b/packages/kv-store/src/__tests__/keyv.test.ts @@ -0,0 +1,184 @@ +import KeyvSqlite from '@keyv/sqlite' + +import timekeeper from 'timekeeper' +import { Keyv } from '../keyv/keyv.js' +import { DataSource } from 'typeorm' +import { KeyValueStoreEntity } from '../store-adapters/typeorm/entities/keyValueStoreEntity.js' +import { KeyValueTieredStoreAdapter, KeyValueTypeORMStoreAdapter } from '../store-adapters/index.js' +import { KeyvOptions } from '../keyv/keyv-types.js' +import { kvStoreMigrations } from '../store-adapters/typeorm/migrations' + +let dbConnection: DataSource +beforeEach(async () => { + dbConnection = await new DataSource({ + type: 'sqlite', + database: ':memory:', + logging: ['error'], + migrationsRun: true, + synchronize: false, + migrations: [...kvStoreMigrations], + entities: [KeyValueStoreEntity], + }).initialize() +}) +afterEach(async () => { + try { + if (dbConnection?.isInitialized) { + await (await dbConnection).destroy() + } + } catch (error) { + // the disconnect test will close the DB anyway + } +}) +describe('keyv MAP store', () => { + it('should respect ttl', async () => { + const store = new Map() + const keyv = new Keyv(store) + await keyv.set('key', 'value', 100) + + expect(await keyv.get('key')).toEqual('value') + timekeeper.freeze(Date.now() + 150) + expect(await keyv.get('key')).toBeUndefined() + }) + it('should work for all methods', async () => { + const store = new Map() + await testAllKeyvMethods(store) + }) +}) + +describe('keyv sqlite store', () => { + it('should respect ttl', async () => { + const keyv = new Keyv(new KeyvSqlite()) + await keyv.set('key', 'value', 100) + + expect(await keyv.get('key')).toEqual('value') + timekeeper.freeze(Date.now() + 150) + expect(await keyv.get('key')).toBeUndefined() + }) + it('should work for all methods', async () => { + const store = new KeyvSqlite() + await testAllKeyvMethods(store) + }) +}) + +describe('keyv TypeORM store', () => { + it('should respect ttl', async () => { + const store = new KeyValueTypeORMStoreAdapter({ dbConnection, namespace: 'test' }) + const keyv = new Keyv(store) + await keyv.set('key', 'value', 100) + + expect(await keyv.get('key')).toEqual('value') + timekeeper.freeze(Date.now() + 150) + expect(await keyv.get('key')).toBeUndefined() + }) + + it('should set a value that can be retrieved from the proper namespace', async () => { + const store = new KeyValueTypeORMStoreAdapter({ dbConnection, namespace: 'test' }) + const keyv = new Keyv(store) + await keyv.set('key', 'value') + expect(await keyv.get('key')).toEqual('value') + + const alternateStore = new KeyValueTypeORMStoreAdapter({ dbConnection, namespace: 'another' }) + const alternateKeyv = new Keyv(alternateStore) + expect(await alternateKeyv.get('key')).toBeUndefined() + }) + + it('should set multiple values that can be retrieved', async () => { + const store = new KeyValueTypeORMStoreAdapter({ dbConnection, namespace: 'test' }) + const keyv = new Keyv(store) + for (let i = 0; i < 10; i++) { + await keyv.set(`${i}`, `value${i}`) + } + + let values = await keyv.getMany(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'nope']) + expect(values).toHaveLength(11) + + await keyv.delete('0') + expect(await keyv.getMany(['0'])).toHaveLength(1) + values = await keyv.getMany(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'nope']) + expect(values).toHaveLength(11) + expect(values[9]).toBe('value9') + expect(values[10]).toBeUndefined() + + await keyv.clear() + values = await keyv.getMany(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'nope']) + expect(values).toHaveLength(11) + expect(values[0]).toBeUndefined() + expect(values[9]).toBeUndefined() + expect(values[10]).toBeUndefined() + }) + it('should work for all methods', async () => { + const store = new KeyValueTypeORMStoreAdapter({ dbConnection, namespace: 'test-all' }) + await testAllKeyvMethods(store) + await store.disconnect() + }) +}) + +describe('keyv tiered store', () => { + it('should respect ttl', async () => { + const local: Map = new Map() + const remote = new KeyValueTypeORMStoreAdapter({ dbConnection }) + + const options: KeyvOptions = { + namespace: 'example', + store: new KeyValueTieredStoreAdapter({ local, remote }), + } + const keyv = new Keyv(undefined, options) + await keyv.set('key', 'value', 100) + + expect(await keyv.get('key')).toEqual('value') + timekeeper.freeze(Date.now() + 150) + expect(await keyv.get('key')).toBeUndefined() + await keyv.disconnect() + }) + it('should work for all methods', async () => { + const local: Map = new Map() + const remote = new KeyValueTypeORMStoreAdapter({ dbConnection }) + + const options: KeyvOptions = { + namespace: 'example', + store: new KeyValueTieredStoreAdapter({ local, remote }), + } + const store = new Keyv(undefined, options) + + // No raw match test, as remote and local have different raw values + await testAllKeyvMethods(store, false) + }) +}) + +async function testAllKeyvMethods(store: any, rawMatchTest = true) { + const keyv = new Keyv(store) + + // set value + await expect(keyv.set('key1', 'value1')).resolves.toEqual(true) + await expect(keyv.set('key2', 'value2')).resolves.toEqual(true) + + // get value by key + await expect(keyv.get('key1', { raw: false })).resolves.toEqual('value1') + await expect(keyv.get('key1', { raw: true })).resolves.toMatchObject({ value: 'value1' }) + + // get value by non-existing key + await expect(keyv.get('key3', { raw: false })).resolves.toBeUndefined() + await expect(keyv.get('key3', { raw: true })).resolves.toBeUndefined() + + // Get many as non-raw and raw + await expect(keyv.getMany(['key1', 'key2'], { raw: false })).resolves.toMatchObject(['value1', 'value2']) + if (rawMatchTest) { + await expect(keyv.getMany(['key1', 'key2'], { raw: true })).resolves.toMatchObject([ + { value: 'value1' }, + { value: 'value2' }, + ]) + } + + // Check existence + await expect(keyv.has('key1')).resolves.toEqual(true) + await expect(keyv.has('key3')).resolves.toEqual(false) + + // delete key1 only + await expect(keyv.delete('key1')).resolves.toEqual(true) + await expect(keyv.has('key1')).resolves.toEqual(false) + await expect(keyv.has('key2')).resolves.toEqual(true) + + // clear + await keyv.clear() + await expect(keyv.has('key2')).resolves.toEqual(false) +} diff --git a/packages/kv-store/src/__tests__/kvstore.test.ts b/packages/kv-store/src/__tests__/kvstore.test.ts new file mode 100644 index 000000000..fd01b070d --- /dev/null +++ b/packages/kv-store/src/__tests__/kvstore.test.ts @@ -0,0 +1,92 @@ +import { KeyValueStore } from '../key-value-store.js' +import { IKeyValueStore } from '../key-value-types.js' + +interface TestValue { + stringProp: string + numberProp: number + objectProp: { + value: string | number + } +} + +let testValues: TestValue[] = [] + +for (let i = 0; i < 10; i++) { + testValues.push({ + stringProp: `stringValue${i}`, + numberProp: i, + objectProp: { + value: `objectValue${i}`, + }, + }) +} +describe('kvStore with MAP adapter', () => { + let kvStore: IKeyValueStore + beforeEach(async () => { + const store = new Map() + kvStore = new KeyValueStore({ store }) + for (let i = 0; i < 10; i++) { + await kvStore.set(`key${i}`, testValues[i]) + } + }) + afterEach(async () => { + try { + await kvStore.clear() + await kvStore.disconnect() + } catch (error) {} + }) + it('should get non-existing keys as undefined', async () => { + await expect(kvStore.get('nope')).resolves.toBeUndefined() + await expect(kvStore.getAsValueData('nope')).resolves.toMatchObject({ + value: undefined, + expires: undefined, + }) + }) + + it('should get single results', async () => { + for (let i = 0; i < 10; i++) { + await expect(kvStore.get(`key${i}`)).resolves.toEqual(testValues[i]) + await expect(kvStore.getAsValueData(`key${i}`)).resolves.toMatchObject({ + value: testValues[i], + }) + } + }) + + it('should get multiple results', async () => { + // Let's get multiple results + const manyResult = await kvStore.getMany(['key1', 'nope', 'key4']) + expect(manyResult).toHaveLength(3) + expect(manyResult[0]).toEqual(testValues[1]) + expect(manyResult[1]).toBeUndefined() + expect(manyResult[2]).toEqual(testValues[4]) + const manyValueResult = await kvStore.getManyAsValueData(['key1', 'nope', 'key4']) + expect(manyValueResult).toHaveLength(3) + expect(manyValueResult[0]).toEqual({ value: testValues[1], expires: undefined }) + expect(manyValueResult[1]).toEqual({ value: undefined, expires: undefined }) + expect(manyValueResult[2]).toEqual({ value: testValues[4], expires: undefined }) + }) + + it('should check existence of keys', async () => { + await expect(kvStore.has(`key1`)).resolves.toEqual(true) + await expect(kvStore.has(`nope`)).resolves.toEqual(false) + }) + + it('should delete an existing key', async () => { + await expect(kvStore.has(`key1`)).resolves.toEqual(true) + await expect(kvStore.delete(`key1`)).resolves.toEqual(true) + await expect(kvStore.has(`key1`)).resolves.toEqual(false) + }) + + it('should not throw an error when deleting a non-existing key', async () => { + await expect(kvStore.delete(`nope`)).resolves.toEqual(false) + }) + + it('should delete multiple values', async () => { + await expect(kvStore.deleteMany(['key2', 'nope', 'key6'])).resolves.toEqual([true, false, true]) + }) + + it('should clear all', async () => { + await kvStore.clear() + await expect(kvStore.has(`key1`)).resolves.toEqual(false) + }) +}) diff --git a/packages/kv-store/src/index.ts b/packages/kv-store/src/index.ts new file mode 100644 index 000000000..415492c9d --- /dev/null +++ b/packages/kv-store/src/index.ts @@ -0,0 +1,10 @@ +/** + * Provides a {@link @veramo/did-manager#DIDManager | plugin} for the + * {@link @veramo/core#Agent} that implements {@link @veramo/core-types#IDIDManager} interface. + * + * @packageDocumentation + */ +export { KeyValueStore } from './key-value-store.js' +export * from './store-adapters/tiered/index.js' +export * from './store-adapters/typeorm/index.js' +export * from './key-value-types.js' diff --git a/packages/kv-store/src/key-value-store.ts b/packages/kv-store/src/key-value-store.ts new file mode 100644 index 000000000..4230a9cc0 --- /dev/null +++ b/packages/kv-store/src/key-value-store.ts @@ -0,0 +1,123 @@ +import { + IKeyValueStore, + IKeyValueStoreOnArgs, + IKeyValueStoreOptions, + IValueData, + ValueStoreType, +} from './key-value-types.js' +import { Keyv } from './keyv/keyv.js' +import { KeyvDeserializedData, KeyvOptions, KeyvStoredData } from './keyv/keyv-types.js' + +/** + * Class that implements the {@link @veramo/kv-store#IKeyValueStore} interface + * + * This class is a Key Value store that leverages a port of the keyv package internally. + * The store does not leak the types of the Keyv port, + * so it could be replaced with another implementations if we want to. + * + * @public + */ +export class KeyValueStore implements IKeyValueStore { + /** + * The main keyv typescript port which delegates to the storage adapters and takes care of some common functionality + * + * @internal + */ + private readonly keyv: Keyv + + constructor(options: IKeyValueStoreOptions) { + this.keyv = new Keyv(options.uri, options as KeyvOptions) + } + + async get(key: string): Promise { + const result = await this.keyv.get(key, { raw: false }) + if (result === null || result === undefined) { + return undefined + } + return result as ValueType + } + + async getAsValueData(key: string): Promise> { + const result = await this.keyv.get(key, { raw: true }) + if (result === null || result === undefined) { + // We always return a ValueData object for this method + return { value: undefined, expires: undefined } + } + return this.toDeserializedValueData(result) + } + + async getMany(keys: string[]): Promise> { + if (!keys || keys.length === 0) { + return [] + } + let result = await this.keyv.getMany(keys, { raw: false }) + + // Making sure we return the same array length as the amount of key(s) passed in + if (result === null || result === undefined || result.length === 0) { + result = new Array() + for (const key of keys) { + result.push(undefined) + } + } + return result.map((v) => (!!v ? (v as ValueType) : undefined)) + } + + async getManyAsValueData(keys: string[]): Promise>> { + if (!keys || keys.length === 0) { + return [] + } + let result = await this.keyv.getMany(keys, { raw: true }) + + // Making sure we return the same array length as the amount of key(s) passed in + if (result === null || result === undefined || result.length === 0) { + result = new Array>() + for (const key of keys) { + result.push({ value: undefined, expires: undefined } as unknown as KeyvDeserializedData) + } + } + return result.map((v) => + !!v ? this.toDeserializedValueData(v) : { value: undefined, expires: undefined }, + ) + } + + async set(key: string, value: ValueType, ttl?: number): Promise> { + return this.keyv.set(key, value, ttl).then(() => this.getAsValueData(key)) + } + + async has(key: string): Promise { + return this.keyv.has(key) + } + + async delete(key: string): Promise { + return this.keyv.delete(key) + } + + async deleteMany(keys: string[]): Promise { + return Promise.all(keys.map((key) => this.keyv.delete(key))) + } + + async clear(): Promise> { + return this.keyv.clear().then(() => this) + } + + async disconnect(): Promise { + return this.keyv.disconnect() + } + + // Public so parties using the kv store directly can add listeners if they want + async kvStoreOn(args: IKeyValueStoreOnArgs): Promise> { + this.keyv.on(args.eventName, args.listener) + return this + } + + private toDeserializedValueData(result: any): IValueData { + if (result === null || result === undefined) { + throw Error(`Result cannot be undefined or null at this this point`) + } else if (typeof result !== 'object') { + return { value: result, expires: undefined } + } else if (!('value' in result)) { + return { value: result, expires: undefined } + } + return result as IValueData + } +} diff --git a/packages/kv-store/src/key-value-types.ts b/packages/kv-store/src/key-value-types.ts new file mode 100644 index 000000000..8b3f5b6c2 --- /dev/null +++ b/packages/kv-store/src/key-value-types.ts @@ -0,0 +1,132 @@ +/** + * This is how the store will actually store the value. + * It contains an optional `expires` property, which indicates when the value would expire + * + * @beta + */ +export interface IValueData { + value?: ValueType + expires?: number +} + +/** + * Event listener arguments + * + * @beta + */ +export interface IKeyValueStoreOnArgs { + eventName: string | symbol + listener: (...args: any[]) => void +} + +/** + * Options for the Key Value store + * + * @beta + */ +export interface IKeyValueStoreOptions { + [key: string]: any + + /** Namespace for the current instance. */ + namespace?: string | undefined + + /** The connection string URI. */ + uri?: string | undefined + /** The storage adapter instance to be used by Keyv. or any other implementation */ + store: IKeyValueStoreAdapter | Map + /** Default TTL. Can be overridden by specifying a TTL on `.set()`. */ + ttl?: number | undefined + + emitErrors?: boolean +} + +/** + * A store adapter implementation needs to provide namespace support + * + * @beta + */ +export interface IKeyValueStoreAdapter { + namespace?: string | undefined +} + +/** + * The types that can be stored by a store adapter + * + * @public + */ +export type ValueStoreType = object | string | number | boolean + +/** + * A Key Value store is responsible for managing Values identified by keys. + * + * @beta + */ +export interface IKeyValueStore { + /** + * Get a single value by key. Can be undefined as the underlying store typically will not throw an error for a non existing key + * + * @param key - Contains the key to search for + */ + get(key: string): Promise + + /** + * Get a single item as Value Data from the store. Will always return a Value Data Object, but the value in it can be undefined in case the actual store does not contain the value + * + * @param key - Contains the key to search for + */ + getAsValueData(key: string): Promise> + + /** + * Get multiple values from the store. Will always return an array with values, but the values can be undefined in case the actual store does not contain the value for the respective key + * + * @param keys - Contains the keys to search for + */ + getMany(keys: string[]): Promise> + + /** + * Get multiple items as Value Data from the store. Will always return an array with Value Data Object, but the value in it can be undefined in case the actual store does not contain the value + * + * @param keys - Contains the keys to search for + */ + getManyAsValueData(keys: string[]): Promise>> + + /** + * Store a single value + * + * @param key - The key + * @param value - The value + * @param ttl - An optional number how long to store the value in milliseconds. If not provided will be stored indefinitely + */ + set(key: string, value: ValueType, ttl?: number): Promise> + + /** + * Delete a value from the store by key + * + * @param key - The key to delete the value for + */ + delete(key: string): Promise + + /** + * Delete multiple values by provided keys + * + * @param keys - The keys to delete the values for + */ + deleteMany(keys: string[]): Promise + + /** + * Clear the whole store (delete all values) + */ + clear(): Promise> + + /** + * Determine whether the store has the value belonging to the provided key + * + * @param key The key to search for + */ + has(key: string): Promise + + /** + * Disconnect the backing store. After this operation the store typically cannot be reused, unless the store object is re-instantiated + */ + disconnect(): Promise +} diff --git a/packages/kv-store/src/keyv/keyv-types.ts b/packages/kv-store/src/keyv/keyv-types.ts new file mode 100644 index 000000000..e23714e39 --- /dev/null +++ b/packages/kv-store/src/keyv/keyv-types.ts @@ -0,0 +1,84 @@ +/** + * Please be aware these types are compatible with the keyv package, to ensure we can use its store-adapters for free. + * + * Be very careful when extending/changing!. Normally you will want to create an adapter or decorator for your additional behaviour or requirements + */ +import { OrPromise } from '@veramo/utils' + +export interface KeyvOptions { + [key: string]: any + + /** Namespace for the current instance. */ + namespace?: string | undefined + /** A custom serialization function. */ + serialize?: (data: KeyvDeserializedData) => OrPromise + /** A custom deserialization function. */ + deserialize?: (data: any) => OrPromise | undefined> + /** The connection string URI. */ + uri?: string | undefined + /** The storage adapter instance to be used by Keyv. */ + store?: KeyvStore | undefined + /** Default TTL. Can be overridden by specififying a TTL on `.set()`. */ + ttl?: number | undefined + /** Specify an adapter to use. e.g `'redis'` or `'mongodb'`. */ + adapter?: + | 'redis' + | 'mongodb' + | 'mongo' + | 'sqlite' + | 'postgresql' + | 'postgres' + | 'mysql' + | object + | string + | undefined + /** Enable compression option **/ + compression?: KeyvCompressionAdapter | undefined + + emitErrors?: boolean +} + +export interface KeyvCompressionAdapter { + compress(value: any, options?: any): OrPromise + + decompress(value: any, options?: any): OrPromise + + serialize(value: any): OrPromise + + deserialize(value: any): OrPromise +} + +export interface KeyvDeserializedData { + value: Value + expires: number | undefined +} + +export type KeyvStoredData = KeyvDeserializedData | string | Value | undefined + +export interface KeyvStore { + namespace?: string | undefined + + opts: KeyvOptions + + on?(eventName: string | symbol, listener: (...args: any[]) => void): void + get( + key: string | string[], + options?: { raw?: boolean }, + ): OrPromise | Array>> + + getMany?(keys: string[], options?: { raw?: boolean }): OrPromise>> | undefined + + iterator?(namespace?: string | undefined): AsyncGenerator + + set(key: string, value: Value, ttl?: number): any + + delete(key: string | string[]): OrPromise + + deleteMany?(keys: string[]): OrPromise + + clear(): OrPromise + + has?(key: string): OrPromise + + disconnect?(): void +} diff --git a/packages/kv-store/src/keyv/keyv.ts b/packages/kv-store/src/keyv/keyv.ts new file mode 100644 index 000000000..0e1fa3a0c --- /dev/null +++ b/packages/kv-store/src/keyv/keyv.ts @@ -0,0 +1,319 @@ +import { EventEmitter } from 'events' +import JSONB from 'json-buffer' +import { KeyvDeserializedData, KeyvOptions, KeyvStore, KeyvStoredData } from './keyv-types.js' + +/** + * Please note that this is code adapted from @link https://github.com/jaredwray/keyv to support Typescript and ESM in Veramo + * + * The code should support the storage plugins available for the keyv project. + * Veramo itself supports NodeJS, Browser and React-Native environment. + * Please be aware that these requirements probably aren't true for any keyv storage plugins. + * + * One of the big changes compared to the upstream project is that this port does not have dynamic loading of store-adapters based on URIs. + * We believe that any Veramo Key Value store should use explicitly defined store-adapters. + * + * The port is part of the Veramo Key Value Store module, as we do not want to make an official maintained port out of it. + * Veramo exposes its own API/interfaces for the Key Value store, meaning we could also support any other implementation in the future + * + * The Veramo kv-store module provides out of the box support for in memory/maps, sqlite and typeorm implementations, + * including a tiered local/remote implementation that support all environments. + * + * We welcome any new storage modules + */ +export class Keyv extends EventEmitter implements KeyvStore { + readonly opts: KeyvOptions + readonly namespace: string + iterator?: (namespace?: string) => AsyncGenerator + + constructor( + uri?: string | Map | KeyvStore | undefined, + options?: KeyvOptions, + ) { + super() + const emitErrors = options?.emitErrors === undefined ? true : options.emitErrors + uri = uri ?? options?.uri + /*if (!uri) { + throw Error('No URI provided') + }*/ + this.opts = { + namespace: 'keyv', + serialize: JSONB.stringify, + deserialize: JSONB.parse, + ...(typeof uri === 'string' ? { uri } : uri), + ...options, + } + + if (!this.opts.store) { + if (typeof uri !== 'string') { + this.opts.store = uri as KeyvStore + } /* else { + const adapterOptions = { ...this.opts } + this.opts.store = loadStore(adapterOptions) + }*/ + } + if (!this.opts.store) { + throw Error('No store') + } + + if (this.opts.compression) { + const compression = this.opts.compression + this.opts.serialize = compression.serialize.bind(compression) + this.opts.deserialize = compression.deserialize.bind(compression) + } + + if (typeof this.opts.store.on === 'function' && emitErrors) { + this.opts.store.on('error', (error) => this.emit('error', error)) + } + + this.opts.store.namespace = this.opts.namespace || 'keyv' + this.namespace = this.opts.store.namespace + + const generateIterator = (iterator: any, keyv: Keyv) => + async function* () { + for await (const [key, raw] of typeof iterator === 'function' + ? iterator(keyv.store.namespace) + : iterator) { + const data = await keyv.deserialize(raw) + if (keyv.store.namespace && !key.includes(keyv.store.namespace)) { + continue + } + + if (data && typeof data.expires === 'number' && Date.now() > data.expires) { + keyv.delete(key) + continue + } + + yield [keyv._getKeyUnprefix(key), data?.value] + } + } + + // Attach iterators + // @ts-ignore + if (typeof this.store[Symbol.iterator] === 'function' && this.store instanceof Map) { + this.iterator = generateIterator(this.store, this) + } else if (typeof this.store.iterator === 'function' && this.store.opts && this._checkIterableAdapter()) { + this.iterator = generateIterator(this.store.iterator.bind(this.store), this) + } + } + + get store(): Required> { + return this.opts.store as Required> + } + + get deserialize() { + return this.opts.deserialize! + } + + get serialize() { + return this.opts.serialize! + } + + _checkIterableAdapter() { + return ( + (this.store.opts.dialect && iterableAdapters.includes(this.store.opts.dialect)) || + (this.store.opts.url && + iterableAdapters.findIndex((element) => this.store.opts.url.includes(element)) >= 0) + ) + } + + _getKeyPrefix(key: string): string { + return `${this.opts.namespace}:${key}` + } + + _getKeyPrefixArray(keys: string[]): string[] { + return keys.map((key) => this._getKeyPrefix(key)) + } + + _getKeyUnprefix(key: string): string { + return key.split(':').splice(1).join(':') + } + + async getMany(keys: string[], options?: { raw?: boolean }): Promise>> { + const keyPrefixed = this._getKeyPrefixArray(keys) + let promise: Promise>> + if (this.store.getMany !== undefined) { + promise = this.store.getMany(keyPrefixed, options) as Promise[]> //.then(value => !!value ? value.values() : undefined) + // todo: Probably wise to check expired ValueData here, if the getMany does not implement this feature itself! + } else { + promise = Promise.all( + keyPrefixed.map((k) => this.store.get(k, options) as Promise>), + ) + } + const allValues = Promise.resolve(promise) + const results: Promise>[] = [] + + return Promise.resolve(allValues) + .then((all) => { + keys.forEach((key, index) => { + const data = all[index] + + let result = + typeof data === 'string' + ? this.deserialize(data) + : !!data && this.opts.compression + ? this.deserialize(data) + : data + + if ( + result && + typeof result === 'object' && + 'expires' in result && + typeof result.expires === 'number' && + Date.now() > result.expires + ) { + this.delete(key) + result = undefined + } + + const final = ( + options && options.raw + ? result + : result && typeof result === 'object' && 'value' in result + ? result.value + : result + ) as Promise> + + results.push(final) + }) + }) + .then(() => Promise.all(results)) + } + + async get( + key: string | string[], + options?: { raw?: boolean }, + ): Promise | KeyvStoredData[] | undefined> { + const isArray = Array.isArray(key) + return Promise.resolve() + .then(() => + isArray + ? this.getMany(this._getKeyPrefixArray(key), options) + : this.store.get(this._getKeyPrefix(key)), + ) + .then((data) => + typeof data === 'string' + ? this.deserialize(data) + : this.opts.compression + ? this.deserialize(data) + : data, + ) + .then((data) => { + if (data === undefined || data === null) { + return undefined + } + const rows = Array.isArray(data) ? data : [data] + + if (isArray) { + const result = [] + + for (let row of rows) { + if (row === undefined || row === null) { + result.push(undefined) + continue + } + + if (this.isExpired(row)) { + this.delete(key).then(() => undefined) + result.push(undefined) + } else { + result.push(options && options.raw ? row : toValue(row)) + } + } + + return result + } else if (!Array.isArray(data)) { + if (this.isExpired(data)) { + return this.delete(key).then(() => undefined) + } + } + + return options && options.raw + ? data + : Array.isArray(data) + ? data.map((d) => toValue(d)) + : toValue(data) + }) + } + + private isExpired(data: KeyvDeserializedData | string | Value): boolean { + return ( + typeof data !== 'string' && + data && + typeof data === 'object' && + 'expires' in data && + typeof data.expires === 'number' && + Date.now() > data.expires + ) + } + + set(key: string, value: Value, ttl?: number) { + const keyPrefixed = this._getKeyPrefix(key) + if (typeof ttl === 'undefined') { + ttl = this.opts.ttl + } + if (ttl === 0) { + ttl = undefined + } + + // @ts-ignore + return Promise.resolve() + .then(() => { + const expires = typeof ttl === 'number' ? Date.now() + ttl : undefined + if (typeof value === 'symbol') { + this.emit('error', 'symbol cannot be serialized') + } + + const input = { value, expires } + return this.serialize(input) + }) + .then((value) => this.store.set(keyPrefixed, value as Value, ttl)) + .then(() => true) + } + + delete(key: string | string[]) { + if (!Array.isArray(key)) { + const keyPrefixed = this._getKeyPrefix(key) + return Promise.resolve().then(() => this.store.delete(keyPrefixed)) + } + + const keyPrefixed = this._getKeyPrefixArray(key) + if (this.store.deleteMany !== undefined) { + return Promise.resolve().then(() => this.store.deleteMany!(keyPrefixed)) + } + + const promises = [] + for (const key of keyPrefixed) { + promises.push(this.store.delete(key)) + } + + return Promise.allSettled(promises).then((values) => values.every((x) => x.valueOf() === true)) + } + + async clear(): Promise { + return Promise.resolve().then(() => this.store.clear()) + } + + has(key: string) { + const keyPrefixed = this._getKeyPrefix(key) + return Promise.resolve().then(async () => { + if (typeof this.store.has === 'function') { + return this.store.has(keyPrefixed) + } + + const value = await this.store.get(keyPrefixed) + return value !== undefined + }) + } + + disconnect() { + if (typeof this.store.disconnect === 'function') { + return this.store.disconnect() + } + } +} + +const iterableAdapters = ['sqlite', 'postgres', 'mysql', 'mongo', 'redis', 'tiered'] + +function toValue(input: KeyvDeserializedData | string | Value) { + return input !== null && typeof input === 'object' && 'value' in input ? input.value : input +} diff --git a/packages/kv-store/src/store-adapters/index.ts b/packages/kv-store/src/store-adapters/index.ts new file mode 100644 index 000000000..4b88a3c38 --- /dev/null +++ b/packages/kv-store/src/store-adapters/index.ts @@ -0,0 +1,2 @@ +export * from './tiered/index.js' +export * from './typeorm/index.js' diff --git a/packages/kv-store/src/store-adapters/tiered/index.ts b/packages/kv-store/src/store-adapters/tiered/index.ts new file mode 100644 index 000000000..9cfac0569 --- /dev/null +++ b/packages/kv-store/src/store-adapters/tiered/index.ts @@ -0,0 +1,175 @@ +import { EventEmitter } from 'events' +import type { Options, Options_ } from './types.js' +import { KeyvStore, KeyvStoredData } from '../../keyv/keyv-types.js' +import { Keyv } from '../../keyv/keyv.js' +import { IKeyValueStoreAdapter } from '../../key-value-types.js' + +type KeyvTieredIndex = 'local' | 'remote' + +/** + * Tiered keyv store adapter, combining 2 adapters/stores into one + * @alpha + */ +export class KeyValueTieredStoreAdapter + extends EventEmitter + implements KeyvStore, IKeyValueStoreAdapter +{ + opts: Options_ + remote: KeyvStore + local: KeyvStore + iterationLimit?: string | number + + namespace?: string | undefined + + constructor({ remote, local, ...options }: Options) { + super() + this.opts = { + validator: () => true, + dialect: 'tiered', + ...options, + } + + // todo: since we are instantiating a new Keyv object in case we encounter a map, the key prefix applied twice, given it will be part of a an outer keyv object as well. + // Probably wise to simply create a Map Store class. As it is an in memory construct, and will work in terms of resolution it does not have highest priority + this.local = (isMap(local) ? new Keyv(local as Map) : local) as KeyvStore + this.remote = (isMap(remote) ? new Keyv(remote as Map) : remote) as KeyvStore + this.namespace = this.local.namespace + } + + async get( + key: string | string[], + options?: { raw?: boolean }, + ): Promise | Array>> { + if (Array.isArray(key)) { + return await this.getMany(key, options) + } + const localResult = (await this.local.get(key, options)) as KeyvStoredData + + if (localResult === undefined || !this.opts.validator(localResult, key)) { + const remoteResult = await this.remote.get(key, options) + + if (remoteResult !== localResult) { + const value = ( + !!remoteResult + ? typeof remoteResult === 'object' && 'value' in remoteResult + ? remoteResult.value + : remoteResult + : undefined + ) as Value + const ttl = + !!remoteResult && + typeof remoteResult === 'object' && + 'expires' in remoteResult && + remoteResult.expires + ? remoteResult.expires - Date.now() + : undefined + if (!ttl || ttl > 0) { + await this.local.set(key, value, ttl) + } else { + this.local.delete(key) + } + } + return remoteResult + } + + return localResult + } + + async getMany(keys: string[], options?: { raw?: boolean }): Promise>> { + const promises: Array>> = [] + for (const key of keys) { + promises.push(this.get(key, options) as Promise>) + } + + const values = await Promise.all(promises) + const data: Array> | undefined = [] + for (const value of values) { + data.push(value as KeyvStoredData) + } + return data + } + + async set(key: string, value: any, ttl?: number) { + const toSet: KeyvTieredIndex[] = ['local', 'remote'] + return Promise.all(toSet.map(async (store) => this[store].set(key, value, ttl))) + } + + async clear(): Promise { + const toClear: KeyvTieredIndex[] = ['local'] + if (!this.opts.localOnly) { + toClear.push('remote') + } + + await Promise.all(toClear.map(async (store) => this[store].clear())) + + return undefined + } + + async delete(key: string): Promise { + const toDelete: KeyvTieredIndex[] = ['local'] + if (!this.opts.localOnly) { + toDelete.push('remote') + } + + const deleted = await Promise.all(toDelete.map(async (store) => this[store].delete(key))) + + return deleted.every(Boolean) + } + + async deleteMany(keys: string[]): Promise { + const promises = [] + for (const key of keys) { + promises.push(this.delete(key)) + } + + const values = await Promise.all(promises) + + return values.every(Boolean) + } + + async has(key: string): Promise { + let response + if (typeof this.local.has === 'function') { + response = this.local.has(key) + } else { + const value = await this.local.get(key) + response = value !== undefined + } + if (!response || !this.opts.validator(response, key)) { + if (typeof this.remote.has === 'function') { + response = this.remote.has(key) + } else { + const value = await this.remote.get(key) + response = value !== undefined + } + } + return response + } + + async *iterator(namespace?: string): AsyncGenerator { + const limit = Number.parseInt(this.iterationLimit as string, 10) || 10 + this.remote.opts.iterationLimit = limit + if (this.remote && typeof this.remote.iterator === 'function') { + for await (const entries of this.remote.iterator(namespace)) { + yield entries + } + } + } +} + +function isMap(map: any) { + if (map instanceof Map) { + return true + } else if ( + map && + typeof map.clear === 'function' && + typeof map.delete === 'function' && + typeof map.get === 'function' && + typeof map.has === 'function' && + typeof map.set === 'function' + ) { + return true + } + + return false +} diff --git a/packages/kv-store/src/store-adapters/tiered/types.ts b/packages/kv-store/src/store-adapters/tiered/types.ts new file mode 100644 index 000000000..18c84e881 --- /dev/null +++ b/packages/kv-store/src/store-adapters/tiered/types.ts @@ -0,0 +1,16 @@ +import { IKeyValueStoreAdapter } from '../../key-value-types.js' + +export type Options = { + local: IKeyValueStoreAdapter | Map + remote: IKeyValueStoreAdapter | Map + localOnly?: boolean + iterationLimit?: number | string +} + +// eslint-disable-next-line @typescript-eslint/naming-convention +export type Options_ = { + validator: (value: any, key: string) => boolean + dialect: string + iterationLimit?: number | string + localOnly?: boolean +} diff --git a/packages/kv-store/src/store-adapters/typeorm/entities/keyValueStoreEntity.ts b/packages/kv-store/src/store-adapters/typeorm/entities/keyValueStoreEntity.ts new file mode 100644 index 000000000..787fc7575 --- /dev/null +++ b/packages/kv-store/src/store-adapters/typeorm/entities/keyValueStoreEntity.ts @@ -0,0 +1,20 @@ +import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm' + +/** + * + * @beta This API may change without a BREAKING CHANGE notice. + */ +@Entity('keyvaluestore') +export class KeyValueStoreEntity extends BaseEntity { + @PrimaryColumn() + // @ts-ignore + key: string + + @Column({ + type: 'text', + }) + // @ts-ignore + data: string + + expires?: number +} diff --git a/packages/kv-store/src/store-adapters/typeorm/index.ts b/packages/kv-store/src/store-adapters/typeorm/index.ts new file mode 100644 index 000000000..196296839 --- /dev/null +++ b/packages/kv-store/src/store-adapters/typeorm/index.ts @@ -0,0 +1,131 @@ +import { EventEmitter } from 'events' +import { OrPromise } from '@veramo/utils' +import { DataSource, In, Like } from 'typeorm' +import { KeyValueStoreEntity } from './entities/keyValueStoreEntity.js' +import { KeyValueTypeORMOptions, Options_ } from './types.js' +import { KeyvStore, KeyvStoredData } from '../../keyv/keyv-types.js' +import { IKeyValueStoreAdapter } from '../../key-value-types.js' +import JSONB from 'json-buffer' + +export { KeyValueTypeORMOptions } from './types.js' + +/** + * TypeORM based key value store adapter + * @beta + */ +export class KeyValueTypeORMStoreAdapter + extends EventEmitter + implements KeyvStore, IKeyValueStoreAdapter +{ + private readonly dbConnection: OrPromise + readonly namespace: string + opts: Options_ + + constructor(options: KeyValueTypeORMOptions) { + super() + this.dbConnection = options.dbConnection + this.namespace = options.namespace || 'keyv' + this.opts = { + validator: () => true, + dialect: 'typeorm', + serialize: JSONB.stringify, + deserialize: JSONB.parse, + ...options, + } + } + + async get( + key: string | string[], + options?: { raw?: boolean }, + ): Promise | Array>> { + if (Array.isArray(key)) { + return this.getMany(key, options) + } + const connection = await _getConnectedDb(this.dbConnection) + const result = await connection.getRepository(KeyValueStoreEntity).findOneBy({ + key, + }) + return options?.raw !== true || !result ? result?.data : { value: result?.data, expires: result?.expires } + } + + async getMany(keys: string[], options?: { raw?: boolean }): Promise>> { + const connection = await _getConnectedDb(this.dbConnection) + const results = await connection.getRepository(KeyValueStoreEntity).findBy({ + key: In(keys), + }) + const values = keys.map(async (key) => { + const result = results.find((result) => result.key === key) + return options?.raw !== true || !result + ? (result?.data as KeyvStoredData) + : ({ + value: result?.data ? (await this.opts.deserialize(result.data))?.value : undefined, + expires: result?.expires, + } as KeyvStoredData) + }) + + return Promise.all(values) + } + + async set(key: string, value: string, ttl?: number): Promise> { + const connection = await _getConnectedDb(this.dbConnection) + const entity = new KeyValueStoreEntity() + entity.key = key + entity.data = value + entity.expires = ttl + await connection.getRepository(KeyValueStoreEntity).save(entity) + return { value: value, expires: ttl } + } + + async delete(key: string | string[]): Promise { + if (Array.isArray(key)) { + return this.deleteMany(key) + } + const connection = await _getConnectedDb(this.dbConnection) + const result = await connection.getRepository(KeyValueStoreEntity).delete({ key }) + return result.affected === 1 + } + + async deleteMany(keys: string[]): Promise { + const connection = await _getConnectedDb(this.dbConnection) + const results = await connection.getRepository(KeyValueStoreEntity).delete({ + key: In(keys), + }) + return !!results.affected && results.affected >= 1 + } + + async clear(): Promise { + const connection = await _getConnectedDb(this.dbConnection) + await connection.getRepository(KeyValueStoreEntity).delete({ + key: Like(`${this.namespace}:%`), + }) + } + + async has(key: string): Promise { + const connection = await _getConnectedDb(this.dbConnection) + const result = await connection.getRepository(KeyValueStoreEntity).countBy({ + key, + }) + return result === 1 + } + + async disconnect(): Promise { + const connection = await _getConnectedDb(this.dbConnection) + connection.destroy() + } +} + +/** + * Ensures that the provided DataSource is connected. + * + * @param dbConnection - a TypeORM DataSource or a Promise that resolves to a DataSource + * @internal + */ +export async function _getConnectedDb(dbConnection: OrPromise): Promise { + if (dbConnection instanceof Promise) { + return await dbConnection + } else if (!dbConnection.isInitialized) { + return await (dbConnection).initialize() + } else { + return dbConnection + } +} diff --git a/packages/kv-store/src/store-adapters/typeorm/migrations/1.createKVDatabase.ts b/packages/kv-store/src/store-adapters/typeorm/migrations/1.createKVDatabase.ts new file mode 100644 index 000000000..3a33baf01 --- /dev/null +++ b/packages/kv-store/src/store-adapters/typeorm/migrations/1.createKVDatabase.ts @@ -0,0 +1,50 @@ +import { MigrationInterface, QueryRunner, Table } from 'typeorm' +import Debug from 'debug' +const debug = Debug('veramo:data-store:initial-migration') + +/** + * Create the database layout for Veramo 3.0 + * + * @public + */ +export class CreateKVDatabaseMigration implements MigrationInterface { + private readonly _tableName: string + readonly name: string + + constructor(tableName?: string) { + this._tableName = tableName || 'keyvaluestore' + this.name = `CreateKVDatabase${tableName}1680297189001` + } + + async up(queryRunner: QueryRunner): Promise { + function getTableName(givenName: string): string { + return ( + queryRunner.connection.entityMetadatas.find((meta) => meta.givenTableName === givenName)?.tableName || + givenName + ) + } + + debug(`creating ${this._tableName} table`) + // CREATE TABLE "keyvaluestore" ("key" varchar PRIMARY KEY NOT NULL, "data" text NOT NULL) + await queryRunner.createTable( + new Table({ + name: getTableName(this._tableName), + columns: [ + { name: 'key', type: 'varchar', isPrimary: true }, + { name: 'data', type: 'text', isNullable: false }, + ], + indices: [ + { + columnNames: ['key'], + isUnique: true, + }, + ], + }), + true, + ) + } + + async down(queryRunner: QueryRunner): Promise { + throw new Error('illegal_operation: cannot roll back initial migration') + } +} diff --git a/packages/kv-store/src/store-adapters/typeorm/migrations/index.ts b/packages/kv-store/src/store-adapters/typeorm/migrations/index.ts new file mode 100644 index 000000000..ee1b5715e --- /dev/null +++ b/packages/kv-store/src/store-adapters/typeorm/migrations/index.ts @@ -0,0 +1,10 @@ +import { CreateKVDatabaseMigration } from './1.createKVDatabase.js' + +/** + * The migrations array that SHOULD be used when initializing a TypeORM database connection. + * + * These ensure the correct creation of tables and the proper migrations of data when tables change between versions. + * + * @public + */ +export const kvStoreMigrations = [CreateKVDatabaseMigration] diff --git a/packages/kv-store/src/store-adapters/typeorm/types.ts b/packages/kv-store/src/store-adapters/typeorm/types.ts new file mode 100644 index 000000000..80ec3ce67 --- /dev/null +++ b/packages/kv-store/src/store-adapters/typeorm/types.ts @@ -0,0 +1,25 @@ +import { OrPromise } from '@veramo/utils' +import { DataSource } from 'typeorm' +import { KeyvDeserializedData } from '../../keyv/keyv-types.js' + +/** + * @public + */ +export type KeyValueTypeORMOptions = { + dbConnection: OrPromise + + namespace?: string +} + +/** + * Internal options for the TypeORM adapter + * @internal + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export type Options_ = { + validator: (value: any, key: string) => boolean + dialect: string + serialize: (data: KeyvDeserializedData) => OrPromise + /** A custom deserialization function. */ + deserialize: (data: any) => OrPromise | undefined> +} diff --git a/packages/kv-store/tsconfig.json b/packages/kv-store/tsconfig.json new file mode 100644 index 000000000..7748859c3 --- /dev/null +++ b/packages/kv-store/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.settings.json", + "compilerOptions": { + "esModuleInterop": true, + "rootDir": "src", + "outDir": "build", + "declarationDir": "build" + }, + "references": [ + { "path": "../core-types" }, + { "path": "../utils" } + ] +} diff --git a/packages/tsconfig.json b/packages/tsconfig.json index 2b1f1a392..930872688 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -23,6 +23,7 @@ { "path": "key-manager" }, { "path": "kms-local" }, { "path": "kms-web3" }, + { "path": "kv-store" }, { "path": "message-handler" }, { "path": "remote-client" }, { "path": "remote-server" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8aadea4d3..b250cf30f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1195,6 +1195,64 @@ importers: specifier: 4.9.4 version: 4.9.4 + packages/kv-store: + dependencies: + '@veramo/core-types': + specifier: workspace:* + version: link:../core-types + '@veramo/utils': + specifier: workspace:* + version: link:../utils + debug: + specifier: ^4.3.4 + version: 4.3.4 + events: + specifier: ^3.3.0 + version: 3.3.0 + json-buffer: + specifier: ^3.0.1 + version: 3.0.1 + typeorm: + specifier: ^0.3.10 + version: 0.3.11(sqlite3@5.1.4)(ts-node@10.9.1) + uint8arrays: + specifier: ^3.1.1 + version: 3.1.1 + devDependencies: + '@keyv/compress-brotli': + specifier: ^1.1.3 + version: 1.1.3 + '@keyv/compress-gzip': + specifier: ^1.2.3 + version: 1.2.3 + '@keyv/sqlite': + specifier: ^3.6.5 + version: 3.6.5 + '@keyv/test-suite': + specifier: ^1.9.2 + version: 1.9.2 + '@types/debug': + specifier: ^4.1.7 + version: 4.1.7 + '@types/json-buffer': + specifier: ^3.0.0 + version: 3.0.0 + eslint: + specifier: ^8.33.0 + version: 8.33.0 + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.1.1(eslint@8.33.0) + keyv: + specifier: ^4.5.2 + version: 4.5.2 + timekeeper: + specifier: ^2.2.0 + version: 2.2.0 + typescript: + specifier: ^4.9.5 + version: 4.9.5 + packages/message-handler: dependencies: '@veramo/core-types': @@ -1489,7 +1547,7 @@ importers: version: 19.6.1 react-scripts: specifier: 5.0.1 - version: 5.0.1(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.20.7)(eslint@8.31.0)(react@18.2.0)(ts-node@10.9.1)(typescript@4.9.4) + version: 5.0.1(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.20.7)(eslint@8.33.0)(react@18.2.0)(ts-node@10.9.1)(typescript@4.9.4) ts-jest: specifier: 29.0.5 version: 29.0.5(@babel/core@7.20.12)(babel-jest@29.5.0)(jest@29.3.1)(typescript@4.9.4) @@ -1671,7 +1729,7 @@ packages: transitivePeerDependencies: - supports-color - /@babel/eslint-parser@7.19.1(@babel/core@7.20.12)(eslint@8.31.0): + /@babel/eslint-parser@7.19.1(@babel/core@7.20.12)(eslint@8.33.0): resolution: {integrity: sha512-AqNf2QWt1rtu2/1rLswy6CDP7H9Oh3mMhk177Y67Rg8d7RD9WfOLLv8CGn6tisFvS2htm86yIe1yLF6I1UDaGQ==} engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} peerDependencies: @@ -1680,7 +1738,7 @@ packages: dependencies: '@babel/core': 7.20.12 '@nicolo-ribaudo/eslint-scope-5-internals': 5.1.1-v1 - eslint: 8.31.0 + eslint: 8.33.0 eslint-visitor-keys: 2.1.0 semver: 6.3.0 dev: true @@ -3026,7 +3084,7 @@ packages: cosmiconfig-typescript-loader: 1.0.9(@types/node@18.11.18)(cosmiconfig@7.1.0)(typescript@4.9.4) cross-spawn: 7.0.3 lodash: 4.17.21 - react-scripts: 5.0.1(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.20.7)(eslint@8.31.0)(react@18.2.0)(ts-node@10.9.1)(typescript@4.9.4) + react-scripts: 5.0.1(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.20.7)(eslint@8.33.0)(react@18.2.0)(ts-node@10.9.1)(typescript@4.9.4) semver: 7.3.8 webpack-merge: 5.8.0 transitivePeerDependencies: @@ -4735,6 +4793,48 @@ packages: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.4.14 + /@keyv/compress-brotli@1.1.3: + resolution: {integrity: sha512-6QdtGH/iHKj3GGN4a+m/EfhmfVeeVq2Z9yOlDwaHh18Pe9UncFCIJNpbahVdgxn2gedYjKOQR7s06SyPF2Y4Nw==} + engines: {node: '>= 12'} + dependencies: + compress-brotli: 1.3.8 + dev: true + + /@keyv/compress-gzip@1.2.3: + resolution: {integrity: sha512-nwPlHX18bWXNKbmIi0nGfwJlXvqusISbX9IpQssY0Uj/olddhz4s0K8hKJAFr13iEsD8PhJA5stnKB2UNwBasQ==} + engines: {node: '>= 12'} + dependencies: + '@types/pako': 2.0.0 + json-buffer: 3.0.1 + pako: 2.1.0 + dev: true + + /@keyv/sqlite@3.6.5: + resolution: {integrity: sha512-PZPsXcZYHPKUCX1DO7e6dTMGmk5aX7uW8QFzEavOPRMVloIp2QMv+Glt162pu+Dw5NunJinCbvD2DvYwinyNZw==} + engines: {node: '>= 12'} + dependencies: + pify: 5.0.0 + sqlite3: 5.1.6 + transitivePeerDependencies: + - bluebird + - encoding + - supports-color + dev: true + + /@keyv/test-suite@1.9.2: + resolution: {integrity: sha512-0pB36ZP22fBSeZuwwQRrFeuuRu6L10lEOJMv+5Fq6XXbXjN1Tvaoh7et6pqnk35qW0s0RSlOsY0m6lqydb7b6A==} + dependencies: + bignumber.js: 9.1.1 + delay: 5.0.0 + json-bigint: 1.0.0 + sqlite3: 5.1.6 + timekeeper: 2.2.0 + transitivePeerDependencies: + - bluebird + - encoding + - supports-color + dev: true + /@leichtgewicht/ip-codec@2.0.4: resolution: {integrity: sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==} dev: true @@ -7310,6 +7410,10 @@ packages: parse5: 7.1.2 dev: true + /@types/json-buffer@3.0.0: + resolution: {integrity: sha512-3YP80IxxFJB4b5tYC2SUPwkg0XQLiu0nWvhRgEatgjf+29IcWO9X1k8xRv5DGssJ/lCrjYTjQPcobJr2yWIVuQ==} + dev: true + /@types/json-schema@7.0.11: resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} @@ -7386,6 +7490,10 @@ packages: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} dev: true + /@types/pako@2.0.0: + resolution: {integrity: sha512-10+iaz93qR5WYxTo+PMifD5TSxiOtdRaxBf7INGGXMQgTCu8Z/7GYWYFUOS3q/G0nE5boj1r4FEB+WSy7s5gbA==} + dev: true + /@types/parse-json@4.0.0: resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} dev: true @@ -7562,7 +7670,7 @@ packages: dev: true optional: true - /@typescript-eslint/eslint-plugin@5.48.1(@typescript-eslint/parser@5.48.1)(eslint@8.31.0)(typescript@4.9.4): + /@typescript-eslint/eslint-plugin@5.48.1(@typescript-eslint/parser@5.48.1)(eslint@8.33.0)(typescript@4.9.4): resolution: {integrity: sha512-9nY5K1Rp2ppmpb9s9S2aBiF3xo5uExCehMDmYmmFqqyxgenbHJ3qbarcLt4ITgaD6r/2ypdlcFRdcuVPnks+fQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -7573,12 +7681,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/parser': 5.48.1(eslint@8.31.0)(typescript@4.9.4) + '@typescript-eslint/parser': 5.48.1(eslint@8.33.0)(typescript@4.9.4) '@typescript-eslint/scope-manager': 5.48.1 - '@typescript-eslint/type-utils': 5.48.1(eslint@8.31.0)(typescript@4.9.4) - '@typescript-eslint/utils': 5.48.1(eslint@8.31.0)(typescript@4.9.4) + '@typescript-eslint/type-utils': 5.48.1(eslint@8.33.0)(typescript@4.9.4) + '@typescript-eslint/utils': 5.48.1(eslint@8.33.0)(typescript@4.9.4) debug: 4.3.4 - eslint: 8.31.0 + eslint: 8.33.0 ignore: 5.2.4 natural-compare-lite: 1.4.0 regexpp: 3.2.0 @@ -7589,20 +7697,20 @@ packages: - supports-color dev: true - /@typescript-eslint/experimental-utils@5.48.1(eslint@8.31.0)(typescript@4.9.4): + /@typescript-eslint/experimental-utils@5.48.1(eslint@8.33.0)(typescript@4.9.4): resolution: {integrity: sha512-8OoIZZuOeqsm5cxn2f01qHWtVC3M4iixSsfZXPiQUg4Sl4LiU+b5epcJFwxNfqeoLl+SGncELyi3x99zI6C0ng==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - '@typescript-eslint/utils': 5.48.1(eslint@8.31.0)(typescript@4.9.4) - eslint: 8.31.0 + '@typescript-eslint/utils': 5.48.1(eslint@8.33.0)(typescript@4.9.4) + eslint: 8.33.0 transitivePeerDependencies: - supports-color - typescript dev: true - /@typescript-eslint/parser@5.48.1(eslint@8.31.0)(typescript@4.9.4): + /@typescript-eslint/parser@5.48.1(eslint@8.33.0)(typescript@4.9.4): resolution: {integrity: sha512-4yg+FJR/V1M9Xoq56SF9Iygqm+r5LMXvheo6DQ7/yUWynQ4YfCRnsKuRgqH4EQ5Ya76rVwlEpw4Xu+TgWQUcdA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -7616,7 +7724,7 @@ packages: '@typescript-eslint/types': 5.48.1 '@typescript-eslint/typescript-estree': 5.48.1(typescript@4.9.4) debug: 4.3.4 - eslint: 8.31.0 + eslint: 8.33.0 typescript: 4.9.4 transitivePeerDependencies: - supports-color @@ -7630,7 +7738,7 @@ packages: '@typescript-eslint/visitor-keys': 5.48.1 dev: true - /@typescript-eslint/type-utils@5.48.1(eslint@8.31.0)(typescript@4.9.4): + /@typescript-eslint/type-utils@5.48.1(eslint@8.33.0)(typescript@4.9.4): resolution: {integrity: sha512-Hyr8HU8Alcuva1ppmqSYtM/Gp0q4JOp1F+/JH5D1IZm/bUBrV0edoewQZiEc1r6I8L4JL21broddxK8HAcZiqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -7641,9 +7749,9 @@ packages: optional: true dependencies: '@typescript-eslint/typescript-estree': 5.48.1(typescript@4.9.4) - '@typescript-eslint/utils': 5.48.1(eslint@8.31.0)(typescript@4.9.4) + '@typescript-eslint/utils': 5.48.1(eslint@8.33.0)(typescript@4.9.4) debug: 4.3.4 - eslint: 8.31.0 + eslint: 8.33.0 tsutils: 3.21.0(typescript@4.9.4) typescript: 4.9.4 transitivePeerDependencies: @@ -7676,7 +7784,7 @@ packages: - supports-color dev: true - /@typescript-eslint/utils@5.48.1(eslint@8.31.0)(typescript@4.9.4): + /@typescript-eslint/utils@5.48.1(eslint@8.33.0)(typescript@4.9.4): resolution: {integrity: sha512-SmQuSrCGUOdmGMwivW14Z0Lj8dxG1mOFZ7soeJ0TQZEJcs3n5Ndgkg0A4bcMFzBELqLJ6GTHnEU+iIoaD6hFGA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -7687,9 +7795,9 @@ packages: '@typescript-eslint/scope-manager': 5.48.1 '@typescript-eslint/types': 5.48.1 '@typescript-eslint/typescript-estree': 5.48.1(typescript@4.9.4) - eslint: 8.31.0 + eslint: 8.33.0 eslint-scope: 5.1.1 - eslint-utils: 3.0.0(eslint@8.31.0) + eslint-utils: 3.0.0(eslint@8.33.0) semver: 7.3.8 transitivePeerDependencies: - supports-color @@ -9918,6 +10026,14 @@ packages: dev: false optional: true + /compress-brotli@1.3.8: + resolution: {integrity: sha512-lVcQsjhxhIXsuupfy9fmZUFtAIdBmXA7EGY6GBdgZ++qkM9zG4YFT8iU7FoBxzryNDMOpD1HIFHUSX4D87oqhQ==} + engines: {node: '>= 12'} + dependencies: + '@types/json-buffer': 3.0.0 + json-buffer: 3.0.1 + dev: true + /compressible@2.0.18: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} @@ -10803,6 +10919,11 @@ packages: rimraf: 3.0.2 slash: 3.0.0 + /delay@5.0.0: + resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==} + engines: {node: '>=10'} + dev: true + /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -11389,7 +11510,7 @@ packages: source-map: 0.6.1 dev: true - /eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.20.7)(eslint@8.31.0)(jest@27.5.1)(typescript@4.9.4): + /eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.20.7)(eslint@8.33.0)(jest@27.5.1)(typescript@4.9.4): resolution: {integrity: sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -11400,20 +11521,20 @@ packages: optional: true dependencies: '@babel/core': 7.20.12 - '@babel/eslint-parser': 7.19.1(@babel/core@7.20.12)(eslint@8.31.0) + '@babel/eslint-parser': 7.19.1(@babel/core@7.20.12)(eslint@8.33.0) '@rushstack/eslint-patch': 1.2.0 - '@typescript-eslint/eslint-plugin': 5.48.1(@typescript-eslint/parser@5.48.1)(eslint@8.31.0)(typescript@4.9.4) - '@typescript-eslint/parser': 5.48.1(eslint@8.31.0)(typescript@4.9.4) + '@typescript-eslint/eslint-plugin': 5.48.1(@typescript-eslint/parser@5.48.1)(eslint@8.33.0)(typescript@4.9.4) + '@typescript-eslint/parser': 5.48.1(eslint@8.33.0)(typescript@4.9.4) babel-preset-react-app: 10.0.1 confusing-browser-globals: 1.0.11 - eslint: 8.31.0 - eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.20.7)(eslint@8.31.0) - eslint-plugin-import: 2.26.0(@typescript-eslint/parser@5.48.1)(eslint@8.31.0) - eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.48.1)(eslint@8.31.0)(jest@27.5.1)(typescript@4.9.4) - eslint-plugin-jsx-a11y: 6.7.0(eslint@8.31.0) - eslint-plugin-react: 7.32.0(eslint@8.31.0) - eslint-plugin-react-hooks: 4.6.0(eslint@8.31.0) - eslint-plugin-testing-library: 5.9.1(eslint@8.31.0)(typescript@4.9.4) + eslint: 8.33.0 + eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.20.7)(eslint@8.33.0) + eslint-plugin-import: 2.26.0(@typescript-eslint/parser@5.48.1)(eslint@8.33.0) + eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.48.1)(eslint@8.33.0)(jest@27.5.1)(typescript@4.9.4) + eslint-plugin-jsx-a11y: 6.7.0(eslint@8.33.0) + eslint-plugin-react: 7.32.0(eslint@8.33.0) + eslint-plugin-react-hooks: 4.6.0(eslint@8.33.0) + eslint-plugin-testing-library: 5.9.1(eslint@8.33.0)(typescript@4.9.4) typescript: 4.9.4 transitivePeerDependencies: - '@babel/plugin-syntax-flow' @@ -11433,7 +11554,7 @@ packages: - supports-color dev: true - /eslint-module-utils@2.7.4(@typescript-eslint/parser@5.48.1)(eslint-import-resolver-node@0.3.6)(eslint@8.31.0): + /eslint-module-utils@2.7.4(@typescript-eslint/parser@5.48.1)(eslint-import-resolver-node@0.3.6)(eslint@8.33.0): resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==} engines: {node: '>=4'} peerDependencies: @@ -11454,15 +11575,15 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.48.1(eslint@8.31.0)(typescript@4.9.4) + '@typescript-eslint/parser': 5.48.1(eslint@8.33.0)(typescript@4.9.4) debug: 3.2.7 - eslint: 8.31.0 + eslint: 8.33.0 eslint-import-resolver-node: 0.3.6 transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-flowtype@8.0.3(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.20.7)(eslint@8.31.0): + /eslint-plugin-flowtype@8.0.3(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.20.7)(eslint@8.33.0): resolution: {integrity: sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ==} engines: {node: '>=12.0.0'} peerDependencies: @@ -11472,12 +11593,12 @@ packages: dependencies: '@babel/plugin-syntax-flow': 7.18.6(@babel/core@7.20.12) '@babel/plugin-transform-react-jsx': 7.20.7(@babel/core@7.20.12) - eslint: 8.31.0 + eslint: 8.33.0 lodash: 4.17.21 string-natural-compare: 3.0.1 dev: true - /eslint-plugin-import@2.26.0(@typescript-eslint/parser@5.48.1)(eslint@8.31.0): + /eslint-plugin-import@2.26.0(@typescript-eslint/parser@5.48.1)(eslint@8.33.0): resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==} engines: {node: '>=4'} peerDependencies: @@ -11487,14 +11608,14 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.48.1(eslint@8.31.0)(typescript@4.9.4) + '@typescript-eslint/parser': 5.48.1(eslint@8.33.0)(typescript@4.9.4) array-includes: 3.1.6 array.prototype.flat: 1.3.1 debug: 2.6.9 doctrine: 2.1.0 - eslint: 8.31.0 + eslint: 8.33.0 eslint-import-resolver-node: 0.3.6 - eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.48.1)(eslint-import-resolver-node@0.3.6)(eslint@8.31.0) + eslint-module-utils: 2.7.4(@typescript-eslint/parser@5.48.1)(eslint-import-resolver-node@0.3.6)(eslint@8.33.0) has: 1.0.3 is-core-module: 2.11.0 is-glob: 4.0.3 @@ -11508,7 +11629,7 @@ packages: - supports-color dev: true - /eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.48.1)(eslint@8.31.0)(jest@27.5.1)(typescript@4.9.4): + /eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.48.1)(eslint@8.33.0)(jest@27.5.1)(typescript@4.9.4): resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} peerDependencies: @@ -11521,16 +11642,16 @@ packages: jest: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.48.1(@typescript-eslint/parser@5.48.1)(eslint@8.31.0)(typescript@4.9.4) - '@typescript-eslint/experimental-utils': 5.48.1(eslint@8.31.0)(typescript@4.9.4) - eslint: 8.31.0 + '@typescript-eslint/eslint-plugin': 5.48.1(@typescript-eslint/parser@5.48.1)(eslint@8.33.0)(typescript@4.9.4) + '@typescript-eslint/experimental-utils': 5.48.1(eslint@8.33.0)(typescript@4.9.4) + eslint: 8.33.0 jest: 27.5.1(ts-node@10.9.1) transitivePeerDependencies: - supports-color - typescript dev: true - /eslint-plugin-jsx-a11y@6.7.0(eslint@8.31.0): + /eslint-plugin-jsx-a11y@6.7.0(eslint@8.33.0): resolution: {integrity: sha512-EGGRKhzejSzXKtjmEjWNtr4SK/DkMkSzkBH7g7e7moBDXZXrqaUIxkmD7uF93upMysc4dKYEJwupu7Dff+ShwA==} engines: {node: '>=4.0'} peerDependencies: @@ -11545,7 +11666,7 @@ packages: axobject-query: 3.1.1 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 8.31.0 + eslint: 8.33.0 has: 1.0.3 jsx-ast-utils: 3.3.3 language-tags: 1.0.5 @@ -11555,16 +11676,25 @@ packages: semver: 6.3.0 dev: true - /eslint-plugin-react-hooks@4.6.0(eslint@8.31.0): + /eslint-plugin-promise@6.1.1(eslint@8.33.0): + resolution: {integrity: sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + eslint: 8.33.0 + dev: true + + /eslint-plugin-react-hooks@4.6.0(eslint@8.33.0): resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} engines: {node: '>=10'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 dependencies: - eslint: 8.31.0 + eslint: 8.33.0 dev: true - /eslint-plugin-react@7.32.0(eslint@8.31.0): + /eslint-plugin-react@7.32.0(eslint@8.33.0): resolution: {integrity: sha512-vSBi1+SrPiLZCGvxpiZIa28fMEUaMjXtCplrvxcIxGzmFiYdsXQDwInEjuv5/i/2CTTxbkS87tE8lsQ0Qxinbw==} engines: {node: '>=4'} peerDependencies: @@ -11574,7 +11704,7 @@ packages: array.prototype.flatmap: 1.3.1 array.prototype.tosorted: 1.1.1 doctrine: 2.1.0 - eslint: 8.31.0 + eslint: 8.33.0 estraverse: 5.3.0 jsx-ast-utils: 3.3.3 minimatch: 3.1.2 @@ -11588,14 +11718,14 @@ packages: string.prototype.matchall: 4.0.8 dev: true - /eslint-plugin-testing-library@5.9.1(eslint@8.31.0)(typescript@4.9.4): + /eslint-plugin-testing-library@5.9.1(eslint@8.33.0)(typescript@4.9.4): resolution: {integrity: sha512-6BQp3tmb79jLLasPHJmy8DnxREe+2Pgf7L+7o09TSWPfdqqtQfRZmZNetr5mOs3yqZk/MRNxpN3RUpJe0wB4LQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} peerDependencies: eslint: ^7.5.0 || ^8.0.0 dependencies: - '@typescript-eslint/utils': 5.48.1(eslint@8.31.0)(typescript@4.9.4) - eslint: 8.31.0 + '@typescript-eslint/utils': 5.48.1(eslint@8.33.0)(typescript@4.9.4) + eslint: 8.33.0 transitivePeerDependencies: - supports-color - typescript @@ -11617,13 +11747,13 @@ packages: estraverse: 5.3.0 dev: true - /eslint-utils@3.0.0(eslint@8.31.0): + /eslint-utils@3.0.0(eslint@8.33.0): resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} peerDependencies: eslint: '>=5' dependencies: - eslint: 8.31.0 + eslint: 8.33.0 eslint-visitor-keys: 2.1.0 dev: true @@ -11637,7 +11767,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint-webpack-plugin@3.2.0(eslint@8.31.0)(webpack@5.75.0): + /eslint-webpack-plugin@3.2.0(eslint@8.33.0)(webpack@5.75.0): resolution: {integrity: sha512-avrKcGncpPbPSUHX6B3stNGzkKFto3eL+DKM4+VyMrVnhPc3vRczVlCq3uhuFOdRvDHTVXuzwk1ZKUrqDQHQ9w==} engines: {node: '>= 12.13.0'} peerDependencies: @@ -11645,7 +11775,7 @@ packages: webpack: ^5.0.0 dependencies: '@types/eslint': 8.4.10 - eslint: 8.31.0 + eslint: 8.33.0 jest-worker: 28.1.3 micromatch: 4.0.5 normalize-path: 3.0.0 @@ -11653,8 +11783,8 @@ packages: webpack: 5.75.0 dev: true - /eslint@8.31.0: - resolution: {integrity: sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA==} + /eslint@8.33.0: + resolution: {integrity: sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: @@ -11669,7 +11799,7 @@ packages: doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.1.1 - eslint-utils: 3.0.0(eslint@8.31.0) + eslint-utils: 3.0.0(eslint@8.33.0) eslint-visitor-keys: 3.3.0 espree: 9.4.1 esquery: 1.4.0 @@ -12681,7 +12811,7 @@ packages: for-in: 1.0.2 dev: true - /fork-ts-checker-webpack-plugin@6.5.2(eslint@8.31.0)(typescript@4.9.4)(webpack@5.75.0): + /fork-ts-checker-webpack-plugin@6.5.2(eslint@8.33.0)(typescript@4.9.4)(webpack@5.75.0): resolution: {integrity: sha512-m5cUmF30xkZ7h4tWUgTAcEaKmUW7tfyUyTqNNOz7OxWJ0v1VWKTcOvH8FWHUwSjlW/356Ijc9vi3XfcPstpQKA==} engines: {node: '>=10', yarn: '>=1.0.0'} peerDependencies: @@ -12701,7 +12831,7 @@ packages: chokidar: 3.5.3 cosmiconfig: 6.0.0 deepmerge: 4.2.2 - eslint: 8.31.0 + eslint: 8.33.0 fs-extra: 9.1.0 glob: 7.2.3 memfs: 3.4.13 @@ -16290,6 +16420,15 @@ packages: engines: {node: '>=4'} hasBin: true + /json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + dependencies: + bignumber.js: 9.1.1 + dev: true + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + /json-canonicalize@1.0.4: resolution: {integrity: sha512-YNr/ePzgReHwlnAm3EVV1pcimwesI+1DZr5v7WBKOc1zE1t7pjxWAPRxJFT3ll6flLIdRe0DPia/8cl2FLAZNA==} dev: false @@ -16449,6 +16588,12 @@ packages: readable-stream: 3.6.0 dev: true + /keyv@4.5.2: + resolution: {integrity: sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==} + dependencies: + json-buffer: 3.0.1 + dev: true + /kind-of@1.1.0: resolution: {integrity: sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==} engines: {node: '>=0.10.0'} @@ -17978,12 +18123,6 @@ packages: dependencies: yallist: 4.0.0 - /minipass@4.0.0: - resolution: {integrity: sha512-g2Uuh2jEKoht+zvO6vJqXmYpflPqzRBT+Th2h01DKh5z7wbY/AZ2gCQ78cP70YoHPyFdY30YBV5WxgLOEwOykw==} - engines: {node: '>=8'} - dependencies: - yallist: 4.0.0 - /minipass@4.2.8: resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} engines: {node: '>=8'} @@ -19250,6 +19389,10 @@ packages: - supports-color dev: true + /pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + dev: true + /param-case@3.0.4: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} dependencies: @@ -20815,7 +20958,7 @@ packages: resolution: {integrity: sha512-WUSQJ4P/wWcusaH+zZmbECOk7H5N2pOIl0vzheeornkIMhu+qrNdGFm0bDZLCb0hSF0jf/kH1SgkNGfBdTc4wA==} dev: false - /react-dev-utils@12.0.1(eslint@8.31.0)(typescript@4.9.4)(webpack@5.75.0): + /react-dev-utils@12.0.1(eslint@8.33.0)(typescript@4.9.4)(webpack@5.75.0): resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==} engines: {node: '>=14'} peerDependencies: @@ -20834,7 +20977,7 @@ packages: escape-string-regexp: 4.0.0 filesize: 8.0.7 find-up: 5.0.0 - fork-ts-checker-webpack-plugin: 6.5.2(eslint@8.31.0)(typescript@4.9.4)(webpack@5.75.0) + fork-ts-checker-webpack-plugin: 6.5.2(eslint@8.33.0)(typescript@4.9.4)(webpack@5.75.0) global-modules: 2.0.0 globby: 11.1.0 gzip-size: 6.0.0 @@ -21044,7 +21187,7 @@ packages: dev: false optional: true - /react-scripts@5.0.1(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.20.7)(eslint@8.31.0)(react@18.2.0)(ts-node@10.9.1)(typescript@4.9.4): + /react-scripts@5.0.1(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.20.7)(eslint@8.33.0)(react@18.2.0)(ts-node@10.9.1)(typescript@4.9.4): resolution: {integrity: sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==} engines: {node: '>=14.0.0'} hasBin: true @@ -21071,9 +21214,9 @@ packages: css-minimizer-webpack-plugin: 3.4.1(webpack@5.75.0) dotenv: 10.0.0 dotenv-expand: 5.1.0 - eslint: 8.31.0 - eslint-config-react-app: 7.0.1(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.20.7)(eslint@8.31.0)(jest@27.5.1)(typescript@4.9.4) - eslint-webpack-plugin: 3.2.0(eslint@8.31.0)(webpack@5.75.0) + eslint: 8.33.0 + eslint-config-react-app: 7.0.1(@babel/plugin-syntax-flow@7.18.6)(@babel/plugin-transform-react-jsx@7.20.7)(eslint@8.33.0)(jest@27.5.1)(typescript@4.9.4) + eslint-webpack-plugin: 3.2.0(eslint@8.33.0)(webpack@5.75.0) file-loader: 6.2.0(webpack@5.75.0) fs-extra: 10.1.0 html-webpack-plugin: 5.5.0(webpack@5.75.0) @@ -21090,7 +21233,7 @@ packages: prompts: 2.4.2 react: 18.2.0 react-app-polyfill: 3.0.0 - react-dev-utils: 12.0.1(eslint@8.31.0)(typescript@4.9.4)(webpack@5.75.0) + react-dev-utils: 12.0.1(eslint@8.33.0)(typescript@4.9.4)(webpack@5.75.0) react-refresh: 0.11.0 resolve: 1.22.1 resolve-url-loader: 4.0.0 @@ -21744,7 +21887,7 @@ packages: /rxjs@7.8.0: resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==} dependencies: - tslib: 2.4.1 + tslib: 2.5.0 /safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -22510,6 +22653,24 @@ packages: - encoding - supports-color + /sqlite3@5.1.6: + resolution: {integrity: sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw==} + requiresBuild: true + peerDependenciesMeta: + node-gyp: + optional: true + dependencies: + '@mapbox/node-pre-gyp': 1.0.10 + node-addon-api: 4.3.0 + tar: 6.1.13 + optionalDependencies: + node-gyp: 8.4.1 + transitivePeerDependencies: + - bluebird + - encoding + - supports-color + dev: true + /ssri@8.0.1: resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} engines: {node: '>= 8'} @@ -23016,7 +23177,7 @@ packages: dependencies: chownr: 2.0.0 fs-minipass: 2.1.0 - minipass: 4.0.0 + minipass: 4.2.8 minizlib: 2.1.2 mkdirp: 1.0.4 yallist: 4.0.0 @@ -23194,6 +23355,10 @@ packages: engines: {node: '>=0.10.0'} dev: false + /timekeeper@2.2.0: + resolution: {integrity: sha512-W3AmPTJWZkRwu+iSNxPIsLZ2ByADsOLbbLxe46UJyWj3mlYLlwucKiq+/dPm0l9wTzqoF3/2PH0AGFCebjq23A==} + dev: true + /tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -23774,6 +23939,12 @@ packages: engines: {node: '>=4.2.0'} hasBin: true + /typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + /ua-parser-js@0.7.35: resolution: {integrity: sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==} dev: false