diff --git a/.github/workflows/release_node.yml b/.github/workflows/release_node.yml index 84b5acc4..0cc029cf 100644 --- a/.github/workflows/release_node.yml +++ b/.github/workflows/release_node.yml @@ -20,7 +20,7 @@ jobs: with: node-version: 18 registry-url: https://registry.npmjs.org/ - - run: npm i -g pnpm + - run: npm i -g pnpm@8 - name: setup npmrc run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > .npmrc - name: setup pnpm config diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 68ed7806..59dd47b7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,7 +15,7 @@ jobs: uses: actions/setup-node@v3 with: node-version: 18.x - - run: npm i -g pnpm + - run: npm i -g pnpm@8 - name: install dependencies run: pnpm install - name: build @@ -54,7 +54,7 @@ jobs: uses: actions/setup-node@v3 with: node-version: 18.x - - run: npm i -g pnpm + - run: npm i -g pnpm@8 - run: pnpm install - run: pnpm build - name: tests diff --git a/packages/libsql/examples/deno.ts b/packages/libsql/examples/deno.ts new file mode 100644 index 00000000..e4dadd89 --- /dev/null +++ b/packages/libsql/examples/deno.ts @@ -0,0 +1,36 @@ +const BOT_TOKEN = ""; +const TURSO_URL = ""; +const TURSO_TOKEN = ""; + +import { Bot } from "https://lib.deno.dev/x/grammy@1.x/mod.ts"; +import { LibSQLAdapter } from "https://deno.land/x/grammy_storages/libsql/src/mod.ts"; +import { createClient } from "npm:@libsql/client"; + +const bot = new Bot(BOT_TOKEN); +const tursoClient = createClient({ + url: TURSO_URL, + authToken: TURSO_TOKEN, +}); + +const libsqlAdapter = await LibSQLAdapter.create>({ table: "a-b.c$d", client: tursoClient }); + +bot.command('up', async ctx => { + const item = ctx.match; + const key = "x-y.z$w"; + const value = await libsqlAdapter.read(key) ?? {}; + const itemValue = (value[item] ?? 0) + 1; + value[item] = itemValue; + await libsqlAdapter.write(key, value); + await ctx.reply(`Incremented ${item} to ${itemValue}`); +}); +bot.command('down', async ctx => { + const item = ctx.match; + const key = "x-y.z$w"; + const value = await libsqlAdapter.read(key) ?? {}; + const itemValue = (value[item] ?? 0) - 1; + value[item] = itemValue; + await libsqlAdapter.write(key, value); + await ctx.reply(`Decremented ${item} to ${itemValue}`); +}); + +bot.start(); diff --git a/packages/libsql/examples/node.ts b/packages/libsql/examples/node.ts new file mode 100644 index 00000000..4bdac814 --- /dev/null +++ b/packages/libsql/examples/node.ts @@ -0,0 +1,36 @@ +const BOT_TOKEN = ""; +const TURSO_URL = ""; +const TURSO_TOKEN = ""; + +import { Bot } from "grammy"; +import { LibSQLAdapter } from "@grammyjs/storage-libsql"; +import { createClient } from "@libsql/client"; + +const bot = new Bot(BOT_TOKEN); +const tursoClient = createClient({ + url: TURSO_URL, + authToken: TURSO_TOKEN, +}); + +const libsqlAdapter = await LibSQLAdapter.create>({ table: "a-b.c$d", client: tursoClient }); + +bot.command('up', async ctx => { + const item = ctx.match; + const key = "x-y.z$w"; + const value = await libsqlAdapter.read(key) ?? {}; + const itemValue = (value[item] ?? 0) + 1; + value[item] = itemValue; + await libsqlAdapter.write(key, value); + await ctx.reply(`Incremented ${item} to ${itemValue}`); +}); +bot.command('down', async ctx => { + const item = ctx.match; + const key = "x-y.z$w"; + const value = await libsqlAdapter.read(key) ?? {}; + const itemValue = (value[item] ?? 0) - 1; + value[item] = itemValue; + await libsqlAdapter.write(key, value); + await ctx.reply(`Decremented ${item} to ${itemValue}`); +}); + +bot.start(); diff --git a/packages/libsql/package.json b/packages/libsql/package.json new file mode 100644 index 00000000..8f106487 --- /dev/null +++ b/packages/libsql/package.json @@ -0,0 +1,42 @@ +{ + "name": "@grammyjs/storage-libsql", + "version": "2.4.2", + "private": false, + "description": "Turso's libSQL storage for grammY library.", + "main": "./dist/cjs/mod.js", + "module": "./dist/esm/mod.js", + "exports": { + ".": { + "import": "./dist/esm/mod.js", + "require": "./dist/cjs/mod.js" + } + }, + "files": [ + "README.md", + "dist", + "package.json", + "LICENSE" + ], + "scripts": { + "test": "echo \"Error: no tests found\"", + "test:deno": "echo \"Error: no tests found\"", + "prebuild": "rimraf dist", + "build": "deno2node tsconfig.cjs.json && deno2node tsconfig.esm.json && pnpm postbuild", + "postbuild": "tsx ../../tools/postBuildFixup.ts --path=dist", + "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s --commit-path ." + }, + "repository": { + "type": "git", + "url": "git+https://github.com/grammyjs/storages.git" + }, + "author": "KnightNiwrem ", + "license": "MIT", + "bugs": { + "url": "https://github.com/grammyjs/storages/issues" + }, + "homepage": "https://github.com/grammyjs/storages/tree/main/packages/libsql#readme", + "devDependencies": { + "@libsql/client": "^0.14.0", + "grammy": "^1.21.1" + } +} diff --git a/packages/libsql/src/deps.deno.ts b/packages/libsql/src/deps.deno.ts new file mode 100644 index 00000000..c6e47685 --- /dev/null +++ b/packages/libsql/src/deps.deno.ts @@ -0,0 +1,2 @@ +export type { StorageAdapter } from 'https://lib.deno.dev/x/grammy@1.x/mod.ts'; +export type { Client } from 'npm:@libsql/client'; diff --git a/packages/libsql/src/deps.node.ts b/packages/libsql/src/deps.node.ts new file mode 100644 index 00000000..29f475a3 --- /dev/null +++ b/packages/libsql/src/deps.node.ts @@ -0,0 +1,2 @@ +export type { StorageAdapter } from 'grammy'; +export type { Client } from '@libsql/client'; diff --git a/packages/libsql/src/mod.ts b/packages/libsql/src/mod.ts new file mode 100644 index 00000000..3fc06a36 --- /dev/null +++ b/packages/libsql/src/mod.ts @@ -0,0 +1,70 @@ +import type { StorageAdapter } from './deps.deno.ts'; +import type { Client } from './deps.deno.ts'; + +/** + * Storage adapter for Turso's libSQL. + */ +export class LibSQLAdapter implements StorageAdapter { + protected client: Client; + protected table: string; + + private constructor(opts: { client: Client, table: string }) { + this.client = opts.client; + this.table = opts.table; + } + + /** + * @param opts options + * @param opts.table - Name of table where data should be stored + * @param opts.client - Turos' libSQL client + * @returns A libSQL storage adapter + * + */ + static async create(opts: { client: Client, table: string }) { + const createTableStatement = ` + CREATE TABLE IF NOT EXISTS "${opts.table}" ( + key TEXT NOT NULL, + value TEXT + );`; + await opts.client.execute(createTableStatement); + + const createIndexStatement = `CREATE UNIQUE INDEX IF NOT EXISTS "IDX_${opts.table}" ON "${opts.table}" (key);`; + await opts.client.execute(createIndexStatement); + + return new LibSQLAdapter(opts); + } + + /** + * @param key The unique index + * @returns The value for the given key, or undefined if not found + */ + async read(key: string) { + const readStatement = `SELECT value from "${this.table}" WHERE key = ? LIMIT 1;`; + const readResult = await this.client.execute({ sql: readStatement, args: [key] }); + + const value = readResult.rows[0]?.value as string; + if (!value) { + return undefined; + } + + return JSON.parse(value) as T; + } + + /** + * @param key The unique index + * @param value The JSON-stringifiable value for the given key + */ + async write(key: string, value: T) { + const jsonValue = JSON.stringify(value); + const writeStatement = `INSERT OR REPLACE INTO "${this.table}" (key, value) values (?, ?);`; + await this.client.execute({ sql: writeStatement, args: [key, jsonValue] }); + } + + /** + * @param key The unique index + */ + async delete(key: string) { + const deleteStatement = `DELETE FROM "${this.table}" WHERE key = ?;`; + await this.client.execute({ sql: deleteStatement, args: [key] }); + } +} diff --git a/packages/libsql/tsconfig.cjs.json b/packages/libsql/tsconfig.cjs.json new file mode 100644 index 00000000..418b1fac --- /dev/null +++ b/packages/libsql/tsconfig.cjs.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "./dist/cjs" + }, +} diff --git a/packages/libsql/tsconfig.esm.json b/packages/libsql/tsconfig.esm.json new file mode 100644 index 00000000..a3b5d3fd --- /dev/null +++ b/packages/libsql/tsconfig.esm.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "es6", + "outDir": "./dist/esm" + }, +} diff --git a/packages/libsql/tsconfig.json b/packages/libsql/tsconfig.json new file mode 100644 index 00000000..a0b43515 --- /dev/null +++ b/packages/libsql/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [ + "src" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06cfcb3d..03b7b0d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,6 +105,15 @@ importers: specifier: ^2.6.11 version: 2.6.11 + packages/libsql: + devDependencies: + '@libsql/client': + specifier: ^0.14.0 + version: 0.14.0 + grammy: + specifier: ^1.21.1 + version: 1.21.1 + packages/mongodb: devDependencies: '@grammyjs/storage-utils': @@ -988,12 +997,118 @@ packages: - typescript dev: true + /@libsql/client@0.14.0: + resolution: {integrity: sha512-/9HEKfn6fwXB5aTEEoMeFh4CtG0ZzbncBb1e++OCdVpgKZ/xyMsIVYXm0w7Pv4RUel803vE6LwniB3PqD72R0Q==} + dependencies: + '@libsql/core': 0.14.0 + '@libsql/hrana-client': 0.7.0 + js-base64: 3.7.7 + libsql: 0.4.7 + promise-limit: 2.7.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: true + + /@libsql/core@0.14.0: + resolution: {integrity: sha512-nhbuXf7GP3PSZgdCY2Ecj8vz187ptHlZQ0VRc751oB2C1W8jQUXKKklvt7t1LJiUTQBVJuadF628eUk+3cRi4Q==} + dependencies: + js-base64: 3.7.7 + dev: true + + /@libsql/darwin-arm64@0.4.7: + resolution: {integrity: sha512-yOL742IfWUlUevnI5PdnIT4fryY3LYTdLm56bnY0wXBw7dhFcnjuA7jrH3oSVz2mjZTHujxoITgAE7V6Z+eAbg==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@libsql/darwin-x64@0.4.7: + resolution: {integrity: sha512-ezc7V75+eoyyH07BO9tIyJdqXXcRfZMbKcLCeF8+qWK5nP8wWuMcfOVywecsXGRbT99zc5eNra4NEx6z5PkSsA==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@libsql/hrana-client@0.7.0: + resolution: {integrity: sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw==} + dependencies: + '@libsql/isomorphic-fetch': 0.3.1 + '@libsql/isomorphic-ws': 0.1.5 + js-base64: 3.7.7 + node-fetch: 3.3.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: true + + /@libsql/isomorphic-fetch@0.3.1: + resolution: {integrity: sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw==} + engines: {node: '>=18.0.0'} + dev: true + + /@libsql/isomorphic-ws@0.1.5: + resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==} + dependencies: + '@types/ws': 8.5.14 + ws: 8.16.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: true + + /@libsql/linux-arm64-gnu@0.4.7: + resolution: {integrity: sha512-WlX2VYB5diM4kFfNaYcyhw5y+UJAI3xcMkEUJZPtRDEIu85SsSFrQ+gvoKfcVh76B//ztSeEX2wl9yrjF7BBCA==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@libsql/linux-arm64-musl@0.4.7: + resolution: {integrity: sha512-6kK9xAArVRlTCpWeqnNMCoXW1pe7WITI378n4NpvU5EJ0Ok3aNTIC2nRPRjhro90QcnmLL1jPcrVwO4WD1U0xw==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@libsql/linux-x64-gnu@0.4.7: + resolution: {integrity: sha512-CMnNRCmlWQqqzlTw6NeaZXzLWI8bydaXDke63JTUCvu8R+fj/ENsLrVBtPDlxQ0wGsYdXGlrUCH8Qi9gJep0yQ==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@libsql/linux-x64-musl@0.4.7: + resolution: {integrity: sha512-nI6tpS1t6WzGAt1Kx1n1HsvtBbZ+jHn0m7ogNNT6pQHZQj7AFFTIMeDQw/i/Nt5H38np1GVRNsFe99eSIMs9XA==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@libsql/win32-x64-msvc@0.4.7: + resolution: {integrity: sha512-7pJzOWzPm6oJUxml+PCDRzYQ4A1hTMHAciTAHfFK4fkbDZX33nWPVG7Y3vqdKtslcwAzwmrNDc6sXy2nwWnbiw==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@mongodb-js/saslprep@1.1.4: resolution: {integrity: sha512-8zJ8N1x51xo9hwPh6AWnKdLGEC5N3lDa6kms1YHmFBoRhTpJR6HG8wWk0td1MVCu9cD4YBrvjZEtd5Obw0Fbnw==} dependencies: sparse-bitfield: 3.0.3 dev: true + /@neon-rs/load@0.0.4: + resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} + dev: true + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1843,6 +1958,12 @@ packages: '@types/webidl-conversions': 7.0.0 dev: true + /@types/ws@8.5.14: + resolution: {integrity: sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==} + dependencies: + '@types/node': 20.11.22 + dev: true + /@typescript-eslint/eslint-plugin@7.1.0(@typescript-eslint/parser@7.1.0)(eslint@8.57.0)(typescript@5.3.3): resolution: {integrity: sha512-j6vT/kCulhG5wBmGtstKeiVr1rdXE4nk+DT1k6trYkwlrvW9eOF5ZbgKnd/YR6PcM4uTEXa0h6Fcvf6X7Dxl0w==} engines: {node: ^16.0.0 || >=18.0.0} @@ -2921,6 +3042,11 @@ packages: resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} dev: true + /data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + dev: true + /dateformat@3.0.3: resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} dev: true @@ -3558,6 +3684,14 @@ packages: pend: 1.2.0 dev: true + /fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + dev: true + /figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -3668,6 +3802,13 @@ packages: mime-types: 2.1.35 dev: true + /formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + dependencies: + fetch-blob: 3.2.0 + dev: true + /fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} dev: true @@ -4544,6 +4685,10 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dev: true + /js-base64@3.7.7: + resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} + dev: true + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: true @@ -4833,6 +4978,23 @@ packages: - supports-color dev: true + /libsql@0.4.7: + resolution: {integrity: sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw==} + cpu: [x64, arm64, wasm32] + os: [darwin, linux, win32] + dependencies: + '@neon-rs/load': 0.0.4 + detect-libc: 2.0.2 + optionalDependencies: + '@libsql/darwin-arm64': 0.4.7 + '@libsql/darwin-x64': 0.4.7 + '@libsql/linux-arm64-gnu': 0.4.7 + '@libsql/linux-arm64-musl': 0.4.7 + '@libsql/linux-x64-gnu': 0.4.7 + '@libsql/linux-x64-musl': 0.4.7 + '@libsql/win32-x64-msvc': 0.4.7 + dev: true + /lilconfig@3.0.0: resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==} engines: {node: '>=14'} @@ -5630,6 +5792,11 @@ packages: semver: 7.6.0 dev: true + /node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + dev: true + /node-fetch@2.6.11: resolution: {integrity: sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==} engines: {node: 4.x || >=6.0.0} @@ -5677,6 +5844,15 @@ packages: dependencies: whatwg-url: 5.0.0 + /node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + dev: true + /node-forge@1.3.1: resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} engines: {node: '>= 6.13.0'} @@ -6652,6 +6828,10 @@ packages: optional: true dev: true + /promise-limit@2.7.0: + resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==} + dev: true + /promise-retry@2.0.1: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} @@ -8131,6 +8311,11 @@ packages: defaults: 1.0.4 dev: true + /web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + dev: true + /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}