├── .gitignore ├── test ├── tsconfig.json ├── package.json ├── __tests__ │ ├── mjs.test.mjs │ ├── cjs.test.cjs │ └── index.test.ts └── util.js ├── .replit ├── tsconfig.json ├── .semaphore └── semaphore.yml ├── package.json ├── src ├── result.ts └── index.ts ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | dev* 2 | dist 3 | node_modules 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./", 5 | "noImplicitAny": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@replit/database-test", 3 | "private": true, 4 | "dependencies": { 5 | "@replit/database": "file:.." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/__tests__/mjs.test.mjs: -------------------------------------------------------------------------------- 1 | import { test, expect } from "vitest"; 2 | import Client from '@replit/database' 3 | import { getToken } from '../util' 4 | 5 | test('esm smoke test', async () => { 6 | const db = new Client(await getToken()); 7 | 8 | expect(db).not.toBeUndefined() 9 | }) 10 | -------------------------------------------------------------------------------- /test/__tests__/cjs.test.cjs: -------------------------------------------------------------------------------- 1 | import { test, expect } from "vitest"; 2 | const Client = require('@replit/database') 3 | const utils = require('../util') 4 | 5 | test('cjs smoke test', async () => { 6 | const db = new Client(await utils.getToken()); 7 | 8 | expect(db).not.toBeUndefined() 9 | }) 10 | -------------------------------------------------------------------------------- /.replit: -------------------------------------------------------------------------------- 1 | run = "npm run test" 2 | hidden = [".config", "package-lock.json"] 3 | modules = ["nodejs-18:v35-20240401-269b323"] 4 | 5 | [nix] 6 | channel = "stable-23_05" 7 | 8 | [gitHubImport] 9 | requiredFiles = [".replit", "package.json", "package-lock.json"] 10 | 11 | [[ports]] 12 | localPort = 24678 #vitest 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "lib": ["ESNext"], 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "moduleResolution": "node", 10 | "typeRoots": ["./node_modules/@types"], 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true 14 | }, 15 | "exclude": ["node_modules", ".build"] 16 | } 17 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | async function getToken() { 2 | if (process.env.REPL_ID) { 3 | // testing locally 4 | return undefined; 5 | } else { 6 | // testing from CI 7 | const pass = process.env.USE_FILE 8 | ? process.env.RIDT_PASSWORD 9 | : process.env.JWT_PASSWORD; 10 | const url = process.env.USE_FILE 11 | ? "https://database-test-ridt-util.replit.app" 12 | : "https://database-test-jwt-util.replit.app"; 13 | const resp = await fetch(url, { 14 | headers: { 15 | Authorization: `Basic ${Buffer.from(`test:${pass}`).toString( 16 | "base64", 17 | )}`, 18 | }, 19 | }); 20 | return await resp.text(); 21 | } 22 | } 23 | 24 | module.exports = { 25 | getToken, 26 | } 27 | -------------------------------------------------------------------------------- /.semaphore/semaphore.yml: -------------------------------------------------------------------------------- 1 | version: v1.0 2 | name: database-node 3 | agent: 4 | machine: 5 | type: e1-standard-2 6 | os_image: ubuntu2004 7 | blocks: 8 | - name: Test 9 | task: 10 | jobs: 11 | - name: Test (JWT) 12 | commands: 13 | - checkout 14 | - sem-version node 18 15 | - cache restore 16 | - npm install 17 | - cache store 18 | - npm run build --if-present 19 | - npm test 20 | - name: Test (Repl Identity) 21 | commands: 22 | - checkout 23 | - sem-version node 18 24 | - cache restore 25 | - npm install 26 | - cache store 27 | - npm run build --if-present 28 | - USE_FILE=1 npm test 29 | secrets: 30 | - name: replit-database 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@replit/database", 3 | "version": "3.0.1", 4 | "engines": { 5 | "node": ">=18" 6 | }, 7 | "description": "Client for Repl.it Database", 8 | "homepage": "https://docs.replit.com/hosting/databases/replit-database", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/replit/database-node.git" 12 | }, 13 | "license": "MIT", 14 | "scripts": { 15 | "build": "rm -rf dist/ && tsup src/index.ts --platform node --format cjs,esm --dts --cjsInterop --splitting", 16 | "prepublishOnly": "npm run build", 17 | "test": "npm run build && cd test && npm install && cd ../ && vitest run test/*" 18 | }, 19 | "exports": { 20 | ".": { 21 | "require": "./dist/index.js", 22 | "import": "./dist/index.mjs", 23 | "types": "./dist/index.d.ts" 24 | } 25 | }, 26 | "main": "./dist/index.js", 27 | "types": "./dist/index.d.ts", 28 | "files": [ 29 | "dist" 30 | ], 31 | "devDependencies": { 32 | "@types/node": "^18.11.18", 33 | "tsup": "^7.2.0", 34 | "typescript": "^5.4.3", 35 | "vitest": "^1.4.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/result.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A Result type that can be used to represent a successful value or an error. 3 | * It forces the consumer to check whether the returned type is an error or not, 4 | * `result.ok` acts as a discriminant between success and failure 5 | * @public 6 | */ 7 | export type Result = 8 | | OkResult 9 | | ErrResult; 10 | 11 | /** 12 | * Represents a successful result 13 | * @public 14 | */ 15 | export interface OkResult { 16 | ok: true; 17 | value: T; 18 | error?: undefined; 19 | } 20 | 21 | /** 22 | * Represents a failure result 23 | * @public 24 | */ 25 | export interface ErrResult { 26 | ok: false; 27 | error: E; 28 | value?: undefined; 29 | errorExtras?: ErrorExtras; 30 | } 31 | 32 | /** 33 | * A helper function to create an error Result type 34 | */ 35 | export function Err( 36 | error: E, 37 | errorExtras?: ErrorExtras, 38 | ): ErrResult { 39 | return { ok: false, error, errorExtras }; 40 | } 41 | 42 | /** 43 | * A helper function to create a successful Result type 44 | **/ 45 | export function Ok(value: T): OkResult { 46 | return { ok: true, value }; 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Repl.it 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | --- 24 | The following license is carried over from the original source code: 25 | 26 | MIT License 27 | 28 | Copyright (c) 2020 Junhao Zhang 29 | 30 | Permission is hereby granted, free of charge, to any person obtaining a copy 31 | of this software and associated documentation files (the "Software"), to deal 32 | in the Software without restriction, including without limitation the rights 33 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 34 | copies of the Software, and to permit persons to whom the Software is 35 | furnished to do so, subject to the following conditions: 36 | 37 | The above copyright notice and this permission notice shall be included in all 38 | copies or substantial portions of the Software. 39 | 40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 41 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 42 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 43 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 44 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 45 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 46 | SOFTWARE. 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Replit Database Client 2 | [![Run on Repl.it](https://img.shields.io/badge/run-on_Replit-f26208?logo=replit)](https://replit.com/github/replit/database-node) [![npm: @replit/database](https://img.shields.io/npm/v/%40replit%2Fdatabase)](https://www.npmjs.com/package/@replit/database) 3 | 4 | The Replit Database client is a simple way to use [Replit Database](https://docs.replit.com/hosting/databases/replit-database) in your Node.js repls. The client expects to run within a Replit managed server context. Use this library in servers or other applications that execute on a Replit server, rather than in your user's browser. 5 | 6 | ## Installation 7 | Install the TypeScript Library with 8 | ```sh 9 | npm install @replit/database 10 | ``` 11 | 12 | This library supports [Bun](https://replit.com/@replit/Bun?v=1), [Deno](https://replit.com/@replit/Deno?v=1), and [Node.js](https://replit.com/@replit/Nodejs?v=1) (Node versions 18+ or any Node version [polyfilled with the fetch API](https://github.com/node-fetch/node-fetch#providing-global-access)). 13 | 14 | 15 | ## Quickstart 16 | ```typescript 17 | import Client from "@replit/database"; 18 | 19 | const client = new Client(); 20 | await client.set("key", "value"); 21 | let value = await client.get("key"); 22 | 23 | console.log(value); // { ok: true, value: "value" } 24 | 25 | ``` 26 | 27 | ## Docs 28 | 29 | Initiate a new database client: 30 | ```typescript 31 | import Client from "@replit/database"; 32 | 33 | /** 34 | * Initiates Class. 35 | * @param {String} dbUrl Custom database URL 36 | */ 37 | new Client() 38 | ``` 39 | 40 | Retrieve a value for a key from the database: 41 | ```typescript 42 | /** 43 | * Gets a key 44 | * @param {String} key Key 45 | * @param {boolean} [options.raw=false] Makes it so that we return the raw string value. Default is false. 46 | * @returns Promise | ErrResult> 47 | */ 48 | const value = await client.get(key, /* options?: {raw: boolean} */) 49 | console.log(value) 50 | // { ok: true, value: "value" } | { ok: false, error: RequestError } 51 | ``` 52 | 53 | Sets a value for a key in the database: 54 | ```typescript 55 | /** 56 | * Sets a key 57 | * @param {String} key Key 58 | * @param {any} value Value 59 | */ 60 | await client.set(key, value) 61 | ``` 62 | 63 | Deletes a key from the database: 64 | ```typescript 65 | /** 66 | * Deletes a key 67 | * @param {String} key Key 68 | */ 69 | const result = await client.delete(key) 70 | console.log(result.ok) // boolean 71 | ``` 72 | 73 | Lists all keys starting with the provided prefix: 74 | ```typescript 75 | /** 76 | * List key starting with a prefix if provided. Otherwise list all keys. 77 | * @param {String} prefix The prefix to filter by. 78 | */ 79 | const keys = await client.list("prefix-") 80 | console.log(keys) // { ok: true, value: [...] } | { ok: false, error: RequestError } 81 | ``` 82 | 83 | Clears the database: 84 | ```typescript 85 | /** 86 | * Clears the database. 87 | * @returns a Promise containing this 88 | */ 89 | await client.empty() 90 | ``` 91 | 92 | Gets all records in the database: 93 | ```typescript 94 | /** 95 | * Get all key/value pairs and return as an object 96 | * @param {boolean} [options.raw=false] Makes it so that we return the raw 97 | * string value for each key. Default is false. 98 | */ 99 | const records = await client.getAll(/* options?: {raw: boolean} */) 100 | ``` 101 | 102 | Sets multiple key value pairs: 103 | ```typescript 104 | /** 105 | * Sets multiple keys from an object. 106 | * @param {Object} obj The object. 107 | */ 108 | await client.setMultiple({keyOne: "valueOne", keyTwo: "valueTwo"}) 109 | ``` 110 | 111 | Deletes multiple keys from the database: 112 | ```typescript 113 | /** 114 | * Delete multiple entries by key. 115 | * @param {Array} args Keys 116 | */ 117 | await client.deleteMultiple(['keyOne', 'keyTwo']) 118 | ``` 119 | 120 | 121 | ## Tests 122 | ```sh 123 | npm i 124 | npm run test 125 | ``` 126 | -------------------------------------------------------------------------------- /test/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, afterEach, test, expect } from "vitest"; 2 | import Client from "@replit/database"; 3 | import * as util from "../util"; 4 | 5 | let client: Client; 6 | 7 | beforeAll(async () => { 8 | client = new Client(await util.getToken()); 9 | await client.empty(); 10 | }); 11 | 12 | afterEach(async () => { 13 | await client.empty(); 14 | }); 15 | 16 | test("create a client with a key", async () => { 17 | expect(client).toBeTruthy(); 18 | expect(typeof client["dbUrl"]).toBe("string"); 19 | }); 20 | 21 | test("sets a value", async () => { 22 | expect((await client.set("key", "value")).value).toEqual(client); 23 | expect( 24 | (await client.setMultiple({ key: "value", second: "secondThing" })).value, 25 | ).toEqual(client); 26 | }); 27 | 28 | test("list keys", async () => { 29 | await client.setMultiple({ 30 | key: "value", 31 | second: "secondThing", 32 | }); 33 | 34 | const result = (await client.list()).value; 35 | const expected = ["key", "second"]; 36 | expect(result).toEqual(expect.arrayContaining(expected)); 37 | }); 38 | 39 | test("gets a value", async () => { 40 | await client.set("key", "value"); 41 | 42 | expect((await client.get("key")).value).toEqual("value"); 43 | }); 44 | 45 | test("gets a value with forward slash in the key", async () => { 46 | await client.set("k/e/y", "v/a/l/u/e"); 47 | 48 | expect((await client.get("k/e/y")).value).toEqual("v/a/l/u/e"); 49 | }); 50 | 51 | test("gets a value double forward slashes in the key", async () => { 52 | await client.set("k//e//y", "value"); 53 | 54 | expect((await client.get("k//e//y")).value).toEqual("value"); 55 | }); 56 | 57 | test("get many values", async () => { 58 | await client.setMultiple({ 59 | key: "value", 60 | another: "value", 61 | }); 62 | 63 | expect((await client.getAll()).value).toEqual({ 64 | key: "value", 65 | another: "value", 66 | }); 67 | }); 68 | 69 | test("delete a value", async () => { 70 | await client.setMultiple({ 71 | key: "value", 72 | deleteThis: "please", 73 | somethingElse: "in delete multiple", 74 | andAnother: "again same thing", 75 | }); 76 | 77 | expect((await client.delete("deleteThis")).value).toEqual(client); 78 | expect( 79 | (await client.deleteMultiple("somethingElse", "andAnother")).value, 80 | ).toEqual(client); 81 | expect((await client.list()).value).toEqual(["key"]); 82 | expect((await client.empty()).value).toEqual(client); 83 | expect((await client.list()).value).toEqual([]); 84 | }); 85 | 86 | test("delete a value with a key with newlines", async () => { 87 | await client.setMultiple({ 88 | "key\nnewline": "value", 89 | key: "nonewline", 90 | }); 91 | 92 | expect((await client.delete("key")).value).toEqual(client); 93 | expect((await client.list()).value).toEqual(["key\nnewline"]); 94 | expect((await client.delete("key\nnewline")).value).toEqual(client); 95 | expect((await client.list()).value).toEqual([]); 96 | }); 97 | 98 | test("delete a value with a key with double forward slashes", async () => { 99 | await client.set("k//e//y", "value"); 100 | 101 | expect((await client.delete("k//e//y")).value).toEqual(client); 102 | const result = await client.get("k//e//y"); 103 | expect(result.ok).toEqual(false); 104 | expect(result.error?.statusCode).toEqual(404); 105 | }); 106 | 107 | test("list keys with newline", async () => { 108 | await client.setMultiple({ 109 | "key\nwit": "first", 110 | keywidout: "second", 111 | }); 112 | 113 | const expected = ["keywidout", "key\nwit"]; 114 | const result = await client.list(); 115 | 116 | expect(result.value).toEqual(expect.arrayContaining(expected)); 117 | }); 118 | 119 | test("ensure that we escape values when setting", async () => { 120 | expect((await client.set("a", "1;b=2")).value).toEqual(client); 121 | expect((await client.list()).value).toEqual(["a"]); 122 | expect((await client.get("a")).value).toEqual("1;b=2"); 123 | }); 124 | 125 | test("getting an error", async () => { 126 | await client.set("key", "value"); 127 | 128 | expect((await client.get("key2")).ok).toBeFalsy(); 129 | expect((await client.get("key2")).error?.statusCode).toEqual(404); 130 | }); 131 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs"; 2 | import { Err, ErrResult, Ok, OkResult, Result } from "./result"; 3 | 4 | export default class Client { 5 | private _dbUrl: string; // use this.dbUrl internally 6 | 7 | private lastDbUrlRefreshTime: number | undefined; 8 | 9 | /** 10 | * Initiates Class. 11 | * @param {String} dbUrl Custom database URL 12 | */ 13 | constructor(dbUrl?: string) { 14 | if (dbUrl) { 15 | this._dbUrl = dbUrl; 16 | } else { 17 | this._dbUrl = getDbUrl(); 18 | this.lastDbUrlRefreshTime = Date.now(); 19 | } 20 | } 21 | 22 | private get dbUrl(): string { 23 | if (!this.lastDbUrlRefreshTime) { 24 | return this._dbUrl; 25 | } 26 | 27 | if (Date.now() < this.lastDbUrlRefreshTime + 1000 * 60 * 60) { 28 | return this._dbUrl; 29 | } 30 | 31 | // refresh url 32 | this._dbUrl = getDbUrl(); 33 | this.lastDbUrlRefreshTime = Date.now(); 34 | 35 | return this._dbUrl; 36 | } 37 | 38 | /** 39 | * Gets a key 40 | * @param {String} key Key 41 | * @param {boolean} [options.raw=false] Makes it so that we return the raw string value. Default is false. 42 | */ 43 | async get( 44 | key: string, 45 | options?: { raw: boolean }, 46 | ): Promise | ErrResult> { 47 | const response = await doFetch({ 48 | urlPath: `${this.dbUrl}/${encodeURIComponent(key)}`, 49 | }); 50 | if (!response.ok) { 51 | return Err(response.error); 52 | } 53 | 54 | const text = await response.value.text(); 55 | if (options && options.raw) { 56 | return Ok(text); 57 | } 58 | 59 | if (!text) { 60 | return Ok(null); 61 | } 62 | 63 | try { 64 | // Try to parse as JSON, if it fails, we return an error 65 | const parsed = JSON.parse(text); 66 | return Ok(parsed === null || parsed === undefined ? null : parsed); 67 | } catch { 68 | return Err({ 69 | message: `Failed to parse value of ${key}, try passing a raw option to get the raw value`, 70 | }); 71 | } 72 | } 73 | 74 | /** 75 | * Sets a key 76 | * @param {String} key Key 77 | * @param {any} value Value 78 | */ 79 | async set( 80 | key: string, 81 | value: any, 82 | ): Promise | ErrResult> { 83 | const strValue = JSON.stringify(value); 84 | 85 | const response = await doFetch({ 86 | urlPath: this.dbUrl, 87 | method: "POST", 88 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, 89 | body: encodeURIComponent(key) + "=" + encodeURIComponent(strValue), 90 | }); 91 | 92 | if (response.ok) { 93 | return Ok(this); 94 | } else { 95 | return Err(response.error); 96 | } 97 | } 98 | 99 | /** 100 | * Deletes a key 101 | * @param {String} key Key 102 | */ 103 | async delete( 104 | key: string, 105 | ): Promise | ErrResult> { 106 | const response = await doFetch({ 107 | urlPath: `${this.dbUrl}/${encodeURIComponent(key)}`, 108 | method: "DELETE", 109 | }); 110 | 111 | if (response.ok) { 112 | return Ok(this); 113 | } else { 114 | return Err(response.error); 115 | } 116 | } 117 | 118 | /** 119 | * List key starting with a prefix if provided. Otherwise list all keys. 120 | * @param {String} prefix The prefix to filter by. 121 | */ 122 | async list( 123 | prefix: string = "", 124 | ): Promise | ErrResult> { 125 | const response = await doFetch({ 126 | urlPath: `${this.dbUrl}?encode=true&prefix=${encodeURIComponent(prefix)}`, 127 | }); 128 | 129 | if (!response.ok) { 130 | return Err(response.error); 131 | } 132 | 133 | const text = await response.value.text(); 134 | if (!text.length) { 135 | return Ok([]); 136 | } 137 | 138 | return Ok(text.split("\n").map(decodeURIComponent)); 139 | } 140 | 141 | /** 142 | * Clears the database. 143 | * @returns a Promise containing this 144 | */ 145 | async empty() { 146 | const keys = await this.list(); 147 | if (!keys.ok) { 148 | return Err(keys.error); 149 | } 150 | 151 | const promises: Array> = []; 152 | for (const key of keys.value) { 153 | promises.push(this.delete(key)); 154 | } 155 | 156 | const response = await Promise.all(promises); 157 | const errors = response.filter((r) => !r.ok).map((r) => r.error); 158 | if (errors.length) { 159 | return Err({ message: "Failed to empty databse" }, errors); 160 | } 161 | 162 | return Ok(this); 163 | } 164 | 165 | /** 166 | * Get all key/value pairs and return as an object 167 | * @param {boolean} [options.raw=false] Makes it so that we return the raw 168 | * string value for each key. Default is false. 169 | */ 170 | async getAll(options?: { raw: boolean }) { 171 | const keys = await this.list(); 172 | if (!keys.ok) { 173 | return Err(keys.error); 174 | } 175 | 176 | let output: Record = {}; 177 | for (const key of keys.value) { 178 | const value = await this.get(key, options); 179 | if (!value.ok) { 180 | return Err(value.error); 181 | } 182 | output[key] = value.value; 183 | } 184 | 185 | return Ok(output); 186 | } 187 | 188 | /** 189 | * Sets multiple keys from an object. 190 | * @param {Object} obj The object. 191 | */ 192 | async setMultiple(obj: Record) { 193 | const promises: Array> = []; 194 | 195 | for (const key in obj) { 196 | let val = obj[key]; 197 | promises.push(this.set(key, val)); 198 | } 199 | 200 | const response = await Promise.all(promises); 201 | const errors = response.filter((r) => !r.ok).map((r) => r.error); 202 | if (errors.length) { 203 | return Err({ message: "Failed to set multiple" }, errors); 204 | } 205 | 206 | return Ok(this); 207 | } 208 | 209 | /** 210 | * Delete multiple entries by key. 211 | * @param {Array} args Keys 212 | */ 213 | async deleteMultiple(...args: Array) { 214 | const promises: Array> = []; 215 | 216 | for (const arg of args) { 217 | promises.push(this.delete(arg)); 218 | } 219 | 220 | const response = await Promise.all(promises); 221 | const errors = response.filter((r) => !r.ok).map((r) => r.error); 222 | 223 | if (errors.length) { 224 | return Err({ message: "Failed to delete keys" }, errors); 225 | } 226 | 227 | return Ok(this); 228 | } 229 | } 230 | 231 | interface RequestError { 232 | message: string; 233 | statusCode?: number; 234 | } 235 | 236 | async function doFetch({ 237 | urlPath, 238 | ...rest 239 | }: { 240 | urlPath: string; 241 | } & RequestInit): Promise> { 242 | try { 243 | const response = await fetch(new URL(urlPath), rest); 244 | 245 | if (response.status !== 200 && response.status !== 204) { 246 | return Err({ 247 | message: await response.text(), 248 | statusCode: response.status, 249 | }); 250 | } 251 | 252 | return Ok(response); 253 | } catch (e) { 254 | return Err({ message: e instanceof Error ? e.message : "unknown error" }); 255 | } 256 | } 257 | 258 | const replitDBFilename = "/tmp/replitdb"; 259 | function getDbUrl(): string { 260 | let dbUrl: string | undefined; 261 | try { 262 | dbUrl = readFileSync(replitDBFilename, "utf8"); 263 | } catch (err) { 264 | dbUrl = process.env.REPLIT_DB_URL; 265 | } 266 | 267 | if (!dbUrl) { 268 | throw new Error("expected dbUrl, got undefined"); 269 | } 270 | 271 | return dbUrl; 272 | } 273 | --------------------------------------------------------------------------------