From f423a58c9c98253a87e0ae40d284be6b330e7e62 Mon Sep 17 00:00:00 2001 From: TimMikeladze Date: Mon, 2 Jan 2023 12:37:33 -0800 Subject: [PATCH 01/11] =?UTF-8?q?=F0=9F=90=9B=20flatten=20paths=20when=20u?= =?UTF-8?q?sing=20get/set/del=20util=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit flatten paths when using get/set/del util functions 🐛 Bugfix --- README.md | 2 +- __tests__/GistDatabase.test.ts | 2 +- src/GistDatabase.ts | 33 +++++++++------------------------ 3 files changed, 11 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index b1cecf5..b9966c6 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ The main file is called `database.json` (this is the file corresponding to the i When a value is created or updated a new `.json` gist is created for the document. It contains the provided value plus additional metadata such as TTL. The id of this newly created gist is then added to the lookup table in `database.json`. -Each gist can contain up to 10 files, with each file having a maximum size of 1m. +Each gist can contain up to 10 files, with each file having a maximum size of 1mb. When data is written or read for a specific key, this library will chunk the data and pack it into multiple files within the gist to optimize storage. diff --git a/__tests__/GistDatabase.test.ts b/__tests__/GistDatabase.test.ts index 761140e..404120c 100644 --- a/__tests__/GistDatabase.test.ts +++ b/__tests__/GistDatabase.test.ts @@ -102,7 +102,7 @@ it('get and set and del', () => { } } expect(GistDatabase.get(obj, ['a'])).toBe(1) - expect(GistDatabase.get(obj, ['b', 'c'])).toEqual({}) + expect(GistDatabase.get(obj, ['b', 'c'])).toBeUndefined() let res = GistDatabase.set(obj, ['a'], 2) expect(GistDatabase.get(res, ['a'])).toBe(2) diff --git a/src/GistDatabase.ts b/src/GistDatabase.ts index 0488181..1d6495b 100644 --- a/src/GistDatabase.ts +++ b/src/GistDatabase.ts @@ -442,39 +442,24 @@ export class GistDatabase { } public static get(obj: T, path: string[]): T { - if (path.length === 0) { - return obj - } - const [key, ...rest] = path - if (obj[key] === undefined) { - return undefined - } - return GistDatabase.get(obj[key], rest) + const key = path.join('.') + return obj[key] } public static set(obj: T, path: string[], value: any): T { - if (path.length === 0) { - return value - } - const [key, ...rest] = path + const key = path.join('.') return { ...obj, - [key]: GistDatabase.set(obj[key], rest, value) + [key]: value } } public static del(obj: T, path: string[]): T { - if (path.length === 0) { - return undefined - } - const [key, ...rest] = path - if (obj[key] === undefined) { - return obj - } - return { - ...obj, - [key]: GistDatabase.del(obj[key], rest) - } + const key = path.join('.') + + delete obj[key] + + return obj } public static ttlIsExpired(ttl: DocRef['ttl']) { From 8dd73d28a3565e14c98684b4245733dfca79a972 Mon Sep 17 00:00:00 2001 From: TimMikeladze Date: Mon, 2 Jan 2023 12:40:14 -0800 Subject: [PATCH 02/11] =?UTF-8?q?=F0=9F=94=96=20|=20v1.2.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 297191e..d15d8bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gist-database", - "version": "1.2.0", + "version": "1.2.1", "description": "", "author": "Tim Mikeladze ", "keywords": [ From 34052dc874694bf9c679337a5edb6cd3d07217e0 Mon Sep 17 00:00:00 2001 From: TimMikeladze Date: Mon, 2 Jan 2023 13:28:27 -0800 Subject: [PATCH 03/11] =?UTF-8?q?=F0=9F=90=9B=20updating=20existing=20docu?= =?UTF-8?q?ment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updating existing document 🐛 Bugfix --- __tests__/GistDatabase.test.ts | 50 +++++++++++++++ __tests__/data.ts | 113 +++++++++++++++++++++++++++++++++ src/GistDatabase.ts | 2 +- 3 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 __tests__/data.ts diff --git a/__tests__/GistDatabase.test.ts b/__tests__/GistDatabase.test.ts index 404120c..39b361e 100644 --- a/__tests__/GistDatabase.test.ts +++ b/__tests__/GistDatabase.test.ts @@ -1,4 +1,5 @@ import { CompressionType, GistDatabase, GistResponse } from '../src' +import { pendingAlbums } from './data' const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) @@ -114,3 +115,52 @@ it('get and set and del', () => { expect(GistDatabase.get(res, ['b', 'c'])).toBeUndefined() expect(GistDatabase.get(res, ['a'])).toBe(2) }) + +describe('GistDatabase - advanced scenario', () => { + let db: GistDatabase + beforeAll(async () => { + db = new GistDatabase({ + token: process.env.GIST_TOKEN, + compression: CompressionType.pretty + }) + }) + afterAll(async () => { + await db.destroy() + }) + + it('sets and gets value', async () => { + await db.set('pendingAlbums', { + value: pendingAlbums + }) + + let found = await db.get('pendingAlbums') + + expect(found).toMatchObject({ + value: pendingAlbums + }) + + expect(found.value.albums).toHaveLength(3) + + pendingAlbums.albums.pop() + + expect(found).not.toMatchObject({ + value: pendingAlbums + }) + + expect(found.value.albums).toHaveLength(3) + + expect(pendingAlbums.albums).toHaveLength(2) + + await db.set('pendingAlbums', { + value: pendingAlbums + }) + + found = await db.get('pendingAlbums') + + expect(found.value.albums).toHaveLength(2) + + expect(found).toMatchObject({ + value: pendingAlbums + }) + }) +}) diff --git a/__tests__/data.ts b/__tests__/data.ts new file mode 100644 index 0000000..504138d --- /dev/null +++ b/__tests__/data.ts @@ -0,0 +1,113 @@ +export const pendingAlbums = { + lastSyncAt: 1672692167887, + albums: [ + { + detail: { + artists: [ + { + external_urls: { + spotify: 'https://open.spotify.com/artist/4snI0qikpQST1U1VWAxEY6' + }, + id: '4snI0qikpQST1U1VWAxEY6', + name: 'Alai Oli' + } + ], + external_urls: { + spotify: 'https://open.spotify.com/album/4VpptIyFNY5qigfujqM3bs' + }, + id: '4VpptIyFNY5qigfujqM3bs', + images: [ + { + height: 640, + url: 'https://i.scdn.co/image/ab67616d0000b273c4dac9da97cbf7651701ab3d', + width: 640 + }, + { + height: 300, + url: 'https://i.scdn.co/image/ab67616d00001e02c4dac9da97cbf7651701ab3d', + width: 300 + }, + { + height: 64, + url: 'https://i.scdn.co/image/ab67616d00004851c4dac9da97cbf7651701ab3d', + width: 64 + } + ], + name: 'Снег и пепел, Volume 1: синглы и раритеты', + release_date: '2022-07-01' + } + }, + { + detail: { + artists: [ + { + external_urls: { + spotify: 'https://open.spotify.com/artist/5LfIyLdBqyQ6dubTemDmr9' + }, + id: '5LfIyLdBqyQ6dubTemDmr9', + name: 'Стереополина' + } + ], + external_urls: { + spotify: 'https://open.spotify.com/album/0EM9DoKLZX5Rm62TvPxuoy' + }, + id: '0EM9DoKLZX5Rm62TvPxuoy', + images: [ + { + height: 640, + url: 'https://i.scdn.co/image/ab67616d0000b273dc6099f7988415e3595bcec8', + width: 640 + }, + { + height: 300, + url: 'https://i.scdn.co/image/ab67616d00001e02dc6099f7988415e3595bcec8', + width: 300 + }, + { + height: 64, + url: 'https://i.scdn.co/image/ab67616d00004851dc6099f7988415e3595bcec8', + width: 64 + } + ], + name: 'Гости без будущего', + release_date: '2022-11-11' + } + }, + { + detail: { + artists: [ + { + external_urls: { + spotify: 'https://open.spotify.com/artist/5tlNJfV9UIpgnbWmvUEFu7' + }, + id: '5tlNJfV9UIpgnbWmvUEFu7', + name: 'Gentleman' + } + ], + external_urls: { + spotify: 'https://open.spotify.com/album/7timh5uVzLFbEt8bDPaIzq' + }, + id: '7timh5uVzLFbEt8bDPaIzq', + images: [ + { + height: 640, + url: 'https://i.scdn.co/image/ab67616d0000b2730994588d076f8470f6b9c8aa', + width: 640 + }, + { + height: 300, + url: 'https://i.scdn.co/image/ab67616d00001e020994588d076f8470f6b9c8aa', + width: 300 + }, + { + height: 64, + url: 'https://i.scdn.co/image/ab67616d000048510994588d076f8470f6b9c8aa', + width: 64 + } + ], + name: 'Blaue Stunde (Deluxe Version)', + release_date: '2021-05-14' + } + } + ] +} diff --git a/src/GistDatabase.ts b/src/GistDatabase.ts index 1d6495b..b04408b 100644 --- a/src/GistDatabase.ts +++ b/src/GistDatabase.ts @@ -333,7 +333,7 @@ export class GistDatabase { this.options.compression ) - const id = GistDatabase.get(database, path) + const { id } = GistDatabase.get(database, path) || {} let gist: GistResponse From b6810828302a4b28142e3ef7f38df228c8aac87b Mon Sep 17 00:00:00 2001 From: TimMikeladze Date: Mon, 2 Jan 2023 13:28:41 -0800 Subject: [PATCH 04/11] =?UTF-8?q?=E2=9C=A8=20getDatabaseId=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getDatabaseId function ✨ New feature --- src/GistDatabase.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/GistDatabase.ts b/src/GistDatabase.ts index b04408b..36fa960 100644 --- a/src/GistDatabase.ts +++ b/src/GistDatabase.ts @@ -68,6 +68,10 @@ export class GistDatabase { }) } + public getDatabaseId() { + return this.options.id + } + public static createDatabaseRoot( options: GistDatabaseOptions ): Promise { From ce69e9ebb4ec733ccc5188443a95be2a2ce80590 Mon Sep 17 00:00:00 2001 From: TimMikeladze Date: Mon, 2 Jan 2023 13:29:33 -0800 Subject: [PATCH 05/11] =?UTF-8?q?=F0=9F=90=9B=20update=20leftover=20MAX=5F?= =?UTF-8?q?FILE=5FSIZE=5FBYTES=20value?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit update leftover MAX_FILE_SIZE_BYTES value 🐛 Bugfix --- src/GistDatabase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GistDatabase.ts b/src/GistDatabase.ts index 36fa960..1b61978 100644 --- a/src/GistDatabase.ts +++ b/src/GistDatabase.ts @@ -53,7 +53,7 @@ export const defaultOptions: Partial = { export class GistDatabase { private readonly options: GistDatabaseOptions public readonly gistApi: ReturnType - public static MAX_FILE_SIZE_BYTES = 999999 // 0.99mb + public static MAX_FILE_SIZE_BYTES = 1000000 // 1mb public static MAX_FILES_PER_GIST = 10 public isNewDatabase: boolean public initialized: boolean = false From 70b5eac006d9d65849c4dfab424974ced7584da0 Mon Sep 17 00:00:00 2001 From: TimMikeladze Date: Mon, 2 Jan 2023 13:30:10 -0800 Subject: [PATCH 06/11] =?UTF-8?q?=F0=9F=94=96=20|=20v1.3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d15d8bc..1e1d873 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gist-database", - "version": "1.2.1", + "version": "1.3.0", "description": "", "author": "Tim Mikeladze ", "keywords": [ From e9572c95101e53536a5739d3bbbe68975f782d49 Mon Sep 17 00:00:00 2001 From: TimMikeladze Date: Mon, 2 Jan 2023 22:17:48 -0800 Subject: [PATCH 07/11] =?UTF-8?q?=E2=9C=85=20=F0=9F=93=9A=20=E2=9C=A8=20?= =?UTF-8?q?=F0=9F=9A=A8=20encryptionKey=20option=20and=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit encryptionKey option and documentation ✅ Adding a test, 📚 Documentation, ✨ New feature, 🚨 Tests --- .env.template | 2 + README.md | 12 ++++ __tests__/GistDatabase.test.ts | 7 ++- package.json | 2 + src/GistDatabase.ts | 103 +++++++++++++++++++++++---------- src/cli.ts | 1 - yarn.lock | 10 ++++ 7 files changed, 105 insertions(+), 32 deletions(-) diff --git a/.env.template b/.env.template index 912b8f4..7e1583b 100644 --- a/.env.template +++ b/.env.template @@ -1,2 +1,4 @@ # https://github.com/settings/tokens?type=beta with the gist read/write permissions GIST_TOKEN="" +# Some random string to use for encryption +GIST_ENCRYPTION_KEY="" diff --git a/README.md b/README.md index b9966c6..121397a 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,18 @@ When initializing `GistDatabase` you can pass an optional parameter `compression - `msgpck` - [msgpack](https://msgpack.org/) compression using [msgpackr](https://www.npmjs.com/package/msgpackr) - `pretty` - Store data as well-formatted JSON, this is useful for debugging purposes or databases where the content needs to be easily human-readable. +## 🔐 Encryption + +When initializing `GistDatabase` you can pass an optional parameter called `encryptionKey` to enable `aes-256-gcm` encryption and decryption using the [cryptr](https://github.com/MauriceButler/cryptr) package. + +```ts +const db = new GistDatabase({ + token: process.env.GIST_TOKEN, + id: process.env.GIST_ID, + encryptionKey: process.env.ENCRYPTION_KEY +}) +``` + ## ⚠️ Limitations 1. This is **not** a replacement for a **production database!** Do not store data that you cannot afford to lose or that needs to remain consistent. If it's important, use the proper database solution for your problem. diff --git a/__tests__/GistDatabase.test.ts b/__tests__/GistDatabase.test.ts index 39b361e..ca709a0 100644 --- a/__tests__/GistDatabase.test.ts +++ b/__tests__/GistDatabase.test.ts @@ -3,17 +3,22 @@ import { pendingAlbums } from './data' const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) +let index = 0 + for (const compressionType of Object.values(CompressionType)) { describe(`GistDatabase - compression: ${compressionType}`, () => { let db: GistDatabase beforeAll(async () => { db = new GistDatabase({ token: process.env.GIST_TOKEN, - compression: compressionType + compression: compressionType, + encryptionKey: + index % 2 === 0 ? process.env.GIST_ENCRYPTION_KEY : undefined }) }) afterAll(async () => { await db.destroy() + index = index + 1 }) it('sets and gets', async () => { const res = await db.set('test_one', { diff --git a/package.json b/package.json index 1e1d873..33adcfc 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ } }, "devDependencies": { + "@types/cryptr": "4.0.1", "@types/jest": "29.2.5", "@types/node": "18.11.18", "@typescript-eslint/eslint-plugin": "5.47.1", @@ -90,6 +91,7 @@ "dependencies": { "buffer": "6.0.3", "cross-fetch": "3.1.5", + "cryptr": "6.1.0", "is-plain-obj": "4.1.0", "msgpackr": "1.8.1" }, diff --git a/src/GistDatabase.ts b/src/GistDatabase.ts index 1b61978..9d4618b 100644 --- a/src/GistDatabase.ts +++ b/src/GistDatabase.ts @@ -2,16 +2,21 @@ import isPlainObject from 'is-plain-obj' import { getGistApi } from './gistApi' import { Blob } from 'buffer' import { pack, unpack } from 'msgpackr' +import Cryptr from 'cryptr' export enum CompressionType { + // eslint-disable-next-line no-unused-vars msgpack = 'msgpack', + // eslint-disable-next-line no-unused-vars none = 'none', + // eslint-disable-next-line no-unused-vars pretty = 'pretty' } export interface GistDatabaseOptions { compression?: CompressionType description?: string + encryptionKey?: string id?: string public?: boolean token: string @@ -57,6 +62,7 @@ export class GistDatabase { public static MAX_FILES_PER_GIST = 10 public isNewDatabase: boolean public initialized: boolean = false + private readonly cryptr: Cryptr constructor(options: GistDatabaseOptions) { this.options = { @@ -66,6 +72,9 @@ export class GistDatabase { this.gistApi = getGistApi({ token: this.options.token }) + this.cryptr = this.options.encryptionKey + ? new Cryptr(this.options.encryptionKey) + : undefined } public getDatabaseId() { @@ -79,12 +88,16 @@ export class GistDatabase { token: options.token }) + const cryptr = options.encryptionKey + ? new Cryptr(options.encryptionKey) + : undefined + return gistApi('/gists', 'POST', { description: options.description, public: options.public, files: { 'database.json': { - content: GistDatabase.serialize({}, options.compression) + content: GistDatabase.serialize({}, options.compression, cryptr) } } }) as Promise @@ -118,7 +131,8 @@ export class GistDatabase { const root = await this.getRoot() const database = GistDatabase.deserialize( root.files['database.json'].content, - this.options.compression + this.options.compression, + this.cryptr ) return Object.keys(database) } @@ -138,7 +152,8 @@ export class GistDatabase { const database = GistDatabase.deserialize( root.files['database.json'].content, - this.options.compression + this.options.compression, + this.cryptr ) const found: DocRef = GistDatabase.get(database, path) @@ -167,7 +182,8 @@ export class GistDatabase { const data = GistDatabase.unpack( gist.files, - this.options.compression + this.options.compression, + this.cryptr ) as DocRef & Doc if (!data) { @@ -196,7 +212,11 @@ export class GistDatabase { return (await this.get(key)) !== undefined } - public static unpack(files: GistResponse['files'], type: CompressionType) { + public static unpack( + files: GistResponse['files'], + type: CompressionType, + cryptr: Cryptr + ) { const keys = Object.keys(files) if (!keys.length) { return undefined @@ -205,7 +225,7 @@ export class GistDatabase { for (const key of keys) { data = { ...data, - ...GistDatabase.deserialize(files[key].content, type) + ...GistDatabase.deserialize(files[key].content, type, cryptr) } } return data @@ -215,7 +235,8 @@ export class GistDatabase { path, value, { ttl, createdAt }, - type: CompressionType + type: CompressionType, + cryptr: Cryptr ) { const data = { value, @@ -226,8 +247,9 @@ export class GistDatabase { } // eslint-disable-next-line no-undef - const size = new Blob([JSON.stringify(GistDatabase.serialize(value, type))]) - .size + const size = new Blob([ + JSON.stringify(GistDatabase.serialize(value, type, cryptr)) + ]).size if ( size > @@ -268,11 +290,11 @@ export class GistDatabase { const [firstHalf, secondHalf] = bisect(obj) const firstHalfSize = new Blob([ - this.serialize(keysToValues(firstHalf, obj), type) + this.serialize(keysToValues(firstHalf, obj), type, cryptr) ]).size const secondHalfSize = new Blob([ - this.serialize(keysToValues(secondHalf, obj), type) + this.serialize(keysToValues(secondHalf, obj), type, cryptr) ]).size if ( @@ -280,7 +302,7 @@ export class GistDatabase { firstHalfSize + secondHalfSize ) { results[GistDatabase.formatPath(path, index)] = { - content: this.serialize(obj, type) + content: this.serialize(obj, type, cryptr) } finished = true } else { @@ -334,7 +356,8 @@ export class GistDatabase { const database = GistDatabase.deserialize( root.files['database.json'].content, - this.options.compression + this.options.compression, + this.cryptr ) const { id } = GistDatabase.get(database, path) || {} @@ -349,7 +372,8 @@ export class GistDatabase { ttl, createdAt: Date.now() }, - this.options.compression + this.options.compression, + this.cryptr ) gist = (await this.gistApi(`/gists/${id}`, 'PATCH', { @@ -364,7 +388,8 @@ export class GistDatabase { ttl, createdAt: Date.now() }, - this.options.compression + this.options.compression, + this.cryptr ) gist = (await this.gistApi('/gists', 'POST', { @@ -386,7 +411,11 @@ export class GistDatabase { await this.gistApi(`/gists/${this.options.id}`, 'PATCH', { files: { 'database.json': { - content: GistDatabase.serialize(database, this.options.compression) + content: GistDatabase.serialize( + database, + this.options.compression, + this.cryptr + ) } } }) @@ -404,7 +433,8 @@ export class GistDatabase { const root = await this.getRoot() const database = GistDatabase.deserialize( root.files['database.json'].content, - this.options.compression + this.options.compression, + this.cryptr ) const found: DocRef = GistDatabase.get(database, path) @@ -419,7 +449,11 @@ export class GistDatabase { await this.gistApi(`/gists/${this.options.id}`, 'PATCH', { files: { 'database.json': { - content: GistDatabase.serialize(newDatabase, this.options.compression) + content: GistDatabase.serialize( + newDatabase, + this.options.compression, + this.cryptr + ) } } }) @@ -433,7 +467,8 @@ export class GistDatabase { const root = await this.getRoot() const database = GistDatabase.deserialize( root.files['database.json'].content, - this.options.compression + this.options.compression, + this.cryptr ) await Promise.allSettled( @@ -474,23 +509,31 @@ export class GistDatabase { return (Array.isArray(path) ? path.join('.') : path) + '_' + index + '.json' } - public static serialize(value: any, type: CompressionType) { - if (type === CompressionType.msgpack) { - const serialized = pack(value) - return JSON.stringify(serialized) - } else if (type === CompressionType.pretty) { - return JSON.stringify(value, null, 2) - } else { - return JSON.stringify(value) + public static serialize(value: any, type: CompressionType, cryptr: Cryptr) { + const getData = () => { + if (type === CompressionType.msgpack) { + const serialized = pack(value) + return JSON.stringify(serialized) + } else if (type === CompressionType.pretty) { + return JSON.stringify(value, null, 2) + } else { + return JSON.stringify(value) + } + } + if (cryptr) { + return cryptr.encrypt(getData()) } + return getData() } - public static deserialize(value: any, type: CompressionType) { + public static deserialize(value: any, type: CompressionType, cryptr: Cryptr) { if (type === CompressionType.msgpack) { - const buffer = Buffer.from(JSON.parse(value)) + const buffer = Buffer.from( + JSON.parse(cryptr ? cryptr.decrypt(value) : value) + ) return unpack(buffer) } else { - return JSON.parse(value) + return JSON.parse(cryptr ? cryptr.decrypt(value) : value) } } } diff --git a/src/cli.ts b/src/cli.ts index 207062e..7ce117f 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,4 +1,3 @@ -import { getGistApi } from './gistApi' import { GistDatabase } from './GistDatabase' if (!process.env.GIST_TOKEN) { diff --git a/yarn.lock b/yarn.lock index 4dde176..0abfc64 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1635,6 +1635,11 @@ dependencies: "@babel/types" "^7.3.0" +"@types/cryptr@4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/cryptr/-/cryptr-4.0.1.tgz#d4128be91d1064a2678e695a318b217912881083" + integrity sha512-Nn8fvr+8XYWK5h422lj4xQACfOg6vdhKI+Rh9ERa5Mg0cZvPL9Vn3845vSY07dNZnEs5cqkSNVMdhmf70lAYkA== + "@types/estree@*": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" @@ -2614,6 +2619,11 @@ crypto-random-string@^4.0.0: dependencies: type-fest "^1.0.1" +cryptr@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/cryptr/-/cryptr-6.1.0.tgz#d53798c4986bab76aaf21cd822107e26eb8af4e5" + integrity sha512-vJBrDcKAZ/OpMW4EptbGTD2EfNEywh6QtsPDX5kd2u1MNBmTLGC3IlOoRh8Ov7dmZhFfRjUJ07u4eCoJ8+oM9A== + css-declaration-sorter@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz#be5e1d71b7a992433fb1c542c7a1b835e45682ec" From d3fd84d4cc1049da168e3a2239d590a6fb1ca2fd Mon Sep 17 00:00:00 2001 From: TimMikeladze Date: Mon, 2 Jan 2023 22:18:42 -0800 Subject: [PATCH 08/11] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20typescript=20eslint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit typescript eslint ⬆️ Upgrading dependencies --- package.json | 4 +-- yarn.lock | 90 ++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 71 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 33adcfc..2bbbb30 100644 --- a/package.json +++ b/package.json @@ -68,8 +68,8 @@ "@types/cryptr": "4.0.1", "@types/jest": "29.2.5", "@types/node": "18.11.18", - "@typescript-eslint/eslint-plugin": "5.47.1", - "@typescript-eslint/parser": "5.47.1", + "@typescript-eslint/eslint-plugin": "5.48.0", + "@typescript-eslint/parser": "5.48.0", "commit-it": "0.0.7", "dotenv": "16.0.3", "eslint": "8.31.0", diff --git a/yarn.lock b/yarn.lock index 0abfc64..e1de2e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1738,14 +1738,14 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@5.47.1": - version "5.47.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.47.1.tgz#50cc5085578a7fa22cd46a0806c2e5eae858af02" - integrity sha512-r4RZ2Jl9kcQN7K/dcOT+J7NAimbiis4sSM9spvWimsBvDegMhKLA5vri2jG19PmIPbDjPeWzfUPQ2hjEzA4Nmg== - dependencies: - "@typescript-eslint/scope-manager" "5.47.1" - "@typescript-eslint/type-utils" "5.47.1" - "@typescript-eslint/utils" "5.47.1" +"@typescript-eslint/eslint-plugin@5.48.0": + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.0.tgz#54f8368d080eb384a455f60c2ee044e948a8ce67" + integrity sha512-SVLafp0NXpoJY7ut6VFVUU9I+YeFsDzeQwtK0WZ+xbRN3mtxJ08je+6Oi2N89qDn087COdO0u3blKZNv9VetRQ== + dependencies: + "@typescript-eslint/scope-manager" "5.48.0" + "@typescript-eslint/type-utils" "5.48.0" + "@typescript-eslint/utils" "5.48.0" debug "^4.3.4" ignore "^5.2.0" natural-compare-lite "^1.4.0" @@ -1760,14 +1760,14 @@ dependencies: "@typescript-eslint/utils" "5.47.1" -"@typescript-eslint/parser@5.47.1": - version "5.47.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.47.1.tgz#c4bf16f8c3c7608ce4bf8ff804b677fc899f173f" - integrity sha512-9Vb+KIv29r6GPu4EboWOnQM7T+UjpjXvjCPhNORlgm40a9Ia9bvaPJswvtae1gip2QEeVeGh6YquqAzEgoRAlw== +"@typescript-eslint/parser@5.48.0": + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.48.0.tgz#02803355b23884a83e543755349809a50b7ed9ba" + integrity sha512-1mxNA8qfgxX8kBvRDIHEzrRGrKHQfQlbW6iHyfHYS0Q4X1af+S6mkLNtgCOsGVl8+/LUPrqdHMssAemkrQ01qg== dependencies: - "@typescript-eslint/scope-manager" "5.47.1" - "@typescript-eslint/types" "5.47.1" - "@typescript-eslint/typescript-estree" "5.47.1" + "@typescript-eslint/scope-manager" "5.48.0" + "@typescript-eslint/types" "5.48.0" + "@typescript-eslint/typescript-estree" "5.48.0" debug "^4.3.4" "@typescript-eslint/scope-manager@5.47.1": @@ -1778,13 +1778,21 @@ "@typescript-eslint/types" "5.47.1" "@typescript-eslint/visitor-keys" "5.47.1" -"@typescript-eslint/type-utils@5.47.1": - version "5.47.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.47.1.tgz#aee13314f840ab336c1adb49a300856fd16d04ce" - integrity sha512-/UKOeo8ee80A7/GJA427oIrBi/Gd4osk/3auBUg4Rn9EahFpevVV1mUK8hjyQD5lHPqX397x6CwOk5WGh1E/1w== +"@typescript-eslint/scope-manager@5.48.0": + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.48.0.tgz#607731cb0957fbc52fd754fd79507d1b6659cecf" + integrity sha512-0AA4LviDtVtZqlyUQnZMVHydDATpD9SAX/RC5qh6cBd3xmyWvmXYF+WT1oOmxkeMnWDlUVTwdODeucUnjz3gow== dependencies: - "@typescript-eslint/typescript-estree" "5.47.1" - "@typescript-eslint/utils" "5.47.1" + "@typescript-eslint/types" "5.48.0" + "@typescript-eslint/visitor-keys" "5.48.0" + +"@typescript-eslint/type-utils@5.48.0": + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.48.0.tgz#40496dccfdc2daa14a565f8be80ad1ae3882d6d6" + integrity sha512-vbtPO5sJyFjtHkGlGK4Sthmta0Bbls4Onv0bEqOGm7hP9h8UpRsHJwsrCiWtCUndTRNQO/qe6Ijz9rnT/DB+7g== + dependencies: + "@typescript-eslint/typescript-estree" "5.48.0" + "@typescript-eslint/utils" "5.48.0" debug "^4.3.4" tsutils "^3.21.0" @@ -1793,6 +1801,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.47.1.tgz#459f07428aec5a8c4113706293c2ae876741ac8e" integrity sha512-CmALY9YWXEpwuu6377ybJBZdtSAnzXLSQcxLSqSQSbC7VfpMu/HLVdrnVJj7ycI138EHqocW02LPJErE35cE9A== +"@typescript-eslint/types@5.48.0": + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.48.0.tgz#d725da8dfcff320aab2ac6f65c97b0df30058449" + integrity sha512-UTe67B0Ypius0fnEE518NB2N8gGutIlTojeTg4nt0GQvikReVkurqxd2LvYa9q9M5MQ6rtpNyWTBxdscw40Xhw== + "@typescript-eslint/typescript-estree@5.47.1": version "5.47.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.47.1.tgz#b9d8441308aca53df7f69b2c67a887b82c9ed418" @@ -1806,6 +1819,19 @@ semver "^7.3.7" tsutils "^3.21.0" +"@typescript-eslint/typescript-estree@5.48.0": + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.48.0.tgz#a7f04bccb001003405bb5452d43953a382c2fac2" + integrity sha512-7pjd94vvIjI1zTz6aq/5wwE/YrfIyEPLtGJmRfyNR9NYIW+rOvzzUv3Cmq2hRKpvt6e9vpvPUQ7puzX7VSmsEw== + dependencies: + "@typescript-eslint/types" "5.48.0" + "@typescript-eslint/visitor-keys" "5.48.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + "@typescript-eslint/utils@5.47.1": version "5.47.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.47.1.tgz#595f25ac06e9ee28c339fd43c709402820b13d7b" @@ -1820,6 +1846,20 @@ eslint-utils "^3.0.0" semver "^7.3.7" +"@typescript-eslint/utils@5.48.0": + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.48.0.tgz#eee926af2733f7156ad8d15e51791e42ce300273" + integrity sha512-x2jrMcPaMfsHRRIkL+x96++xdzvrdBCnYRd5QiW5Wgo1OB4kDYPbC1XjWP/TNqlfK93K/lUL92erq5zPLgFScQ== + dependencies: + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.48.0" + "@typescript-eslint/types" "5.48.0" + "@typescript-eslint/typescript-estree" "5.48.0" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + semver "^7.3.7" + "@typescript-eslint/visitor-keys@5.47.1": version "5.47.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.47.1.tgz#d35c2da544dbb685db9c5b5b85adac0a1d74d1f2" @@ -1828,6 +1868,14 @@ "@typescript-eslint/types" "5.47.1" eslint-visitor-keys "^3.3.0" +"@typescript-eslint/visitor-keys@5.48.0": + version "5.48.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.48.0.tgz#4446d5e7f6cadde7140390c0e284c8702d944904" + integrity sha512-5motVPz5EgxQ0bHjut3chzBkJ3Z3sheYVcSwS5BpHZpLqSptSmELNtGixmgj65+rIfhvtQTz5i9OP2vtzdDH7Q== + dependencies: + "@typescript-eslint/types" "5.48.0" + eslint-visitor-keys "^3.3.0" + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" From 89d8839850be100975b6bc250889cee06370cc0a Mon Sep 17 00:00:00 2001 From: TimMikeladze Date: Mon, 2 Jan 2023 22:19:01 -0800 Subject: [PATCH 09/11] =?UTF-8?q?=F0=9F=94=96=20|=20v1.4.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2bbbb30..79d9809 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gist-database", - "version": "1.3.0", + "version": "1.4.0", "description": "", "author": "Tim Mikeladze ", "keywords": [ From a89b8ce4a0b429b8d928ed9cded1efd0c7d5fd74 Mon Sep 17 00:00:00 2001 From: TimMikeladze Date: Tue, 3 Jan 2023 00:29:37 -0800 Subject: [PATCH 10/11] =?UTF-8?q?=E2=9C=85=20=F0=9F=93=9A=20=E2=9C=A8=20?= =?UTF-8?q?=F0=9F=9A=A8=20add=20rev=20id=20for=20dirty=20optimistic=20lock?= =?UTF-8?q?ing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add rev id for dirty optimistic locking ✅ Adding a test, 📚 Documentation, ✨ New feature, 🚨 Tests --- README.md | 42 ++++++++++++++-- __tests__/GistDatabase.test.ts | 64 ++++++++++++++++++++++-- package.json | 3 +- src/GistDatabase.ts | 91 ++++++++++++++++++++++++---------- yarn.lock | 5 ++ 5 files changed, 173 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 121397a..e1838ef 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ interface ExampleData { foo?: string } -const res = await db.set('key', { +const original = await db.set('key', { value: { hello: 'world' } @@ -86,7 +86,8 @@ const found = await db.get('key') hello: "world" }, id: "xxxxxxxxxxxxxxxxxxx", - url: "https://api.github.com/gists/xxxxxxxxxxx" + url: "https://api.github.com/gists/xxxxxxxxxxx", + rev: "xxxxx" } **/ @@ -105,9 +106,36 @@ const updated = await db.set('key', { }, id: "xxxxxxxxxxxxxxxxxxx", url: "https://api.github.com/gists/xxxxxxxxxxx" + rev: "yyyyy", } **/ +// A rev can be used to ensure that the data is not overwritten by another process. If the rev does not match the current rev, the update will fail. +try { + await updated.set('key', { + value: { + hello: 'world', + foo: 'bar' + }, + rev: original.rev // this will throw an error + // rev: Database.rev() // leave field blank or manually generate a new rev + }) +} catch (err) { + // An error will be thrown due to the rev mismatch + console.log(err) +} + +// Trying to fetch an outdated rev will also throw an error +try { + await updated.get('key', { + rev: original.rev // this will throw an error + // rev: updated.rev // this will succeed + }) +} catch (err) { + // An error will be thrown due to the rev mismatch + console.log(err) +} + await db.has('key') // true await db.keys() // ['key'] @@ -158,10 +186,18 @@ When initializing `GistDatabase` you can pass an optional parameter called `encr const db = new GistDatabase({ token: process.env.GIST_TOKEN, id: process.env.GIST_ID, - encryptionKey: process.env.ENCRYPTION_KEY + encryptionKey: process.env.GIST_ENCRYPTION_KEY }) ``` +## 🧮 Revisions + +Each time a value is set, a new `rev` id is generated using the [nanoid](https://github.com/ai/nanoid) package. This revision is used to ensure that the data is not overwritten by another process. Before data is written the document for the corresponding key will be fetched its revision id checked with one provided. If they do not match the update will fail and an error will be thrown. + +By default, revisions are not checked when getting or setting data. To enable revision checking, pass the `rev` parameter to `get` or `set`. Typically, this would be the `rev` value returned from the previous `get` or `set` call for the same key. + +This is a dirty implementation of optimistic locking. It is not a perfect solution, but it is a simple way of **trying** to keep data consistent during concurrent writes. If you're looking for consistency guarantees then you should use a proper database solution, not this library. + ## ⚠️ Limitations 1. This is **not** a replacement for a **production database!** Do not store data that you cannot afford to lose or that needs to remain consistent. If it's important, use the proper database solution for your problem. diff --git a/__tests__/GistDatabase.test.ts b/__tests__/GistDatabase.test.ts index ca709a0..37be645 100644 --- a/__tests__/GistDatabase.test.ts +++ b/__tests__/GistDatabase.test.ts @@ -6,6 +6,7 @@ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) let index = 0 for (const compressionType of Object.values(CompressionType)) { + // for (const compressionType of [CompressionType.none]) { describe(`GistDatabase - compression: ${compressionType}`, () => { let db: GistDatabase beforeAll(async () => { @@ -33,7 +34,7 @@ for (const compressionType of Object.values(CompressionType)) { id: expect.any(String), gist: expect.any(Object) }) - expect(await db.get('test_one')).toEqual({ + expect(await db.get('test_one')).toMatchObject({ value: { name: 'test_one' }, @@ -55,12 +56,13 @@ for (const compressionType of Object.values(CompressionType)) { name: 'test_two' } }) - expect(await db.get('test_two')).toEqual({ + expect(await db.get('test_two')).toMatchObject({ value: { name: 'test_two' }, id: expect.any(String), - gist: expect.any(Object) + gist: expect.any(Object), + rev: expect.any(String) }) await db.delete('test_two') expect(await db.get('test_two')).toBeUndefined() @@ -97,6 +99,62 @@ for (const compressionType of Object.values(CompressionType)) { undefined ]) }) + + it('sets and gets with revs', async () => { + const initialRevision = GistDatabase.rev() + + const key = 'revs_tests' + + const res = await db.set(key, { + value: { + name: key + }, + rev: initialRevision + }) + + expect(res).toMatchObject({ + value: { + name: key + }, + id: expect.any(String), + gist: expect.any(Object), + rev: initialRevision + }) + + const updated = await db.set(key, { + value: { + name: key + } + }) + + await expect( + db.set(key, { + value: { + name: key + }, + rev: initialRevision + }) + ).rejects.toThrowError() + + await db.set(key, { + value: { + name: key + }, + rev: updated.rev + }) + + const found = await db.get(key) + + expect(found.rev).toBeDefined() + expect(found.rev).not.toEqual(initialRevision) + expect(found.rev).not.toEqual(updated.rev) + + await expect( + db.get(key, { + rev: initialRevision + }) + ).rejects.toThrowError() + }) }) } diff --git a/package.json b/package.json index 79d9809..1c92788 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,8 @@ "cross-fetch": "3.1.5", "cryptr": "6.1.0", "is-plain-obj": "4.1.0", - "msgpackr": "1.8.1" + "msgpackr": "1.8.1", + "nanoid": "4.0.0" }, "resolutions": { "json5": ">=2.2.2" diff --git a/src/GistDatabase.ts b/src/GistDatabase.ts index 9d4618b..a1fcec8 100644 --- a/src/GistDatabase.ts +++ b/src/GistDatabase.ts @@ -3,6 +3,7 @@ import { getGistApi } from './gistApi' import { Blob } from 'buffer' import { pack, unpack } from 'msgpackr' import Cryptr from 'cryptr' +import { nanoid } from 'nanoid' export enum CompressionType { // eslint-disable-next-line no-unused-vars @@ -40,6 +41,7 @@ export type Doc = { id: string } id: string + rev: string value: T } @@ -63,6 +65,7 @@ export class GistDatabase { public isNewDatabase: boolean public initialized: boolean = false private readonly cryptr: Cryptr + public static ROOT_GIST_NAME = 'database.json' constructor(options: GistDatabaseOptions) { this.options = { @@ -96,7 +99,7 @@ export class GistDatabase { description: options.description, public: options.public, files: { - 'database.json': { + [GistDatabase.ROOT_GIST_NAME]: { content: GistDatabase.serialize({}, options.compression, cryptr) } } @@ -130,7 +133,7 @@ export class GistDatabase { public async keys(): Promise { const root = await this.getRoot() const database = GistDatabase.deserialize( - root.files['database.json'].content, + root.files[GistDatabase.ROOT_GIST_NAME].content, this.options.compression, this.cryptr ) @@ -145,30 +148,37 @@ export class GistDatabase { )) as GistResponse } - public async get(key: string | string[]): Promise> { + public async get( + key: string | string[], + { + rev + }: { + rev?: string + } = {} + ): Promise> { const path = Array.isArray(key) ? key : [key] const root = await this.getRoot() const database = GistDatabase.deserialize( - root.files['database.json'].content, + root.files[GistDatabase.ROOT_GIST_NAME].content, this.options.compression, this.cryptr ) - const found: DocRef = GistDatabase.get(database, path) + const foundDocRef: DocRef = GistDatabase.get(database, path) - if (!found) { + if (!foundDocRef) { return undefined } - if (found.ttl.ttl && GistDatabase.ttlIsExpired(found.ttl)) { - await this.gistApi(`/gists/${found.id}`, 'DELETE') + if (foundDocRef.ttl.ttl && GistDatabase.ttlIsExpired(foundDocRef.ttl)) { + await this.gistApi(`/gists/${foundDocRef.id}`, 'DELETE') return undefined } const gist = (await this.gistApi( - `/gists/${found.id}`, + `/gists/${foundDocRef.id}`, 'GET' )) as GistResponse @@ -180,27 +190,32 @@ export class GistDatabase { return undefined } - const data = GistDatabase.unpack( + const doc = GistDatabase.unpack( gist.files, this.options.compression, this.cryptr ) as DocRef & Doc - if (!data) { + if (!doc) { return undefined } - const ttl = data.ttl + const ttl = doc.ttl if (ttl.ttl && GistDatabase.ttlIsExpired(ttl)) { - await this.gistApi(`/gists/${found.id}`, 'DELETE') + await this.gistApi(`/gists/${foundDocRef.id}`, 'DELETE') return undefined } + if (rev && doc.rev !== rev) { + throw new Error(GistDatabase.formatRevisionError(doc.rev, rev)) + } + return { gist, - id: found.id, - value: data.value + id: foundDocRef.id, + value: doc.value, + rev: doc.rev } } @@ -234,7 +249,7 @@ export class GistDatabase { public static async pack( path, value, - { ttl, createdAt }, + { ttl, createdAt, rev }, type: CompressionType, cryptr: Cryptr ) { @@ -243,7 +258,8 @@ export class GistDatabase { ttl: { ttl, createdAt - } + }, + rev } // eslint-disable-next-line no-undef @@ -342,6 +358,7 @@ export class GistDatabase { key: string | string[], args: { description?: string + rev?: string ttl?: number value?: T } @@ -355,7 +372,7 @@ export class GistDatabase { const root = await this.getRoot() const database = GistDatabase.deserialize( - root.files['database.json'].content, + root.files[GistDatabase.ROOT_GIST_NAME].content, this.options.compression, this.cryptr ) @@ -364,13 +381,25 @@ export class GistDatabase { let gist: GistResponse + let created = false + + const newRev = nanoid() + if (id) { + if (args.rev) { + const doc = await this.get(key) + if (doc && doc.rev !== args.rev) { + throw new Error(GistDatabase.formatRevisionError(doc.rev, args.rev)) + } + } + const files = await GistDatabase.pack( path, value, { ttl, - createdAt: Date.now() + createdAt: Date.now(), + rev: newRev }, this.options.compression, this.cryptr @@ -386,7 +415,8 @@ export class GistDatabase { value, { ttl, - createdAt: Date.now() + createdAt: Date.now(), + rev: args.rev || GistDatabase.rev() }, this.options.compression, this.cryptr @@ -410,7 +440,7 @@ export class GistDatabase { await this.gistApi(`/gists/${this.options.id}`, 'PATCH', { files: { - 'database.json': { + [GistDatabase.ROOT_GIST_NAME]: { content: GistDatabase.serialize( database, this.options.compression, @@ -419,12 +449,15 @@ export class GistDatabase { } } }) + + created = true } return { value: value as T, gist, - id: gist.id + id: gist.id, + rev: created && args.rev ? args.rev : newRev } } @@ -432,7 +465,7 @@ export class GistDatabase { const path = Array.isArray(key) ? key : [key] const root = await this.getRoot() const database = GistDatabase.deserialize( - root.files['database.json'].content, + root.files[GistDatabase.ROOT_GIST_NAME].content, this.options.compression, this.cryptr ) @@ -448,7 +481,7 @@ export class GistDatabase { await this.gistApi(`/gists/${this.options.id}`, 'PATCH', { files: { - 'database.json': { + [GistDatabase.ROOT_GIST_NAME]: { content: GistDatabase.serialize( newDatabase, this.options.compression, @@ -466,7 +499,7 @@ export class GistDatabase { public async destroy() { const root = await this.getRoot() const database = GistDatabase.deserialize( - root.files['database.json'].content, + root.files[GistDatabase.ROOT_GIST_NAME].content, this.options.compression, this.cryptr ) @@ -536,4 +569,12 @@ export class GistDatabase { return JSON.parse(cryptr ? cryptr.decrypt(value) : value) } } + + public static rev() { + return nanoid() + } + + public static formatRevisionError(expected: string, received: string) { + return `rev mismatch, expected ${expected} but was received ${received}` + } } diff --git a/yarn.lock b/yarn.lock index e1de2e1..6d0e45a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5219,6 +5219,11 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +nanoid@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-4.0.0.tgz#6e144dee117609232c3f415c34b0e550e64999a5" + integrity sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg== + nanoid@^3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" From ec0e83d4892938a1a09342b66bfcedb7d80fb8d5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Jan 2023 08:34:06 +0000 Subject: [PATCH 11/11] Update dependency husky to v8.0.3 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 1c92788..3749e57 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "eslint-plugin-node": "11.1.0", "eslint-plugin-promise": "6.1.1", "eslint-plugin-typescript-sort-keys": "2.1.0", - "husky": "8.0.2", + "husky": "8.0.3", "jest": "29.3.1", "lint-staged": "13.1.0", "microbundle": "0.15.1", diff --git a/yarn.lock b/yarn.lock index 6d0e45a..82686d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3922,10 +3922,10 @@ human-signals@^3.0.1: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-3.0.1.tgz#c740920859dafa50e5a3222da9d3bf4bb0e5eef5" integrity sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ== -husky@8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.2.tgz#5816a60db02650f1f22c8b69b928fd6bcd77a236" - integrity sha512-Tkv80jtvbnkK3mYWxPZePGFpQ/tT3HNSs/sasF9P2YfkMezDl3ON37YN6jUUI4eTg5LcyVynlb6r4eyvOmspvg== +husky@8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184" + integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg== iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24"