├── .denov ├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── couch.ts ├── couch_test.ts ├── docker-compose.yml ├── fixtures └── sample.json ├── modules-lock.json ├── modules.json ├── updb.sh └── vendor └── https └── deno.land └── std └── testing └── asserts.ts /.denov: -------------------------------------------------------------------------------- 1 | v1.0.0 -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_size = 2 3 | 4 | [Makefile] 5 | indent_style = tab -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [keroxp] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - "**" 6 | pull_request: 7 | types: 8 | - labeled 9 | jobs: 10 | build: 11 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.label.name == 'CI start') 12 | runs-on: ubuntu-latest 13 | services: 14 | redis: 15 | image: couchdb:2.3.0 16 | ports: 17 | - 5984:5984 18 | steps: 19 | - uses: actions/checkout@v1 20 | - uses: denolib/setup-deno@master 21 | - name: Run Tests 22 | run: | 23 | sleep 10 24 | deno test -A couch_test.ts -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | deno.d.ts 2 | .idea 3 | commands 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Yusuke Sakurai 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 | # deno-couchdb 2 | 3 | [![Build Status](https://github.com/keroxp/deno-couchdb/workflows/CI/badge.svg)](https://github.com/keroxp/deno-couchdb/actions) 4 | ![https://img.shields.io/github/tag/keroxp/deno-couchdb.svg](https://img.shields.io/github/tag/keroxp/deno-couchdb.svg) 5 | [![license](https://img.shields.io/github/license/keroxp/deno-couchdb.svg)](https://github.com/keroxp/deno-couchdb) 6 | 7 | CouchDB client for Deno built top of fetch 8 | 9 | # Usage 10 | 11 | ```ts 12 | import { CouchClient } from "https://denopkg.com/keroxp/deno-couchdb/couch.ts"; 13 | 14 | export type User = { 15 | id: number; 16 | name: string; 17 | years: number[]; 18 | }; 19 | async function main() { 20 | // create couch client with endpoint 21 | const couch = new CouchClient("http://localhost:5984"); 22 | // choose db to use 23 | const db = couch.database("users"); 24 | // check if specified database exists 25 | if (!(await couch.databaseExists("users"))) { 26 | // create new database 27 | await couch.createDatabase("users"); 28 | } 29 | // insert new document 30 | const uesr = { 31 | id: 100, 32 | name: "deno", 33 | years: [2018, 2019], 34 | }; 35 | const { id, rev } = await db.insert(user); 36 | // get existing document 37 | let user = await db.get(id); // {id: 100, name: "deno", years: [2018,2019]} 38 | // update existing document 39 | user.years.push(2020); 40 | await db.put(id, user, { rev }); 41 | // delete existing document 42 | await db.delete(id); 43 | } 44 | ``` 45 | 46 | # Compatibility Table 47 | 48 | ## Document 49 | 50 | - [x] `HEAD /{db}/{docid}` 51 | - [x] `GET /{db}/{docid}` 52 | - [x] `PUT /{db}/{docid}` 53 | - [x] `DELETE /{db}/{docid}` 54 | - [x] `COPY /{db}/{docid}` 55 | 56 | ## Attachments 57 | 58 | - [x] `HEAD /{db}/{docid}/{attname}` 59 | - [x] `GET /{db}/{docid}/{attname}` 60 | - [x] `PUT /{db}/{docid}/{attname}` 61 | - [x] `DELETE /{db}/{docid}/{attname}` 62 | 63 | ## Server 64 | 65 | - WIP... 66 | 67 | ## Contributing 68 | 69 | WELCOME!\ 70 | There are still missing features and actually I'm not familiar with CouchDB😇 71 | -------------------------------------------------------------------------------- /couch.ts: -------------------------------------------------------------------------------- 1 | import Reader = Deno.Reader; 2 | import Buffer = Deno.Buffer; 3 | import copy = Deno.copy; 4 | 5 | export type CouchResponse = { 6 | id: string; 7 | ok: boolean; 8 | rev: string; 9 | }; 10 | export type CouchMetadata = { 11 | couchdb: string; 12 | uuid: string; 13 | vendor: { 14 | name: string; 15 | version: string; 16 | }; 17 | version: string; 18 | }; 19 | export type CouchDatabaseInfo = { 20 | cluster: { 21 | n: number; 22 | q: number; 23 | r: number; 24 | w: number; 25 | }; 26 | compact_running: boolean; 27 | data_size: number; 28 | db_name: string; 29 | disk_format_version: number; 30 | disk_size: number; 31 | doc_count: number; 32 | doc_del_count: number; 33 | instance_start_time: "0"; 34 | /** @deprecated */ 35 | other: { 36 | data_size: number; 37 | }; 38 | purge_seq: number; 39 | sizes: { 40 | active: number; 41 | external: number; 42 | file: number; 43 | }; 44 | update_seq: string; 45 | }; 46 | 47 | export type CouchDocument = { 48 | _id: string; 49 | _ok: boolean; 50 | _rev: string; 51 | _deleted?: boolean; 52 | _attachments?: { 53 | [file: string]: { 54 | content_type: string; 55 | digest: string; 56 | data?: string; 57 | length: number; 58 | revpos: number; 59 | stub?: boolean; 60 | encoding?: string; 61 | encoded_length?: number; 62 | }; 63 | }; 64 | _conflicts?: any[]; 65 | _deleted_conflicts?: any[]; 66 | _local_seq?: string; 67 | _revs_info?: any[]; 68 | _revisions?: { 69 | ids: string; 70 | start: number; 71 | }; 72 | }; 73 | 74 | export class CouchError extends Error { 75 | constructor( 76 | readonly status: number, 77 | readonly error: string, 78 | readonly reason?: string, 79 | ) { 80 | super(status + ":" + error); 81 | } 82 | } 83 | 84 | export type NotModified = Symbol; 85 | export const NotModified = Symbol("NotModified"); 86 | export type CouchOptions = { 87 | basicAuth?: { 88 | username: string; 89 | password: string; 90 | }; 91 | }; 92 | 93 | interface Fetcher { 94 | (path: string, params: { 95 | method: string; 96 | body?: string | ArrayBuffer; 97 | headers?: Headers; 98 | }): Promise; 99 | } 100 | function makeFetch( 101 | endpoint: string, 102 | opts: CouchOptions = {}, 103 | ): Fetcher { 104 | return ( 105 | path: string, 106 | { 107 | method = "GET", 108 | body, 109 | headers = new Headers(), 110 | }: { 111 | method: string; 112 | body?: string | ArrayBuffer; 113 | headers?: Headers; 114 | }, 115 | ) => { 116 | if (opts.basicAuth) { 117 | const { username, password } = opts.basicAuth; 118 | const authorization = `Basic ${btoa(username + ":" + password)}`; 119 | headers.set("authorization", authorization); 120 | } 121 | return fetch(`${endpoint}` + path, { 122 | headers, 123 | body, 124 | method, 125 | }); 126 | }; 127 | } 128 | 129 | class CouchDatabase { 130 | private fetch: Fetcher; 131 | 132 | constructor( 133 | readonly endpoint: string, 134 | readonly db: string, 135 | readonly opts: CouchOptions = {}, 136 | ) { 137 | this.fetch = makeFetch(`${this.endpoint}/${this.db}`, this.opts); 138 | } 139 | async insert( 140 | doc: T, 141 | opts?: { 142 | batch?: "ok"; 143 | fullCommit?: boolean; 144 | }, 145 | ): Promise<{ 146 | id: string; 147 | ok: boolean; 148 | rev: string; 149 | }> { 150 | const headers = new Headers({ 151 | "content-type": "application/json", 152 | accept: "application/json", 153 | }); 154 | let path = ""; 155 | if (opts) { 156 | if (opts.fullCommit != null) { 157 | headers.set("X-Couch-Full-Commit", opts.fullCommit ? "true" : "false"); 158 | } 159 | if (opts.batch != null) { 160 | path += "?batch=ok"; 161 | } 162 | } 163 | const body = JSON.stringify(doc); 164 | const res = await this.fetch(path, { 165 | method: "POST", 166 | headers, 167 | body, 168 | }); 169 | if (res.status === 201 || res.status === 202) { 170 | return res.json(); 171 | } 172 | throw new CouchError(res.status, await res.text()); 173 | } 174 | 175 | async info(id: string): Promise { 176 | const res = await this.fetch(`/${id}`, { 177 | method: "HEAD", 178 | }); 179 | await res.text(); 180 | if (res.status === 200 || res.status === 304) { 181 | return res.headers; 182 | } else if (res.status === 404) { 183 | return void 0; 184 | } 185 | throw new CouchError(res.status, await res.text()); 186 | } 187 | 188 | async get( 189 | id: string, 190 | opts?: Partial<{ 191 | attachments: boolean; 192 | att_encoding_info: boolean; 193 | atts_since: any[]; 194 | conflicts: boolean; 195 | deleted_conflicts: boolean; 196 | latest: boolean; 197 | local_seq: boolean; 198 | meta: boolean; 199 | open_revs: any[]; 200 | rev: string; 201 | revs: boolean; 202 | revs_info: boolean; 203 | }>, 204 | ): Promise<(CouchDocument & T) | NotModified> { 205 | const res = await this._get("json", id, opts); 206 | return res.json(); 207 | } 208 | 209 | async getMultipart( 210 | id: string, 211 | opts?: Partial<{ 212 | attachments: boolean; 213 | att_encoding_info: boolean; 214 | atts_since: any[]; 215 | conflicts: boolean; 216 | deleted_conflicts: boolean; 217 | latest: boolean; 218 | local_seq: boolean; 219 | meta: boolean; 220 | open_revs: any[]; 221 | rev: string; 222 | revs: boolean; 223 | revs_info: boolean; 224 | }>, 225 | ): Promise { 226 | return this._get("multipart", id, opts); 227 | } 228 | 229 | private async _get( 230 | accept: "json" | "multipart", 231 | id: string, 232 | opts?: Partial<{ 233 | attachments: boolean; 234 | att_encoding_info: boolean; 235 | atts_since: any[]; 236 | conflicts: boolean; 237 | deleted_conflicts: boolean; 238 | latest: boolean; 239 | local_seq: boolean; 240 | meta: boolean; 241 | open_revs: any[]; 242 | rev: string; 243 | revs: boolean; 244 | revs_info: boolean; 245 | }>, 246 | ): Promise { 247 | const params = new URLSearchParams(); 248 | if (opts != null) { 249 | if (opts.attachments != null) { 250 | params.set("attachments", opts.attachments ? "true" : "false"); 251 | } 252 | if (opts.att_encoding_info != null) { 253 | params.set( 254 | "att_encoding_info", 255 | opts.att_encoding_info ? "true" : "false", 256 | ); 257 | } 258 | } 259 | const res = await this.fetch(`/${id}?${params.toString()}`, { 260 | method: "GET", 261 | headers: new Headers({ accept }), 262 | }); 263 | if (res.status === 200 || res.status === 304) { 264 | return res; 265 | } 266 | throw new CouchError(res.status, await res.text()); 267 | } 268 | 269 | async put( 270 | id: string, 271 | doc: T, 272 | opts?: { 273 | fullCommit?: boolean; 274 | rev?: string; 275 | batch?: "ok"; 276 | new_edits?: boolean; 277 | }, 278 | ): Promise<{ id: string; ok: boolean; rev: string }> { 279 | const body = JSON.stringify(doc); 280 | const headers = new Headers({ 281 | "content-type": "application/json", 282 | accept: "application/json", 283 | }); 284 | let params = new URLSearchParams(); 285 | if (opts) { 286 | if (opts.fullCommit != null) { 287 | headers.set("X-Couch-Full-Commit", opts.fullCommit ? "true" : "false"); 288 | } 289 | if (opts.rev) { 290 | params.append("rev", opts.rev); 291 | } 292 | if (opts.batch) { 293 | params.append("batch", "ok"); 294 | } 295 | if (opts.new_edits != null) { 296 | params.append("new_edits", opts.new_edits ? "true" : "false"); 297 | } 298 | } 299 | const res = await this.fetch(`/${id}?${params.toString()}`, { 300 | method: "PUT", 301 | headers, 302 | body, 303 | }); 304 | if (res.status === 201 || res.status === 202) { 305 | return res.json(); 306 | } 307 | throw new CouchError(res.status, await res.text()); 308 | } 309 | 310 | async copy( 311 | id: string, 312 | destination: string, 313 | opts?: { 314 | rev?: string; 315 | batch?: "ok"; 316 | fullCommit?: boolean; 317 | }, 318 | ): Promise { 319 | const params = new URLSearchParams(); 320 | const headers = new Headers({ 321 | destination, 322 | accept: "application/json", 323 | }); 324 | if (opts) { 325 | if (opts.fullCommit != null) { 326 | headers.set("X-Couch-Full-Commit", opts.fullCommit ? "true" : "false"); 327 | } 328 | if (opts.rev) { 329 | params.append("rev", opts.rev); 330 | } 331 | if (opts.batch) { 332 | params.append("batch", "ok"); 333 | } 334 | } 335 | const res = await this.fetch(`/${id}?${params.toString()}`, { 336 | method: "COPY", 337 | headers, 338 | }); 339 | if (res.status === 201 || res.status === 202) { 340 | return res.json(); 341 | } 342 | throw new CouchError(res.status, await res.text()); 343 | } 344 | 345 | async delete( 346 | id: string, 347 | rev: string, 348 | opts?: { 349 | batch?: "ok"; 350 | fullCommit?: boolean; 351 | }, 352 | ): Promise { 353 | const headers = new Headers({ 354 | "content-type": "application/json", 355 | }); 356 | let params = new URLSearchParams(); 357 | params.append("rev", rev); 358 | if (opts) { 359 | if (opts.fullCommit != null) { 360 | headers.set("X-Couch-Full-Commit", opts.fullCommit ? "true" : "false"); 361 | } 362 | if (opts.batch) { 363 | params.append("batch", "ok"); 364 | } 365 | } 366 | const res = await this.fetch(`/${id}?${params.toString()}`, { 367 | method: "DELETE", 368 | headers: new Headers({ 369 | accept: "application/json", 370 | }), 371 | }); 372 | if (res.status === 200 || res.status === 202) { 373 | return res.json(); 374 | } 375 | throw new CouchError(res.status, await res.text()); 376 | } 377 | 378 | async attachmentInfo( 379 | id: string, 380 | attachment: string, 381 | opts?: { 382 | rev: string; 383 | }, 384 | ): Promise { 385 | const params = new URLSearchParams(); 386 | if (opts) { 387 | params.append("rev", opts.rev); 388 | } 389 | const res = await this.fetch(`/${id}/${attachment}?${params.toString()}`, { 390 | method: "GET", 391 | headers: new Headers({ 392 | accept: "application/json", 393 | }), 394 | }); 395 | await res.text(); 396 | if (res.status === 200) { 397 | return res.headers; 398 | } else if (res.status === 404) { 399 | return; 400 | } 401 | throw new CouchError(res.status, await res.text()); 402 | } 403 | 404 | async getAttachment( 405 | id: string, 406 | attachment: string, 407 | opts?: { 408 | rev: string; 409 | }, 410 | ): Promise { 411 | const params = new URLSearchParams(); 412 | if (opts) { 413 | params.append("rev", opts.rev); 414 | } 415 | const res = await this.fetch(`/${id}/${attachment}?${params.toString()}`, { 416 | method: "GET", 417 | }); 418 | if (res.status === 200) { 419 | return res.arrayBuffer(); 420 | } 421 | throw new CouchError(res.status, await res.text()); 422 | } 423 | 424 | async putAttachment( 425 | id: string, 426 | attachment: string, 427 | { 428 | data, 429 | contentType, 430 | rev, 431 | }: { 432 | data: Reader; 433 | contentType: string; 434 | rev?: string; 435 | }, 436 | ): Promise { 437 | const params = new URLSearchParams(); 438 | const headers = new Headers({ 439 | "content-type": contentType, 440 | accept: "application/json", 441 | }); 442 | if (rev != null) { 443 | params.append("rev", rev); 444 | } 445 | // TODO: use ReadableStream if possible 446 | const buf = new Buffer(); 447 | await copy(data, buf); 448 | const res = await this.fetch(`/${id}/${attachment}?${params.toString()}`, { 449 | method: "PUT", 450 | headers, 451 | body: buf.bytes(), 452 | }); 453 | if (res.status === 201 || res.status === 202) { 454 | return res.json(); 455 | } 456 | throw new CouchError(res.status, await res.text()); 457 | } 458 | 459 | async deleteAttachment( 460 | id: string, 461 | attachment: string, 462 | rev: string, 463 | opts?: { 464 | fullCommit?: boolean; 465 | batch?: "ok"; 466 | }, 467 | ): Promise { 468 | const params = new URLSearchParams(); 469 | const headers = new Headers({ 470 | accept: "application/json", 471 | }); 472 | params.append("rev", rev); 473 | if (opts) { 474 | if (opts.batch != null) { 475 | params.append("batch", "ok"); 476 | } 477 | if (opts.fullCommit != null) { 478 | headers.set("X-Couch-Full-Commit", opts.fullCommit ? "true" : "false"); 479 | } 480 | } 481 | const res = await this.fetch(`/${id}/${attachment}?${params.toString()}`, { 482 | method: "GET", 483 | }); 484 | if (res.status === 200 || res.status === 202) { 485 | return res.json(); 486 | } 487 | throw new CouchError(res.status, await res.text()); 488 | } 489 | 490 | async find( 491 | selector: any, 492 | opts: Partial<{ 493 | limit: number; 494 | skip: number; 495 | sort: (string | { [key: string]: "asc" | "desc" })[]; 496 | fields: string[]; 497 | use_index: string | [string, string]; 498 | r: number; 499 | bookmark: string; 500 | update: boolean; 501 | stable: boolean; 502 | stale: string; 503 | execution_stats: boolean; 504 | }> = {}, 505 | ): Promise<{ 506 | docs: T[]; 507 | warning?: string; 508 | execution_stats?: { 509 | total_keys_examined: number; 510 | total_docs_examined: number; 511 | total_quorum_docs_examined: number; 512 | results_returned: number; 513 | execution_time_ms: number; 514 | }; 515 | bookmark?: string; 516 | }> { 517 | const body = JSON.stringify({ 518 | selector, 519 | limit: opts.limit, 520 | skip: opts.skip, 521 | sort: opts.sort, 522 | fields: opts.fields, 523 | use_index: opts.use_index, 524 | r: opts.r, 525 | bookmark: opts.bookmark, 526 | update: opts.update, 527 | stale: opts.stale, 528 | stable: opts.stable, 529 | execution_stats: opts.execution_stats, 530 | }); 531 | const res = await this.fetch(`/_find`, { 532 | method: "POST", 533 | headers: new Headers({ 534 | "content-type": "application/json", 535 | accept: "application/json", 536 | }), 537 | body, 538 | }); 539 | if (res.status === 200) { 540 | return res.json(); 541 | } 542 | throw new CouchError(res.status, await res.text()); 543 | } 544 | } 545 | 546 | export class CouchClient { 547 | constructor(readonly endpoint: string, readonly opts: CouchOptions = {}) { 548 | this.fetch = makeFetch(this.endpoint, this.opts); 549 | } 550 | 551 | private readonly fetch: Fetcher; 552 | async metadata(): Promise { 553 | const res = await this.fetch("/", { 554 | method: "GET", 555 | headers: new Headers({ 556 | accept: "application/json", 557 | }), 558 | }); 559 | if (res.status === 200) { 560 | return res.json(); 561 | } 562 | throw new CouchError(res.status, await res.text()); 563 | } 564 | 565 | // DB 566 | async databaseExists(name: string): Promise { 567 | const res = await this.fetch(`/${name}`, { method: "HEAD" }); 568 | await res.text(); 569 | if (res.status === 200) { 570 | return true; 571 | } else if (res.status === 404) { 572 | return false; 573 | } 574 | throw new CouchError(res.status, await res.text()); 575 | } 576 | 577 | async getDatabase(name: string): Promise { 578 | const res = await this.fetch(`/${name}`, { method: "GET" }); 579 | if (res.status === 200) { 580 | return res.json(); 581 | } 582 | throw new CouchError(res.status, await res.text()); 583 | } 584 | 585 | async createDatabase( 586 | name: string, 587 | opts?: { 588 | q?: number; 589 | n?: number; 590 | }, 591 | ): Promise<{ ok: boolean }> { 592 | const params = new URLSearchParams(); 593 | if (opts != null) { 594 | if (opts.q != null) { 595 | params.append("q", opts.q + ""); 596 | } 597 | if (opts.n != null) { 598 | params.append("n", opts.n + ""); 599 | } 600 | } 601 | const res = await this.fetch(`/${name}?${params.toString()}`, { 602 | method: "PUT", 603 | headers: new Headers({ 604 | accept: "application/json", 605 | }), 606 | }); 607 | if (res.status === 201 || res.status === 202) { 608 | return res.json(); 609 | } else if (400 <= res.status && res.status < 500) { 610 | const { error, reason } = await res.json(); 611 | throw new CouchError(res.status, error, reason); 612 | } else { 613 | throw new CouchError(res.status, await res.text()); 614 | } 615 | } 616 | 617 | async deleteDatabase( 618 | name: string, 619 | ): Promise<{ 620 | ok: boolean; 621 | }> { 622 | const res = await this.fetch(`/${name}`, { 623 | method: "DELETE", 624 | }); 625 | if (res.status === 200 || res.status === 202) { 626 | return res.json(); 627 | } else if (400 <= res.status && res.status < 500) { 628 | const { error, reason } = await res.json(); 629 | throw new CouchError(res.status, error, reason); 630 | } 631 | throw new CouchError(res.status, await res.text()); 632 | } 633 | 634 | database(db: string): CouchDatabase { 635 | return new CouchDatabase(this.endpoint, db, this.opts); 636 | } 637 | } 638 | -------------------------------------------------------------------------------- /couch_test.ts: -------------------------------------------------------------------------------- 1 | import { CouchClient } from "./couch.ts"; 2 | import { 3 | assert, 4 | assertEquals, 5 | } from "./vendor/https/deno.land/std/testing/asserts.ts"; 6 | import open = Deno.open; 7 | const { test } = Deno; 8 | 9 | const kDbName = "testdb"; 10 | const endpoint = Deno.env.get("COUCHDB_ENDPOINT") || "http://127.0.0.1:5984"; 11 | const client = new CouchClient(endpoint); 12 | const db = client.database(kDbName); 13 | 14 | if (await client.databaseExists(kDbName)) { 15 | await client.deleteDatabase(kDbName); 16 | } 17 | await client.createDatabase(kDbName); 18 | 19 | async function useDatabase(f: (db: string) => Promise) { 20 | const name = "testdb-" + Math.round(Math.random() * 10000000); 21 | return client 22 | .databaseExists(name) 23 | .then((ok) => (ok ? null : client.createDatabase(name))) 24 | .then((_) => f(name)) 25 | .finally(async () => { 26 | if (await client.databaseExists(name)) { 27 | await client.deleteDatabase(name); 28 | } 29 | }); 30 | } 31 | 32 | test({ 33 | name: "metadata", 34 | fn: async () => { 35 | const data = await client.metadata(); 36 | assertEquals(data.couchdb, "Welcome"); 37 | }, 38 | }); 39 | test({ 40 | name: "databaseExists", 41 | fn: async () => { 42 | const exists = await client.databaseExists("nodb"); 43 | assertEquals(exists, false); 44 | }, 45 | }); 46 | 47 | test({ 48 | name: "createDatabase", 49 | fn: async () => { 50 | const db = "testdb1"; 51 | try { 52 | assertEquals(await client.databaseExists(db), false); 53 | const { ok } = await client.createDatabase(db); 54 | assertEquals(ok, true); 55 | } finally { 56 | await client.deleteDatabase(db); 57 | } 58 | }, 59 | }); 60 | test({ 61 | name: "getDatabase", 62 | fn: async function getDatabase() { 63 | await useDatabase(async (db) => { 64 | const info = await client.getDatabase(db); 65 | assertEquals(info.db_name, db); 66 | }); 67 | }, 68 | }); 69 | 70 | test({ 71 | name: "deleteDatabase", 72 | fn: async function deleteDatabase() { 73 | await useDatabase(async (db) => { 74 | const { ok } = await client.deleteDatabase(db); 75 | assertEquals(ok, true); 76 | assertEquals(await client.databaseExists(db), false); 77 | }); 78 | }, 79 | }); 80 | 81 | test({ 82 | name: "createDocument", 83 | fn: async function createDocument() { 84 | const obj = { 85 | name: "deno", 86 | nice: true, 87 | }; 88 | await useDatabase(async (_) => { 89 | const res = await db.insert(obj); 90 | assertEquals(res.ok, true); 91 | }); 92 | }, 93 | }); 94 | test({ 95 | name: "getDocument", 96 | fn: async function getDocument() { 97 | const obj = { 98 | name: "deno", 99 | nice: true, 100 | years: [2018, 2019], 101 | }; 102 | await useDatabase(async (_) => { 103 | const res = await db.insert(obj); 104 | assertEquals(res.ok, true); 105 | const doc = await db.get(res.id); 106 | assertEquals(doc["_id"], res.id); 107 | assertEquals(doc["name"], "deno"); 108 | assertEquals(doc["nice"], true); 109 | assertEquals(doc["years"], [2018, 2019]); 110 | }); 111 | }, 112 | }); 113 | test({ 114 | name: "putDocument", 115 | fn: async function putDocument() { 116 | const doc = { 117 | name: "deno", 118 | nice: true, 119 | years: [2018, 2019], 120 | }; 121 | const _id = "denode"; 122 | const { id, rev, ok } = await db.put(_id, doc); 123 | assertEquals(id, _id); 124 | assertEquals(ok, true); 125 | const doc2 = { 126 | name: "node", 127 | nice: true, 128 | years: [2009, 2019], 129 | }; 130 | const res = await db.put(id, doc2, { rev }); 131 | assertEquals(res.id, id); 132 | const _doc = await db.get(id); 133 | assertEquals(_doc["name"], "node"); 134 | assertEquals(_doc["nice"], true); 135 | assertEquals(_doc["years"], [2009, 2019]); 136 | }, 137 | }); 138 | test({ 139 | name: "documentInfo", 140 | fn: async function documentInfo() { 141 | const { id } = await db.insert({ name: "deno" }); 142 | const info = await db.info(id); 143 | assert(info !== void 0, "info must be defined"); 144 | assertEquals(await db.info("xxx"), void 0); 145 | }, 146 | }); 147 | test({ 148 | name: "deleteDocument", 149 | fn: async function deleteDocument() { 150 | const doc = { 151 | name: "deno", 152 | }; 153 | const { id, rev } = await db.insert(doc); 154 | const res = await db.delete(id, rev); 155 | assertEquals(res.id, id); 156 | assertEquals(await db.info(id), void 0); 157 | }, 158 | }); 159 | 160 | test({ 161 | name: "copyDocument", 162 | fn: async function copyDocument() { 163 | await db.put("deno", { 164 | myNameIs: "deno", 165 | }); 166 | await db.copy("deno", "node"); 167 | const o = await db.get("node"); 168 | assertEquals(o["myNameIs"], "deno"); 169 | }, 170 | }); 171 | 172 | test({ 173 | name: "findDocument", 174 | fn: async function findDocument() { 175 | await Promise.all([ 176 | db.insert({ 177 | id: 100, 178 | name: "deno", 179 | }), 180 | db.insert({ 181 | id: 101, 182 | name: "node", 183 | }), 184 | ]); 185 | const res = await db.find({ 186 | id: 100, 187 | }); 188 | assertEquals(res.docs.length, 1); 189 | assertEquals(res.docs[0]["id"], 100); 190 | assertEquals(res.docs[0]["name"], "deno"); 191 | }, 192 | }); 193 | 194 | test({ 195 | name: "putAttachment", 196 | fn: async function putAttachment() { 197 | const { id, rev } = await db.insert({ 198 | name: "couch.ts", 199 | }); 200 | const data = await open("./fixtures/sample.json"); 201 | const res = await db.putAttachment(id, "fixtures/sample.json", { 202 | contentType: "application/json", 203 | data, 204 | rev, 205 | }); 206 | assertEquals(res.ok, true); 207 | data.close(); 208 | }, 209 | }); 210 | 211 | test({ 212 | name: "getAttachment", 213 | fn: async function getAttachment() { 214 | const { id, rev } = await db.insert({ 215 | name: "couch.ts", 216 | }); 217 | const data = await open("./fixtures/sample.json"); 218 | await db.putAttachment(id, "fixtures/sample.json", { 219 | contentType: "application/json", 220 | data, 221 | rev, 222 | }); 223 | const attach = await db.getAttachment(id, "fixtures/sample.json"); 224 | const json = new TextDecoder().decode(attach); 225 | const content = JSON.parse(json); 226 | assertEquals(content["deno"], "land"); 227 | data.close(); 228 | }, 229 | }); 230 | 231 | test({ 232 | name: "deleteAttachment", 233 | fn: async function deleteAttachment() { 234 | const { id, rev } = await db.insert({ 235 | name: "couch.ts", 236 | }); 237 | const data = await open("./fixtures/sample.json"); 238 | const at = await db.putAttachment(id, "fixtures/sample.json", { 239 | contentType: "application/json", 240 | data, 241 | rev, 242 | }); 243 | await db.deleteAttachment(id, "fixtures/sample.json", at.rev); 244 | const res = await db.attachmentInfo(id, "fixtures/sample.json", { 245 | rev, 246 | }); 247 | assertEquals(res, void 0); 248 | data.close(); 249 | }, 250 | }); 251 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keroxp/deno-couchdb/ce30110c04c39de0c53f6df6272ed22c7f417a9f/docker-compose.yml -------------------------------------------------------------------------------- /fixtures/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno": "land" 3 | } 4 | -------------------------------------------------------------------------------- /modules-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://deno.land/std": { 3 | "version": "@0.93.0", 4 | "modules": [ 5 | "/testing/asserts.ts" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /modules.json: -------------------------------------------------------------------------------- 1 | { 2 | "https://deno.land/std": { 3 | "version": "@0.93.0", 4 | "modules": [ 5 | "/testing/asserts.ts" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /updb.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker run -p 5984:5984 -d -it couchdb:2.3.0 4 | -------------------------------------------------------------------------------- /vendor/https/deno.land/std/testing/asserts.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/std@0.93.0/testing/asserts.ts"; 2 | --------------------------------------------------------------------------------