├── .editorconfig ├── .env.test.example ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── __test__ ├── base │ ├── delete.spec.ts │ ├── fetch.spec.ts │ ├── get.spec.ts │ ├── insert.spec.ts │ ├── put.spec.ts │ ├── putMany.spec.ts │ └── update.spec.ts ├── constants │ └── url.spec.ts ├── deta.spec.ts ├── drive │ ├── delete.spec.ts │ ├── deleteMany.spec.ts │ ├── get.spec.ts │ ├── list.spec.ts │ └── put.spec.ts ├── env.spec.ts ├── files │ └── logo.svg └── utils │ ├── deta.ts │ └── general.ts ├── jest.config.json ├── package-lock.json ├── package.json ├── rollup.config.js ├── scripts └── jest │ └── globalSetup.ts ├── src ├── base │ ├── base.ts │ ├── index.ts │ └── utils.ts ├── constants │ ├── api.ts │ ├── general.ts │ └── url.ts ├── deta.ts ├── drive │ ├── drive.ts │ └── index.ts ├── index.browser.ts ├── index.node.ts ├── index.ts ├── types │ ├── action.ts │ ├── base │ │ ├── request.ts │ │ └── response.ts │ ├── basic.ts │ ├── drive │ │ ├── request.ts │ │ └── response.ts │ └── key.ts └── utils │ ├── buffer.ts │ ├── date.ts │ ├── node.ts │ ├── number.ts │ ├── object.ts │ ├── request.ts │ ├── string.ts │ └── undefinedOrNull.ts ├── tsconfig.eslint.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 2 4 | 5 | # Unix-style newlines with a newline ending in every file 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | -------------------------------------------------------------------------------- /.env.test.example: -------------------------------------------------------------------------------- 1 | PROJECT_KEY= 2 | DB_NAME= 3 | DRIVE_NAME= 4 | USE_AUTH_TOKEN=false # set this to false if you don't want to test using AUTH_TOKEN 5 | AUTH_TOKEN= 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .eslintrc.js 4 | rollup.config.js 5 | coverage 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['airbnb-typescript/base', 'plugin:prettier/recommended'], 3 | parserOptions: { 4 | project: './tsconfig.eslint.json', 5 | }, 6 | rules: { 7 | 'import/prefer-default-export': 0, 8 | 'class-methods-use-this': 0, 9 | 'no-await-in-loop': 0, 10 | 'no-constant-condition': 0, 11 | 'global-require': 0, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.swp 10 | 11 | pids 12 | logs 13 | results 14 | tmp 15 | 16 | # Build 17 | public/css/main.css 18 | 19 | # Coverage reports 20 | coverage 21 | 22 | # API keys and secrets 23 | .env 24 | .env.test 25 | 26 | # Dependency directory 27 | node_modules 28 | bower_components 29 | 30 | # Editors 31 | .idea 32 | *.iml 33 | 34 | # OS metadata 35 | .DS_Store 36 | Thumbs.db 37 | 38 | # Ignore built ts files 39 | dist 40 | 41 | # ignore yarn.lock 42 | yarn.lock 43 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint:fix 5 | npm run format 6 | npm run test 7 | git add . 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | tsconfig.json 4 | coverage 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "semi": true, 4 | "singleQuote": true, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Abstract Computing UG (haftungsbeschränkt) 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deta 2 | 3 | Deta library for Javascript 4 | -------------------------------------------------------------------------------- /__test__/base/delete.spec.ts: -------------------------------------------------------------------------------- 1 | import { Base } from '../utils/deta'; 2 | 3 | const db = Base(); 4 | 5 | describe('Base#delete', () => { 6 | beforeAll(async () => { 7 | const inputs = [ 8 | [ 9 | { name: 'alex', age: 77 }, 10 | 'delete_two', 11 | { name: 'alex', age: 77, key: 'delete_two' }, 12 | ], 13 | [ 14 | 'hello, worlds', 15 | 'delete_three', 16 | { value: 'hello, worlds', key: 'delete_three' }, 17 | ], 18 | [7, 'delete_four', { value: 7, key: 'delete_four' }], 19 | [ 20 | ['a', 'b', 'c'], 21 | 'delete_my_abc', 22 | { value: ['a', 'b', 'c'], key: 'delete_my_abc' }, 23 | ], 24 | ]; 25 | 26 | const promises = inputs.map(async (input) => { 27 | const [value, key, expected] = input; 28 | const data = await db.put(value, key as string); 29 | expect(data).toEqual(expected); 30 | }); 31 | 32 | await Promise.all(promises); 33 | }); 34 | 35 | it.each([ 36 | ['delete_two'], 37 | ['delete_three'], 38 | ['delete_four'], 39 | ['delete_my_abc'], 40 | ['this is some random key'], 41 | ])('delete data by using key `delete("%s")`', async (key) => { 42 | const data = await db.delete(key); 43 | expect(data).toBeNull(); 44 | }); 45 | 46 | it.each([ 47 | [' ', new Error('Key is empty')], 48 | ['', new Error('Key is empty')], 49 | [null, new Error('Key is empty')], 50 | [undefined, new Error('Key is empty')], 51 | ])( 52 | 'delete data by using invalid key `delete("%s")`', 53 | async (key, expected) => { 54 | try { 55 | const data = await db.delete(key as string); 56 | expect(data).toBeNull(); 57 | } catch (err) { 58 | expect(err).toEqual(expected); 59 | } 60 | } 61 | ); 62 | }); 63 | -------------------------------------------------------------------------------- /__test__/base/fetch.spec.ts: -------------------------------------------------------------------------------- 1 | import { Base } from '../utils/deta'; 2 | import { FetchOptions } from '../../src/types/base/request'; 3 | 4 | const db = Base(); 5 | 6 | describe('Base#fetch', () => { 7 | beforeAll(async () => { 8 | const inputs = [ 9 | [ 10 | { 11 | key: 'fetch-key-1', 12 | name: 'Wesley', 13 | user_age: 27, 14 | hometown: 'San Francisco', 15 | email: 'wesley@deta.sh', 16 | }, 17 | { 18 | key: 'fetch-key-1', 19 | name: 'Wesley', 20 | user_age: 27, 21 | hometown: 'San Francisco', 22 | email: 'wesley@deta.sh', 23 | }, 24 | ], 25 | [ 26 | { 27 | key: 'fetch-key-2', 28 | name: 'Beverly', 29 | user_age: 51, 30 | hometown: 'Copernicus City', 31 | email: 'beverly@deta.sh', 32 | }, 33 | { 34 | key: 'fetch-key-2', 35 | name: 'Beverly', 36 | user_age: 51, 37 | hometown: 'Copernicus City', 38 | email: 'beverly@deta.sh', 39 | }, 40 | ], 41 | [ 42 | { 43 | key: 'fetch-key-3', 44 | name: 'Kevin Garnett', 45 | user_age: 43, 46 | hometown: 'Greenville', 47 | email: 'kevin@email.com', 48 | }, 49 | { 50 | key: 'fetch-key-3', 51 | name: 'Kevin Garnett', 52 | user_age: 43, 53 | hometown: 'Greenville', 54 | email: 'kevin@email.com', 55 | }, 56 | ], 57 | ]; 58 | 59 | const promises = inputs.map(async (input) => { 60 | const [value, expected] = input; 61 | const data = await db.put(value); 62 | expect(data).toEqual(expected); 63 | }); 64 | 65 | await Promise.all(promises); 66 | }); 67 | 68 | afterAll(async () => { 69 | const inputs = [['fetch-key-1'], ['fetch-key-2'], ['fetch-key-3']]; 70 | 71 | const promises = inputs.map(async (input) => { 72 | const [key] = input; 73 | const data = await db.delete(key); 74 | expect(data).toBeNull(); 75 | }); 76 | 77 | await Promise.all(promises); 78 | }); 79 | 80 | it.each([ 81 | [ 82 | { key: 'fetch-key-1' }, 83 | { 84 | count: 1, 85 | items: [ 86 | { 87 | key: 'fetch-key-1', 88 | name: 'Wesley', 89 | user_age: 27, 90 | hometown: 'San Francisco', 91 | email: 'wesley@deta.sh', 92 | }, 93 | ], 94 | }, 95 | ], 96 | [ 97 | { 'key?pfx': 'fetch' }, 98 | { 99 | count: 3, 100 | items: [ 101 | { 102 | key: 'fetch-key-1', 103 | name: 'Wesley', 104 | user_age: 27, 105 | hometown: 'San Francisco', 106 | email: 'wesley@deta.sh', 107 | }, 108 | { 109 | key: 'fetch-key-2', 110 | name: 'Beverly', 111 | user_age: 51, 112 | hometown: 'Copernicus City', 113 | email: 'beverly@deta.sh', 114 | }, 115 | { 116 | key: 'fetch-key-3', 117 | name: 'Kevin Garnett', 118 | user_age: 43, 119 | hometown: 'Greenville', 120 | email: 'kevin@email.com', 121 | }, 122 | ], 123 | }, 124 | ], 125 | [ 126 | { 'key?>': 'fetch-key-2' }, 127 | { 128 | count: 1, 129 | items: [ 130 | { 131 | key: 'fetch-key-3', 132 | name: 'Kevin Garnett', 133 | user_age: 43, 134 | hometown: 'Greenville', 135 | email: 'kevin@email.com', 136 | }, 137 | ], 138 | }, 139 | ], 140 | [ 141 | { 'key?<': 'fetch-key-2' }, 142 | { 143 | count: 1, 144 | items: [ 145 | { 146 | key: 'fetch-key-1', 147 | name: 'Wesley', 148 | user_age: 27, 149 | hometown: 'San Francisco', 150 | email: 'wesley@deta.sh', 151 | }, 152 | ], 153 | }, 154 | ], 155 | [ 156 | { 'key?>=': 'fetch-key-2' }, 157 | { 158 | count: 2, 159 | items: [ 160 | { 161 | key: 'fetch-key-2', 162 | name: 'Beverly', 163 | user_age: 51, 164 | hometown: 'Copernicus City', 165 | email: 'beverly@deta.sh', 166 | }, 167 | { 168 | key: 'fetch-key-3', 169 | name: 'Kevin Garnett', 170 | user_age: 43, 171 | hometown: 'Greenville', 172 | email: 'kevin@email.com', 173 | }, 174 | ], 175 | }, 176 | ], 177 | [ 178 | { 'key?<=': 'fetch-key-2' }, 179 | { 180 | count: 2, 181 | items: [ 182 | { 183 | key: 'fetch-key-1', 184 | name: 'Wesley', 185 | user_age: 27, 186 | hometown: 'San Francisco', 187 | email: 'wesley@deta.sh', 188 | }, 189 | { 190 | key: 'fetch-key-2', 191 | name: 'Beverly', 192 | user_age: 51, 193 | hometown: 'Copernicus City', 194 | email: 'beverly@deta.sh', 195 | }, 196 | ], 197 | }, 198 | ], 199 | [ 200 | { 'key?r': ['fetch-key-1', 'fetch-key-3'] }, 201 | { 202 | count: 3, 203 | items: [ 204 | { 205 | key: 'fetch-key-1', 206 | name: 'Wesley', 207 | user_age: 27, 208 | hometown: 'San Francisco', 209 | email: 'wesley@deta.sh', 210 | }, 211 | { 212 | key: 'fetch-key-2', 213 | name: 'Beverly', 214 | user_age: 51, 215 | hometown: 'Copernicus City', 216 | email: 'beverly@deta.sh', 217 | }, 218 | { 219 | key: 'fetch-key-3', 220 | name: 'Kevin Garnett', 221 | user_age: 43, 222 | hometown: 'Greenville', 223 | email: 'kevin@email.com', 224 | }, 225 | ], 226 | }, 227 | ], 228 | [ 229 | [ 230 | { 'key?>=': 'fetch-key-1', 'user_age?>': 40, 'user_age?<': 50 }, 231 | { 'key?>=': 'fetch-key-1', 'user_age?<': 40 }, 232 | ], 233 | { 234 | count: 2, 235 | items: [ 236 | { 237 | key: 'fetch-key-1', 238 | name: 'Wesley', 239 | user_age: 27, 240 | hometown: 'San Francisco', 241 | email: 'wesley@deta.sh', 242 | }, 243 | { 244 | key: 'fetch-key-3', 245 | name: 'Kevin Garnett', 246 | user_age: 43, 247 | hometown: 'Greenville', 248 | email: 'kevin@email.com', 249 | }, 250 | ], 251 | }, 252 | ], 253 | [ 254 | [{ name: 'Wesley' }, { user_age: 51 }], 255 | { 256 | count: 2, 257 | items: [ 258 | { 259 | key: 'fetch-key-1', 260 | name: 'Wesley', 261 | user_age: 27, 262 | hometown: 'San Francisco', 263 | email: 'wesley@deta.sh', 264 | }, 265 | { 266 | key: 'fetch-key-2', 267 | name: 'Beverly', 268 | user_age: 51, 269 | hometown: 'Copernicus City', 270 | email: 'beverly@deta.sh', 271 | }, 272 | ], 273 | }, 274 | ], 275 | [ 276 | { 'user_age?lt': 30 }, 277 | { 278 | count: 1, 279 | items: [ 280 | { 281 | key: 'fetch-key-1', 282 | name: 'Wesley', 283 | user_age: 27, 284 | hometown: 'San Francisco', 285 | email: 'wesley@deta.sh', 286 | }, 287 | ], 288 | }, 289 | ], 290 | [ 291 | { user_age: 27 }, 292 | { 293 | count: 1, 294 | items: [ 295 | { 296 | key: 'fetch-key-1', 297 | name: 'Wesley', 298 | user_age: 27, 299 | hometown: 'San Francisco', 300 | email: 'wesley@deta.sh', 301 | }, 302 | ], 303 | }, 304 | ], 305 | [ 306 | { user_age: 27, name: 'Wesley' }, 307 | { 308 | count: 1, 309 | items: [ 310 | { 311 | key: 'fetch-key-1', 312 | name: 'Wesley', 313 | user_age: 27, 314 | hometown: 'San Francisco', 315 | email: 'wesley@deta.sh', 316 | }, 317 | ], 318 | }, 319 | ], 320 | [ 321 | { 'user_age?gt': 27 }, 322 | { 323 | count: 2, 324 | items: [ 325 | { 326 | key: 'fetch-key-2', 327 | name: 'Beverly', 328 | user_age: 51, 329 | hometown: 'Copernicus City', 330 | email: 'beverly@deta.sh', 331 | }, 332 | { 333 | key: 'fetch-key-3', 334 | name: 'Kevin Garnett', 335 | user_age: 43, 336 | hometown: 'Greenville', 337 | email: 'kevin@email.com', 338 | }, 339 | ], 340 | }, 341 | ], 342 | [ 343 | { 'user_age?lte': 43 }, 344 | { 345 | count: 2, 346 | items: [ 347 | { 348 | key: 'fetch-key-1', 349 | name: 'Wesley', 350 | user_age: 27, 351 | hometown: 'San Francisco', 352 | email: 'wesley@deta.sh', 353 | }, 354 | { 355 | key: 'fetch-key-3', 356 | name: 'Kevin Garnett', 357 | user_age: 43, 358 | hometown: 'Greenville', 359 | email: 'kevin@email.com', 360 | }, 361 | ], 362 | }, 363 | ], 364 | [ 365 | { 'user_age?gte': 43 }, 366 | { 367 | count: 2, 368 | items: [ 369 | { 370 | key: 'fetch-key-2', 371 | name: 'Beverly', 372 | user_age: 51, 373 | hometown: 'Copernicus City', 374 | email: 'beverly@deta.sh', 375 | }, 376 | { 377 | key: 'fetch-key-3', 378 | name: 'Kevin Garnett', 379 | user_age: 43, 380 | hometown: 'Greenville', 381 | email: 'kevin@email.com', 382 | }, 383 | ], 384 | }, 385 | ], 386 | [ 387 | { 'hometown?pfx': 'San' }, 388 | { 389 | count: 1, 390 | items: [ 391 | { 392 | key: 'fetch-key-1', 393 | name: 'Wesley', 394 | user_age: 27, 395 | hometown: 'San Francisco', 396 | email: 'wesley@deta.sh', 397 | }, 398 | ], 399 | }, 400 | ], 401 | [ 402 | { 'user_age?r': [20, 45] }, 403 | { 404 | count: 2, 405 | items: [ 406 | { 407 | key: 'fetch-key-1', 408 | name: 'Wesley', 409 | user_age: 27, 410 | hometown: 'San Francisco', 411 | email: 'wesley@deta.sh', 412 | }, 413 | { 414 | key: 'fetch-key-3', 415 | name: 'Kevin Garnett', 416 | user_age: 43, 417 | hometown: 'Greenville', 418 | email: 'kevin@email.com', 419 | }, 420 | ], 421 | }, 422 | ], 423 | [ 424 | { 'email?contains': '@email.com' }, 425 | { 426 | count: 1, 427 | items: [ 428 | { 429 | key: 'fetch-key-3', 430 | name: 'Kevin Garnett', 431 | user_age: 43, 432 | hometown: 'Greenville', 433 | email: 'kevin@email.com', 434 | }, 435 | ], 436 | }, 437 | ], 438 | [ 439 | { 'email?not_contains': '@deta.sh' }, 440 | { 441 | count: 1, 442 | items: [ 443 | { 444 | key: 'fetch-key-3', 445 | name: 'Kevin Garnett', 446 | user_age: 43, 447 | hometown: 'Greenville', 448 | email: 'kevin@email.com', 449 | }, 450 | ], 451 | }, 452 | ], 453 | [ 454 | [{ 'user_age?gt': 50 }, { hometown: 'Greenville' }], 455 | { 456 | count: 2, 457 | items: [ 458 | { 459 | key: 'fetch-key-2', 460 | name: 'Beverly', 461 | user_age: 51, 462 | hometown: 'Copernicus City', 463 | email: 'beverly@deta.sh', 464 | }, 465 | { 466 | key: 'fetch-key-3', 467 | name: 'Kevin Garnett', 468 | user_age: 43, 469 | hometown: 'Greenville', 470 | email: 'kevin@email.com', 471 | }, 472 | ], 473 | }, 474 | ], 475 | [ 476 | { 'user_age?ne': 51 }, 477 | { 478 | count: 2, 479 | items: [ 480 | { 481 | key: 'fetch-key-1', 482 | name: 'Wesley', 483 | user_age: 27, 484 | hometown: 'San Francisco', 485 | email: 'wesley@deta.sh', 486 | }, 487 | { 488 | key: 'fetch-key-3', 489 | name: 'Kevin Garnett', 490 | user_age: 43, 491 | hometown: 'Greenville', 492 | email: 'kevin@email.com', 493 | }, 494 | ], 495 | }, 496 | ], 497 | [ 498 | { fetch_does_not_exist: 'fetch_value_does_not_exist' }, 499 | { 500 | count: 0, 501 | last: undefined, 502 | items: [], 503 | }, 504 | ], 505 | [ 506 | {}, 507 | { 508 | count: 3, 509 | items: [ 510 | { 511 | key: 'fetch-key-1', 512 | name: 'Wesley', 513 | user_age: 27, 514 | hometown: 'San Francisco', 515 | email: 'wesley@deta.sh', 516 | }, 517 | { 518 | key: 'fetch-key-2', 519 | name: 'Beverly', 520 | user_age: 51, 521 | hometown: 'Copernicus City', 522 | email: 'beverly@deta.sh', 523 | }, 524 | { 525 | key: 'fetch-key-3', 526 | name: 'Kevin Garnett', 527 | user_age: 43, 528 | hometown: 'Greenville', 529 | email: 'kevin@email.com', 530 | }, 531 | ], 532 | }, 533 | ], 534 | [ 535 | [], 536 | { 537 | count: 3, 538 | items: [ 539 | { 540 | key: 'fetch-key-1', 541 | name: 'Wesley', 542 | user_age: 27, 543 | hometown: 'San Francisco', 544 | email: 'wesley@deta.sh', 545 | }, 546 | { 547 | key: 'fetch-key-2', 548 | name: 'Beverly', 549 | user_age: 51, 550 | hometown: 'Copernicus City', 551 | email: 'beverly@deta.sh', 552 | }, 553 | { 554 | key: 'fetch-key-3', 555 | name: 'Kevin Garnett', 556 | user_age: 43, 557 | hometown: 'Greenville', 558 | email: 'kevin@email.com', 559 | }, 560 | ], 561 | }, 562 | ], 563 | ])('fetch data by using fetch query `fetch(%p)`', async (query, expected) => { 564 | const res = await db.fetch(query); 565 | expect(res).toEqual(expected); 566 | }); 567 | 568 | it.each([ 569 | [ 570 | { name: 'Wesley' }, 571 | { limit: 1 }, 572 | { 573 | count: 1, 574 | items: [ 575 | { 576 | key: 'fetch-key-1', 577 | name: 'Wesley', 578 | user_age: 27, 579 | hometown: 'San Francisco', 580 | email: 'wesley@deta.sh', 581 | }, 582 | ], 583 | }, 584 | ], 585 | [ 586 | { 'user_age?ne': 51 }, 587 | { limit: 1 }, 588 | { 589 | count: 1, 590 | last: 'fetch-key-1', 591 | items: [ 592 | { 593 | key: 'fetch-key-1', 594 | name: 'Wesley', 595 | user_age: 27, 596 | hometown: 'San Francisco', 597 | email: 'wesley@deta.sh', 598 | }, 599 | ], 600 | }, 601 | ], 602 | [ 603 | { 'user_age?ne': 51 }, 604 | { limit: 1, last: 'fetch-key-1' }, 605 | { 606 | count: 1, 607 | items: [ 608 | { 609 | key: 'fetch-key-3', 610 | name: 'Kevin Garnett', 611 | user_age: 43, 612 | hometown: 'Greenville', 613 | email: 'kevin@email.com', 614 | }, 615 | ], 616 | }, 617 | ], 618 | [ 619 | [{ 'user_age?gt': 50 }, { hometown: 'Greenville' }], 620 | { limit: 2 }, 621 | { 622 | count: 2, 623 | items: [ 624 | { 625 | key: 'fetch-key-2', 626 | name: 'Beverly', 627 | user_age: 51, 628 | hometown: 'Copernicus City', 629 | email: 'beverly@deta.sh', 630 | }, 631 | { 632 | key: 'fetch-key-3', 633 | name: 'Kevin Garnett', 634 | user_age: 43, 635 | hometown: 'Greenville', 636 | email: 'kevin@email.com', 637 | }, 638 | ], 639 | }, 640 | ], 641 | [ 642 | [], 643 | { limit: 2 }, 644 | { 645 | count: 2, 646 | last: 'fetch-key-2', 647 | items: [ 648 | { 649 | key: 'fetch-key-1', 650 | name: 'Wesley', 651 | user_age: 27, 652 | hometown: 'San Francisco', 653 | email: 'wesley@deta.sh', 654 | }, 655 | { 656 | key: 'fetch-key-2', 657 | name: 'Beverly', 658 | user_age: 51, 659 | hometown: 'Copernicus City', 660 | email: 'beverly@deta.sh', 661 | }, 662 | ], 663 | }, 664 | ], 665 | [ 666 | {}, 667 | { limit: 2 }, 668 | { 669 | count: 2, 670 | last: 'fetch-key-2', 671 | items: [ 672 | { 673 | key: 'fetch-key-1', 674 | name: 'Wesley', 675 | user_age: 27, 676 | hometown: 'San Francisco', 677 | email: 'wesley@deta.sh', 678 | }, 679 | { 680 | key: 'fetch-key-2', 681 | name: 'Beverly', 682 | user_age: 51, 683 | hometown: 'Copernicus City', 684 | email: 'beverly@deta.sh', 685 | }, 686 | ], 687 | }, 688 | ], 689 | [ 690 | {}, 691 | { limit: 3 }, 692 | { 693 | count: 3, 694 | items: [ 695 | { 696 | key: 'fetch-key-1', 697 | name: 'Wesley', 698 | user_age: 27, 699 | hometown: 'San Francisco', 700 | email: 'wesley@deta.sh', 701 | }, 702 | { 703 | key: 'fetch-key-2', 704 | name: 'Beverly', 705 | user_age: 51, 706 | hometown: 'Copernicus City', 707 | email: 'beverly@deta.sh', 708 | }, 709 | { 710 | key: 'fetch-key-3', 711 | name: 'Kevin Garnett', 712 | user_age: 43, 713 | hometown: 'Greenville', 714 | email: 'kevin@email.com', 715 | }, 716 | ], 717 | }, 718 | ], 719 | [ 720 | [], 721 | { limit: 3 }, 722 | { 723 | count: 3, 724 | items: [ 725 | { 726 | key: 'fetch-key-1', 727 | name: 'Wesley', 728 | user_age: 27, 729 | hometown: 'San Francisco', 730 | email: 'wesley@deta.sh', 731 | }, 732 | { 733 | key: 'fetch-key-2', 734 | name: 'Beverly', 735 | user_age: 51, 736 | hometown: 'Copernicus City', 737 | email: 'beverly@deta.sh', 738 | }, 739 | { 740 | key: 'fetch-key-3', 741 | name: 'Kevin Garnett', 742 | user_age: 43, 743 | hometown: 'Greenville', 744 | email: 'kevin@email.com', 745 | }, 746 | ], 747 | }, 748 | ], 749 | ])( 750 | 'fetch data using query and options `fetch(%p, %p)`', 751 | async (query, options, expected) => { 752 | const res = await db.fetch(query, options as FetchOptions); 753 | expect(res).toEqual(expected); 754 | } 755 | ); 756 | 757 | it('fetch data `fetch()`', async () => { 758 | const expected = [ 759 | { 760 | key: 'fetch-key-1', 761 | name: 'Wesley', 762 | user_age: 27, 763 | hometown: 'San Francisco', 764 | email: 'wesley@deta.sh', 765 | }, 766 | { 767 | key: 'fetch-key-2', 768 | name: 'Beverly', 769 | user_age: 51, 770 | hometown: 'Copernicus City', 771 | email: 'beverly@deta.sh', 772 | }, 773 | { 774 | key: 'fetch-key-3', 775 | name: 'Kevin Garnett', 776 | user_age: 43, 777 | hometown: 'Greenville', 778 | email: 'kevin@email.com', 779 | }, 780 | ]; 781 | const { items } = await db.fetch(); 782 | expect(items).toEqual(expected); 783 | }); 784 | 785 | it.each([ 786 | [{ 'key?': 'fetch-key-one' }, new Error('Bad query')], 787 | [{ 'key??': 'fetch-key-one' }, new Error('Bad query')], 788 | [{ 'key?pfx': 12 }, new Error('Bad query')], 789 | [{ 'key?r': [] }, new Error('Bad query')], 790 | [{ 'key?r': ['fetch-key-one'] }, new Error('Bad query')], 791 | [{ 'key?r': 'Hello world' }, new Error('Bad query')], 792 | [{ 'key?>': 12 }, new Error('Bad query')], 793 | [{ 'key?>=': 12 }, new Error('Bad query')], 794 | [{ 'key?<': 12 }, new Error('Bad query')], 795 | [{ 'key?<=': 12 }, new Error('Bad query')], 796 | [{ 'key?random': 'fetch-key-one' }, new Error('Bad query')], 797 | [ 798 | [{ 'key?<=': 'fetch-key-one' }, { key: 'fetch-key-one' }], 799 | new Error('Bad query'), 800 | ], 801 | [ 802 | [{ 'key?<=': 'fetch-key-one' }, { 'key?<=': 'fetch-key-two' }], 803 | new Error('Bad query'), 804 | ], 805 | [[{ user_age: 27 }, { 'key?<=': 'fetch-key-two' }], new Error('Bad query')], 806 | [ 807 | [ 808 | { user_age: 27, key: 'fetch-key-two', 'key?>': 'fetch-key-three' }, 809 | { 'key?<=': 'fetch-key-two' }, 810 | ], 811 | new Error('Bad query'), 812 | ], 813 | ])( 814 | 'fetch data using invalid fetch key query `fetch(%p)`', 815 | async (query, expected) => { 816 | try { 817 | const res = await db.fetch(query); 818 | expect(res).toBeNull(); 819 | } catch (err) { 820 | expect(err).toEqual(expected); 821 | } 822 | } 823 | ); 824 | 825 | it.each([ 826 | [{ 'name?': 'Beverly' }, new Error('Bad query')], 827 | [{ 'name??': 'Beverly' }, new Error('Bad query')], 828 | [{ '?': 'Beverly' }, new Error('Bad query')], 829 | [{ 'user_age?r': [] }, new Error('Bad query')], 830 | [{ 'user_age?r': [21] }, new Error('Bad query')], 831 | [{ 'name?random': 'Beverly' }, new Error('Bad query')], 832 | [{ 'name?pfx': 12 }, new Error('Bad query')], 833 | ])( 834 | 'fetch data using invalid fetch query `fetch(%p)`', 835 | async (query, expected) => { 836 | try { 837 | const res = await db.fetch(query); 838 | expect(res).toBeNull(); 839 | } catch (err) { 840 | expect(err).toEqual(expected); 841 | } 842 | } 843 | ); 844 | }); 845 | -------------------------------------------------------------------------------- /__test__/base/get.spec.ts: -------------------------------------------------------------------------------- 1 | import { Base } from '../utils/deta'; 2 | 3 | const db = Base(); 4 | 5 | describe('Base#get', () => { 6 | beforeAll(async () => { 7 | const inputs = [ 8 | [ 9 | { name: 'alex', age: 77, key: 'get_one' }, 10 | { name: 'alex', age: 77, key: 'get_one' }, 11 | ], 12 | ]; 13 | 14 | const promises = inputs.map(async (input) => { 15 | const [value, expected] = input; 16 | const data = await db.put(value); 17 | expect(data).toEqual(expected); 18 | }); 19 | 20 | await Promise.all(promises); 21 | }); 22 | 23 | afterAll(async () => { 24 | const inputs = [['get_one']]; 25 | 26 | const promises = inputs.map(async (input) => { 27 | const [key] = input; 28 | const data = await db.delete(key); 29 | expect(data).toBeNull(); 30 | }); 31 | 32 | await Promise.all(promises); 33 | }); 34 | 35 | it.each([ 36 | ['get_one', { name: 'alex', age: 77, key: 'get_one' }], 37 | ['this is some random key', null], 38 | ])('get data by using key `get("%s")`', async (key, expected) => { 39 | const data = await db.get(key); 40 | expect(data).toEqual(expected); 41 | }); 42 | 43 | it.each([ 44 | [' ', new Error('Key is empty')], 45 | ['', new Error('Key is empty')], 46 | [null, new Error('Key is empty')], 47 | [undefined, new Error('Key is empty')], 48 | ])('get data by using invalid key `get("%s")`', async (key, expected) => { 49 | try { 50 | const data = await db.get(key as string); 51 | expect(data).toBeNull(); 52 | } catch (err) { 53 | expect(err).toEqual(expected); 54 | } 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /__test__/base/insert.spec.ts: -------------------------------------------------------------------------------- 1 | import { Base } from '../utils/deta'; 2 | import { Day } from '../../src/utils/date'; 3 | import { BaseGeneral } from '../../src/constants/general'; 4 | import { mockSystemTime, useRealTime } from '../utils/general'; 5 | 6 | const db = Base(); 7 | 8 | describe('Base#insert', () => { 9 | beforeAll(() => { 10 | mockSystemTime(); 11 | }); 12 | 13 | afterAll(() => { 14 | useRealTime(); 15 | }); 16 | 17 | it.each([ 18 | [ 19 | { name: 'alex', age: 77 }, 20 | { name: 'alex', age: 77 }, 21 | ], 22 | ['hello, worlds', { value: 'hello, worlds' }], 23 | [7, { value: 7 }], 24 | ])( 25 | 'by only passing data, without key `insert(%p)`', 26 | async (input, expected) => { 27 | const data = await db.insert(input); 28 | expect(data).toEqual(expect.objectContaining(expected)); 29 | const deleteRes = await db.delete(data.key as string); 30 | expect(deleteRes).toBeNull(); 31 | } 32 | ); 33 | 34 | it.each([ 35 | [ 36 | { 37 | key: 'insert-user-a', 38 | username: 'jimmy', 39 | profile: { 40 | age: 32, 41 | active: false, 42 | hometown: 'pittsburgh', 43 | }, 44 | on_mobile: true, 45 | likes: ['anime'], 46 | purchases: 1, 47 | }, 48 | { 49 | key: 'insert-user-a', 50 | username: 'jimmy', 51 | profile: { 52 | age: 32, 53 | active: false, 54 | hometown: 'pittsburgh', 55 | }, 56 | on_mobile: true, 57 | likes: ['anime'], 58 | purchases: 1, 59 | }, 60 | ], 61 | ])( 62 | 'by passing data and key in object itself `insert(%p)`', 63 | async (input, expected) => { 64 | const data = await db.insert(input); 65 | expect(data).toEqual(expected); 66 | const deleteRes = await db.delete(data.key as string); 67 | expect(deleteRes).toBeNull(); 68 | } 69 | ); 70 | 71 | it.each([ 72 | [7, 'insert-newKey', { value: 7, key: 'insert-newKey' }], 73 | [ 74 | ['a', 'b', 'c'], 75 | 'insert-my-abc2', 76 | { value: ['a', 'b', 'c'], key: 'insert-my-abc2' }, 77 | ], 78 | ])( 79 | 'by passing data as first parameter and key as second parameter `insert(%p, "%s")`', 80 | async (value, key, expected) => { 81 | const data = await db.insert(value, key); 82 | expect(data).toEqual(expected); 83 | const deleteRes = await db.delete(data.key as string); 84 | expect(deleteRes).toBeNull(); 85 | } 86 | ); 87 | 88 | it('insert data with expireIn option', async () => { 89 | const value = 7; 90 | const key = 'insert-newKey-one'; 91 | const options = { expireIn: 500 }; 92 | const expected = { 93 | value: 7, 94 | key, 95 | [BaseGeneral.TTL_ATTRIBUTE]: new Day().addSeconds(500).getEpochSeconds(), 96 | }; 97 | const data = await db.insert(value, key, options); 98 | expect(data).toEqual(expected); 99 | const deleteRes = await db.delete(data.key as string); 100 | expect(deleteRes).toBeNull(); 101 | }); 102 | 103 | it('insert data with expireAt option', async () => { 104 | const value = 7; 105 | const key = 'insert-newKey-two'; 106 | const options = { expireAt: new Date() }; 107 | const expected = { 108 | value: 7, 109 | key, 110 | [BaseGeneral.TTL_ATTRIBUTE]: new Day().getEpochSeconds(), 111 | }; 112 | const data = await db.insert(value, key, options); 113 | expect(data).toEqual(expected); 114 | const deleteRes = await db.delete(data.key as string); 115 | expect(deleteRes).toBeNull(); 116 | }); 117 | 118 | it.each([ 119 | [ 120 | 7, 121 | 'insert-newKey-three', 122 | { expireIn: 5, expireAt: new Date() }, 123 | new Error("can't set both expireIn and expireAt options"), 124 | ], 125 | [ 126 | 7, 127 | 'insert-newKey-three', 128 | { expireIn: 'invalid' }, 129 | new Error('option expireIn should have a value of type number'), 130 | ], 131 | [ 132 | 7, 133 | 'insert-newKey-three', 134 | { expireIn: new Date() }, 135 | new Error('option expireIn should have a value of type number'), 136 | ], 137 | [ 138 | 7, 139 | 'insert-newKey-three', 140 | { expireIn: {} }, 141 | new Error('option expireIn should have a value of type number'), 142 | ], 143 | [ 144 | 7, 145 | 'insert-newKey-three', 146 | { expireIn: [] }, 147 | new Error('option expireIn should have a value of type number'), 148 | ], 149 | [ 150 | 7, 151 | 'insert-newKey-three', 152 | { expireAt: 'invalid' }, 153 | new Error('option expireAt should have a value of type number or Date'), 154 | ], 155 | [ 156 | 7, 157 | 'insert-newKey-three', 158 | { expireAt: {} }, 159 | new Error('option expireAt should have a value of type number or Date'), 160 | ], 161 | [ 162 | 7, 163 | 'insert-newKey-three', 164 | { expireAt: [] }, 165 | new Error('option expireAt should have a value of type number or Date'), 166 | ], 167 | ])( 168 | 'by passing data as first parameter, key as second parameter and invalid options as third parameter `insert(%p, "%s", %p)`', 169 | async (value, key, options, expected) => { 170 | try { 171 | const data = await db.insert(value, key, options as any); 172 | expect(data).toBeNull(); 173 | } catch (err) { 174 | expect(err).toEqual(expected); 175 | } 176 | } 177 | ); 178 | 179 | it.each([ 180 | [ 181 | { name: 'alex', age: 77 }, 182 | 'insert-two', 183 | new Error('Item with key insert-two already exists'), 184 | ], 185 | [ 186 | 'hello, worlds', 187 | 'insert-three', 188 | new Error('Item with key insert-three already exists'), 189 | ], 190 | ])( 191 | 'by passing key that already exist `insert(%p, "%s")`', 192 | async (value, key, expected) => { 193 | const data = await db.insert(value, key); // simulate key already exists 194 | try { 195 | const res = await db.insert(value, key); 196 | expect(res).toBeNull(); 197 | } catch (err) { 198 | expect(err).toEqual(expected); 199 | } 200 | // cleanup 201 | const deleteRes = await db.delete(data.key as string); 202 | expect(deleteRes).toBeNull(); 203 | } 204 | ); 205 | 206 | it('by passing key that already exists in the payload', async () => { 207 | const value = { key: 'foo', data: 'bar' }; 208 | const entry = await db.insert(value); 209 | try { 210 | const res = await db.insert(value); 211 | expect(res).toBeNull(); 212 | } catch (err) { 213 | expect(err).toEqual(new Error('Item with key foo already exists')); 214 | } 215 | // cleanup 216 | const deleteRes = await db.delete(entry.key as string); 217 | expect(deleteRes).toBeNull(); 218 | }); 219 | }); 220 | -------------------------------------------------------------------------------- /__test__/base/put.spec.ts: -------------------------------------------------------------------------------- 1 | import { Base } from '../utils/deta'; 2 | import { Day } from '../../src/utils/date'; 3 | import { BaseGeneral } from '../../src/constants/general'; 4 | import { mockSystemTime, useRealTime } from '../utils/general'; 5 | 6 | const db = Base(); 7 | 8 | describe('Base#put', () => { 9 | beforeAll(() => { 10 | mockSystemTime(); 11 | }); 12 | 13 | afterAll(() => { 14 | useRealTime(); 15 | }); 16 | 17 | it.each([ 18 | [ 19 | { name: 'alex', age: 77 }, 20 | { name: 'alex', age: 77 }, 21 | ], 22 | ['hello, worlds', { value: 'hello, worlds' }], 23 | [7, { value: 7 }], 24 | ])('by only passing data, without key `put(%p)`', async (input, expected) => { 25 | const data = await db.put(input); 26 | expect(data).toEqual(expect.objectContaining(expected)); 27 | const deleteRes = await db.delete(data?.key as string); 28 | expect(deleteRes).toBeNull(); 29 | }); 30 | 31 | it('by passing data and key in object itself', async () => { 32 | const input = { name: 'alex', age: 77, key: 'put_one' }; 33 | const data = await db.put(input); 34 | expect(data).toEqual(input); 35 | const deleteRes = await db.delete(data?.key as string); 36 | expect(deleteRes).toBeNull(); 37 | }); 38 | 39 | it.each([ 40 | [ 41 | { name: 'alex', age: 77 }, 42 | 'put_two', 43 | { name: 'alex', age: 77, key: 'put_two' }, 44 | ], 45 | [ 46 | 'hello, worlds', 47 | 'put_three', 48 | { value: 'hello, worlds', key: 'put_three' }, 49 | ], 50 | [7, 'put_four', { value: 7, key: 'put_four' }], 51 | [ 52 | ['a', 'b', 'c'], 53 | 'put_my_abc', 54 | { value: ['a', 'b', 'c'], key: 'put_my_abc' }, 55 | ], 56 | [ 57 | { key: 'put_hello', value: ['a', 'b', 'c'] }, 58 | 'put_my_abc', 59 | { value: ['a', 'b', 'c'], key: 'put_my_abc' }, 60 | ], 61 | [ 62 | { key: 'put_hello', world: ['a', 'b', 'c'] }, 63 | 'put_my_abc', 64 | { world: ['a', 'b', 'c'], key: 'put_my_abc' }, 65 | ], 66 | ])( 67 | 'by passing data as first parameter and key as second parameter `put(%p, "%s")`', 68 | async (value, key, expected) => { 69 | const data = await db.put(value, key); 70 | expect(data).toEqual(expected); 71 | const deleteRes = await db.delete(data?.key as string); 72 | expect(deleteRes).toBeNull(); 73 | } 74 | ); 75 | 76 | it('put data with expireIn option', async () => { 77 | const value = { name: 'alex', age: 77 }; 78 | const key = 'put_two'; 79 | const options = { expireIn: 5 }; 80 | const expected = { 81 | name: 'alex', 82 | age: 77, 83 | key, 84 | [BaseGeneral.TTL_ATTRIBUTE]: new Day().addSeconds(5).getEpochSeconds(), 85 | }; 86 | const data = await db.put(value, key, options); 87 | expect(data).toEqual(expected); 88 | const deleteRes = await db.delete(data?.key as string); 89 | expect(deleteRes).toBeNull(); 90 | }); 91 | 92 | it('put data with expireAt option', async () => { 93 | const value = 'hello, worlds'; 94 | const key = 'put_three'; 95 | const options = { expireAt: new Date() }; 96 | const expected = { 97 | value: 'hello, worlds', 98 | key, 99 | [BaseGeneral.TTL_ATTRIBUTE]: new Day().getEpochSeconds(), 100 | }; 101 | const data = await db.put(value, key, options); 102 | expect(data).toEqual(expected); 103 | const deleteRes = await db.delete(data?.key as string); 104 | expect(deleteRes).toBeNull(); 105 | }); 106 | 107 | it.each([ 108 | [ 109 | ['a', 'b', 'c'], 110 | 'put_my_abc', 111 | { expireIn: 5, expireAt: new Date() }, 112 | new Error("can't set both expireIn and expireAt options"), 113 | ], 114 | [ 115 | ['a', 'b', 'c'], 116 | 'put_my_abc', 117 | { expireIn: 'invalid' }, 118 | new Error('option expireIn should have a value of type number'), 119 | ], 120 | [ 121 | ['a', 'b', 'c'], 122 | 'put_my_abc', 123 | { expireIn: new Date() }, 124 | new Error('option expireIn should have a value of type number'), 125 | ], 126 | [ 127 | ['a', 'b', 'c'], 128 | 'put_my_abc', 129 | { expireIn: {} }, 130 | new Error('option expireIn should have a value of type number'), 131 | ], 132 | [ 133 | ['a', 'b', 'c'], 134 | 'put_my_abc', 135 | { expireIn: [] }, 136 | new Error('option expireIn should have a value of type number'), 137 | ], 138 | [ 139 | ['a', 'b', 'c'], 140 | 'put_my_abc', 141 | { expireAt: 'invalid' }, 142 | new Error('option expireAt should have a value of type number or Date'), 143 | ], 144 | [ 145 | ['a', 'b', 'c'], 146 | 'put_my_abc', 147 | { expireAt: {} }, 148 | new Error('option expireAt should have a value of type number or Date'), 149 | ], 150 | [ 151 | ['a', 'b', 'c'], 152 | 'put_my_abc', 153 | { expireAt: [] }, 154 | new Error('option expireAt should have a value of type number or Date'), 155 | ], 156 | ])( 157 | 'by passing data as first parameter, key as second parameter and invalid options as third parameter `put(%p, "%s", %p)`', 158 | async (value, key, options, expected) => { 159 | try { 160 | const data = await db.put(value, key, options as any); 161 | expect(data).toBeNull(); 162 | } catch (err) { 163 | expect(err).toEqual(expected); 164 | } 165 | } 166 | ); 167 | }); 168 | -------------------------------------------------------------------------------- /__test__/base/putMany.spec.ts: -------------------------------------------------------------------------------- 1 | import { Base } from '../utils/deta'; 2 | import { Day } from '../../src/utils/date'; 3 | import { BaseGeneral } from '../../src/constants/general'; 4 | import { mockSystemTime, useRealTime } from '../utils/general'; 5 | 6 | const db = Base(); 7 | 8 | describe('Base#putMany', () => { 9 | beforeAll(() => { 10 | mockSystemTime(); 11 | }); 12 | 13 | afterAll(() => { 14 | useRealTime(); 15 | }); 16 | 17 | it.each([ 18 | [ 19 | [ 20 | { name: 'Beverly', hometown: 'Copernicus City' }, 21 | 'dude', 22 | ['Namaskāra', 'marhabaan', 'hello', 'yeoboseyo'], 23 | ], 24 | { 25 | processed: { 26 | items: [ 27 | { 28 | hometown: 'Copernicus City', 29 | name: 'Beverly', 30 | }, 31 | { 32 | value: 'dude', 33 | }, 34 | { 35 | value: ['Namaskāra', 'marhabaan', 'hello', 'yeoboseyo'], 36 | }, 37 | ], 38 | }, 39 | }, 40 | ], 41 | ])('putMany items, without key `putMany(%p)`', async (items, expected) => { 42 | const data = await db.putMany(items); 43 | expect(data).toMatchObject(expected); 44 | data?.processed?.items.forEach(async (val: any) => { 45 | const deleteRes = await db.delete(val.key); 46 | expect(deleteRes).toBeNull(); 47 | }); 48 | }); 49 | 50 | it('putMany data with expireIn option', async () => { 51 | const items = [ 52 | { name: 'Beverly', hometown: 'Copernicus City' }, 53 | { name: 'Jon', hometown: 'New York' }, 54 | ]; 55 | const options = { 56 | expireIn: 233, 57 | }; 58 | const expected = { 59 | processed: { 60 | items: [ 61 | { 62 | hometown: 'Copernicus City', 63 | name: 'Beverly', 64 | [BaseGeneral.TTL_ATTRIBUTE]: new Day() 65 | .addSeconds(233) 66 | .getEpochSeconds(), 67 | }, 68 | { 69 | hometown: 'New York', 70 | name: 'Jon', 71 | [BaseGeneral.TTL_ATTRIBUTE]: new Day() 72 | .addSeconds(233) 73 | .getEpochSeconds(), 74 | }, 75 | ], 76 | }, 77 | }; 78 | const data = await db.putMany(items, options); 79 | expect(data).toMatchObject(expected); 80 | data?.processed?.items.forEach(async (val: any) => { 81 | const deleteRes = await db.delete(val.key); 82 | expect(deleteRes).toBeNull(); 83 | }); 84 | }); 85 | 86 | it('putMany data with expireAt option', async () => { 87 | const items = [ 88 | { name: 'Beverly', hometown: 'Copernicus City' }, 89 | { name: 'Jon', hometown: 'New York' }, 90 | ]; 91 | const options = { 92 | expireAt: new Date(), 93 | }; 94 | const expected = { 95 | processed: { 96 | items: [ 97 | { 98 | hometown: 'Copernicus City', 99 | name: 'Beverly', 100 | [BaseGeneral.TTL_ATTRIBUTE]: new Day().getEpochSeconds(), 101 | }, 102 | { 103 | hometown: 'New York', 104 | name: 'Jon', 105 | [BaseGeneral.TTL_ATTRIBUTE]: new Day().getEpochSeconds(), 106 | }, 107 | ], 108 | }, 109 | }; 110 | const data = await db.putMany(items, options); 111 | expect(data).toMatchObject(expected); 112 | data?.processed?.items.forEach(async (val: any) => { 113 | const deleteRes = await db.delete(val.key); 114 | expect(deleteRes).toBeNull(); 115 | }); 116 | }); 117 | 118 | it.each([ 119 | [ 120 | [ 121 | { name: 'Beverly', hometown: 'Copernicus City' }, 122 | { name: 'Jon', hometown: 'New York' }, 123 | ], 124 | { 125 | expireIn: 5, 126 | expireAt: new Date(), 127 | }, 128 | new Error("can't set both expireIn and expireAt options"), 129 | ], 130 | [ 131 | [ 132 | { name: 'Beverly', hometown: 'Copernicus City' }, 133 | { name: 'Jon', hometown: 'New York' }, 134 | ], 135 | { expireIn: 'invalid' }, 136 | new Error('option expireIn should have a value of type number'), 137 | ], 138 | [ 139 | [ 140 | { name: 'Beverly', hometown: 'Copernicus City' }, 141 | { name: 'Jon', hometown: 'New York' }, 142 | ], 143 | { expireIn: new Date() }, 144 | new Error('option expireIn should have a value of type number'), 145 | ], 146 | [ 147 | [ 148 | { name: 'Beverly', hometown: 'Copernicus City' }, 149 | { name: 'Jon', hometown: 'New York' }, 150 | ], 151 | { expireIn: {} }, 152 | new Error('option expireIn should have a value of type number'), 153 | ], 154 | [ 155 | [ 156 | { name: 'Beverly', hometown: 'Copernicus City' }, 157 | { name: 'Jon', hometown: 'New York' }, 158 | ], 159 | { expireIn: [] }, 160 | new Error('option expireIn should have a value of type number'), 161 | ], 162 | [ 163 | [ 164 | { name: 'Beverly', hometown: 'Copernicus City' }, 165 | { name: 'Jon', hometown: 'New York' }, 166 | ], 167 | { expireAt: 'invalid' }, 168 | new Error('option expireAt should have a value of type number or Date'), 169 | ], 170 | [ 171 | [ 172 | { name: 'Beverly', hometown: 'Copernicus City' }, 173 | { name: 'Jon', hometown: 'New York' }, 174 | ], 175 | { expireAt: {} }, 176 | new Error('option expireAt should have a value of type number or Date'), 177 | ], 178 | [ 179 | [ 180 | { name: 'Beverly', hometown: 'Copernicus City' }, 181 | { name: 'Jon', hometown: 'New York' }, 182 | ], 183 | { expireAt: [] }, 184 | new Error('option expireAt should have a value of type number or Date'), 185 | ], 186 | ])( 187 | 'putMany items, with invalid options `putMany(%p, %p)`', 188 | async (items, options, expected) => { 189 | try { 190 | const data = await db.putMany(items, options as any); 191 | expect(data).toBeNull(); 192 | } catch (err) { 193 | expect(err).toEqual(expected); 194 | } 195 | } 196 | ); 197 | 198 | it.each([ 199 | [ 200 | [ 201 | { 202 | key: 'put-many-key-1', 203 | name: 'Wesley', 204 | user_age: 27, 205 | hometown: 'San Francisco', 206 | email: 'wesley@deta.sh', 207 | }, 208 | { 209 | key: 'put-many-key-2', 210 | name: 'Beverly', 211 | user_age: 51, 212 | hometown: 'Copernicus City', 213 | email: 'beverly@deta.sh', 214 | }, 215 | { 216 | key: 'put-many-key-3', 217 | name: 'Kevin Garnett', 218 | user_age: 43, 219 | hometown: 'Greenville', 220 | email: 'kevin@email.com', 221 | }, 222 | ], 223 | { 224 | processed: { 225 | items: [ 226 | { 227 | key: 'put-many-key-1', 228 | name: 'Wesley', 229 | user_age: 27, 230 | hometown: 'San Francisco', 231 | email: 'wesley@deta.sh', 232 | }, 233 | { 234 | key: 'put-many-key-2', 235 | name: 'Beverly', 236 | user_age: 51, 237 | hometown: 'Copernicus City', 238 | email: 'beverly@deta.sh', 239 | }, 240 | { 241 | key: 'put-many-key-3', 242 | name: 'Kevin Garnett', 243 | user_age: 43, 244 | hometown: 'Greenville', 245 | email: 'kevin@email.com', 246 | }, 247 | ], 248 | }, 249 | }, 250 | ], 251 | ])('putMany items, with key `putMany(%p)`', async (items, expected) => { 252 | const data = await db.putMany(items); 253 | expect(data).toMatchObject(expected); 254 | data?.processed?.items.forEach(async (val: any) => { 255 | const deleteRes = await db.delete(val.key); 256 | expect(deleteRes).toBeNull(); 257 | }); 258 | }); 259 | 260 | it('putMany items is not an instance of array', async () => { 261 | const value: any = 'hello'; 262 | try { 263 | const res = await db.putMany(value); 264 | expect(res).toBeNull(); 265 | } catch (err) { 266 | expect(err).toEqual(new Error('Items must be an array')); 267 | } 268 | }); 269 | 270 | it('putMany items length is more then 25', async () => { 271 | const items = new Array(26); 272 | try { 273 | const res = await db.putMany(items); 274 | expect(res).toBeNull(); 275 | } catch (err) { 276 | expect(err).toEqual( 277 | new Error("We can't put more than 25 items at a time") 278 | ); 279 | } 280 | }); 281 | 282 | it('putMany items length is zero', async () => { 283 | const items = new Array(0); 284 | try { 285 | const res = await db.putMany(items); 286 | expect(res).toBeNull(); 287 | } catch (err) { 288 | expect(err).toEqual(new Error("Items can't be empty")); 289 | } 290 | }); 291 | }); 292 | -------------------------------------------------------------------------------- /__test__/base/update.spec.ts: -------------------------------------------------------------------------------- 1 | import { Base } from '../utils/deta'; 2 | import { Day } from '../../src/utils/date'; 3 | import { BaseGeneral } from '../../src/constants/general'; 4 | import { mockSystemTime, useRealTime } from '../utils/general'; 5 | 6 | const db = Base(); 7 | 8 | describe('Base#update', () => { 9 | beforeAll(async () => { 10 | mockSystemTime(); 11 | const inputs = [ 12 | [ 13 | { 14 | key: 'update-user-a', 15 | username: 'jimmy', 16 | profile: { 17 | age: 32, 18 | active: false, 19 | hometown: 'pittsburgh', 20 | }, 21 | on_mobile: true, 22 | likes: ['anime'], 23 | dislikes: ['comedy'], 24 | purchases: 1, 25 | }, 26 | { 27 | key: 'update-user-a', 28 | username: 'jimmy', 29 | profile: { 30 | age: 32, 31 | active: false, 32 | hometown: 'pittsburgh', 33 | }, 34 | on_mobile: true, 35 | likes: ['anime'], 36 | dislikes: ['comedy'], 37 | purchases: 1, 38 | }, 39 | ], 40 | ]; 41 | 42 | const promises = inputs.map(async (input) => { 43 | const [value, expected] = input; 44 | const data = await db.put(value); 45 | expect(data).toEqual(expected); 46 | }); 47 | 48 | await Promise.all(promises); 49 | }); 50 | 51 | afterAll(async () => { 52 | useRealTime(); 53 | const inputs = [['update-user-a']]; 54 | 55 | const promises = inputs.map(async (input) => { 56 | const [key] = input; 57 | const data = await db.delete(key); 58 | expect(data).toBeNull(); 59 | }); 60 | 61 | await Promise.all(promises); 62 | }); 63 | 64 | it.each([ 65 | [ 66 | { 67 | 'profile.age': 33, 68 | 'profile.active': true, 69 | 'profile.email': 'jimmy@deta.sh', 70 | 'profile.hometown': db.util.trim(), 71 | on_mobile: db.util.trim(), 72 | purchases: db.util.increment(2), 73 | likes: db.util.append('ramen'), 74 | dislikes: db.util.prepend('action'), 75 | }, 76 | 'update-user-a', 77 | undefined, 78 | { 79 | key: 'update-user-a', 80 | username: 'jimmy', 81 | profile: { 82 | age: 33, 83 | active: true, 84 | email: 'jimmy@deta.sh', 85 | }, 86 | likes: ['anime', 'ramen'], 87 | dislikes: ['action', 'comedy'], 88 | purchases: 3, 89 | }, 90 | ], 91 | [ 92 | { 93 | purchases: db.util.increment(), 94 | likes: db.util.append(['momo']), 95 | dislikes: db.util.prepend(['romcom']), 96 | }, 97 | 'update-user-a', 98 | {}, 99 | { 100 | key: 'update-user-a', 101 | username: 'jimmy', 102 | profile: { 103 | age: 33, 104 | active: true, 105 | email: 'jimmy@deta.sh', 106 | }, 107 | likes: ['anime', 'ramen', 'momo'], 108 | dislikes: ['romcom', 'action', 'comedy'], 109 | purchases: 4, 110 | }, 111 | ], 112 | ])( 113 | 'update data `update(%p, "%s", %p)`', 114 | async (updates, key, options, expected) => { 115 | const data = await db.update(updates, key, options as any); 116 | expect(data).toBeNull(); 117 | const updatedData = await db.get(key); 118 | expect(updatedData).toEqual(expected); 119 | } 120 | ); 121 | 122 | it.each([ 123 | [ 124 | { 125 | purchases: db.util.increment(), 126 | }, 127 | 'update-user-a', 128 | { 129 | expireIn: 5, 130 | expireAt: new Date(), 131 | }, 132 | new Error("can't set both expireIn and expireAt options"), 133 | ], 134 | [ 135 | { 136 | purchases: db.util.increment(), 137 | }, 138 | 'update-user-a', 139 | { expireIn: 'invalid' }, 140 | new Error('option expireIn should have a value of type number'), 141 | ], 142 | [ 143 | { 144 | purchases: db.util.increment(), 145 | }, 146 | 'update-user-a', 147 | { expireIn: new Date() }, 148 | new Error('option expireIn should have a value of type number'), 149 | ], 150 | [ 151 | { 152 | purchases: db.util.increment(), 153 | }, 154 | 'update-user-a', 155 | { expireIn: {} }, 156 | new Error('option expireIn should have a value of type number'), 157 | ], 158 | [ 159 | { 160 | purchases: db.util.increment(), 161 | }, 162 | 'update-user-a', 163 | { expireIn: [] }, 164 | new Error('option expireIn should have a value of type number'), 165 | ], 166 | [ 167 | { 168 | purchases: db.util.increment(), 169 | }, 170 | 'update-user-a', 171 | { expireAt: 'invalid' }, 172 | new Error('option expireAt should have a value of type number or Date'), 173 | ], 174 | [ 175 | { 176 | purchases: db.util.increment(), 177 | }, 178 | 'update-user-a', 179 | { expireAt: {} }, 180 | new Error('option expireAt should have a value of type number or Date'), 181 | ], 182 | [ 183 | { 184 | purchases: db.util.increment(), 185 | }, 186 | 'update-user-a', 187 | { expireAt: [] }, 188 | new Error('option expireAt should have a value of type number or Date'), 189 | ], 190 | ])( 191 | 'update data with invalid options `update(%p, "%s", %p)`', 192 | async (updates, key, options, expected) => { 193 | try { 194 | const data = await db.update(updates, key, options as any); 195 | expect(data).toBeNull(); 196 | } catch (err) { 197 | expect(err).toEqual(expected); 198 | } 199 | } 200 | ); 201 | 202 | it('update data with expireIn option', async () => { 203 | const updates = { 204 | purchases: db.util.increment(), 205 | }; 206 | const key = 'update-user-a'; 207 | const options = { 208 | expireIn: 5, 209 | }; 210 | const expected = { 211 | key, 212 | username: 'jimmy', 213 | profile: { 214 | age: 33, 215 | active: true, 216 | email: 'jimmy@deta.sh', 217 | }, 218 | likes: ['anime', 'ramen', 'momo'], 219 | dislikes: ['romcom', 'action', 'comedy'], 220 | purchases: 5, 221 | [BaseGeneral.TTL_ATTRIBUTE]: new Day().addSeconds(5).getEpochSeconds(), 222 | }; 223 | const data = await db.update(updates, key, options); 224 | expect(data).toBeNull(); 225 | const updatedData = await db.get(key); 226 | expect(updatedData).toEqual(expected); 227 | }); 228 | 229 | it('update data with expireAt option', async () => { 230 | const updates = { 231 | purchases: db.util.increment(), 232 | }; 233 | const key = 'update-user-a'; 234 | const options = { 235 | expireAt: new Date(), 236 | }; 237 | const expected = { 238 | key, 239 | username: 'jimmy', 240 | profile: { 241 | age: 33, 242 | active: true, 243 | email: 'jimmy@deta.sh', 244 | }, 245 | likes: ['anime', 'ramen', 'momo'], 246 | dislikes: ['romcom', 'action', 'comedy'], 247 | purchases: 6, 248 | [BaseGeneral.TTL_ATTRIBUTE]: new Day().getEpochSeconds(), 249 | }; 250 | const data = await db.update(updates, key, options); 251 | expect(data).toBeNull(); 252 | const updatedData = await db.get(key); 253 | expect(updatedData).toEqual(expected); 254 | }); 255 | 256 | it.each([ 257 | [{}, ' ', new Error('Key is empty')], 258 | [{}, '', new Error('Key is empty')], 259 | [{}, null, new Error('Key is empty')], 260 | [{}, undefined, new Error('Key is empty')], 261 | ])( 262 | 'update data by using invalid key `update(%p, "%s")`', 263 | async (updates, key, expected) => { 264 | try { 265 | const data = await db.update(updates, key as string); 266 | expect(data).toBeNull(); 267 | } catch (err) { 268 | expect(err).toEqual(expected); 269 | } 270 | } 271 | ); 272 | }); 273 | -------------------------------------------------------------------------------- /__test__/constants/url.spec.ts: -------------------------------------------------------------------------------- 1 | import url from '../../src/constants/url'; 2 | import { KeyType } from '../../src/types/key'; 3 | 4 | describe('base url', () => { 5 | it.each([ 6 | ['database.deta.sh', 'https://database.deta.sh/v1/:project_id/:base_name'], 7 | [' ', 'https://database.deta.sh/v1/:project_id/:base_name'], 8 | ['', 'https://database.deta.sh/v1/:project_id/:base_name'], 9 | ])('passed host path `url.base("%s")`', (host, expected) => { 10 | const path = url.base(KeyType.ProjectKey, host); 11 | expect(path).toEqual(expected); 12 | }); 13 | 14 | it('host path set in environment variable', () => { 15 | process.env.DETA_BASE_HOST = 'database.deta.sh'; 16 | const path = url.base(KeyType.ProjectKey); 17 | expect(path).toEqual('https://database.deta.sh/v1/:project_id/:base_name'); 18 | }); 19 | 20 | it('host is not passed', () => { 21 | const path = url.base(KeyType.ProjectKey); 22 | expect(path).toEqual('https://database.deta.sh/v1/:project_id/:base_name'); 23 | }); 24 | }); 25 | 26 | describe('drive url', () => { 27 | it.each([ 28 | ['drive.deta.sh', 'https://drive.deta.sh/v1/:project_id/:drive_name'], 29 | [' ', 'https://drive.deta.sh/v1/:project_id/:drive_name'], 30 | ['', 'https://drive.deta.sh/v1/:project_id/:drive_name'], 31 | ])('passed host path `url.drive("%s")`', (host, expected) => { 32 | const path = url.drive(KeyType.ProjectKey, host); 33 | expect(path).toEqual(expected); 34 | }); 35 | 36 | it('host path set in environment variable', () => { 37 | process.env.DETA_DRIVE_HOST = 'drive.deta.sh'; 38 | const path = url.drive(KeyType.ProjectKey); 39 | expect(path).toEqual('https://drive.deta.sh/v1/:project_id/:drive_name'); 40 | }); 41 | 42 | it('host is not passed', () => { 43 | const path = url.drive(KeyType.ProjectKey); 44 | expect(path).toEqual('https://drive.deta.sh/v1/:project_id/:drive_name'); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /__test__/deta.spec.ts: -------------------------------------------------------------------------------- 1 | import { Deta, Base, Drive } from '../src/index.node'; 2 | 3 | const projectKey = process.env.PROJECT_KEY || ''; 4 | const dbName = process.env.DB_NAME || ''; 5 | const driveName = process.env.DRIVE_NAME || ''; 6 | 7 | describe('Deta', () => { 8 | it.each([ 9 | [' ', new Error('Project key is not defined')], 10 | ['', new Error('Project key is not defined')], 11 | [null, new Error('Project key is not defined')], 12 | [undefined, new Error('Project key is not defined')], 13 | ])('invalid project key `Deta("%s")`', (name, expected) => { 14 | try { 15 | const deta = Deta(name as string); 16 | expect(deta).not.toBeNull(); 17 | } catch (err) { 18 | expect(err).toEqual(expected); 19 | } 20 | }); 21 | }); 22 | 23 | describe('Deta#Base', () => { 24 | it.each([ 25 | [' ', new Error('Base name is not defined')], 26 | ['', new Error('Base name is not defined')], 27 | [null, new Error('Base name is not defined')], 28 | [undefined, new Error('Base name is not defined')], 29 | ])('invalid base name `Base("%s")`', (name, expected) => { 30 | try { 31 | const base = Deta('test').Base(name as string); 32 | expect(base).not.toBeNull(); 33 | } catch (err) { 34 | expect(err).toEqual(expected); 35 | } 36 | }); 37 | 38 | it('passing host name', async () => { 39 | const base = Deta(projectKey).Base(dbName, 'database.deta.sh'); 40 | expect(base).not.toBeNull(); 41 | const data = await base.put({ key: 'deta-base-test' }); 42 | expect(data).toEqual({ key: 'deta-base-test' }); 43 | }); 44 | 45 | it('passing host name using environment variable', async () => { 46 | process.env.DETA_BASE_HOST = 'database.deta.sh'; 47 | const base = Deta(projectKey).Base(dbName); 48 | expect(base).not.toBeNull(); 49 | const data = await base.put({ key: 'deta-base-test1' }); 50 | expect(data).toEqual({ key: 'deta-base-test1' }); 51 | }); 52 | 53 | afterAll(async () => { 54 | const base = Deta(projectKey).Base(dbName, 'database.deta.sh'); 55 | 56 | const inputs = [['deta-base-test'], ['deta-base-test1']]; 57 | 58 | const promises = inputs.map(async (input) => { 59 | const [key] = input; 60 | const data = await base.delete(key); 61 | expect(data).toBeNull(); 62 | }); 63 | 64 | await Promise.all(promises); 65 | }); 66 | }); 67 | 68 | describe('Deta#Drive', () => { 69 | it.each([ 70 | [' ', new Error('Drive name is not defined')], 71 | ['', new Error('Drive name is not defined')], 72 | [null, new Error('Drive name is not defined')], 73 | [undefined, new Error('Drive name is not defined')], 74 | ])('invalid drive name `Drive("%s")`', (name, expected) => { 75 | try { 76 | const drive = Deta('test').Drive(name as string); 77 | expect(drive).not.toBeNull(); 78 | } catch (err) { 79 | expect(err).toEqual(expected); 80 | } 81 | }); 82 | 83 | it('passing host name', async () => { 84 | const drive = Deta(projectKey).Drive(driveName, 'drive.deta.sh'); 85 | expect(drive).not.toBeNull(); 86 | const data = await drive.put('deta-drive-test', { data: 'Hello World' }); 87 | expect(data).toEqual('deta-drive-test'); 88 | }); 89 | 90 | it('passing host name using environment variable', async () => { 91 | process.env.DETA_DRIVE_HOST = 'drive.deta.sh'; 92 | const drive = Deta(projectKey).Drive(driveName); 93 | expect(drive).not.toBeNull(); 94 | const data = await drive.put('deta-drive-test1', { data: 'Hello World' }); 95 | expect(data).toEqual('deta-drive-test1'); 96 | }); 97 | 98 | afterAll(async () => { 99 | const drive = Deta(projectKey).Drive(driveName, 'drive.deta.sh'); 100 | const names = ['deta-drive-test', 'deta-drive-test1']; 101 | const expected = { 102 | deleted: ['deta-drive-test', 'deta-drive-test1'], 103 | }; 104 | const data = await drive.deleteMany(names); 105 | expect(data.deleted).toEqual(expect.arrayContaining(expected.deleted)); 106 | }); 107 | }); 108 | 109 | describe('Base', () => { 110 | it.each([ 111 | [' ', new Error('Base name is not defined')], 112 | ['', new Error('Base name is not defined')], 113 | [null, new Error('Base name is not defined')], 114 | [undefined, new Error('Base name is not defined')], 115 | ])('invalid base name `Base("%s")`', (name, expected) => { 116 | try { 117 | process.env.DETA_PROJECT_KEY = 'test'; 118 | const base = Base(name as string); 119 | expect(base).not.toBeNull(); 120 | } catch (err) { 121 | expect(err).toEqual(expected); 122 | } 123 | }); 124 | 125 | it('Project key is not defined in current environment', () => { 126 | try { 127 | const base = Base('deta-base'); 128 | expect(base).not.toBeNull(); 129 | } catch (err) { 130 | expect(err).toEqual(new Error('Project key is not defined')); 131 | } 132 | }); 133 | 134 | it('passing host name', async () => { 135 | process.env.DETA_PROJECT_KEY = projectKey; 136 | const base = Base(dbName, 'database.deta.sh'); 137 | expect(base).not.toBeNull(); 138 | const data = await base.put({ key: 'base-test' }); 139 | expect(data).toEqual({ key: 'base-test' }); 140 | }); 141 | 142 | it('passing host name using environment variable', async () => { 143 | process.env.DETA_PROJECT_KEY = projectKey; 144 | process.env.DETA_BASE_HOST = 'database.deta.sh'; 145 | const base = Base(dbName); 146 | expect(base).not.toBeNull(); 147 | const data = await base.put({ key: 'base-test1' }); 148 | expect(data).toEqual({ key: 'base-test1' }); 149 | }); 150 | 151 | afterAll(async () => { 152 | const base = Deta(projectKey).Base(dbName, 'database.deta.sh'); 153 | 154 | const inputs = [['base-test'], ['base-test1']]; 155 | 156 | const promises = inputs.map(async (input) => { 157 | const [key] = input; 158 | const data = await base.delete(key); 159 | expect(data).toBeNull(); 160 | }); 161 | 162 | await Promise.all(promises); 163 | }); 164 | }); 165 | 166 | describe('Drive', () => { 167 | it.each([ 168 | [' ', new Error('Drive name is not defined')], 169 | ['', new Error('Drive name is not defined')], 170 | [null, new Error('Drive name is not defined')], 171 | [undefined, new Error('Drive name is not defined')], 172 | ])('invalid drive name `Drive("%s")`', (name, expected) => { 173 | try { 174 | process.env.DETA_PROJECT_KEY = 'test'; 175 | const drive = Drive(name as string); 176 | expect(drive).not.toBeNull(); 177 | } catch (err) { 178 | expect(err).toEqual(expected); 179 | } 180 | }); 181 | 182 | it('Project key is not defined in current environment', () => { 183 | try { 184 | const drive = Drive('deta-drive'); 185 | expect(drive).not.toBeNull(); 186 | } catch (err) { 187 | expect(err).toEqual(new Error('Project key is not defined')); 188 | } 189 | }); 190 | 191 | it('passing host name', async () => { 192 | process.env.DETA_PROJECT_KEY = projectKey; 193 | const drive = Drive(driveName, 'drive.deta.sh'); 194 | expect(drive).not.toBeNull(); 195 | const data = await drive.put('drive-test', { data: 'Hello World' }); 196 | expect(data).toEqual('drive-test'); 197 | }); 198 | 199 | it('passing host name using environment variable', async () => { 200 | process.env.DETA_PROJECT_KEY = projectKey; 201 | process.env.DETA_DRIVE_HOST = 'drive.deta.sh'; 202 | const drive = Drive(driveName); 203 | expect(drive).not.toBeNull(); 204 | const data = await drive.put('drive-test1', { data: 'Hello World' }); 205 | expect(data).toEqual('drive-test1'); 206 | }); 207 | 208 | afterAll(async () => { 209 | const drive = Deta(projectKey).Drive(driveName, 'drive.deta.sh'); 210 | const names = ['drive-test', 'drive-test1']; 211 | const expected = { 212 | deleted: ['drive-test', 'drive-test1'], 213 | }; 214 | const data = await drive.deleteMany(names); 215 | expect(data.deleted).toEqual(expect.arrayContaining(expected.deleted)); 216 | }); 217 | }); 218 | -------------------------------------------------------------------------------- /__test__/drive/delete.spec.ts: -------------------------------------------------------------------------------- 1 | import { Drive } from '../utils/deta'; 2 | 3 | const drive = Drive(); 4 | 5 | describe('Drive#delete', () => { 6 | beforeAll(async () => { 7 | const inputs = ['delete-a', 'delete/a', 'delete/child/a']; 8 | 9 | const promises = inputs.map(async (input) => { 10 | const data = await drive.put(input, { data: 'hello' }); 11 | expect(data).toEqual(input); 12 | }); 13 | 14 | await Promise.all(promises); 15 | }); 16 | 17 | it.each(['delete-a', 'delete/a', 'delete/child/a'])( 18 | 'delete file by using name `delete("%s")`', 19 | async (name) => { 20 | const data = await drive.delete(name as string); 21 | expect(data).toEqual(name); 22 | } 23 | ); 24 | 25 | it.each(['delete-aa', 'delete/aa', 'delete/child/aa'])( 26 | 'delete file by using name that does not exists on drive `delete("%s")`', 27 | async (name) => { 28 | const data = await drive.delete(name as string); 29 | expect(data).toEqual(name); 30 | } 31 | ); 32 | 33 | it.each([ 34 | [' ', new Error('Name is empty')], 35 | ['', new Error('Name is empty')], 36 | [null, new Error('Name is empty')], 37 | [undefined, new Error('Name is empty')], 38 | ])( 39 | 'delete file by using invalid name `delete("%s")`', 40 | async (name, expected) => { 41 | try { 42 | const data = await drive.delete(name as string); 43 | expect(data).toEqual(name); 44 | } catch (err) { 45 | expect(err).toEqual(expected); 46 | } 47 | } 48 | ); 49 | }); 50 | -------------------------------------------------------------------------------- /__test__/drive/deleteMany.spec.ts: -------------------------------------------------------------------------------- 1 | import { Drive } from '../utils/deta'; 2 | 3 | const drive = Drive(); 4 | 5 | describe('Drive#deleteMany', () => { 6 | beforeAll(async () => { 7 | const inputs = ['delete-many-a', 'delete-many/a', 'delete-many/child/a']; 8 | 9 | const promises = inputs.map(async (input) => { 10 | const data = await drive.put(input, { data: 'hello' }); 11 | expect(data).toEqual(input); 12 | }); 13 | 14 | await Promise.all(promises); 15 | }); 16 | 17 | it('deleteMany files by using names', async () => { 18 | const names = ['delete-many-a', 'delete-many/a', 'delete-many/child/a']; 19 | const expected = { 20 | deleted: ['delete-many-a', 'delete-many/a', 'delete-many/child/a'], 21 | }; 22 | const data = await drive.deleteMany(names); 23 | expect(data.deleted).toEqual(expect.arrayContaining(expected.deleted)); 24 | }); 25 | 26 | it('deleteMany files by using names that does not exists on drive', async () => { 27 | const names = ['delete-many-aa', 'delete-many/aa']; 28 | const expected = { 29 | deleted: ['delete-many-aa', 'delete-many/aa'], 30 | }; 31 | const data = await drive.deleteMany(names); 32 | expect(data.deleted).toEqual(expect.arrayContaining(expected.deleted)); 33 | }); 34 | 35 | it('deleteMany files by using valid and invalid names', async () => { 36 | const names = ['delete-many-aa', 'delete-many/aa', '', ' ']; 37 | const expected = { 38 | deleted: ['delete-many-aa', 'delete-many/aa'], 39 | failed: { 40 | '': 'invalid name', 41 | ' ': 'invalid name', 42 | }, 43 | }; 44 | const data = await drive.deleteMany(names); 45 | expect(data.deleted).toEqual(expect.arrayContaining(expected.deleted)); 46 | }); 47 | 48 | it.each([ 49 | [[' '], new Error("Names can't be empty")], 50 | [[''], new Error("Names can't be empty")], 51 | [[], new Error("Names can't be empty")], 52 | [ 53 | new Array(1001), 54 | new Error("We can't delete more than 1000 items at a time"), 55 | ], 56 | ])( 57 | 'deleteMany files by using invalid name `deleteMany(%s)`', 58 | async (names, expected) => { 59 | try { 60 | const data = await drive.deleteMany(names); 61 | expect(data).toEqual(names); 62 | } catch (err) { 63 | expect(err).toEqual(expected); 64 | } 65 | } 66 | ); 67 | }); 68 | -------------------------------------------------------------------------------- /__test__/drive/get.spec.ts: -------------------------------------------------------------------------------- 1 | import { Drive } from '../utils/deta'; 2 | 3 | const drive = Drive(); 4 | 5 | describe('Drive#get', () => { 6 | const fileContent = '{"hello":"world"}'; 7 | 8 | beforeAll(async () => { 9 | const inputs = ['get-a', 'get/a', 'get/child/a']; 10 | 11 | const promises = inputs.map(async (input) => { 12 | const data = await drive.put(input, { data: fileContent }); 13 | expect(data).toEqual(input); 14 | }); 15 | 16 | await Promise.all(promises); 17 | }); 18 | 19 | afterAll(async () => { 20 | const inputs = ['get-a', 'get/a', 'get/child/a']; 21 | 22 | const promises = inputs.map(async (input) => { 23 | const data = await drive.delete(input); 24 | expect(data).toEqual(input); 25 | }); 26 | 27 | await Promise.all(promises); 28 | }); 29 | 30 | it.each(['get-a', 'get/a', 'get/child/a'])( 31 | 'get file by using name `get("%s")`', 32 | async (name) => { 33 | const data = await drive.get(name as string); 34 | expect(data).not.toBeNull(); 35 | const value = await data?.text(); 36 | expect(value).toEqual(fileContent); 37 | } 38 | ); 39 | 40 | it.each(['get-aa', 'get/aa', 'get/child/aa'])( 41 | 'get file by using name that does not exists on drive `get("%s")`', 42 | async (name) => { 43 | const data = await drive.get(name as string); 44 | expect(data).toBeNull(); 45 | } 46 | ); 47 | 48 | it.each([ 49 | [' ', new Error('Name is empty')], 50 | ['', new Error('Name is empty')], 51 | [null, new Error('Name is empty')], 52 | [undefined, new Error('Name is empty')], 53 | ])('get file by using invalid name `get("%s")`', async (name, expected) => { 54 | try { 55 | const data = await drive.get(name as string); 56 | expect(data).not.toBeNull(); 57 | } catch (err) { 58 | expect(err).toEqual(expected); 59 | } 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /__test__/drive/list.spec.ts: -------------------------------------------------------------------------------- 1 | import { Drive } from '../utils/deta'; 2 | 3 | const drive = Drive(); 4 | 5 | describe('Drive#list', () => { 6 | beforeAll(async () => { 7 | const inputs = [ 8 | 'list-a', 9 | 'list-b', 10 | 'list-c', 11 | 'list/a', 12 | 'list/b', 13 | 'list/child/a', 14 | 'list/child/b', 15 | ]; 16 | 17 | const promises = inputs.map(async (input) => { 18 | const data = await drive.put(input, { data: 'hello' }); 19 | expect(data).toEqual(input); 20 | }); 21 | 22 | await Promise.all(promises); 23 | }); 24 | 25 | afterAll(async () => { 26 | const names = [ 27 | 'list-a', 28 | 'list-b', 29 | 'list-c', 30 | 'list/a', 31 | 'list/b', 32 | 'list/child/a', 33 | 'list/child/b', 34 | ]; 35 | const expected = { 36 | deleted: [ 37 | 'list-a', 38 | 'list-b', 39 | 'list-c', 40 | 'list/a', 41 | 'list/b', 42 | 'list/child/a', 43 | 'list/child/b', 44 | ], 45 | }; 46 | const data = await drive.deleteMany(names); 47 | expect(data.deleted).toEqual(expect.arrayContaining(expected.deleted)); 48 | }); 49 | 50 | it('list files', async () => { 51 | const expected = { 52 | names: [ 53 | 'list-a', 54 | 'list-b', 55 | 'list-c', 56 | 'list/a', 57 | 'list/b', 58 | 'list/child/a', 59 | 'list/child/b', 60 | ], 61 | }; 62 | const data = await drive.list(); 63 | expect(data).toEqual(expected); 64 | }); 65 | 66 | it.each([ 67 | [ 68 | { limit: 1 }, 69 | { 70 | paging: { 71 | size: 1, 72 | last: 'list-a', 73 | }, 74 | names: ['list-a'], 75 | }, 76 | ], 77 | [ 78 | { limit: 2, prefix: 'list-' }, 79 | { 80 | paging: { 81 | size: 2, 82 | last: 'list-b', 83 | }, 84 | names: ['list-a', 'list-b'], 85 | }, 86 | ], 87 | [ 88 | { 89 | limit: 2, 90 | prefix: 'list', 91 | last: 'list/child/a', 92 | }, 93 | { 94 | names: ['list/child/b'], 95 | }, 96 | ], 97 | [ 98 | { 99 | limit: 2, 100 | prefix: 'list', 101 | last: 'list/child/a', 102 | recursive: true, 103 | }, 104 | { 105 | names: ['list/child/b'], 106 | }, 107 | ], 108 | [ 109 | { 110 | prefix: 'list/', 111 | recursive: false, 112 | }, 113 | { 114 | names: ['list/a', 'list/b', 'list/child/'], 115 | }, 116 | ], 117 | [ 118 | { 119 | limit: 2, 120 | prefix: 'list/', 121 | recursive: false, 122 | }, 123 | { 124 | paging: { 125 | size: 2, 126 | last: 'list/b', 127 | }, 128 | names: ['list/a', 'list/b'], 129 | }, 130 | ], 131 | [ 132 | { 133 | limit: 1, 134 | last: 'list/a', 135 | prefix: 'list/', 136 | recursive: false, 137 | }, 138 | { 139 | paging: { 140 | size: 1, 141 | last: 'list/b', 142 | }, 143 | names: ['list/b'], 144 | }, 145 | ], 146 | [ 147 | { 148 | limit: 2, 149 | last: 'list/a', 150 | prefix: 'list/', 151 | recursive: false, 152 | }, 153 | { 154 | names: ['list/b', 'list/child/'], 155 | }, 156 | ], 157 | [ 158 | { 159 | prefix: 'list', 160 | recursive: false, 161 | }, 162 | { 163 | names: ['list-a', 'list-b', 'list-c', 'list/'], 164 | }, 165 | ], 166 | [ 167 | { 168 | prefix: '/list', 169 | recursive: false, 170 | }, 171 | { 172 | names: [], 173 | }, 174 | ], 175 | ])('list files `get(%p)`', async (option, expected) => { 176 | const data = await drive.list(option); 177 | expect(data).toEqual(expected); 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /__test__/drive/put.spec.ts: -------------------------------------------------------------------------------- 1 | import { Drive } from '../utils/deta'; 2 | 3 | const drive = Drive(); 4 | 5 | describe('Drive#put', () => { 6 | afterAll(async () => { 7 | const names = [ 8 | 'put-data', 9 | 'put-data-1', 10 | 'put-data-2', 11 | 'put-data/a', 12 | 'put-data/child/a', 13 | 'put-test.svg', 14 | ]; 15 | const expected = { 16 | deleted: [ 17 | 'put-data', 18 | 'put-data-1', 19 | 'put-data-2', 20 | 'put-data/a', 21 | 'put-data/child/a', 22 | 'put-test.svg', 23 | ], 24 | }; 25 | const data = await drive.deleteMany(names); 26 | expect(data.deleted).toEqual(expect.arrayContaining(expected.deleted)); 27 | }); 28 | 29 | it('put file', async () => { 30 | const name = 'put-test.svg'; 31 | const data = await drive.put(name, { 32 | path: '__test__/files/logo.svg', 33 | }); 34 | 35 | expect(data).toEqual(name); 36 | }); 37 | 38 | it.each(['put-data', 'put-data/a', 'put-data/child/a'])( 39 | 'put data `put("%s")`', 40 | async (name) => { 41 | const data = await drive.put(name as string, { 42 | data: 'Hello world', 43 | }); 44 | 45 | expect(data).toEqual(name); 46 | } 47 | ); 48 | 49 | it('put data with contentType', async () => { 50 | const name = 'put-data-1'; 51 | const data = await drive.put(name, { 52 | data: 'Hello world', 53 | contentType: 'text/plain', 54 | }); 55 | 56 | expect(data).toEqual(name); 57 | }); 58 | 59 | it('put data as Buffer', async () => { 60 | const name = 'put-data-2'; 61 | const data = await drive.put(name, { 62 | data: Buffer.from('Hello world, Hello'), 63 | }); 64 | 65 | expect(data).toEqual(name); 66 | }); 67 | 68 | it.each([ 69 | [ 70 | ' ', 71 | { 72 | data: 'Hello world', 73 | contentType: 'text/plain', 74 | }, 75 | new Error('Name is empty'), 76 | ], 77 | [ 78 | '', 79 | { 80 | data: 'Hello world', 81 | contentType: 'text/plain', 82 | }, 83 | new Error('Name is empty'), 84 | ], 85 | [ 86 | null, 87 | { 88 | data: 'Hello world', 89 | contentType: 'text/plain', 90 | }, 91 | new Error('Name is empty'), 92 | ], 93 | [ 94 | undefined, 95 | { 96 | data: 'Hello world', 97 | contentType: 'text/plain', 98 | }, 99 | new Error('Name is empty'), 100 | ], 101 | [ 102 | 'put-data-2', 103 | { 104 | path: '__test__/files/logo.svg', 105 | data: 'Hello world', 106 | contentType: 'text/plain', 107 | }, 108 | new Error('Please only provide data or a path. Not both'), 109 | ], 110 | [ 111 | 'put-data-3', 112 | { 113 | contentType: 'text/plain', 114 | }, 115 | new Error('Please provide data or a path. Both are empty'), 116 | ], 117 | [ 118 | 'put-data-4', 119 | { 120 | data: 12 as any, 121 | contentType: 'text/plain', 122 | }, 123 | new Error( 124 | 'Unsupported data format, expected data to be one of: string | Uint8Array | Buffer' 125 | ), 126 | ], 127 | ])( 128 | 'put file by using invalid name or body `put("%s", %p)`', 129 | async (name, body, expected) => { 130 | try { 131 | const data = await drive.put(name as string, body); 132 | expect(data).not.toBeNull(); 133 | } catch (err) { 134 | expect(err).toEqual(expected); 135 | } 136 | } 137 | ); 138 | }); 139 | -------------------------------------------------------------------------------- /__test__/env.spec.ts: -------------------------------------------------------------------------------- 1 | describe('can load env', () => { 2 | it('PROJECT_KEY', () => { 3 | const projectKey = process?.env?.PROJECT_KEY?.trim(); 4 | 5 | expect(projectKey).toBeDefined(); 6 | expect(projectKey).not.toEqual(''); 7 | }); 8 | 9 | it('DB_NAME', () => { 10 | const dbName = process?.env?.DB_NAME?.trim(); 11 | 12 | expect(dbName).toBeDefined(); 13 | expect(dbName).not.toEqual(''); 14 | }); 15 | 16 | it('DRIVE_NAME', () => { 17 | const driveName = process?.env?.DRIVE_NAME?.trim(); 18 | 19 | expect(driveName).toBeDefined(); 20 | expect(driveName).not.toEqual(''); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /__test__/files/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /__test__/utils/deta.ts: -------------------------------------------------------------------------------- 1 | import { Deta } from '../../src/index.node'; 2 | 3 | export function Drive() { 4 | const projectKey = process.env.PROJECT_KEY || ''; 5 | const driveName = process.env.DRIVE_NAME || ''; 6 | 7 | if (process.env.USE_AUTH_TOKEN === 'true') { 8 | const token = process.env.AUTH_TOKEN || ''; 9 | return Deta(projectKey.split('_')[0], token).Drive(driveName); 10 | } 11 | 12 | return Deta(projectKey).Drive(driveName); 13 | } 14 | 15 | export function Base() { 16 | const projectKey = process.env.PROJECT_KEY || ''; 17 | const dbName = process.env.DB_NAME || ''; 18 | 19 | if (process.env.USE_AUTH_TOKEN === 'true') { 20 | const token = process.env.AUTH_TOKEN || ''; 21 | return Deta(projectKey.split('_')[0], token).Base(dbName); 22 | } 23 | 24 | return Deta(projectKey).Base(dbName); 25 | } 26 | -------------------------------------------------------------------------------- /__test__/utils/general.ts: -------------------------------------------------------------------------------- 1 | export function mockSystemTime() { 2 | jest.useFakeTimers('modern'); 3 | const date = new Date(); 4 | date.setSeconds(date.getSeconds() + 60); 5 | jest.setSystemTime(date); 6 | } 7 | 8 | export function useRealTime() { 9 | jest.useRealTimers(); 10 | } 11 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "errorOnDeprecated": true, 4 | "globalSetup": "./scripts/jest/globalSetup.ts", 5 | "preset": "ts-jest", 6 | "modulePathIgnorePatterns": ["dist", "node_modules"], 7 | "testTimeout": 30000 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deta", 3 | "version": "2.0.0", 4 | "description": "Deta library for Javascript", 5 | "scripts": { 6 | "prebuild": "rimraf dist", 7 | "build": "rollup -c", 8 | "format": "prettier --write .", 9 | "lint": "eslint -c .eslintrc.js . --ext .ts", 10 | "lint:fix": "eslint -c .eslintrc.js . --ext .ts --fix", 11 | "test": "jest --config jest.config.json --runInBand", 12 | "prepare": "husky install" 13 | }, 14 | "main": "./dist/index.js", 15 | "browser": "./dist/index.browser.js", 16 | "files": [ 17 | "dist" 18 | ], 19 | "types": "./dist/types/index.node.d.ts", 20 | "author": "Deta", 21 | "license": "MIT", 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/deta/deta-javascript.git" 25 | }, 26 | "keywords": [ 27 | "Deta", 28 | "SDK", 29 | "for", 30 | "Node.js" 31 | ], 32 | "bugs": { 33 | "url": "https://github.com/deta/deta-javascript/issues" 34 | }, 35 | "homepage": "https://github.com/deta/deta-javascript#readme", 36 | "devDependencies": { 37 | "@rollup/plugin-commonjs": "^19.0.0", 38 | "@rollup/plugin-node-resolve": "^13.0.0", 39 | "@rollup/plugin-replace": "^2.4.2", 40 | "@rollup/plugin-typescript": "^8.2.1", 41 | "@types/jest": "^27.0.2", 42 | "@types/node": "^15.0.2", 43 | "@types/node-fetch": "^2.5.10", 44 | "@typescript-eslint/eslint-plugin": "^4.4.1", 45 | "dotenv": "^9.0.0", 46 | "eslint": "^7.25.0", 47 | "eslint-config-airbnb-typescript": "^12.3.1", 48 | "eslint-config-prettier": "^8.3.0", 49 | "eslint-plugin-import": "^2.22.0", 50 | "eslint-plugin-prettier": "^3.4.0", 51 | "husky": "^6.0.0", 52 | "jest": "^27.3.1", 53 | "prettier": "^2.2.1", 54 | "rimraf": "^3.0.2", 55 | "rollup": "^2.47.0", 56 | "rollup-plugin-cleanup": "^3.2.1", 57 | "rollup-plugin-terser": "^7.0.2", 58 | "ts-jest": "^27.0.7", 59 | "typescript": "^4.2.4" 60 | }, 61 | "dependencies": { 62 | "node-fetch": "^2.6.7" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import replace from '@rollup/plugin-replace'; 2 | import { terser } from 'rollup-plugin-terser'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import cleanup from 'rollup-plugin-cleanup'; 5 | import typescript from '@rollup/plugin-typescript'; 6 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 7 | 8 | import pkg from './package.json'; 9 | 10 | export default [ 11 | { 12 | input: './src/index.node.ts', 13 | output: [ 14 | { 15 | file: pkg.main, 16 | format: 'cjs', // commonJS 17 | }, 18 | ], 19 | external: [ 20 | ...Object.keys(pkg.dependencies || {}), 21 | ...Object.keys(pkg.devDependencies || {}), 22 | ], 23 | plugins: [ 24 | typescript({ 25 | tsconfig: './tsconfig.json', 26 | }), 27 | nodeResolve({ 28 | browser: false, 29 | }), 30 | commonjs({ extensions: ['.ts'] }), 31 | cleanup({ 32 | comments: 'none', 33 | }), 34 | ], 35 | }, 36 | { 37 | input: './src/index.browser.ts', 38 | output: [ 39 | { 40 | name: 'deta', 41 | file: pkg.browser, 42 | format: 'es', // browser 43 | }, 44 | ], 45 | plugins: [ 46 | typescript({ 47 | tsconfig: './tsconfig.json', 48 | }), 49 | nodeResolve({ 50 | browser: true, 51 | }), 52 | commonjs({ extensions: ['.ts'] }), 53 | replace({ 54 | 'process.env.DETA_PROJECT_KEY': JSON.stringify(''), 55 | 'process.env.DETA_BASE_HOST': JSON.stringify(''), 56 | 'process.env.DETA_DRIVE_HOST': JSON.stringify(''), 57 | preventAssignment: true, 58 | }), 59 | terser(), 60 | cleanup({ 61 | comments: 'none', 62 | }), 63 | ], 64 | }, 65 | ]; 66 | -------------------------------------------------------------------------------- /scripts/jest/globalSetup.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | import dotenv from 'dotenv'; 3 | 4 | export default function setup() { 5 | dotenv.config({ path: '.env.test' }); 6 | } 7 | -------------------------------------------------------------------------------- /src/base/base.ts: -------------------------------------------------------------------------------- 1 | import url from '../constants/url'; 2 | import { KeyType } from '../types/key'; 3 | import Requests from '../utils/request'; 4 | import { BaseApi } from '../constants/api'; 5 | import { isObject } from '../utils/object'; 6 | import BaseUtils, { getTTL } from './utils'; 7 | import { BaseGeneral } from '../constants/general'; 8 | import { Action, ActionTypes } from '../types/action'; 9 | import { isUndefinedOrNull } from '../utils/undefinedOrNull'; 10 | import { DetaType, CompositeType, ArrayType, ObjectType } from '../types/basic'; 11 | import { 12 | PutOptions, 13 | FetchOptions, 14 | UpdateOptions, 15 | InsertOptions, 16 | PutManyOptions, 17 | } from '../types/base/request'; 18 | 19 | import { 20 | GetResponse, 21 | PutResponse, 22 | FetchResponse, 23 | DeleteResponse, 24 | InsertResponse, 25 | UpdateResponse, 26 | PutManyResponse, 27 | } from '../types/base/response'; 28 | 29 | export default class Base { 30 | private requests: Requests; 31 | 32 | public util: BaseUtils; 33 | 34 | /** 35 | * Base constructor 36 | * 37 | * @param {string} key 38 | * @param {KeyType} type 39 | * @param {string} projectId 40 | * @param {string} baseName 41 | * @param {string} [host] 42 | */ 43 | constructor( 44 | key: string, 45 | type: KeyType, 46 | projectId: string, 47 | baseName: string, 48 | host?: string 49 | ) { 50 | const baseURL = url 51 | .base(type, host) 52 | .replace(':base_name', baseName) 53 | .replace(':project_id', projectId); 54 | this.requests = new Requests(key, type, baseURL); 55 | this.util = new BaseUtils(); 56 | } 57 | 58 | /** 59 | * put data on base 60 | * 61 | * @param {DetaType} data 62 | * @param {string} [key] 63 | * @returns {Promise} 64 | */ 65 | public async put( 66 | data: DetaType, 67 | key?: string, 68 | options?: PutOptions 69 | ): Promise { 70 | const { ttl, error: ttlError } = getTTL( 71 | options?.expireIn, 72 | options?.expireAt 73 | ); 74 | if (ttlError) { 75 | throw ttlError; 76 | } 77 | 78 | const payload: ObjectType[] = [ 79 | { 80 | ...(isObject(data) ? (data as ObjectType) : { value: data }), 81 | ...(key && { key }), 82 | ...(!isUndefinedOrNull(ttl) && { [BaseGeneral.TTL_ATTRIBUTE]: ttl }), 83 | }, 84 | ]; 85 | 86 | const { response, error } = await this.requests.put(BaseApi.PUT_ITEMS, { 87 | items: payload, 88 | }); 89 | if (error) { 90 | throw error; 91 | } 92 | 93 | return response?.processed?.items?.[0] || null; 94 | } 95 | 96 | /** 97 | * get data from base 98 | * 99 | * @param {string} key 100 | * @returns {Promise} 101 | */ 102 | public async get(key: string): Promise { 103 | const trimmedKey = key?.trim(); 104 | if (!trimmedKey) { 105 | throw new Error('Key is empty'); 106 | } 107 | const encodedKey = encodeURIComponent(trimmedKey); 108 | 109 | const { status, response, error } = await this.requests.get( 110 | BaseApi.GET_ITEMS.replace(':key', encodedKey) 111 | ); 112 | 113 | if (error && status !== 404) { 114 | throw error; 115 | } 116 | 117 | if (status === 200) { 118 | return response; 119 | } 120 | 121 | return null; 122 | } 123 | 124 | /** 125 | * delete data on base 126 | * 127 | * @param {string} key 128 | * @returns {Promise} 129 | */ 130 | public async delete(key: string): Promise { 131 | const trimmedKey = key?.trim(); 132 | if (!trimmedKey) { 133 | throw new Error('Key is empty'); 134 | } 135 | const encodedKey = encodeURIComponent(trimmedKey); 136 | 137 | const { error } = await this.requests.delete( 138 | BaseApi.DELETE_ITEMS.replace(':key', encodedKey) 139 | ); 140 | if (error) { 141 | throw error; 142 | } 143 | 144 | return null; 145 | } 146 | 147 | /** 148 | * insert data on base 149 | * 150 | * @param {DetaType} data 151 | * @param {string} [key] 152 | * @returns {Promise} 153 | */ 154 | public async insert( 155 | data: DetaType, 156 | key?: string, 157 | options?: InsertOptions 158 | ): Promise { 159 | const { ttl, error: ttlError } = getTTL( 160 | options?.expireIn, 161 | options?.expireAt 162 | ); 163 | if (ttlError) { 164 | throw ttlError; 165 | } 166 | 167 | const payload: ObjectType = { 168 | ...(isObject(data) ? (data as ObjectType) : { value: data }), 169 | ...(key && { key }), 170 | ...(!isUndefinedOrNull(ttl) && { [BaseGeneral.TTL_ATTRIBUTE]: ttl }), 171 | }; 172 | 173 | const { status, response, error } = await this.requests.post( 174 | BaseApi.INSERT_ITEMS, 175 | { 176 | payload: { 177 | item: payload, 178 | }, 179 | } 180 | ); 181 | if (error && status === 409) { 182 | const resolvedKey = key || payload.key; 183 | throw new Error(`Item with key ${resolvedKey} already exists`); 184 | } 185 | if (error) { 186 | throw error; 187 | } 188 | 189 | return response; 190 | } 191 | 192 | /** 193 | * putMany data on base 194 | * 195 | * @param {DetaType[]} items 196 | * @returns {Promise} 197 | */ 198 | public async putMany( 199 | items: DetaType[], 200 | options?: PutManyOptions 201 | ): Promise { 202 | if (!(items instanceof Array)) { 203 | throw new Error('Items must be an array'); 204 | } 205 | 206 | if (!items.length) { 207 | throw new Error("Items can't be empty"); 208 | } 209 | 210 | if (items.length > 25) { 211 | throw new Error("We can't put more than 25 items at a time"); 212 | } 213 | 214 | const { ttl, error: ttlError } = getTTL( 215 | options?.expireIn, 216 | options?.expireAt 217 | ); 218 | if (ttlError) { 219 | throw ttlError; 220 | } 221 | 222 | const payload: ObjectType[] = items.map((item) => { 223 | const newItem = isObject(item) ? (item as ObjectType) : { value: item }; 224 | return { 225 | ...newItem, 226 | ...(!isUndefinedOrNull(ttl) && { [BaseGeneral.TTL_ATTRIBUTE]: ttl }), 227 | }; 228 | }); 229 | 230 | const { response, error } = await this.requests.put(BaseApi.PUT_ITEMS, { 231 | items: payload, 232 | }); 233 | if (error) { 234 | throw error; 235 | } 236 | 237 | return response; 238 | } 239 | 240 | /** 241 | * update data on base 242 | * 243 | * @param {ObjectType} updates 244 | * @param {string} key 245 | * @returns {Promise} 246 | */ 247 | public async update( 248 | updates: ObjectType, 249 | key: string, 250 | options?: UpdateOptions 251 | ): Promise { 252 | const trimmedKey = key?.trim(); 253 | if (!trimmedKey) { 254 | throw new Error('Key is empty'); 255 | } 256 | 257 | const { ttl, error: ttlError } = getTTL( 258 | options?.expireIn, 259 | options?.expireAt 260 | ); 261 | if (ttlError) { 262 | throw ttlError; 263 | } 264 | 265 | const payload: { 266 | set: ObjectType; 267 | increment: ObjectType; 268 | append: ObjectType; 269 | prepend: ObjectType; 270 | delete: ArrayType; 271 | } = { 272 | set: { 273 | ...(!isUndefinedOrNull(ttl) && { [BaseGeneral.TTL_ATTRIBUTE]: ttl }), 274 | }, 275 | increment: {}, 276 | append: {}, 277 | prepend: {}, 278 | delete: [], 279 | }; 280 | 281 | Object.entries(updates).forEach(([objKey, objValue]) => { 282 | const action = 283 | objValue instanceof Action 284 | ? objValue 285 | : new Action(ActionTypes.Set, objValue); 286 | 287 | const { operation, value } = action; 288 | switch (operation) { 289 | case ActionTypes.Trim: { 290 | payload.delete.push(objKey); 291 | break; 292 | } 293 | default: { 294 | payload[operation][objKey] = value; 295 | } 296 | } 297 | }); 298 | 299 | const encodedKey = encodeURIComponent(trimmedKey); 300 | const { error } = await this.requests.patch( 301 | BaseApi.PATCH_ITEMS.replace(':key', encodedKey), 302 | payload 303 | ); 304 | if (error) { 305 | throw error; 306 | } 307 | 308 | return null; 309 | } 310 | 311 | /** 312 | * fetch data from base 313 | * 314 | * @param {CompositeType} [query] 315 | * @param {FetchOptions} [options] 316 | * @returns {Promise} 317 | */ 318 | public async fetch( 319 | query: CompositeType = [], 320 | options?: FetchOptions 321 | ): Promise { 322 | const { limit = 1000, last = '', desc = false } = options || {}; 323 | const sort = desc ? 'desc' : ''; 324 | 325 | const payload = { 326 | query: Array.isArray(query) ? query : [query], 327 | limit, 328 | last, 329 | sort, 330 | }; 331 | 332 | const { response, error } = await this.requests.post(BaseApi.QUERY_ITEMS, { 333 | payload, 334 | }); 335 | if (error) { 336 | throw error; 337 | } 338 | 339 | const { items, paging } = response; 340 | const { size: count, last: resLast } = paging; 341 | 342 | return { items, count, last: resLast }; 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /src/base/index.ts: -------------------------------------------------------------------------------- 1 | import Base from './base'; 2 | 3 | export default Base; 4 | -------------------------------------------------------------------------------- /src/base/utils.ts: -------------------------------------------------------------------------------- 1 | import { Day } from '../utils/date'; 2 | import { isNumber } from '../utils/number'; 3 | import { Action, ActionTypes } from '../types/action'; 4 | import { ArrayType, BasicType } from '../types/basic'; 5 | import { isUndefinedOrNull } from '../utils/undefinedOrNull'; 6 | 7 | export default class BaseUtils { 8 | public trim(): Action { 9 | return new Action(ActionTypes.Trim); 10 | } 11 | 12 | public increment(value: number = 1): Action { 13 | return new Action(ActionTypes.Increment, value); 14 | } 15 | 16 | public append(value: BasicType | ArrayType): Action { 17 | return new Action( 18 | ActionTypes.Append, 19 | Array.isArray(value) ? value : [value] 20 | ); 21 | } 22 | 23 | public prepend(value: BasicType | ArrayType): Action { 24 | return new Action( 25 | ActionTypes.Prepend, 26 | Array.isArray(value) ? value : [value] 27 | ); 28 | } 29 | } 30 | 31 | interface TTLResponse { 32 | ttl?: number; 33 | error?: Error; 34 | } 35 | 36 | /** 37 | * getTTL computes and returns ttl value based on expireIn and expireAt params. 38 | * expireIn and expireAt are optional params. 39 | * 40 | * @param {number} [expireIn] 41 | * @param {Date | number} [expireAt] 42 | * @returns {TTLResponse} 43 | */ 44 | export function getTTL( 45 | expireIn?: number, 46 | expireAt?: Date | number 47 | ): TTLResponse { 48 | if (isUndefinedOrNull(expireIn) && isUndefinedOrNull(expireAt)) { 49 | return {}; 50 | } 51 | 52 | if (!isUndefinedOrNull(expireIn) && !isUndefinedOrNull(expireAt)) { 53 | return { error: new Error("can't set both expireIn and expireAt options") }; 54 | } 55 | 56 | if (!isUndefinedOrNull(expireIn)) { 57 | if (!isNumber(expireIn)) { 58 | return { 59 | error: new Error('option expireIn should have a value of type number'), 60 | }; 61 | } 62 | return { ttl: new Day().addSeconds(expireIn as number).getEpochSeconds() }; 63 | } 64 | 65 | if (!(isNumber(expireAt) || expireAt instanceof Date)) { 66 | return { 67 | error: new Error( 68 | 'option expireAt should have a value of type number or Date' 69 | ), 70 | }; 71 | } 72 | 73 | if (expireAt instanceof Date) { 74 | return { ttl: new Day(expireAt).getEpochSeconds() }; 75 | } 76 | 77 | return { ttl: expireAt as number }; 78 | } 79 | -------------------------------------------------------------------------------- /src/constants/api.ts: -------------------------------------------------------------------------------- 1 | export const BaseApi = { 2 | PUT_ITEMS: '/items', 3 | QUERY_ITEMS: '/query', 4 | INSERT_ITEMS: '/items', 5 | GET_ITEMS: '/items/:key', 6 | PATCH_ITEMS: '/items/:key', 7 | DELETE_ITEMS: '/items/:key', 8 | }; 9 | 10 | export const DriveApi = { 11 | GET_FILE: '/files/download?name=:name', 12 | DELETE_FILES: '/files', 13 | LIST_FILES: 14 | '/files?prefix=:prefix&recursive=:recursive&limit=:limit&last=:last', 15 | INIT_CHUNK_UPLOAD: '/uploads?name=:name', 16 | UPLOAD_FILE_CHUNK: '/uploads/:uid/parts?name=:name&part=:part', 17 | COMPLETE_FILE_UPLOAD: '/uploads/:uid?name=:name', 18 | }; 19 | -------------------------------------------------------------------------------- /src/constants/general.ts: -------------------------------------------------------------------------------- 1 | export const BaseGeneral = { 2 | TTL_ATTRIBUTE: '__expires', 3 | }; 4 | -------------------------------------------------------------------------------- /src/constants/url.ts: -------------------------------------------------------------------------------- 1 | import { KeyType } from '../types/key'; 2 | 3 | const url = { 4 | BASE: `:protocol://:host/v1/:project_id/:base_name`, 5 | DRIVE: `:protocol://:host/v1/:project_id/:drive_name`, 6 | }; 7 | 8 | /** 9 | * base function returns API URL for base 10 | * 11 | * @param {string} [host] 12 | * @param {KeyType} keyType 13 | * @returns {string} 14 | */ 15 | function base(keyType: KeyType, host?: string): string { 16 | const browserAppTokenHost = typeof window !== 'undefined' && keyType === KeyType.DummyKey 17 | ? `${window.location.host}/__space/v0/base` 18 | : undefined; 19 | 20 | const nodeHost = typeof process !== 'undefined' 21 | ? process.env.DETA_BASE_HOST?.trim() 22 | : undefined; 23 | 24 | host = host?.trim() ? host : undefined; 25 | const hostPath = host?.trim() ?? browserAppTokenHost ?? nodeHost ?? 'database.deta.sh'; 26 | const protocol = browserAppTokenHost?.startsWith('localhost') ? 'http' : 'https'; 27 | 28 | return url.BASE.replace(':protocol', protocol).replace(':host', hostPath); 29 | } 30 | 31 | /** 32 | * drive function returns API URL for drive 33 | * 34 | * @param {string} [host] 35 | * @param {KeyType} keyType 36 | * @returns {string} 37 | */ 38 | function drive(keyType: KeyType, host?: string): string { 39 | const browserAppTokenHost = typeof window !== 'undefined' && keyType === KeyType.DummyKey 40 | ? `${window.location.host}/__space/v0/drive` 41 | : undefined; 42 | 43 | const nodeHost = typeof process !== 'undefined' 44 | ? process.env.DETA_DRIVE_HOST?.trim() 45 | : undefined; 46 | 47 | host = host?.trim() ? host : undefined; 48 | const hostPath = host?.trim() ?? browserAppTokenHost ?? nodeHost ?? 'drive.deta.sh'; 49 | const protocol = browserAppTokenHost?.startsWith('localhost') ? 'http' : 'https'; 50 | 51 | return url.DRIVE.replace(':protocol', protocol).replace(':host', hostPath); 52 | } 53 | 54 | export default { 55 | base, 56 | drive, 57 | }; 58 | -------------------------------------------------------------------------------- /src/deta.ts: -------------------------------------------------------------------------------- 1 | import BaseClass from './base'; 2 | import DriveClass from './drive'; 3 | import { KeyType } from './types/key'; 4 | 5 | export default class Deta { 6 | private key: string; 7 | 8 | private type: KeyType; 9 | 10 | private projectId: string; 11 | 12 | /** 13 | * Deta constructor 14 | * 15 | * @param {string} key 16 | * @param {KeyType} type 17 | * @param {string} projectId 18 | */ 19 | constructor(key: string, type: KeyType, projectId: string) { 20 | this.key = key; 21 | this.type = type; 22 | this.projectId = projectId; 23 | } 24 | 25 | /** 26 | * Base returns instance of Base class 27 | * 28 | * @param {string} baseName 29 | * @param {string} [host] 30 | * @returns {BaseClass} 31 | */ 32 | public Base(baseName: string, host?: string): BaseClass { 33 | const name = baseName?.trim(); 34 | if (!name) { 35 | throw new Error('Base name is not defined'); 36 | } 37 | return new BaseClass(this.key, this.type, this.projectId, name, host); 38 | } 39 | 40 | /** 41 | * Drive returns instance of Drive class 42 | * 43 | * @param {string} driveName 44 | * @param {string} [host] 45 | * @returns {DriveClass} 46 | */ 47 | public Drive(driveName: string, host?: string): DriveClass { 48 | const name = driveName?.trim(); 49 | if (!name) { 50 | throw new Error('Drive name is not defined'); 51 | } 52 | return new DriveClass(this.key, this.type, this.projectId, name, host); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/drive/drive.ts: -------------------------------------------------------------------------------- 1 | import url from '../constants/url'; 2 | import { isNode } from '../utils/node'; 3 | import { KeyType } from '../types/key'; 4 | import Requests from '../utils/request'; 5 | import { isString } from '../utils/string'; 6 | import { DriveApi } from '../constants/api'; 7 | import { ObjectType } from '../types/basic'; 8 | import { PutOptions, ListOptions } from '../types/drive/request'; 9 | import { stringToUint8Array, bufferToUint8Array } from '../utils/buffer'; 10 | 11 | import { 12 | GetResponse, 13 | PutResponse, 14 | ListResponse, 15 | UploadResponse, 16 | DeleteResponse, 17 | DeleteManyResponse, 18 | } from '../types/drive/response'; 19 | 20 | export default class Drive { 21 | private requests: Requests; 22 | 23 | /** 24 | * Drive constructor 25 | * 26 | * @param {string} key 27 | * @param {KeyType} type 28 | * @param {string} projectId 29 | * @param {string} driveName 30 | * @param {string} [host] 31 | */ 32 | constructor( 33 | key: string, 34 | type: KeyType, 35 | projectId: string, 36 | driveName: string, 37 | host?: string 38 | ) { 39 | const baseURL = url 40 | .drive(type, host) 41 | .replace(':drive_name', driveName) 42 | .replace(':project_id', projectId); 43 | this.requests = new Requests(key, type, baseURL); 44 | } 45 | 46 | /** 47 | * get file from drive 48 | * 49 | * @param {string} name 50 | * @returns {Promise} 51 | */ 52 | public async get(name: string): Promise { 53 | const trimmedName = name?.trim(); 54 | if (!trimmedName) { 55 | throw new Error('Name is empty'); 56 | } 57 | 58 | const encodedName = encodeURIComponent(trimmedName); 59 | 60 | const { status, response, error } = await this.requests.get( 61 | DriveApi.GET_FILE.replace(':name', encodedName), 62 | { 63 | blobResponse: true, 64 | } 65 | ); 66 | if (status === 404 && error) { 67 | return null; 68 | } 69 | 70 | if (error) { 71 | throw error; 72 | } 73 | 74 | return response; 75 | } 76 | 77 | /** 78 | * delete file from drive 79 | * 80 | * @param {string} name 81 | * @returns {Promise} 82 | */ 83 | public async delete(name: string): Promise { 84 | const trimmedName = name?.trim(); 85 | if (!trimmedName) { 86 | throw new Error('Name is empty'); 87 | } 88 | 89 | const payload: ObjectType = { 90 | names: [name], 91 | }; 92 | 93 | const { response, error } = await this.requests.delete( 94 | DriveApi.DELETE_FILES, 95 | payload 96 | ); 97 | if (error) { 98 | throw error; 99 | } 100 | 101 | return response?.deleted?.[0] || name; 102 | } 103 | 104 | /** 105 | * deleteMany file from drive 106 | * 107 | * @param {string[]} names 108 | * @returns {Promise} 109 | */ 110 | public async deleteMany(names: string[]): Promise { 111 | if (!names.length) { 112 | throw new Error("Names can't be empty"); 113 | } 114 | 115 | if (names.length > 1000) { 116 | throw new Error("We can't delete more than 1000 items at a time"); 117 | } 118 | 119 | const payload: ObjectType = { 120 | names, 121 | }; 122 | 123 | const { status, response, error } = await this.requests.delete( 124 | DriveApi.DELETE_FILES, 125 | payload 126 | ); 127 | 128 | if (status === 400 && error) { 129 | throw new Error("Names can't be empty"); 130 | } 131 | 132 | if (error) { 133 | throw error; 134 | } 135 | 136 | return response; 137 | } 138 | 139 | /** 140 | * list files from drive 141 | * 142 | * @param {ListOptions} [options] 143 | * @returns {Promise} 144 | */ 145 | public async list(options?: ListOptions): Promise { 146 | const { 147 | recursive = true, 148 | prefix = '', 149 | limit = 1000, 150 | last = '', 151 | } = options || {}; 152 | 153 | const { response, error } = await this.requests.get( 154 | DriveApi.LIST_FILES.replace(':prefix', prefix) 155 | .replace(':recursive', recursive.toString()) 156 | .replace(':limit', limit.toString()) 157 | .replace(':last', last) 158 | ); 159 | if (error) { 160 | throw error; 161 | } 162 | 163 | return response; 164 | } 165 | 166 | /** 167 | * put files on drive 168 | * 169 | * @param {string} name 170 | * @param {PutOptions} options 171 | * @returns {Promise} 172 | */ 173 | public async put(name: string, options: PutOptions): Promise { 174 | const trimmedName = name?.trim(); 175 | if (!trimmedName) { 176 | throw new Error('Name is empty'); 177 | } 178 | 179 | const encodedName = encodeURIComponent(trimmedName); 180 | 181 | if (options.path && options.data) { 182 | throw new Error('Please only provide data or a path. Not both'); 183 | } 184 | 185 | if (!options.path && !options.data) { 186 | throw new Error('Please provide data or a path. Both are empty'); 187 | } 188 | 189 | if (options.path && !isNode()) { 190 | throw new Error("Can't use path in browser environment"); 191 | } 192 | 193 | let buffer = new Uint8Array(); 194 | 195 | if (options.path) { 196 | const fs = require('fs').promises; 197 | const buf = await fs.readFile(options.path); 198 | buffer = new Uint8Array(buf); 199 | } 200 | 201 | if (options.data) { 202 | if (isNode() && options.data instanceof Buffer) { 203 | buffer = bufferToUint8Array(options.data as Buffer); 204 | } else if (isString(options.data)) { 205 | buffer = stringToUint8Array(options.data as string); 206 | } else if (options.data instanceof Uint8Array) { 207 | buffer = options.data as Uint8Array; 208 | } else { 209 | throw new Error( 210 | 'Unsupported data format, expected data to be one of: string | Uint8Array | Buffer' 211 | ); 212 | } 213 | } 214 | 215 | const { response, error } = await this.upload( 216 | encodedName, 217 | buffer, 218 | options.contentType || 'binary/octet-stream' 219 | ); 220 | if (error) { 221 | throw error; 222 | } 223 | 224 | return response as string; 225 | } 226 | 227 | /** 228 | * upload files on drive 229 | * 230 | * @param {string} name 231 | * @param {Uint8Array} data 232 | * @param {string} contentType 233 | * @returns {Promise} 234 | */ 235 | private async upload( 236 | name: string, 237 | data: Uint8Array, 238 | contentType: string 239 | ): Promise { 240 | const contentLength = data.byteLength; 241 | const chunkSize = 1024 * 1024 * 10; // 10MB 242 | 243 | const { response, error } = await this.requests.post( 244 | DriveApi.INIT_CHUNK_UPLOAD.replace(':name', name), 245 | { 246 | headers: { 247 | 'Content-Type': contentType, 248 | }, 249 | } 250 | ); 251 | if (error) { 252 | return { error }; 253 | } 254 | 255 | const { upload_id: uid, name: resName } = response; 256 | 257 | let part = 1; 258 | for (let idx = 0; idx < contentLength; idx += chunkSize) { 259 | const start = idx; 260 | const end = Math.min(idx + chunkSize, contentLength); 261 | 262 | const chunk = data.slice(start, end); 263 | const { error: err } = await this.requests.post( 264 | DriveApi.UPLOAD_FILE_CHUNK.replace(':uid', uid) 265 | .replace(':name', name) 266 | .replace(':part', part.toString()), 267 | { 268 | payload: chunk, 269 | headers: { 270 | 'Content-Type': contentType, 271 | }, 272 | } 273 | ); 274 | if (err) { 275 | return { error: err }; 276 | } 277 | 278 | part += 1; 279 | } 280 | 281 | const { error: err } = await this.requests.patch( 282 | DriveApi.COMPLETE_FILE_UPLOAD.replace(':uid', uid).replace(':name', name) 283 | ); 284 | if (err) { 285 | return { error: err }; 286 | } 287 | 288 | return { response: resName }; 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/drive/index.ts: -------------------------------------------------------------------------------- 1 | import Drive from './drive'; 2 | 3 | export default Drive; 4 | -------------------------------------------------------------------------------- /src/index.browser.ts: -------------------------------------------------------------------------------- 1 | export * from './index'; 2 | -------------------------------------------------------------------------------- /src/index.node.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | // fetch polyfill for nodejs 4 | if (!globalThis.fetch) { 5 | // @ts-ignore 6 | globalThis.fetch = fetch; 7 | } 8 | 9 | export * from './index'; 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import DetaClass from './deta'; 2 | import BaseClass from './base'; 3 | import DriveClass from './drive'; 4 | import { KeyType } from './types/key'; 5 | 6 | /** 7 | * Deta returns instance of Deta class 8 | * 9 | * @param {string} [projectKey] 10 | * @param {string} [authToken] 11 | * @returns {DetaClass} 12 | */ 13 | export function Deta(projectKey?: string, authToken?: string): DetaClass { 14 | const token = authToken?.trim(); 15 | const key = projectKey?.trim(); 16 | if (token && key) { 17 | return new DetaClass(token, KeyType.AuthToken, key); 18 | } 19 | 20 | const apiKey = key || (typeof process !== 'undefined' ? process.env.DETA_PROJECT_KEY?.trim() : undefined); 21 | if (apiKey) { 22 | return new DetaClass(apiKey, KeyType.ProjectKey, apiKey.split('_')[0]); 23 | } 24 | 25 | if (typeof window !== 'undefined') { 26 | return new DetaClass('dummy', KeyType.DummyKey, 'dummy'); 27 | } 28 | 29 | throw new Error('Project key is not defined'); 30 | } 31 | 32 | /** 33 | * Base returns instance of Base class 34 | * 35 | * @param {string} baseName 36 | * @param {string} [host] 37 | * @returns {BaseClass} 38 | */ 39 | export function Base(baseName: string, host?: string): BaseClass { 40 | return Deta().Base(baseName, host); 41 | } 42 | 43 | /** 44 | * Drive returns instance of Drive class 45 | * 46 | * @param {string} driveName 47 | * @param {string} [host] 48 | * @returns {DriveClass} 49 | */ 50 | export function Drive(driveName: string, host?: string): DriveClass { 51 | return Deta().Drive(driveName, host); 52 | } 53 | -------------------------------------------------------------------------------- /src/types/action.ts: -------------------------------------------------------------------------------- 1 | export enum ActionTypes { 2 | Set = 'set', 3 | Trim = 'trim', 4 | Increment = 'increment', 5 | Append = 'append', 6 | Prepend = 'prepend', 7 | } 8 | 9 | export class Action { 10 | public readonly operation: ActionTypes; 11 | 12 | public readonly value: any; 13 | 14 | constructor(action: ActionTypes, value?: any) { 15 | this.operation = action; 16 | this.value = value; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/types/base/request.ts: -------------------------------------------------------------------------------- 1 | export interface FetchOptions { 2 | limit?: number; 3 | last?: string; 4 | desc?: boolean; 5 | } 6 | 7 | export interface PutOptions { 8 | expireIn?: number; 9 | expireAt?: Date | number; 10 | } 11 | 12 | export interface InsertOptions { 13 | expireIn?: number; 14 | expireAt?: Date | number; 15 | } 16 | 17 | export interface PutManyOptions { 18 | expireIn?: number; 19 | expireAt?: Date | number; 20 | } 21 | 22 | export interface UpdateOptions { 23 | expireIn?: number; 24 | expireAt?: Date | number; 25 | } 26 | -------------------------------------------------------------------------------- /src/types/base/response.ts: -------------------------------------------------------------------------------- 1 | import { NullType, ObjectType, ArrayType } from '../basic'; 2 | 3 | export type DeleteResponse = NullType; 4 | 5 | export type PutResponse = ObjectType | NullType; 6 | 7 | export type GetResponse = ObjectType | NullType; 8 | 9 | export type InsertResponse = ObjectType; 10 | 11 | export interface PutManyResponse { 12 | processed: { 13 | items: ArrayType; 14 | }; 15 | } 16 | 17 | export type UpdateResponse = NullType; 18 | 19 | export interface FetchResponse { 20 | items: ObjectType[]; 21 | count: number; 22 | last?: string; 23 | } 24 | -------------------------------------------------------------------------------- /src/types/basic.ts: -------------------------------------------------------------------------------- 1 | import { Action } from './action'; 2 | 3 | export type BasicType = string | number | boolean; 4 | 5 | export type NullType = null; 6 | 7 | export type UndefinedType = undefined; 8 | 9 | export type ObjectType = { 10 | [key: string]: 11 | | ObjectType 12 | | ArrayType 13 | | BasicType 14 | | NullType 15 | | UndefinedType 16 | | Action; 17 | }; 18 | 19 | export type ArrayType = Array< 20 | ArrayType | ObjectType | BasicType | NullType | UndefinedType 21 | >; 22 | 23 | export type CompositeType = ArrayType | ObjectType; 24 | 25 | export type DetaType = ArrayType | ObjectType | BasicType; 26 | -------------------------------------------------------------------------------- /src/types/drive/request.ts: -------------------------------------------------------------------------------- 1 | export interface PutOptions { 2 | data?: string | Uint8Array | Buffer; 3 | path?: string; 4 | contentType?: string; 5 | } 6 | 7 | export interface ListOptions { 8 | recursive?: boolean; 9 | prefix?: string; 10 | limit?: number; 11 | last?: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/types/drive/response.ts: -------------------------------------------------------------------------------- 1 | import { NullType } from '../basic'; 2 | 3 | export type GetResponse = Blob | NullType; 4 | 5 | export type DeleteResponse = string; 6 | 7 | export interface DeleteManyResponse { 8 | deleted: string[]; 9 | failed: { [key: string]: string }; 10 | } 11 | 12 | export interface ListResponse { 13 | names: string[]; 14 | paging: { 15 | size: number; 16 | last: string; 17 | }; 18 | } 19 | 20 | export type PutResponse = string; 21 | 22 | export interface UploadResponse { 23 | response?: any; 24 | error?: Error; 25 | } 26 | -------------------------------------------------------------------------------- /src/types/key.ts: -------------------------------------------------------------------------------- 1 | export enum KeyType { 2 | AuthToken, 3 | ProjectKey, 4 | DummyKey, 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/buffer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * stringToUint8Array converts string to Uint8Array 3 | * 4 | * @param {string} str 5 | * @returns {Uint8Array} 6 | */ 7 | export function stringToUint8Array(str: string): Uint8Array { 8 | const array = new Uint8Array(str.length); 9 | for (let i = 0; i < str.length; i += 1) { 10 | array[i] = str.charCodeAt(i); 11 | } 12 | return array; 13 | } 14 | 15 | /** 16 | * bufferToUint8Array converts Buffer to Uint8Array 17 | * 18 | * @param {Buffer} buffer 19 | * @returns {Uint8Array} 20 | */ 21 | export function bufferToUint8Array(buffer: Buffer): Uint8Array { 22 | const array = new Uint8Array(buffer.length); 23 | for (let i = 0; i < buffer.length; i += 1) { 24 | array[i] = buffer[i]; 25 | } 26 | return array; 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/date.ts: -------------------------------------------------------------------------------- 1 | export class Day { 2 | private date: Date; 3 | 4 | /** 5 | * Day constructor 6 | * 7 | * @param {Date} [date] 8 | */ 9 | constructor(date?: Date) { 10 | this.date = date || new Date(); 11 | } 12 | 13 | /** 14 | * addSeconds returns new Day object 15 | * by adding provided number of seconds. 16 | * 17 | * @param {number} seconds 18 | * @returns {Day} 19 | */ 20 | public addSeconds(seconds: number): Day { 21 | this.date = new Date(this.date.getTime() + 1000 * seconds); 22 | return this; 23 | } 24 | 25 | /** 26 | * getEpochSeconds returns number of seconds after epoch. 27 | * 28 | * @returns {number} 29 | */ 30 | public getEpochSeconds(): number { 31 | return Math.floor(this.date.getTime() / 1000.0); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/node.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * isNode returns true if the runtime environment is node 3 | * 4 | * @returns {boolean} 5 | */ 6 | export function isNode(): boolean { 7 | return ( 8 | typeof process !== 'undefined' && 9 | process.versions != null && 10 | process.versions.node != null 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/number.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * isNumber returns true if the provided value is of type number 3 | * 4 | * @param {any} value 5 | * @returns {boolean} 6 | */ 7 | export function isNumber(value: any): boolean { 8 | return typeof value === 'number'; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/object.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * isObject returns true if the provided value is an instance of object 3 | * 4 | * @param {any} value 5 | * @returns {boolean} 6 | */ 7 | export function isObject(value: any): boolean { 8 | return Object.prototype.toString.call(value) === '[object Object]'; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import { KeyType } from '../types/key'; 2 | 3 | interface RequestInit { 4 | payload?: any; 5 | headers?: { [key: string]: string }; 6 | } 7 | 8 | interface GetConfig { 9 | blobResponse: boolean; 10 | } 11 | 12 | interface RequestConfig { 13 | body?: any; 14 | method?: string; 15 | baseURL?: string; 16 | headers?: { [key: string]: string }; 17 | blobResponse?: boolean; 18 | } 19 | 20 | interface Response { 21 | status: number; 22 | response?: any; 23 | error?: Error; 24 | } 25 | 26 | enum Method { 27 | Put = 'PUT', 28 | Get = 'GET', 29 | Post = 'POST', 30 | Patch = 'PATCH', 31 | Delete = 'DELETE', 32 | } 33 | 34 | export default class Requests { 35 | private requestConfig: RequestConfig; 36 | 37 | /** 38 | * Requests constructor 39 | * 40 | * @param {string} key 41 | * @param {KeyType} type 42 | * @param {string} baseURL 43 | */ 44 | constructor(key: string, type: KeyType, baseURL: string) { 45 | this.requestConfig = { 46 | baseURL, 47 | headers: 48 | type === KeyType.AuthToken 49 | ? { Authorization: key } 50 | : { 'X-API-Key': key }, 51 | }; 52 | } 53 | 54 | /** 55 | * put sends a HTTP put request 56 | * 57 | * @param {string} uri 58 | * @param {any} payload 59 | * @returns {Promise} 60 | */ 61 | public async put(uri: string, payload: any): Promise { 62 | return Requests.fetch(uri, { 63 | ...this.requestConfig, 64 | body: payload, 65 | method: Method.Put, 66 | }); 67 | } 68 | 69 | /** 70 | * delete sends a HTTP delete request 71 | * 72 | * @param {string} uri 73 | * @param {any} [payload] 74 | * @returns {Promise} 75 | */ 76 | public async delete(uri: string, payload?: any): Promise { 77 | return Requests.fetch(uri, { 78 | ...this.requestConfig, 79 | body: payload, 80 | method: Method.Delete, 81 | }); 82 | } 83 | 84 | /** 85 | * get sends a HTTP get request 86 | * 87 | * @param {string} uri 88 | * @returns {Promise} 89 | */ 90 | public async get(uri: string, config?: GetConfig): Promise { 91 | return Requests.fetch(uri, { 92 | ...this.requestConfig, 93 | method: Method.Get, 94 | blobResponse: config?.blobResponse, 95 | }); 96 | } 97 | 98 | /** 99 | * post sends a HTTP post request 100 | * 101 | * @param {string} uri 102 | * @param {any} payload 103 | * @param {[key: string]: string} headers 104 | * @returns {Promise} 105 | */ 106 | public async post(uri: string, init: RequestInit): Promise { 107 | return Requests.fetch(uri, { 108 | ...this.requestConfig, 109 | body: init.payload, 110 | method: Method.Post, 111 | headers: { ...this.requestConfig.headers, ...init.headers }, 112 | }); 113 | } 114 | 115 | /** 116 | * patch sends a HTTP patch request 117 | * 118 | * @param {string} uri 119 | * @param {any} payload 120 | * @returns {Promise} 121 | */ 122 | public async patch(uri: string, payload?: any): Promise { 123 | return Requests.fetch(uri, { 124 | ...this.requestConfig, 125 | body: payload, 126 | method: Method.Patch, 127 | }); 128 | } 129 | 130 | private static async fetch( 131 | url: string, 132 | config: RequestConfig 133 | ): Promise { 134 | try { 135 | const body = 136 | config.body instanceof Uint8Array 137 | ? config.body 138 | : JSON.stringify(config.body); 139 | 140 | const contentType = 141 | config?.headers?.['Content-Type'] || 'application/json'; 142 | 143 | const headers = { 144 | ...config.headers, 145 | 'Content-Type': contentType, 146 | }; 147 | 148 | const response = await fetch(`${config.baseURL}${url}`, { 149 | body, 150 | headers, 151 | method: config.method, 152 | }); 153 | 154 | if (!response.ok) { 155 | const data = await response.json(); 156 | const message = data?.errors?.[0] || 'Something went wrong'; 157 | return { 158 | status: response.status, 159 | error: new Error(message), 160 | }; 161 | } 162 | 163 | if (config.blobResponse) { 164 | const blob = await response.blob(); 165 | return { status: response.status, response: blob }; 166 | } 167 | 168 | const json = await response.json(); 169 | return { status: response.status, response: json }; 170 | } catch (err) { 171 | return { status: 500, error: new Error('Something went wrong') }; 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/utils/string.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * isString returns true if the provided value is an instance of string 3 | * 4 | * @param {any} value 5 | * @returns {boolean} 6 | */ 7 | export function isString(value: any): boolean { 8 | return typeof value === 'string' || value instanceof String; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/undefinedOrNull.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * isUndefinedOrNull returns true if the provided value is of type undefined or null 3 | * 4 | * @param {any} value 5 | * @returns {boolean} 6 | */ 7 | export function isUndefinedOrNull(value: any): boolean { 8 | return value === undefined || value === null; 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["./src", "./scripts", "./__test__"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 6 | "module": "ESNext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./types", /* Redirect output structure to the directory. */ 16 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | "strictNullChecks": true, /* Enable strict null checks. */ 29 | "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | }, 66 | "include": [ 67 | "./src", 68 | ] 69 | } 70 | --------------------------------------------------------------------------------