├── .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 |
--------------------------------------------------------------------------------
/__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 |
--------------------------------------------------------------------------------