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 b1cecf5..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'] @@ -136,7 +164,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. @@ -150,6 +178,26 @@ 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.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 761140e..37be645 100644 --- a/__tests__/GistDatabase.test.ts +++ b/__tests__/GistDatabase.test.ts @@ -1,18 +1,25 @@ import { CompressionType, GistDatabase, GistResponse } from '../src' +import { pendingAlbums } from './data' 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 () => { 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', { @@ -27,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' }, @@ -49,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() @@ -91,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() + }) }) } @@ -102,7 +166,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) @@ -114,3 +178,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/package.json b/package.json index 297191e..3749e57 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gist-database", - "version": "1.2.0", + "version": "1.4.0", "description": "", "author": "Tim Mikeladze ", "keywords": [ @@ -65,10 +65,11 @@ } }, "devDependencies": { + "@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", @@ -78,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", @@ -90,8 +91,10 @@ "dependencies": { "buffer": "6.0.3", "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 0488181..a1fcec8 100644 --- a/src/GistDatabase.ts +++ b/src/GistDatabase.ts @@ -2,16 +2,22 @@ import isPlainObject from 'is-plain-obj' 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 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 @@ -35,6 +41,7 @@ export type Doc = { id: string } id: string + rev: string value: T } @@ -53,10 +60,12 @@ 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 + private readonly cryptr: Cryptr + public static ROOT_GIST_NAME = 'database.json' constructor(options: GistDatabaseOptions) { this.options = { @@ -66,6 +75,13 @@ export class GistDatabase { this.gistApi = getGistApi({ token: this.options.token }) + this.cryptr = this.options.encryptionKey + ? new Cryptr(this.options.encryptionKey) + : undefined + } + + public getDatabaseId() { + return this.options.id } public static createDatabaseRoot( @@ -75,12 +91,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) + [GistDatabase.ROOT_GIST_NAME]: { + content: GistDatabase.serialize({}, options.compression, cryptr) } } }) as Promise @@ -113,8 +133,9 @@ export class GistDatabase { public async keys(): Promise { const root = await this.getRoot() const database = GistDatabase.deserialize( - root.files['database.json'].content, - this.options.compression + root.files[GistDatabase.ROOT_GIST_NAME].content, + this.options.compression, + this.cryptr ) return Object.keys(database) } @@ -127,29 +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, - this.options.compression + 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 @@ -161,26 +190,32 @@ export class GistDatabase { return undefined } - const data = GistDatabase.unpack( + const doc = GistDatabase.unpack( gist.files, - this.options.compression + 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 } } @@ -192,7 +227,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 @@ -201,7 +240,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 @@ -210,20 +249,23 @@ export class GistDatabase { public static async pack( path, value, - { ttl, createdAt }, - type: CompressionType + { ttl, createdAt, rev }, + type: CompressionType, + cryptr: Cryptr ) { const data = { value, ttl: { ttl, createdAt - } + }, + rev } // 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 > @@ -264,11 +306,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 ( @@ -276,7 +318,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 { @@ -316,6 +358,7 @@ export class GistDatabase { key: string | string[], args: { description?: string + rev?: string ttl?: number value?: T } @@ -329,23 +372,37 @@ export class GistDatabase { const root = await this.getRoot() const database = GistDatabase.deserialize( - root.files['database.json'].content, - this.options.compression + root.files[GistDatabase.ROOT_GIST_NAME].content, + this.options.compression, + this.cryptr ) - const id = GistDatabase.get(database, path) + const { id } = GistDatabase.get(database, path) || {} 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.options.compression, + this.cryptr ) gist = (await this.gistApi(`/gists/${id}`, 'PATCH', { @@ -358,9 +415,11 @@ export class GistDatabase { value, { ttl, - createdAt: Date.now() + createdAt: Date.now(), + rev: args.rev || GistDatabase.rev() }, - this.options.compression + this.options.compression, + this.cryptr ) gist = (await this.gistApi('/gists', 'POST', { @@ -381,17 +440,24 @@ export class GistDatabase { await this.gistApi(`/gists/${this.options.id}`, 'PATCH', { files: { - 'database.json': { - content: GistDatabase.serialize(database, this.options.compression) + [GistDatabase.ROOT_GIST_NAME]: { + content: GistDatabase.serialize( + database, + this.options.compression, + this.cryptr + ) } } }) + + created = true } return { value: value as T, gist, - id: gist.id + id: gist.id, + rev: created && args.rev ? args.rev : newRev } } @@ -399,8 +465,9 @@ export class GistDatabase { const path = Array.isArray(key) ? key : [key] const root = await this.getRoot() const database = GistDatabase.deserialize( - root.files['database.json'].content, - this.options.compression + root.files[GistDatabase.ROOT_GIST_NAME].content, + this.options.compression, + this.cryptr ) const found: DocRef = GistDatabase.get(database, path) @@ -414,8 +481,12 @@ export class GistDatabase { await this.gistApi(`/gists/${this.options.id}`, 'PATCH', { files: { - 'database.json': { - content: GistDatabase.serialize(newDatabase, this.options.compression) + [GistDatabase.ROOT_GIST_NAME]: { + content: GistDatabase.serialize( + newDatabase, + this.options.compression, + this.cryptr + ) } } }) @@ -428,8 +499,9 @@ export class GistDatabase { public async destroy() { const root = await this.getRoot() const database = GistDatabase.deserialize( - root.files['database.json'].content, - this.options.compression + root.files[GistDatabase.ROOT_GIST_NAME].content, + this.options.compression, + this.cryptr ) await Promise.allSettled( @@ -442,39 +514,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']) { @@ -485,23 +542,39 @@ 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) } } + + public static rev() { + return nanoid() + } + + public static formatRevisionError(expected: string, received: string) { + return `rev mismatch, expected ${expected} but was received ${received}` + } } 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..82686d1 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" @@ -1733,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== +"@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.47.1" - "@typescript-eslint/type-utils" "5.47.1" - "@typescript-eslint/utils" "5.47.1" + "@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" @@ -1755,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": @@ -1773,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" @@ -1788,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" @@ -1801,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" @@ -1815,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" @@ -1823,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" @@ -2614,6 +2667,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" @@ -3864,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" @@ -5161,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"