kinto-http-client.sys.mjs (101834B)
1 /* 2 * 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 * 15 * This file is generated from kinto.js - do not modify directly. 16 */ 17 18 /* eslint @typescript-eslint/no-unused-vars: off */ 19 import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs"; 20 21 /* 22 * Version 17.0.0 - f643998 23 */ 24 25 import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs"; 26 27 /****************************************************************************** 28 Copyright (c) Microsoft Corporation. 29 30 Permission to use, copy, modify, and/or distribute this software for any 31 purpose with or without fee is hereby granted. 32 33 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 34 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 35 AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 36 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 37 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 38 OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 39 PERFORMANCE OF THIS SOFTWARE. 40 ***************************************************************************** */ 41 /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ 42 43 function __decorate(decorators, target, key, desc) { 44 var c = arguments.length, 45 r = 46 c < 3 47 ? target 48 : desc === null 49 ? (desc = Object.getOwnPropertyDescriptor(target, key)) 50 : desc, 51 d; 52 if (typeof Reflect === "object" && typeof Reflect.decorate === "function") 53 r = Reflect.decorate(decorators, target, key, desc); 54 else 55 for (var i = decorators.length - 1; i >= 0; i--) 56 if ((d = decorators[i])) 57 r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 58 return (c > 3 && r && Object.defineProperty(target, key, r), r); 59 } 60 61 typeof SuppressedError === "function" 62 ? SuppressedError 63 : function (error, suppressed, message) { 64 var e = new Error(message); 65 return ( 66 (e.name = "SuppressedError"), 67 (e.error = error), 68 (e.suppressed = suppressed), 69 e 70 ); 71 }; 72 73 /** 74 * Chunks an array into n pieces. 75 * 76 * @private 77 * @param {Array} array 78 * @param {Number} n 79 * @return {Array} 80 */ 81 function partition(array, n) { 82 if (n <= 0) { 83 return [array]; 84 } 85 return array.reduce((acc, x, i) => { 86 if (i === 0 || i % n === 0) { 87 acc.push([x]); 88 } else { 89 acc[acc.length - 1].push(x); 90 } 91 return acc; 92 }, []); 93 } 94 /** 95 * Returns a Promise always resolving after the specified amount in milliseconds. 96 * 97 * @return Promise<void> 98 */ 99 function delay(ms) { 100 return new Promise((resolve) => setTimeout(resolve, ms)); 101 } 102 /** 103 * Always returns a resource data object from the provided argument. 104 * 105 * @private 106 * @param {Object|String} resource 107 * @return {Object} 108 */ 109 function toDataBody(resource) { 110 if (isObject(resource)) { 111 return resource; 112 } 113 if (typeof resource === "string") { 114 return { id: resource }; 115 } 116 throw new Error("Invalid argument."); 117 } 118 /** 119 * Transforms an object into an URL query string, stripping out any undefined 120 * values. 121 * 122 * @param {Object} obj 123 * @return {String} 124 */ 125 function qsify(obj) { 126 const encode = (v) => 127 encodeURIComponent(typeof v === "boolean" ? String(v) : v); 128 const stripped = cleanUndefinedProperties(obj); 129 return Object.keys(stripped) 130 .map((k) => { 131 const ks = encode(k) + "="; 132 if (Array.isArray(stripped[k])) { 133 return ks + stripped[k].map((v) => encode(v)).join(","); 134 } 135 return ks + encode(stripped[k]); 136 }) 137 .join("&"); 138 } 139 /** 140 * Checks if a version is within the provided range. 141 * 142 * @param {String} version The version to check. 143 * @param {String} minVersion The minimum supported version (inclusive). 144 * @param {String} maxVersion The minimum supported version (exclusive). 145 * @throws {Error} If the version is outside of the provided range. 146 */ 147 function checkVersion(version, minVersion, maxVersion) { 148 const extract = (str) => str.split(".").map((x) => parseInt(x, 10)); 149 const [verMajor, verMinor] = extract(version); 150 const [minMajor, minMinor] = extract(minVersion); 151 const [maxMajor, maxMinor] = extract(maxVersion); 152 const checks = [ 153 verMajor < minMajor, 154 verMajor === minMajor && verMinor < minMinor, 155 verMajor > maxMajor, 156 verMajor === maxMajor && verMinor >= maxMinor, 157 ]; 158 if (checks.some((x) => x)) { 159 throw new Error( 160 `Version ${version} doesn't satisfy ${minVersion} <= x < ${maxVersion}` 161 ); 162 } 163 } 164 /** 165 * Generates a decorator function ensuring a version check is performed against 166 * the provided requirements before executing it. 167 * 168 * @param {String} min The required min version (inclusive). 169 * @param {String} max The required max version (inclusive). 170 * @return {Function} 171 */ 172 function support(min, max) { 173 return function ( 174 // @ts-ignore 175 target, 176 key, 177 descriptor 178 ) { 179 const fn = descriptor.value; 180 return { 181 configurable: true, 182 get() { 183 const wrappedMethod = (...args) => { 184 // "this" is the current instance which its method is decorated. 185 const client = this.client ? this.client : this; 186 return client 187 .fetchHTTPApiVersion() 188 .then((version) => checkVersion(version, min, max)) 189 .then(() => fn.apply(this, args)); 190 }; 191 Object.defineProperty(this, key, { 192 value: wrappedMethod, 193 configurable: true, 194 writable: true, 195 }); 196 return wrappedMethod; 197 }, 198 }; 199 }; 200 } 201 /** 202 * Generates a decorator function ensuring that the specified capabilities are 203 * available on the server before executing it. 204 * 205 * @param {Array<String>} capabilities The required capabilities. 206 * @return {Function} 207 */ 208 function capable(capabilities) { 209 return function ( 210 // @ts-ignore 211 target, 212 key, 213 descriptor 214 ) { 215 const fn = descriptor.value; 216 return { 217 configurable: true, 218 get() { 219 const wrappedMethod = (...args) => { 220 // "this" is the current instance which its method is decorated. 221 const client = this.client ? this.client : this; 222 return client 223 .fetchServerCapabilities() 224 .then((available) => { 225 const missing = capabilities.filter((c) => !(c in available)); 226 if (missing.length) { 227 const missingStr = missing.join(", "); 228 throw new Error( 229 `Required capabilities ${missingStr} not present on server` 230 ); 231 } 232 }) 233 .then(() => fn.apply(this, args)); 234 }; 235 Object.defineProperty(this, key, { 236 value: wrappedMethod, 237 configurable: true, 238 writable: true, 239 }); 240 return wrappedMethod; 241 }, 242 }; 243 }; 244 } 245 /** 246 * Generates a decorator function ensuring an operation is not performed from 247 * within a batch request. 248 * 249 * @param {String} message The error message to throw. 250 * @return {Function} 251 */ 252 function nobatch(message) { 253 return function ( 254 // @ts-ignore 255 target, 256 key, 257 descriptor 258 ) { 259 const fn = descriptor.value; 260 return { 261 configurable: true, 262 get() { 263 const wrappedMethod = (...args) => { 264 // "this" is the current instance which its method is decorated. 265 if (this._isBatch) { 266 throw new Error(message); 267 } 268 return fn.apply(this, args); 269 }; 270 Object.defineProperty(this, key, { 271 value: wrappedMethod, 272 configurable: true, 273 writable: true, 274 }); 275 return wrappedMethod; 276 }, 277 }; 278 }; 279 } 280 /** 281 * Returns true if the specified value is an object (i.e. not an array nor null). 282 * @param {Object} thing The value to inspect. 283 * @return {bool} 284 */ 285 function isObject(thing) { 286 return typeof thing === "object" && thing !== null && !Array.isArray(thing); 287 } 288 /** 289 * Parses a data url. 290 * @param {String} dataURL The data url. 291 * @return {Object} 292 */ 293 function parseDataURL(dataURL) { 294 const regex = /^data:(.*);base64,(.*)/; 295 const match = dataURL.match(regex); 296 if (!match) { 297 throw new Error(`Invalid data-url: ${String(dataURL).substring(0, 32)}...`); 298 } 299 const props = match[1]; 300 const base64 = match[2]; 301 const [type, ...rawParams] = props.split(";"); 302 const params = rawParams.reduce((acc, param) => { 303 const [key, value] = param.split("="); 304 return { ...acc, [key]: value }; 305 }, {}); 306 return { ...params, type, base64 }; 307 } 308 /** 309 * Extracts file information from a data url. 310 * @param {String} dataURL The data url. 311 * @return {Object} 312 */ 313 function extractFileInfo(dataURL) { 314 const { name, type, base64 } = parseDataURL(dataURL); 315 const binary = atob(base64); 316 const array = []; 317 for (let i = 0; i < binary.length; i++) { 318 array.push(binary.charCodeAt(i)); 319 } 320 const blob = new Blob([new Uint8Array(array)], { type }); 321 return { blob, name }; 322 } 323 /** 324 * Creates a FormData instance from a data url and an existing JSON response 325 * body. 326 * @param {String} dataURL The data url. 327 * @param {Object} body The response body. 328 * @param {Object} [options={}] The options object. 329 * @param {Object} [options.filename] Force attachment file name. 330 * @return {FormData} 331 */ 332 function createFormData(dataURL, body, options = {}) { 333 const { filename = "untitled" } = options; 334 const { blob, name } = extractFileInfo(dataURL); 335 const formData = new FormData(); 336 formData.append("attachment", blob, name || filename); 337 for (const property in body) { 338 if (typeof body[property] !== "undefined") { 339 formData.append(property, JSON.stringify(body[property])); 340 } 341 } 342 return formData; 343 } 344 /** 345 * Clones an object with all its undefined keys removed. 346 * @private 347 */ 348 function cleanUndefinedProperties(obj) { 349 const result = {}; 350 for (const key in obj) { 351 if (typeof obj[key] !== "undefined") { 352 result[key] = obj[key]; 353 } 354 } 355 return result; 356 } 357 /** 358 * Handle common query parameters for Kinto requests. 359 * 360 * @param {String} [path] The endpoint base path. 361 * @param {Array} [options.fields] Fields to limit the 362 * request to. 363 * @param {Object} [options.query={}] Additional query arguments. 364 */ 365 function addEndpointOptions(path, options = {}) { 366 const query = { ...options.query }; 367 if (options.fields) { 368 query._fields = options.fields; 369 } 370 const queryString = qsify(query); 371 if (queryString) { 372 return path + "?" + queryString; 373 } 374 return path; 375 } 376 /** 377 * Replace authorization header with an obscured version 378 */ 379 function obscureAuthorizationHeader(headers) { 380 const h = new Headers(headers); 381 if (h.has("authorization")) { 382 h.set("authorization", "**** (suppressed)"); 383 } 384 const obscuredHeaders = {}; 385 for (const [header, value] of h.entries()) { 386 obscuredHeaders[header] = value; 387 } 388 return obscuredHeaders; 389 } 390 391 /** 392 * Kinto server error code descriptors. 393 */ 394 const ERROR_CODES = { 395 104: "Missing Authorization Token", 396 105: "Invalid Authorization Token", 397 106: "Request body was not valid JSON", 398 107: "Invalid request parameter", 399 108: "Missing request parameter", 400 109: "Invalid posted data", 401 110: "Invalid Token / id", 402 111: "Missing Token / id", 403 112: "Content-Length header was not provided", 404 113: "Request body too large", 405 114: "Resource was created, updated or deleted meanwhile", 406 115: "Method not allowed on this end point (hint: server may be readonly)", 407 116: "Requested version not available on this server", 408 117: "Client has sent too many requests", 409 121: "Resource access is forbidden for this user", 410 122: "Another resource violates constraint", 411 201: "Service Temporary unavailable due to high load", 412 202: "Service deprecated", 413 999: "Internal Server Error", 414 }; 415 class NetworkTimeoutError extends Error { 416 constructor(url, options) { 417 super( 418 `Timeout while trying to access ${url} with ${JSON.stringify(options)}` 419 ); 420 if (Error.captureStackTrace) { 421 Error.captureStackTrace(this, NetworkTimeoutError); 422 } 423 this.url = url; 424 this.options = options; 425 } 426 } 427 class UnparseableResponseError extends Error { 428 constructor(response, body, error) { 429 const { status } = response; 430 super( 431 `Response from server unparseable (HTTP ${status || 0}; ${error}): ${body}` 432 ); 433 if (Error.captureStackTrace) { 434 Error.captureStackTrace(this, UnparseableResponseError); 435 } 436 this.status = status; 437 this.response = response; 438 this.stack = error.stack; 439 this.error = error; 440 } 441 } 442 /** 443 * "Error" subclass representing a >=400 response from the server. 444 * 445 * Whether or not this is an error depends on your application. 446 * 447 * The `json` field can be undefined if the server responded with an 448 * empty response body. This shouldn't generally happen. Most "bad" 449 * responses come with a JSON error description, or (if they're 450 * fronted by a CDN or nginx or something) occasionally non-JSON 451 * responses (which become UnparseableResponseErrors, above). 452 */ 453 class ServerResponse extends Error { 454 constructor(response, json) { 455 const { status } = response; 456 let { statusText } = response; 457 let errnoMsg; 458 if (json) { 459 // Try to fill in information from the JSON error. 460 statusText = json.error || statusText; 461 // Take errnoMsg from either ERROR_CODES or json.message. 462 if (json.errno && json.errno in ERROR_CODES) { 463 errnoMsg = ERROR_CODES[json.errno]; 464 } else if (json.message) { 465 errnoMsg = json.message; 466 } 467 // If we had both ERROR_CODES and json.message, and they differ, 468 // combine them. 469 if (errnoMsg && json.message && json.message !== errnoMsg) { 470 errnoMsg += ` (${json.message})`; 471 } 472 } 473 let message = `HTTP ${status} ${statusText}`; 474 if (errnoMsg) { 475 message += `: ${errnoMsg}`; 476 } 477 super(message.trim()); 478 if (Error.captureStackTrace) { 479 Error.captureStackTrace(this, ServerResponse); 480 } 481 this.response = response; 482 this.data = json; 483 } 484 } 485 486 var errors = /*#__PURE__*/ Object.freeze({ 487 __proto__: null, 488 NetworkTimeoutError, 489 ServerResponse, 490 UnparseableResponseError, 491 default: ERROR_CODES, 492 }); 493 494 /** 495 * Enhanced HTTP client for the Kinto protocol. 496 * @private 497 */ 498 class HTTP { 499 /** 500 * Default HTTP request headers applied to each outgoing request. 501 * 502 * @type {Object} 503 */ 504 static get DEFAULT_REQUEST_HEADERS() { 505 return { 506 Accept: "application/json", 507 "Content-Type": "application/json", 508 }; 509 } 510 /** 511 * Default options. 512 * 513 * @type {Object} 514 */ 515 static get defaultOptions() { 516 return { timeout: null, requestMode: "cors" }; 517 } 518 /** 519 * Constructor. 520 * 521 * @param {EventEmitter} events The event handler. 522 * @param {Object} [options={}} The options object. 523 * @param {Number} [options.timeout=null] The request timeout in ms, if any (default: `null`). 524 * @param {String} [options.requestMode="cors"] The HTTP request mode (default: `"cors"`). 525 */ 526 constructor(events, options = {}) { 527 // public properties 528 /** 529 * The event emitter instance. 530 * @type {EventEmitter} 531 */ 532 this.events = events; 533 /** 534 * The request mode. 535 * @see https://fetch.spec.whatwg.org/#requestmode 536 * @type {String} 537 */ 538 this.requestMode = options.requestMode || HTTP.defaultOptions.requestMode; 539 /** 540 * The request timeout. 541 * @type {Number} 542 */ 543 this.timeout = options.timeout || HTTP.defaultOptions.timeout; 544 /** 545 * The fetch() function. 546 * @type {Function} 547 */ 548 this.fetchFunc = options.fetchFunc || globalThis.fetch.bind(globalThis); 549 } 550 /** 551 * @private 552 */ 553 timedFetch(url, options) { 554 let hasTimedout = false; 555 return new Promise((resolve, reject) => { 556 // Detect if a request has timed out. 557 let _timeoutId; 558 if (this.timeout) { 559 _timeoutId = setTimeout(() => { 560 hasTimedout = true; 561 if (options && options.headers) { 562 options = { 563 ...options, 564 headers: obscureAuthorizationHeader(options.headers), 565 }; 566 } 567 reject(new NetworkTimeoutError(url, options)); 568 }, this.timeout); 569 } 570 function proceedWithHandler(fn) { 571 return (arg) => { 572 if (!hasTimedout) { 573 if (_timeoutId) { 574 clearTimeout(_timeoutId); 575 } 576 fn(arg); 577 } 578 }; 579 } 580 this.fetchFunc(url, options) 581 .then(proceedWithHandler(resolve)) 582 .catch(proceedWithHandler(reject)); 583 }); 584 } 585 /** 586 * @private 587 */ 588 async processResponse(response) { 589 const { status, headers } = response; 590 const text = await response.text(); 591 // Check if we have a body; if so parse it as JSON. 592 let json; 593 if (text.length !== 0) { 594 try { 595 json = JSON.parse(text); 596 } catch (err) { 597 throw new UnparseableResponseError(response, text, err); 598 } 599 } 600 if (status >= 400) { 601 throw new ServerResponse(response, json); 602 } 603 return { status, json: json, headers }; 604 } 605 /** 606 * @private 607 */ 608 async retry(url, retryAfter, request, options) { 609 await delay(retryAfter); 610 return this.request(url, request, { 611 ...options, 612 retry: options.retry - 1, 613 }); 614 } 615 /** 616 * Performs an HTTP request to the Kinto server. 617 * 618 * Resolves with an objet containing the following HTTP response properties: 619 * - `{Number} status` The HTTP status code. 620 * - `{Object} json` The JSON response body. 621 * - `{Headers} headers` The response headers object; see the ES6 fetch() spec. 622 * 623 * @param {String} url The URL. 624 * @param {Object} [request={}] The request object, passed to 625 * fetch() as its options object. 626 * @param {Object} [request.headers] The request headers object (default: {}) 627 * @param {Object} [options={}] Options for making the 628 * request 629 * @param {Number} [options.retry] Number of retries (default: 0) 630 * @return {Promise} 631 */ 632 async request(url, request = { headers: {} }, options = { retry: 0 }) { 633 // Ensure default request headers are always set 634 request.headers = { ...HTTP.DEFAULT_REQUEST_HEADERS, ...request.headers }; 635 // If a multipart body is provided, remove any custom Content-Type header as 636 // the fetch() implementation will add the correct one for us. 637 if (request.body && request.body instanceof FormData) { 638 if (request.headers instanceof Headers) { 639 request.headers.delete("Content-Type"); 640 } else if (!Array.isArray(request.headers)) { 641 delete request.headers["Content-Type"]; 642 } 643 } 644 request.mode = this.requestMode; 645 const response = await this.timedFetch(url, request); 646 const { headers } = response; 647 this._checkForDeprecationHeader(headers); 648 this._checkForBackoffHeader(headers); 649 // Check if the server summons the client to retry after a while. 650 const retryAfter = this._checkForRetryAfterHeader(headers); 651 // If number of allowed of retries is not exhausted, retry the same request. 652 if (retryAfter && options.retry > 0) { 653 return this.retry(url, retryAfter, request, options); 654 } 655 return this.processResponse(response); 656 } 657 _checkForDeprecationHeader(headers) { 658 const alertHeader = headers.get("Alert"); 659 if (!alertHeader) { 660 return; 661 } 662 let alert; 663 try { 664 alert = JSON.parse(alertHeader); 665 } catch (err) { 666 console.warn("Unable to parse Alert header message", alertHeader); 667 return; 668 } 669 console.warn(alert.message, alert.url); 670 if (this.events) { 671 this.events.emit("deprecated", alert); 672 } 673 } 674 _checkForBackoffHeader(headers) { 675 let backoffMs; 676 const backoffHeader = headers.get("Backoff"); 677 const backoffSeconds = backoffHeader ? parseInt(backoffHeader, 10) : 0; 678 if (backoffSeconds > 0) { 679 backoffMs = new Date().getTime() + backoffSeconds * 1000; 680 } else { 681 backoffMs = 0; 682 } 683 if (this.events) { 684 this.events.emit("backoff", backoffMs); 685 } 686 } 687 _checkForRetryAfterHeader(headers) { 688 const retryAfter = headers.get("Retry-After"); 689 if (!retryAfter) { 690 return null; 691 } 692 const delay = parseInt(retryAfter, 10) * 1000; 693 const tryAgainAfter = new Date().getTime() + delay; 694 if (this.events) { 695 this.events.emit("retry-after", tryAgainAfter); 696 } 697 return delay; 698 } 699 } 700 701 /** 702 * Endpoints templates. 703 * @type {Object} 704 */ 705 const ENDPOINTS = { 706 root: () => "/", 707 batch: () => "/batch", 708 permissions: () => "/permissions", 709 bucket: (bucket) => "/buckets" + (bucket ? `/${bucket}` : ""), 710 history: (bucket) => `${ENDPOINTS.bucket(bucket)}/history`, 711 collection: (bucket, coll) => 712 `${ENDPOINTS.bucket(bucket)}/collections` + (coll ? `/${coll}` : ""), 713 group: (bucket, group) => 714 `${ENDPOINTS.bucket(bucket)}/groups` + (group ? `/${group}` : ""), 715 record: (bucket, coll, id) => 716 `${ENDPOINTS.collection(bucket, coll)}/records` + (id ? `/${id}` : ""), 717 attachment: (bucket, coll, id) => 718 `${ENDPOINTS.record(bucket, coll, id)}/attachment`, 719 }; 720 721 const requestDefaults = { 722 safe: false, 723 // check if we should set default content type here 724 headers: {}, 725 patch: false, 726 }; 727 /** 728 * @private 729 */ 730 function safeHeader(safe, last_modified) { 731 if (!safe) { 732 return {}; 733 } 734 if (last_modified) { 735 return { "If-Match": `"${last_modified}"` }; 736 } 737 return { "If-None-Match": "*" }; 738 } 739 /** 740 * @private 741 */ 742 function createRequest(path, { data, permissions }, options = {}) { 743 const { headers, safe } = { 744 ...requestDefaults, 745 ...options, 746 }; 747 const method = options.method || (data && data.id) ? "PUT" : "POST"; 748 return { 749 method, 750 path, 751 headers: { ...headers, ...safeHeader(safe) }, 752 body: { data, permissions }, 753 }; 754 } 755 /** 756 * @private 757 */ 758 function updateRequest(path, { data, permissions }, options = {}) { 759 const { headers, safe, patch } = { ...requestDefaults, ...options }; 760 const { last_modified } = { ...data, ...options }; 761 const hasNoData = 762 data && 763 Object.keys(data).filter((k) => k !== "id" && k !== "last_modified") 764 .length === 0; 765 if (hasNoData) { 766 data = undefined; 767 } 768 return { 769 method: patch ? "PATCH" : "PUT", 770 path, 771 headers: { ...headers, ...safeHeader(safe, last_modified) }, 772 body: { data, permissions }, 773 }; 774 } 775 /** 776 * @private 777 */ 778 function jsonPatchPermissionsRequest(path, permissions, opType, options = {}) { 779 const { headers, safe, last_modified } = { ...requestDefaults, ...options }; 780 const ops = []; 781 for (const [type, principals] of Object.entries(permissions)) { 782 if (principals) { 783 for (const principal of principals) { 784 ops.push({ 785 op: opType, 786 path: `/permissions/${type}/${principal}`, 787 }); 788 } 789 } 790 } 791 return { 792 method: "PATCH", 793 path, 794 headers: { 795 ...headers, 796 ...safeHeader(safe, last_modified), 797 "Content-Type": "application/json-patch+json", 798 }, 799 body: ops, 800 }; 801 } 802 /** 803 * @private 804 */ 805 function deleteRequest(path, options = {}) { 806 const { headers, safe, last_modified } = { 807 ...requestDefaults, 808 ...options, 809 }; 810 if (safe && !last_modified) { 811 throw new Error("Safe concurrency check requires a last_modified value."); 812 } 813 return { 814 method: "DELETE", 815 path, 816 headers: { ...headers, ...safeHeader(safe, last_modified) }, 817 }; 818 } 819 /** 820 * @private 821 */ 822 function addAttachmentRequest( 823 path, 824 dataURI, 825 { data, permissions } = {}, 826 options = {} 827 ) { 828 const { headers, safe } = { ...requestDefaults, ...options }; 829 const { last_modified } = { ...data, ...options }; 830 const body = { data, permissions }; 831 const formData = createFormData(dataURI, body, options); 832 return { 833 method: "POST", 834 path, 835 headers: { ...headers, ...safeHeader(safe, last_modified) }, 836 body: formData, 837 }; 838 } 839 840 /** 841 * Exports batch responses as a result object. 842 * 843 * @private 844 * @param {Array} responses The batch subrequest responses. 845 * @param {Array} requests The initial issued requests. 846 * @return {Object} 847 */ 848 function aggregate(responses = [], requests = []) { 849 if (responses.length !== requests.length) { 850 throw new Error("Responses length should match requests one."); 851 } 852 const results = { 853 errors: [], 854 published: [], 855 conflicts: [], 856 skipped: [], 857 }; 858 return responses.reduce((acc, response, index) => { 859 const { status } = response; 860 const request = requests[index]; 861 if (status >= 200 && status < 400) { 862 acc.published.push(response.body); 863 } else if (status === 404) { 864 // Extract the id manually from request path while waiting for Kinto/kinto#818 865 const regex = /(buckets|groups|collections|records)\/([^/]+)$/; 866 const extracts = request.path.match(regex); 867 const id = extracts && extracts.length === 3 ? extracts[2] : undefined; 868 acc.skipped.push({ 869 id, 870 path: request.path, 871 error: response.body, 872 }); 873 } else if (status === 412) { 874 acc.conflicts.push({ 875 // XXX: specifying the type is probably superfluous 876 type: "outgoing", 877 local: request.body, 878 remote: 879 (response.body.details && response.body.details.existing) || null, 880 }); 881 } else { 882 acc.errors.push({ 883 path: request.path, 884 sent: request, 885 error: response.body, 886 }); 887 } 888 return acc; 889 }, results); 890 } 891 892 const byteToHex = []; 893 for (let i = 0; i < 256; ++i) { 894 byteToHex.push((i + 0x100).toString(16).slice(1)); 895 } 896 function unsafeStringify(arr, offset = 0) { 897 return ( 898 byteToHex[arr[offset + 0]] + 899 byteToHex[arr[offset + 1]] + 900 byteToHex[arr[offset + 2]] + 901 byteToHex[arr[offset + 3]] + 902 "-" + 903 byteToHex[arr[offset + 4]] + 904 byteToHex[arr[offset + 5]] + 905 "-" + 906 byteToHex[arr[offset + 6]] + 907 byteToHex[arr[offset + 7]] + 908 "-" + 909 byteToHex[arr[offset + 8]] + 910 byteToHex[arr[offset + 9]] + 911 "-" + 912 byteToHex[arr[offset + 10]] + 913 byteToHex[arr[offset + 11]] + 914 byteToHex[arr[offset + 12]] + 915 byteToHex[arr[offset + 13]] + 916 byteToHex[arr[offset + 14]] + 917 byteToHex[arr[offset + 15]] 918 ).toLowerCase(); 919 } 920 921 let getRandomValues; 922 const rnds8 = new Uint8Array(16); 923 function rng() { 924 if (!getRandomValues) { 925 if (typeof crypto === "undefined" || !crypto.getRandomValues) { 926 throw new Error( 927 "crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported" 928 ); 929 } 930 getRandomValues = crypto.getRandomValues.bind(crypto); 931 } 932 return getRandomValues(rnds8); 933 } 934 935 const randomUUID = 936 typeof crypto !== "undefined" && 937 crypto.randomUUID && 938 crypto.randomUUID.bind(crypto); 939 var native = { randomUUID }; 940 941 function _v4(options, buf, offset) { 942 options = options || {}; 943 const rnds = options.random ?? options.rng?.() ?? rng(); 944 if (rnds.length < 16) { 945 throw new Error("Random bytes length must be >= 16"); 946 } 947 rnds[6] = (rnds[6] & 0x0f) | 0x40; 948 rnds[8] = (rnds[8] & 0x3f) | 0x80; 949 return unsafeStringify(rnds); 950 } 951 function v4(options, buf, offset) { 952 if (native.randomUUID && true && !options) { 953 return native.randomUUID(); 954 } 955 return _v4(options); 956 } 957 958 /** 959 * Abstract representation of a selected collection. 960 * 961 */ 962 class Collection { 963 /** 964 * Constructor. 965 * 966 * @param {KintoClient} client The client instance. 967 * @param {Bucket} bucket The bucket instance. 968 * @param {String} name The collection name. 969 * @param {Object} [options={}] The options object. 970 * @param {Object} [options.headers] The headers object option. 971 * @param {Boolean} [options.safe] The safe option. 972 * @param {Number} [options.retry] The retry option. 973 * @param {Boolean} [options.batch] (Private) Whether this 974 * Collection is operating as part of a batch. 975 */ 976 constructor(client, bucket, name, options = {}) { 977 /** 978 * @ignore 979 */ 980 this.client = client; 981 /** 982 * @ignore 983 */ 984 this.bucket = bucket; 985 /** 986 * The collection name. 987 * @type {String} 988 */ 989 this.name = name; 990 this._endpoints = client.endpoints; 991 /** 992 * @ignore 993 */ 994 this._retry = options.retry || 0; 995 this._safe = !!options.safe; 996 // FIXME: This is kind of ugly; shouldn't the bucket be responsible 997 // for doing the merge? 998 this._headers = { 999 ...this.bucket.headers, 1000 ...options.headers, 1001 }; 1002 } 1003 get execute() { 1004 return this.client.execute.bind(this.client); 1005 } 1006 /** 1007 * Get the value of "headers" for a given request, merging the 1008 * per-request headers with our own "default" headers. 1009 * 1010 * @private 1011 */ 1012 _getHeaders(options) { 1013 return { 1014 ...this._headers, 1015 ...options.headers, 1016 }; 1017 } 1018 /** 1019 * Get the value of "safe" for a given request, using the 1020 * per-request option if present or falling back to our default 1021 * otherwise. 1022 * 1023 * @private 1024 * @param {Object} options The options for a request. 1025 * @returns {Boolean} 1026 */ 1027 _getSafe(options) { 1028 return { safe: this._safe, ...options }.safe; 1029 } 1030 /** 1031 * As _getSafe, but for "retry". 1032 * 1033 * @private 1034 */ 1035 _getRetry(options) { 1036 return { retry: this._retry, ...options }.retry; 1037 } 1038 /** 1039 * Retrieves the total number of records in this collection. 1040 * 1041 * @param {Object} [options={}] The options object. 1042 * @param {Object} [options.headers] The headers object option. 1043 * @param {Number} [options.retry=0] Number of retries to make 1044 * when faced with transient errors. 1045 * @return {Promise<Number, Error>} 1046 */ 1047 async getTotalRecords(options = {}) { 1048 const path = this._endpoints.record(this.bucket.name, this.name); 1049 const request = { 1050 headers: this._getHeaders(options), 1051 path, 1052 method: "HEAD", 1053 }; 1054 const { headers } = await this.client.execute(request, { 1055 raw: true, 1056 retry: this._getRetry(options), 1057 }); 1058 return parseInt(headers.get("Total-Records"), 10); 1059 } 1060 /** 1061 * Retrieves the ETag of the records list, for use with the `since` filtering option. 1062 * 1063 * @param {Object} [options={}] The options object. 1064 * @param {Object} [options.headers] The headers object option. 1065 * @param {Number} [options.retry=0] Number of retries to make 1066 * when faced with transient errors. 1067 * @return {Promise<String, Error>} 1068 */ 1069 async getRecordsTimestamp(options = {}) { 1070 const path = this._endpoints.record(this.bucket.name, this.name); 1071 const request = { 1072 headers: this._getHeaders(options), 1073 path, 1074 method: "HEAD", 1075 }; 1076 const { headers } = await this.client.execute(request, { 1077 raw: true, 1078 retry: this._getRetry(options), 1079 }); 1080 return headers.get("ETag"); 1081 } 1082 /** 1083 * Retrieves collection data. 1084 * 1085 * @param {Object} [options={}] The options object. 1086 * @param {Object} [options.headers] The headers object option. 1087 * @param {Object} [options.query] Query parameters to pass in 1088 * the request. This might be useful for features that aren't 1089 * yet supported by this library. 1090 * @param {Array} [options.fields] Limit response to 1091 * just some fields. 1092 * @param {Number} [options.retry=0] Number of retries to make 1093 * when faced with transient errors. 1094 * @return {Promise<Object, Error>} 1095 */ 1096 async getData(options = {}) { 1097 const path = this._endpoints.collection(this.bucket.name, this.name); 1098 const request = { headers: this._getHeaders(options), path }; 1099 const { data } = await this.client.execute(request, { 1100 retry: this._getRetry(options), 1101 query: options.query, 1102 fields: options.fields, 1103 }); 1104 return data; 1105 } 1106 /** 1107 * Set collection data. 1108 * @param {Object} data The collection data object. 1109 * @param {Object} [options={}] The options object. 1110 * @param {Object} [options.headers] The headers object option. 1111 * @param {Number} [options.retry=0] Number of retries to make 1112 * when faced with transient errors. 1113 * @param {Boolean} [options.safe] The safe option. 1114 * @param {Boolean} [options.patch] The patch option. 1115 * @param {Number} [options.last_modified] The last_modified option. 1116 * @return {Promise<Object, Error>} 1117 */ 1118 async setData(data, options = {}) { 1119 if (!isObject(data)) { 1120 throw new Error("A collection object is required."); 1121 } 1122 const { patch, permissions } = options; 1123 const { last_modified } = { ...data, ...options }; 1124 const path = this._endpoints.collection(this.bucket.name, this.name); 1125 const request = updateRequest( 1126 path, 1127 { data, permissions }, 1128 { 1129 last_modified, 1130 patch, 1131 headers: this._getHeaders(options), 1132 safe: this._getSafe(options), 1133 } 1134 ); 1135 return this.client.execute(request, { 1136 retry: this._getRetry(options), 1137 }); 1138 } 1139 /** 1140 * Retrieves the list of permissions for this collection. 1141 * 1142 * @param {Object} [options={}] The options object. 1143 * @param {Object} [options.headers] The headers object option. 1144 * @param {Number} [options.retry=0] Number of retries to make 1145 * when faced with transient errors. 1146 * @return {Promise<Object, Error>} 1147 */ 1148 async getPermissions(options = {}) { 1149 const path = this._endpoints.collection(this.bucket.name, this.name); 1150 const request = { headers: this._getHeaders(options), path }; 1151 const { permissions } = await this.client.execute(request, { 1152 retry: this._getRetry(options), 1153 }); 1154 return permissions; 1155 } 1156 /** 1157 * Replaces all existing collection permissions with the ones provided. 1158 * 1159 * @param {Object} permissions The permissions object. 1160 * @param {Object} [options={}] The options object 1161 * @param {Object} [options.headers] The headers object option. 1162 * @param {Number} [options.retry=0] Number of retries to make 1163 * when faced with transient errors. 1164 * @param {Boolean} [options.safe] The safe option. 1165 * @param {Number} [options.last_modified] The last_modified option. 1166 * @return {Promise<Object, Error>} 1167 */ 1168 async setPermissions(permissions, options = {}) { 1169 if (!isObject(permissions)) { 1170 throw new Error("A permissions object is required."); 1171 } 1172 const path = this._endpoints.collection(this.bucket.name, this.name); 1173 const data = { last_modified: options.last_modified }; 1174 const request = updateRequest( 1175 path, 1176 { data, permissions }, 1177 { 1178 headers: this._getHeaders(options), 1179 safe: this._getSafe(options), 1180 } 1181 ); 1182 return this.client.execute(request, { 1183 retry: this._getRetry(options), 1184 }); 1185 } 1186 /** 1187 * Append principals to the collection permissions. 1188 * 1189 * @param {Object} permissions The permissions object. 1190 * @param {Object} [options={}] The options object 1191 * @param {Boolean} [options.safe] The safe option. 1192 * @param {Object} [options.headers] The headers object option. 1193 * @param {Number} [options.retry=0] Number of retries to make 1194 * when faced with transient errors. 1195 * @param {Object} [options.last_modified] The last_modified option. 1196 * @return {Promise<Object, Error>} 1197 */ 1198 async addPermissions(permissions, options = {}) { 1199 if (!isObject(permissions)) { 1200 throw new Error("A permissions object is required."); 1201 } 1202 const path = this._endpoints.collection(this.bucket.name, this.name); 1203 const { last_modified } = options; 1204 const request = jsonPatchPermissionsRequest(path, permissions, "add", { 1205 last_modified, 1206 headers: this._getHeaders(options), 1207 safe: this._getSafe(options), 1208 }); 1209 return this.client.execute(request, { 1210 retry: this._getRetry(options), 1211 }); 1212 } 1213 /** 1214 * Remove principals from the collection permissions. 1215 * 1216 * @param {Object} permissions The permissions object. 1217 * @param {Object} [options={}] The options object 1218 * @param {Boolean} [options.safe] The safe option. 1219 * @param {Object} [options.headers] The headers object option. 1220 * @param {Number} [options.retry=0] Number of retries to make 1221 * when faced with transient errors. 1222 * @param {Object} [options.last_modified] The last_modified option. 1223 * @return {Promise<Object, Error>} 1224 */ 1225 async removePermissions(permissions, options = {}) { 1226 if (!isObject(permissions)) { 1227 throw new Error("A permissions object is required."); 1228 } 1229 const path = this._endpoints.collection(this.bucket.name, this.name); 1230 const { last_modified } = options; 1231 const request = jsonPatchPermissionsRequest(path, permissions, "remove", { 1232 last_modified, 1233 headers: this._getHeaders(options), 1234 safe: this._getSafe(options), 1235 }); 1236 return this.client.execute(request, { 1237 retry: this._getRetry(options), 1238 }); 1239 } 1240 /** 1241 * Creates a record in current collection. 1242 * 1243 * @param {Object} record The record to create. 1244 * @param {Object} [options={}] The options object. 1245 * @param {Object} [options.headers] The headers object option. 1246 * @param {Number} [options.retry=0] Number of retries to make 1247 * when faced with transient errors. 1248 * @param {Boolean} [options.safe] The safe option. 1249 * @param {Object} [options.permissions] The permissions option. 1250 * @return {Promise<Object, Error>} 1251 */ 1252 async createRecord(record, options = {}) { 1253 const { permissions } = options; 1254 const path = this._endpoints.record(this.bucket.name, this.name, record.id); 1255 const request = createRequest( 1256 path, 1257 { data: record, permissions }, 1258 { 1259 headers: this._getHeaders(options), 1260 safe: this._getSafe(options), 1261 } 1262 ); 1263 return this.client.execute(request, { 1264 retry: this._getRetry(options), 1265 }); 1266 } 1267 /** 1268 * Adds an attachment to a record, creating the record when it doesn't exist. 1269 * 1270 * @param {String} dataURL The data url. 1271 * @param {Object} [record={}] The record data. 1272 * @param {Object} [options={}] The options object. 1273 * @param {Object} [options.headers] The headers object option. 1274 * @param {Number} [options.retry=0] Number of retries to make 1275 * when faced with transient errors. 1276 * @param {Boolean} [options.safe] The safe option. 1277 * @param {Number} [options.last_modified] The last_modified option. 1278 * @param {Object} [options.permissions] The permissions option. 1279 * @param {String} [options.filename] Force the attachment filename. 1280 * @return {Promise<Object, Error>} 1281 */ 1282 async addAttachment(dataURI, record = {}, options = {}) { 1283 const { permissions } = options; 1284 const id = record.id || v4(); 1285 const path = this._endpoints.attachment(this.bucket.name, this.name, id); 1286 const { last_modified } = { ...record, ...options }; 1287 const addAttachmentRequest$1 = addAttachmentRequest( 1288 path, 1289 dataURI, 1290 { data: record, permissions }, 1291 { 1292 last_modified, 1293 filename: options.filename, 1294 headers: this._getHeaders(options), 1295 safe: this._getSafe(options), 1296 } 1297 ); 1298 await this.client.execute(addAttachmentRequest$1, { 1299 stringify: false, 1300 retry: this._getRetry(options), 1301 }); 1302 return this.getRecord(id); 1303 } 1304 /** 1305 * Removes an attachment from a given record. 1306 * 1307 * @param {Object} recordId The record id. 1308 * @param {Object} [options={}] The options object. 1309 * @param {Object} [options.headers] The headers object option. 1310 * @param {Number} [options.retry=0] Number of retries to make 1311 * when faced with transient errors. 1312 * @param {Boolean} [options.safe] The safe option. 1313 * @param {Number} [options.last_modified] The last_modified option. 1314 */ 1315 async removeAttachment(recordId, options = {}) { 1316 const { last_modified } = options; 1317 const path = this._endpoints.attachment( 1318 this.bucket.name, 1319 this.name, 1320 recordId 1321 ); 1322 const request = deleteRequest(path, { 1323 last_modified, 1324 headers: this._getHeaders(options), 1325 safe: this._getSafe(options), 1326 }); 1327 return this.client.execute(request, { 1328 retry: this._getRetry(options), 1329 }); 1330 } 1331 /** 1332 * Updates a record in current collection. 1333 * 1334 * @param {Object} record The record to update. 1335 * @param {Object} [options={}] The options object. 1336 * @param {Object} [options.headers] The headers object option. 1337 * @param {Number} [options.retry=0] Number of retries to make 1338 * when faced with transient errors. 1339 * @param {Boolean} [options.safe] The safe option. 1340 * @param {Number} [options.last_modified] The last_modified option. 1341 * @param {Object} [options.permissions] The permissions option. 1342 * @return {Promise<Object, Error>} 1343 */ 1344 async updateRecord(record, options = {}) { 1345 if (!isObject(record)) { 1346 throw new Error("A record object is required."); 1347 } 1348 if (!record.id) { 1349 throw new Error("A record id is required."); 1350 } 1351 const { permissions } = options; 1352 const { last_modified } = { ...record, ...options }; 1353 const path = this._endpoints.record(this.bucket.name, this.name, record.id); 1354 const request = updateRequest( 1355 path, 1356 { data: record, permissions }, 1357 { 1358 headers: this._getHeaders(options), 1359 safe: this._getSafe(options), 1360 last_modified, 1361 patch: !!options.patch, 1362 } 1363 ); 1364 return this.client.execute(request, { 1365 retry: this._getRetry(options), 1366 }); 1367 } 1368 /** 1369 * Deletes a record from the current collection. 1370 * 1371 * @param {Object|String} record The record to delete. 1372 * @param {Object} [options={}] The options object. 1373 * @param {Object} [options.headers] The headers object option. 1374 * @param {Number} [options.retry=0] Number of retries to make 1375 * when faced with transient errors. 1376 * @param {Boolean} [options.safe] The safe option. 1377 * @param {Number} [options.last_modified] The last_modified option. 1378 * @return {Promise<Object, Error>} 1379 */ 1380 async deleteRecord(record, options = {}) { 1381 const recordObj = toDataBody(record); 1382 if (!recordObj.id) { 1383 throw new Error("A record id is required."); 1384 } 1385 const { id } = recordObj; 1386 const { last_modified } = { ...recordObj, ...options }; 1387 const path = this._endpoints.record(this.bucket.name, this.name, id); 1388 const request = deleteRequest(path, { 1389 last_modified, 1390 headers: this._getHeaders(options), 1391 safe: this._getSafe(options), 1392 }); 1393 return this.client.execute(request, { 1394 retry: this._getRetry(options), 1395 }); 1396 } 1397 /** 1398 * Deletes records from the current collection. 1399 * 1400 * Sorting is done by passing a `sort` string option: 1401 * 1402 * - The field to order the results by, prefixed with `-` for descending. 1403 * Default: `-last_modified`. 1404 * 1405 * @see http://kinto.readthedocs.io/en/stable/api/1.x/sorting.html 1406 * 1407 * Filtering is done by passing a `filters` option object: 1408 * 1409 * - `{fieldname: "value"}` 1410 * - `{min_fieldname: 4000}` 1411 * - `{in_fieldname: "1,2,3"}` 1412 * - `{not_fieldname: 0}` 1413 * - `{exclude_fieldname: "0,1"}` 1414 * 1415 * @see http://kinto.readthedocs.io/en/stable/api/1.x/filtering.html 1416 * 1417 * @param {Object} [options={}] The options object. 1418 * @param {Object} [options.headers] The headers object option. 1419 * @param {Number} [options.retry=0] Number of retries to make 1420 * when faced with transient errors. 1421 * @param {Object} [options.filters={}] The filters object. 1422 * @param {String} [options.sort="-last_modified"] The sort field. 1423 * @param {String} [options.at] The timestamp to get a snapshot at. 1424 * @param {String} [options.limit=null] The limit field. 1425 * @param {String} [options.pages=1] The number of result pages to aggregate. 1426 * @param {Number} [options.since=null] Only retrieve records modified since the provided timestamp. 1427 * @param {Array} [options.fields] Limit response to just some fields. 1428 * @return {Promise<Object, Error>} 1429 */ 1430 async deleteRecords(options = {}) { 1431 const path = this._endpoints.record(this.bucket.name, this.name); 1432 return this.client.paginatedDelete(path, options, { 1433 headers: this._getHeaders(options), 1434 retry: this._getRetry(options), 1435 }); 1436 } 1437 /** 1438 * Retrieves a record from the current collection. 1439 * 1440 * @param {String} id The record id to retrieve. 1441 * @param {Object} [options={}] The options object. 1442 * @param {Object} [options.headers] The headers object option. 1443 * @param {Object} [options.query] Query parameters to pass in 1444 * the request. This might be useful for features that aren't 1445 * yet supported by this library. 1446 * @param {Array} [options.fields] Limit response to 1447 * just some fields. 1448 * @param {Number} [options.retry=0] Number of retries to make 1449 * when faced with transient errors. 1450 * @return {Promise<Object, Error>} 1451 */ 1452 async getRecord(id, options = {}) { 1453 const path = this._endpoints.record(this.bucket.name, this.name, id); 1454 const request = { headers: this._getHeaders(options), path }; 1455 return this.client.execute(request, { 1456 retry: this._getRetry(options), 1457 query: options.query, 1458 fields: options.fields, 1459 }); 1460 } 1461 /** 1462 * Lists records from the current collection. 1463 * 1464 * Sorting is done by passing a `sort` string option: 1465 * 1466 * - The field to order the results by, prefixed with `-` for descending. 1467 * Default: `-last_modified`. 1468 * 1469 * @see http://kinto.readthedocs.io/en/stable/api/1.x/sorting.html 1470 * 1471 * Filtering is done by passing a `filters` option object: 1472 * 1473 * - `{fieldname: "value"}` 1474 * - `{min_fieldname: 4000}` 1475 * - `{in_fieldname: "1,2,3"}` 1476 * - `{not_fieldname: 0}` 1477 * - `{exclude_fieldname: "0,1"}` 1478 * 1479 * @see http://kinto.readthedocs.io/en/stable/api/1.x/filtering.html 1480 * 1481 * Paginating is done by passing a `limit` option, then calling the `next()` 1482 * method from the resolved result object to fetch the next page, if any. 1483 * 1484 * @param {Object} [options={}] The options object. 1485 * @param {Object} [options.headers] The headers object option. 1486 * @param {Number} [options.retry=0] Number of retries to make 1487 * when faced with transient errors. 1488 * @param {Object} [options.filters={}] The filters object. 1489 * @param {String} [options.sort="-last_modified"] The sort field. 1490 * @param {String} [options.at] The timestamp to get a snapshot at. 1491 * @param {String} [options.limit=null] The limit field. 1492 * @param {String} [options.pages=1] The number of result pages to aggregate. 1493 * @param {Number} [options.since=null] Only retrieve records modified since the provided timestamp. 1494 * @param {Array} [options.fields] Limit response to just some fields. 1495 * @return {Promise<Object, Error>} 1496 */ 1497 async listRecords(options = {}) { 1498 const path = this._endpoints.record(this.bucket.name, this.name); 1499 if (options.at) { 1500 return this.getSnapshot(options.at); 1501 } 1502 return this.client.paginatedList(path, options, { 1503 headers: this._getHeaders(options), 1504 retry: this._getRetry(options), 1505 }); 1506 } 1507 /** 1508 * @private 1509 */ 1510 async isHistoryComplete() { 1511 // We consider that if we have the collection creation event part of the 1512 // history, then all records change events have been tracked. 1513 const { 1514 data: [oldestHistoryEntry], 1515 } = await this.bucket.listHistory({ 1516 limit: 1, 1517 filters: { 1518 action: "create", 1519 resource_name: "collection", 1520 collection_id: this.name, 1521 }, 1522 }); 1523 return !!oldestHistoryEntry; 1524 } 1525 /** 1526 * @private 1527 */ 1528 async getSnapshot(at) { 1529 if (!at || !Number.isInteger(at) || at <= 0) { 1530 throw new Error("Invalid argument, expected a positive integer."); 1531 } 1532 // Retrieve history and check it covers the required time range. 1533 // Ensure we have enough history data to retrieve the complete list of 1534 // changes. 1535 if (!(await this.isHistoryComplete())) { 1536 throw new Error( 1537 "Computing a snapshot is only possible when the full history for a " + 1538 "collection is available. Here, the history plugin seems to have " + 1539 "been enabled after the creation of the collection." 1540 ); 1541 } 1542 // Because of https://github.com/Kinto/kinto-http.js/issues/963 1543 // we cannot simply rely on the history endpoint. 1544 // Our strategy here is to clean-up the history entries from the 1545 // records that were deleted via the plural endpoint. 1546 // We will detect them by comparing the current state of the collection 1547 // and the full history of the collection since its genesis. 1548 // List full history of collection. 1549 const { data: fullHistory } = await this.bucket.listHistory({ 1550 pages: Infinity, // all pages up to target timestamp are required 1551 sort: "last_modified", // chronological order 1552 filters: { 1553 resource_name: "record", 1554 collection_id: this.name, 1555 }, 1556 }); 1557 // Keep latest entry ever, and latest within snapshot window. 1558 // (history is sorted chronologically) 1559 const latestEver = new Map(); 1560 const latestInSnapshot = new Map(); 1561 for (const entry of fullHistory) { 1562 if (entry.target.data.last_modified <= at) { 1563 // Snapshot includes changes right on timestamp. 1564 latestInSnapshot.set(entry.record_id, entry); 1565 } 1566 latestEver.set(entry.record_id, entry); 1567 } 1568 // Current records ids in the collection. 1569 const { data: current } = await this.listRecords({ 1570 pages: Infinity, 1571 fields: ["id"], // we don't need attributes. 1572 }); 1573 const currentIds = new Set(current.map((record) => record.id)); 1574 // If a record is not in the current collection, and its 1575 // latest history entry isn't a delete then this means that 1576 // it was deleted via the plural endpoint (and that we lost track 1577 // of this deletion because of bug #963) 1578 const deletedViaPlural = new Set(); 1579 for (const entry of latestEver.values()) { 1580 if (entry.action != "delete" && !currentIds.has(entry.record_id)) { 1581 deletedViaPlural.add(entry.record_id); 1582 } 1583 } 1584 // Now reconstruct the collection based on latest version in snapshot 1585 // filtering all deleted records. 1586 const reconstructed = []; 1587 for (const entry of latestInSnapshot.values()) { 1588 if (entry.action != "delete" && !deletedViaPlural.has(entry.record_id)) { 1589 reconstructed.push(entry.target.data); 1590 } 1591 } 1592 return { 1593 last_modified: String(at), 1594 data: Array.from(reconstructed).sort( 1595 (a, b) => b.last_modified - a.last_modified 1596 ), 1597 next: () => { 1598 throw new Error("Snapshots don't support pagination"); 1599 }, 1600 hasNextPage: false, 1601 totalRecords: reconstructed.length, 1602 }; 1603 } 1604 /** 1605 * Performs batch operations at the current collection level. 1606 * 1607 * @param {Function} fn The batch operation function. 1608 * @param {Object} [options={}] The options object. 1609 * @param {Object} [options.headers] The headers object option. 1610 * @param {Boolean} [options.safe] The safe option. 1611 * @param {Number} [options.retry] The retry option. 1612 * @param {Boolean} [options.aggregate] Produces a grouped result object. 1613 * @return {Promise<Object, Error>} 1614 */ 1615 async batch(fn, options = {}) { 1616 return this.client.batch(fn, { 1617 bucket: this.bucket.name, 1618 collection: this.name, 1619 headers: this._getHeaders(options), 1620 retry: this._getRetry(options), 1621 safe: this._getSafe(options), 1622 aggregate: !!options.aggregate, 1623 }); 1624 } 1625 } 1626 __decorate( 1627 [capable(["attachments"])], 1628 Collection.prototype, 1629 "addAttachment", 1630 null 1631 ); 1632 __decorate( 1633 [capable(["attachments"])], 1634 Collection.prototype, 1635 "removeAttachment", 1636 null 1637 ); 1638 __decorate([capable(["history"])], Collection.prototype, "getSnapshot", null); 1639 1640 /** 1641 * Abstract representation of a selected bucket. 1642 * 1643 */ 1644 class Bucket { 1645 /** 1646 * Constructor. 1647 * 1648 * @param {KintoClient} client The client instance. 1649 * @param {String} name The bucket name. 1650 * @param {Object} [options={}] The headers object option. 1651 * @param {Object} [options.headers] The headers object option. 1652 * @param {Boolean} [options.safe] The safe option. 1653 * @param {Number} [options.retry] The retry option. 1654 */ 1655 constructor(client, name, options = {}) { 1656 /** 1657 * @ignore 1658 */ 1659 this.client = client; 1660 /** 1661 * The bucket name. 1662 * @type {String} 1663 */ 1664 this.name = name; 1665 this._endpoints = client.endpoints; 1666 /** 1667 * @ignore 1668 */ 1669 this._headers = options.headers || {}; 1670 this._retry = options.retry || 0; 1671 this._safe = !!options.safe; 1672 } 1673 get execute() { 1674 return this.client.execute.bind(this.client); 1675 } 1676 get headers() { 1677 return this._headers; 1678 } 1679 /** 1680 * Get the value of "headers" for a given request, merging the 1681 * per-request headers with our own "default" headers. 1682 * 1683 * @private 1684 */ 1685 _getHeaders(options) { 1686 return { 1687 ...this._headers, 1688 ...options.headers, 1689 }; 1690 } 1691 /** 1692 * Get the value of "safe" for a given request, using the 1693 * per-request option if present or falling back to our default 1694 * otherwise. 1695 * 1696 * @private 1697 * @param {Object} options The options for a request. 1698 * @returns {Boolean} 1699 */ 1700 _getSafe(options) { 1701 return { safe: this._safe, ...options }.safe; 1702 } 1703 /** 1704 * As _getSafe, but for "retry". 1705 * 1706 * @private 1707 */ 1708 _getRetry(options) { 1709 return { retry: this._retry, ...options }.retry; 1710 } 1711 /** 1712 * Selects a collection. 1713 * 1714 * @param {String} name The collection name. 1715 * @param {Object} [options={}] The options object. 1716 * @param {Object} [options.headers] The headers object option. 1717 * @param {Boolean} [options.safe] The safe option. 1718 * @return {Collection} 1719 */ 1720 collection(name, options = {}) { 1721 return new Collection(this.client, this, name, { 1722 headers: this._getHeaders(options), 1723 retry: this._getRetry(options), 1724 safe: this._getSafe(options), 1725 }); 1726 } 1727 /** 1728 * Retrieves the ETag of the collection list, for use with the `since` filtering option. 1729 * 1730 * @param {Object} [options={}] The options object. 1731 * @param {Object} [options.headers] The headers object option. 1732 * @param {Number} [options.retry=0] Number of retries to make 1733 * when faced with transient errors. 1734 * @return {Promise<String, Error>} 1735 */ 1736 async getCollectionsTimestamp(options = {}) { 1737 const path = this._endpoints.collection(this.name); 1738 const request = { 1739 headers: this._getHeaders(options), 1740 path, 1741 method: "HEAD", 1742 }; 1743 const { headers } = await this.client.execute(request, { 1744 raw: true, 1745 retry: this._getRetry(options), 1746 }); 1747 return headers.get("ETag"); 1748 } 1749 /** 1750 * Retrieves the ETag of the group list, for use with the `since` filtering option. 1751 * 1752 * @param {Object} [options={}] The options object. 1753 * @param {Object} [options.headers] The headers object option. 1754 * @param {Number} [options.retry=0] Number of retries to make 1755 * when faced with transient errors. 1756 * @return {Promise<String, Error>} 1757 */ 1758 async getGroupsTimestamp(options = {}) { 1759 const path = this._endpoints.group(this.name); 1760 const request = { 1761 headers: this._getHeaders(options), 1762 path, 1763 method: "HEAD", 1764 }; 1765 const { headers } = await this.client.execute(request, { 1766 raw: true, 1767 retry: this._getRetry(options), 1768 }); 1769 return headers.get("ETag"); 1770 } 1771 /** 1772 * Retrieves bucket data. 1773 * 1774 * @param {Object} [options={}] The options object. 1775 * @param {Object} [options.headers] The headers object option. 1776 * @param {Object} [options.query] Query parameters to pass in 1777 * the request. This might be useful for features that aren't 1778 * yet supported by this library. 1779 * @param {Array} [options.fields] Limit response to 1780 * just some fields. 1781 * @param {Number} [options.retry=0] Number of retries to make 1782 * when faced with transient errors. 1783 * @return {Promise<Object, Error>} 1784 */ 1785 async getData(options = {}) { 1786 const path = this._endpoints.bucket(this.name); 1787 const request = { 1788 headers: this._getHeaders(options), 1789 path, 1790 }; 1791 const { data } = await this.client.execute(request, { 1792 retry: this._getRetry(options), 1793 query: options.query, 1794 fields: options.fields, 1795 }); 1796 return data; 1797 } 1798 /** 1799 * Set bucket data. 1800 * @param {Object} data The bucket data object. 1801 * @param {Object} [options={}] The options object. 1802 * @param {Object} [options.headers={}] The headers object option. 1803 * @param {Boolean} [options.safe] The safe option. 1804 * @param {Number} [options.retry=0] Number of retries to make 1805 * when faced with transient errors. 1806 * @param {Boolean} [options.patch] The patch option. 1807 * @param {Number} [options.last_modified] The last_modified option. 1808 * @return {Promise<Object, Error>} 1809 */ 1810 async setData(data, options = {}) { 1811 if (!isObject(data)) { 1812 throw new Error("A bucket object is required."); 1813 } 1814 const bucket = { 1815 ...data, 1816 id: this.name, 1817 }; 1818 // For default bucket, we need to drop the id from the data object. 1819 // Bug in Kinto < 3.1.1 1820 const bucketId = bucket.id; 1821 if (bucket.id === "default") { 1822 delete bucket.id; 1823 } 1824 const path = this._endpoints.bucket(bucketId); 1825 const { patch, permissions } = options; 1826 const { last_modified } = { ...data, ...options }; 1827 const request = updateRequest( 1828 path, 1829 { data: bucket, permissions }, 1830 { 1831 last_modified, 1832 patch, 1833 headers: this._getHeaders(options), 1834 safe: this._getSafe(options), 1835 } 1836 ); 1837 return this.client.execute(request, { 1838 retry: this._getRetry(options), 1839 }); 1840 } 1841 /** 1842 * Retrieves the list of history entries in the current bucket. 1843 * 1844 * @param {Object} [options={}] The options object. 1845 * @param {Object} [options.headers] The headers object option. 1846 * @param {Number} [options.retry=0] Number of retries to make 1847 * when faced with transient errors. 1848 * @return {Promise<Array<Object>, Error>} 1849 */ 1850 async listHistory(options = {}) { 1851 const path = this._endpoints.history(this.name); 1852 return this.client.paginatedList(path, options, { 1853 headers: this._getHeaders(options), 1854 retry: this._getRetry(options), 1855 }); 1856 } 1857 /** 1858 * Retrieves the list of collections in the current bucket. 1859 * 1860 * @param {Object} [options={}] The options object. 1861 * @param {Object} [options.filters={}] The filters object. 1862 * @param {Object} [options.headers] The headers object option. 1863 * @param {Number} [options.retry=0] Number of retries to make 1864 * when faced with transient errors. 1865 * @param {Array} [options.fields] Limit response to 1866 * just some fields. 1867 * @return {Promise<Array<Object>, Error>} 1868 */ 1869 async listCollections(options = {}) { 1870 const path = this._endpoints.collection(this.name); 1871 return this.client.paginatedList(path, options, { 1872 headers: this._getHeaders(options), 1873 retry: this._getRetry(options), 1874 }); 1875 } 1876 /** 1877 * Creates a new collection in current bucket. 1878 * 1879 * @param {String|undefined} id The collection id. 1880 * @param {Object} [options={}] The options object. 1881 * @param {Boolean} [options.safe] The safe option. 1882 * @param {Object} [options.headers] The headers object option. 1883 * @param {Number} [options.retry=0] Number of retries to make 1884 * when faced with transient errors. 1885 * @param {Object} [options.permissions] The permissions object. 1886 * @param {Object} [options.data] The data object. 1887 * @return {Promise<Object, Error>} 1888 */ 1889 async createCollection(id, options = {}) { 1890 const { permissions, data = {} } = options; 1891 data.id = id; 1892 const path = this._endpoints.collection(this.name, id); 1893 const request = createRequest( 1894 path, 1895 { data, permissions }, 1896 { 1897 headers: this._getHeaders(options), 1898 safe: this._getSafe(options), 1899 } 1900 ); 1901 return this.client.execute(request, { 1902 retry: this._getRetry(options), 1903 }); 1904 } 1905 /** 1906 * Deletes a collection from the current bucket. 1907 * 1908 * @param {Object|String} collection The collection to delete. 1909 * @param {Object} [options={}] The options object. 1910 * @param {Object} [options.headers] The headers object option. 1911 * @param {Number} [options.retry=0] Number of retries to make 1912 * when faced with transient errors. 1913 * @param {Boolean} [options.safe] The safe option. 1914 * @param {Number} [options.last_modified] The last_modified option. 1915 * @return {Promise<Object, Error>} 1916 */ 1917 async deleteCollection(collection, options = {}) { 1918 const collectionObj = toDataBody(collection); 1919 if (!collectionObj.id) { 1920 throw new Error("A collection id is required."); 1921 } 1922 const { id } = collectionObj; 1923 const { last_modified } = { ...collectionObj, ...options }; 1924 const path = this._endpoints.collection(this.name, id); 1925 const request = deleteRequest(path, { 1926 last_modified, 1927 headers: this._getHeaders(options), 1928 safe: this._getSafe(options), 1929 }); 1930 return this.client.execute(request, { 1931 retry: this._getRetry(options), 1932 }); 1933 } 1934 /** 1935 * Deletes collections from the current bucket. 1936 * 1937 * @param {Object} [options={}] The options object. 1938 * @param {Object} [options.filters={}] The filters object. 1939 * @param {Object} [options.headers] The headers object option. 1940 * @param {Number} [options.retry=0] Number of retries to make 1941 * when faced with transient errors. 1942 * @param {Array} [options.fields] Limit response to 1943 * just some fields. 1944 * @return {Promise<Array<Object>, Error>} 1945 */ 1946 async deleteCollections(options = {}) { 1947 const path = this._endpoints.collection(this.name); 1948 return this.client.paginatedDelete(path, options, { 1949 headers: this._getHeaders(options), 1950 retry: this._getRetry(options), 1951 }); 1952 } 1953 /** 1954 * Retrieves the list of groups in the current bucket. 1955 * 1956 * @param {Object} [options={}] The options object. 1957 * @param {Object} [options.filters={}] The filters object. 1958 * @param {Object} [options.headers] The headers object option. 1959 * @param {Number} [options.retry=0] Number of retries to make 1960 * when faced with transient errors. 1961 * @param {Array} [options.fields] Limit response to 1962 * just some fields. 1963 * @return {Promise<Array<Object>, Error>} 1964 */ 1965 async listGroups(options = {}) { 1966 const path = this._endpoints.group(this.name); 1967 return this.client.paginatedList(path, options, { 1968 headers: this._getHeaders(options), 1969 retry: this._getRetry(options), 1970 }); 1971 } 1972 /** 1973 * Fetches a group in current bucket. 1974 * 1975 * @param {String} id The group id. 1976 * @param {Object} [options={}] The options object. 1977 * @param {Object} [options.headers] The headers object option. 1978 * @param {Number} [options.retry=0] Number of retries to make 1979 * when faced with transient errors. 1980 * @param {Object} [options.query] Query parameters to pass in 1981 * the request. This might be useful for features that aren't 1982 * yet supported by this library. 1983 * @param {Array} [options.fields] Limit response to 1984 * just some fields. 1985 * @return {Promise<Object, Error>} 1986 */ 1987 async getGroup(id, options = {}) { 1988 const path = this._endpoints.group(this.name, id); 1989 const request = { 1990 headers: this._getHeaders(options), 1991 path, 1992 }; 1993 return this.client.execute(request, { 1994 retry: this._getRetry(options), 1995 query: options.query, 1996 fields: options.fields, 1997 }); 1998 } 1999 /** 2000 * Creates a new group in current bucket. 2001 * 2002 * @param {String|undefined} id The group id. 2003 * @param {Array<String>} [members=[]] The list of principals. 2004 * @param {Object} [options={}] The options object. 2005 * @param {Object} [options.data] The data object. 2006 * @param {Object} [options.permissions] The permissions object. 2007 * @param {Boolean} [options.safe] The safe option. 2008 * @param {Object} [options.headers] The headers object option. 2009 * @param {Number} [options.retry=0] Number of retries to make 2010 * when faced with transient errors. 2011 * @return {Promise<Object, Error>} 2012 */ 2013 async createGroup(id, members = [], options = {}) { 2014 const data = { 2015 ...options.data, 2016 id, 2017 members, 2018 }; 2019 const path = this._endpoints.group(this.name, id); 2020 const { permissions } = options; 2021 const request = createRequest( 2022 path, 2023 { data, permissions }, 2024 { 2025 headers: this._getHeaders(options), 2026 safe: this._getSafe(options), 2027 } 2028 ); 2029 return this.client.execute(request, { 2030 retry: this._getRetry(options), 2031 }); 2032 } 2033 /** 2034 * Updates an existing group in current bucket. 2035 * 2036 * @param {Object} group The group object. 2037 * @param {Object} [options={}] The options object. 2038 * @param {Object} [options.data] The data object. 2039 * @param {Object} [options.permissions] The permissions object. 2040 * @param {Boolean} [options.safe] The safe option. 2041 * @param {Object} [options.headers] The headers object option. 2042 * @param {Number} [options.retry=0] Number of retries to make 2043 * when faced with transient errors. 2044 * @param {Number} [options.last_modified] The last_modified option. 2045 * @return {Promise<Object, Error>} 2046 */ 2047 async updateGroup(group, options = {}) { 2048 if (!isObject(group)) { 2049 throw new Error("A group object is required."); 2050 } 2051 if (!group.id) { 2052 throw new Error("A group id is required."); 2053 } 2054 const data = { 2055 ...options.data, 2056 ...group, 2057 }; 2058 const path = this._endpoints.group(this.name, group.id); 2059 const { patch, permissions } = options; 2060 const { last_modified } = { ...data, ...options }; 2061 const request = updateRequest( 2062 path, 2063 { data, permissions }, 2064 { 2065 last_modified, 2066 patch, 2067 headers: this._getHeaders(options), 2068 safe: this._getSafe(options), 2069 } 2070 ); 2071 return this.client.execute(request, { 2072 retry: this._getRetry(options), 2073 }); 2074 } 2075 /** 2076 * Deletes a group from the current bucket. 2077 * 2078 * @param {Object|String} group The group to delete. 2079 * @param {Object} [options={}] The options object. 2080 * @param {Object} [options.headers] The headers object option. 2081 * @param {Number} [options.retry=0] Number of retries to make 2082 * when faced with transient errors. 2083 * @param {Boolean} [options.safe] The safe option. 2084 * @param {Number} [options.last_modified] The last_modified option. 2085 * @return {Promise<Object, Error>} 2086 */ 2087 async deleteGroup(group, options = {}) { 2088 const groupObj = toDataBody(group); 2089 const { id } = groupObj; 2090 const { last_modified } = { ...groupObj, ...options }; 2091 const path = this._endpoints.group(this.name, id); 2092 const request = deleteRequest(path, { 2093 last_modified, 2094 headers: this._getHeaders(options), 2095 safe: this._getSafe(options), 2096 }); 2097 return this.client.execute(request, { 2098 retry: this._getRetry(options), 2099 }); 2100 } 2101 /** 2102 * Deletes groups from the current bucket. 2103 * 2104 * @param {Object} [options={}] The options object. 2105 * @param {Object} [options.filters={}] The filters object. 2106 * @param {Object} [options.headers] The headers object option. 2107 * @param {Number} [options.retry=0] Number of retries to make 2108 * when faced with transient errors. 2109 * @param {Array} [options.fields] Limit response to 2110 * just some fields. 2111 * @return {Promise<Array<Object>, Error>} 2112 */ 2113 async deleteGroups(options = {}) { 2114 const path = this._endpoints.group(this.name); 2115 return this.client.paginatedDelete(path, options, { 2116 headers: this._getHeaders(options), 2117 retry: this._getRetry(options), 2118 }); 2119 } 2120 /** 2121 * Retrieves the list of permissions for this bucket. 2122 * 2123 * @param {Object} [options={}] The options object. 2124 * @param {Object} [options.headers] The headers object option. 2125 * @param {Number} [options.retry=0] Number of retries to make 2126 * when faced with transient errors. 2127 * @return {Promise<Object, Error>} 2128 */ 2129 async getPermissions(options = {}) { 2130 const request = { 2131 headers: this._getHeaders(options), 2132 path: this._endpoints.bucket(this.name), 2133 }; 2134 const { permissions } = await this.client.execute(request, { 2135 retry: this._getRetry(options), 2136 }); 2137 return permissions; 2138 } 2139 /** 2140 * Replaces all existing bucket permissions with the ones provided. 2141 * 2142 * @param {Object} permissions The permissions object. 2143 * @param {Object} [options={}] The options object 2144 * @param {Boolean} [options.safe] The safe option. 2145 * @param {Object} [options.headers={}] The headers object option. 2146 * @param {Number} [options.retry=0] Number of retries to make 2147 * when faced with transient errors. 2148 * @param {Object} [options.last_modified] The last_modified option. 2149 * @return {Promise<Object, Error>} 2150 */ 2151 async setPermissions(permissions, options = {}) { 2152 if (!isObject(permissions)) { 2153 throw new Error("A permissions object is required."); 2154 } 2155 const path = this._endpoints.bucket(this.name); 2156 const { last_modified } = options; 2157 const data = { last_modified }; 2158 const request = updateRequest( 2159 path, 2160 { data, permissions }, 2161 { 2162 headers: this._getHeaders(options), 2163 safe: this._getSafe(options), 2164 } 2165 ); 2166 return this.client.execute(request, { 2167 retry: this._getRetry(options), 2168 }); 2169 } 2170 /** 2171 * Append principals to the bucket permissions. 2172 * 2173 * @param {Object} permissions The permissions object. 2174 * @param {Object} [options={}] The options object 2175 * @param {Boolean} [options.safe] The safe option. 2176 * @param {Object} [options.headers] The headers object option. 2177 * @param {Number} [options.retry=0] Number of retries to make 2178 * when faced with transient errors. 2179 * @param {Object} [options.last_modified] The last_modified option. 2180 * @return {Promise<Object, Error>} 2181 */ 2182 async addPermissions(permissions, options = {}) { 2183 if (!isObject(permissions)) { 2184 throw new Error("A permissions object is required."); 2185 } 2186 const path = this._endpoints.bucket(this.name); 2187 const { last_modified } = options; 2188 const request = jsonPatchPermissionsRequest(path, permissions, "add", { 2189 last_modified, 2190 headers: this._getHeaders(options), 2191 safe: this._getSafe(options), 2192 }); 2193 return this.client.execute(request, { 2194 retry: this._getRetry(options), 2195 }); 2196 } 2197 /** 2198 * Remove principals from the bucket permissions. 2199 * 2200 * @param {Object} permissions The permissions object. 2201 * @param {Object} [options={}] The options object 2202 * @param {Boolean} [options.safe] The safe option. 2203 * @param {Object} [options.headers] The headers object option. 2204 * @param {Number} [options.retry=0] Number of retries to make 2205 * when faced with transient errors. 2206 * @param {Object} [options.last_modified] The last_modified option. 2207 * @return {Promise<Object, Error>} 2208 */ 2209 async removePermissions(permissions, options = {}) { 2210 if (!isObject(permissions)) { 2211 throw new Error("A permissions object is required."); 2212 } 2213 const path = this._endpoints.bucket(this.name); 2214 const { last_modified } = options; 2215 const request = jsonPatchPermissionsRequest(path, permissions, "remove", { 2216 last_modified, 2217 headers: this._getHeaders(options), 2218 safe: this._getSafe(options), 2219 }); 2220 return this.client.execute(request, { 2221 retry: this._getRetry(options), 2222 }); 2223 } 2224 /** 2225 * Performs batch operations at the current bucket level. 2226 * 2227 * @param {Function} fn The batch operation function. 2228 * @param {Object} [options={}] The options object. 2229 * @param {Object} [options.headers] The headers object option. 2230 * @param {Boolean} [options.safe] The safe option. 2231 * @param {Number} [options.retry=0] The retry option. 2232 * @param {Boolean} [options.aggregate] Produces a grouped result object. 2233 * @return {Promise<Object, Error>} 2234 */ 2235 async batch(fn, options = {}) { 2236 return this.client.batch(fn, { 2237 bucket: this.name, 2238 headers: this._getHeaders(options), 2239 retry: this._getRetry(options), 2240 safe: this._getSafe(options), 2241 aggregate: !!options.aggregate, 2242 }); 2243 } 2244 } 2245 __decorate([capable(["history"])], Bucket.prototype, "listHistory", null); 2246 2247 /** 2248 * High level HTTP client for the Kinto API. 2249 * 2250 * @example 2251 * const client = new KintoClient("https://demo.kinto-storage.org/v1"); 2252 * client.bucket("default") 2253 * .collection("my-blog") 2254 * .createRecord({title: "First article"}) 2255 * .then(console.log.bind(console)) 2256 * .catch(console.error.bind(console)); 2257 */ 2258 class KintoClientBase { 2259 /** 2260 * Constructor. 2261 * 2262 * @param {String} remote The remote URL. 2263 * @param {Object} [options={}] The options object. 2264 * @param {Boolean} [options.safe=true] Adds concurrency headers to every requests. 2265 * @param {EventEmitter} [options.events=EventEmitter] The events handler instance. 2266 * @param {Object} [options.headers={}] The key-value headers to pass to each request. 2267 * @param {Object} [options.retry=0] Number of retries when request fails (default: 0) 2268 * @param {String} [options.bucket="default"] The default bucket to use. 2269 * @param {String} [options.requestMode="cors"] The HTTP request mode (from ES6 fetch spec). 2270 * @param {Number} [options.timeout=null] The request timeout in ms, if any. 2271 * @param {Function} [options.fetchFunc=fetch] The function to be used to execute HTTP requests. 2272 */ 2273 constructor(remote, options) { 2274 if (typeof remote !== "string" || !remote.length) { 2275 throw new Error("Invalid remote URL: " + remote); 2276 } 2277 if (remote[remote.length - 1] === "/") { 2278 remote = remote.slice(0, -1); 2279 } 2280 this._backoffReleaseTime = null; 2281 this._requests = []; 2282 this._isBatch = !!options.batch; 2283 this._retry = options.retry || 0; 2284 this._safe = !!options.safe; 2285 this._headers = options.headers || {}; 2286 // public properties 2287 /** 2288 * The remote server base URL. 2289 * @type {String} 2290 */ 2291 this.remote = remote; 2292 /** 2293 * Current server information. 2294 * @ignore 2295 * @type {Object|null} 2296 */ 2297 this.serverInfo = null; 2298 /** 2299 * The event emitter instance. Should comply with the `EventEmitter` 2300 * interface. 2301 * @ignore 2302 * @type {Class} 2303 */ 2304 this.events = options.events; 2305 this.endpoints = ENDPOINTS; 2306 const { fetchFunc, requestMode, timeout } = options; 2307 /** 2308 * The HTTP instance. 2309 * @ignore 2310 * @type {HTTP} 2311 */ 2312 this.http = new HTTP(this.events, { fetchFunc, requestMode, timeout }); 2313 this._registerHTTPEvents(); 2314 } 2315 /** 2316 * The remote endpoint base URL. Setting the value will also extract and 2317 * validate the version. 2318 * @type {String} 2319 */ 2320 get remote() { 2321 return this._remote; 2322 } 2323 /** 2324 * @ignore 2325 */ 2326 set remote(url) { 2327 let version; 2328 try { 2329 version = url.match(/\/(v\d+)\/?$/)[1]; 2330 } catch (err) { 2331 throw new Error("The remote URL must contain the version: " + url); 2332 } 2333 this._remote = url; 2334 this._version = version; 2335 } 2336 /** 2337 * The current server protocol version, eg. `v1`. 2338 * @type {String} 2339 */ 2340 get version() { 2341 return this._version; 2342 } 2343 /** 2344 * Backoff remaining time, in milliseconds. Defaults to zero if no backoff is 2345 * ongoing. 2346 * 2347 * @type {Number} 2348 */ 2349 get backoff() { 2350 const currentTime = new Date().getTime(); 2351 if (this._backoffReleaseTime && currentTime < this._backoffReleaseTime) { 2352 return this._backoffReleaseTime - currentTime; 2353 } 2354 return 0; 2355 } 2356 /** 2357 * Registers HTTP events. 2358 * @private 2359 */ 2360 _registerHTTPEvents() { 2361 // Prevent registering event from a batch client instance 2362 if (!this._isBatch && this.events) { 2363 this.events.on("backoff", (backoffMs) => { 2364 this._backoffReleaseTime = backoffMs; 2365 }); 2366 } 2367 } 2368 /** 2369 * Retrieve a bucket object to perform operations on it. 2370 * 2371 * @param {String} name The bucket name. 2372 * @param {Object} [options={}] The request options. 2373 * @param {Boolean} [options.safe] The resulting safe option. 2374 * @param {Number} [options.retry] The resulting retry option. 2375 * @param {Object} [options.headers] The extended headers object option. 2376 * @return {Bucket} 2377 */ 2378 bucket(name, options = {}) { 2379 return new Bucket(this, name, { 2380 headers: this._getHeaders(options), 2381 safe: this._getSafe(options), 2382 retry: this._getRetry(options), 2383 }); 2384 } 2385 /** 2386 * Set client "headers" for every request, updating previous headers (if any). 2387 * 2388 * @param {Object} headers The headers to merge with existing ones. 2389 */ 2390 setHeaders(headers) { 2391 this._headers = { 2392 ...this._headers, 2393 ...headers, 2394 }; 2395 this.serverInfo = null; 2396 } 2397 /** 2398 * Get the value of "headers" for a given request, merging the 2399 * per-request headers with our own "default" headers. 2400 * 2401 * Note that unlike other options, headers aren't overridden, but 2402 * merged instead. 2403 * 2404 * @private 2405 * @param {Object} options The options for a request. 2406 * @returns {Object} 2407 */ 2408 _getHeaders(options) { 2409 return { 2410 ...this._headers, 2411 ...options.headers, 2412 }; 2413 } 2414 /** 2415 * Get the value of "safe" for a given request, using the 2416 * per-request option if present or falling back to our default 2417 * otherwise. 2418 * 2419 * @private 2420 * @param {Object} options The options for a request. 2421 * @returns {Boolean} 2422 */ 2423 _getSafe(options) { 2424 return { safe: this._safe, ...options }.safe; 2425 } 2426 /** 2427 * As _getSafe, but for "retry". 2428 * 2429 * @private 2430 */ 2431 _getRetry(options) { 2432 return { retry: this._retry, ...options }.retry; 2433 } 2434 /** 2435 * Retrieves the server's "hello" endpoint. This endpoint reveals 2436 * server capabilities and settings as well as telling the client 2437 * "who they are" according to their given authorization headers. 2438 * 2439 * @private 2440 * @param {Object} [options={}] The request options. 2441 * @param {Object} [options.headers={}] Headers to use when making 2442 * this request. 2443 * @param {Number} [options.retry=0] Number of retries to make 2444 * when faced with transient errors. 2445 * @return {Promise<Object, Error>} 2446 */ 2447 async _getHello(options = {}) { 2448 const path = this.remote + ENDPOINTS.root(); 2449 const { json } = await this.http.request( 2450 path, 2451 { headers: this._getHeaders(options) }, 2452 { retry: this._getRetry(options) } 2453 ); 2454 return json; 2455 } 2456 /** 2457 * Retrieves server information and persist them locally. This operation is 2458 * usually performed a single time during the instance lifecycle. 2459 * 2460 * @param {Object} [options={}] The request options. 2461 * @param {Number} [options.retry=0] Number of retries to make 2462 * when faced with transient errors. 2463 * @return {Promise<Object, Error>} 2464 */ 2465 async fetchServerInfo(options = {}) { 2466 if (this.serverInfo) { 2467 return this.serverInfo; 2468 } 2469 this.serverInfo = await this._getHello({ retry: this._getRetry(options) }); 2470 return this.serverInfo; 2471 } 2472 /** 2473 * Retrieves Kinto server settings. 2474 * 2475 * @param {Object} [options={}] The request options. 2476 * @param {Number} [options.retry=0] Number of retries to make 2477 * when faced with transient errors. 2478 * @return {Promise<Object, Error>} 2479 */ 2480 async fetchServerSettings(options = {}) { 2481 const { settings } = await this.fetchServerInfo(options); 2482 return settings; 2483 } 2484 /** 2485 * Retrieve server capabilities information. 2486 * 2487 * @param {Object} [options={}] The request options. 2488 * @param {Number} [options.retry=0] Number of retries to make 2489 * when faced with transient errors. 2490 * @return {Promise<Object, Error>} 2491 */ 2492 async fetchServerCapabilities(options = {}) { 2493 const { capabilities } = await this.fetchServerInfo(options); 2494 return capabilities; 2495 } 2496 /** 2497 * Retrieve authenticated user information. 2498 * 2499 * @param {Object} [options={}] The request options. 2500 * @param {Object} [options.headers={}] Headers to use when making 2501 * this request. 2502 * @param {Number} [options.retry=0] Number of retries to make 2503 * when faced with transient errors. 2504 * @return {Promise<Object, Error>} 2505 */ 2506 async fetchUser(options = {}) { 2507 const { user } = await this._getHello(options); 2508 return user; 2509 } 2510 /** 2511 * Retrieve authenticated user information. 2512 * 2513 * @param {Object} [options={}] The request options. 2514 * @param {Number} [options.retry=0] Number of retries to make 2515 * when faced with transient errors. 2516 * @return {Promise<Object, Error>} 2517 */ 2518 async fetchHTTPApiVersion(options = {}) { 2519 const { http_api_version } = await this.fetchServerInfo(options); 2520 return http_api_version; 2521 } 2522 /** 2523 * Process batch requests, chunking them according to the batch_max_requests 2524 * server setting when needed. 2525 * 2526 * @param {Array} requests The list of batch subrequests to perform. 2527 * @param {Object} [options={}] The options object. 2528 * @return {Promise<Object, Error>} 2529 */ 2530 async _batchRequests(requests, options = {}) { 2531 const headers = this._getHeaders(options); 2532 if (!requests.length) { 2533 return []; 2534 } 2535 const serverSettings = await this.fetchServerSettings({ 2536 retry: this._getRetry(options), 2537 }); 2538 const maxRequests = serverSettings.batch_max_requests; 2539 if (maxRequests && requests.length > maxRequests) { 2540 const chunks = partition(requests, maxRequests); 2541 const results = []; 2542 for (const chunk of chunks) { 2543 const result = await this._batchRequests(chunk, options); 2544 results.push(...result); 2545 } 2546 return results; 2547 } 2548 const { responses } = await this.execute( 2549 { 2550 // FIXME: is this really necessary, since it's also present in 2551 // the "defaults"? 2552 headers, 2553 path: ENDPOINTS.batch(), 2554 method: "POST", 2555 body: { 2556 defaults: { headers }, 2557 requests, 2558 }, 2559 }, 2560 { retry: this._getRetry(options) } 2561 ); 2562 return responses; 2563 } 2564 /** 2565 * Sends batch requests to the remote server. 2566 * 2567 * Note: Reserved for internal use only. 2568 * 2569 * @ignore 2570 * @param {Function} fn The function to use for describing batch ops. 2571 * @param {Object} [options={}] The options object. 2572 * @param {Boolean} [options.safe] The safe option. 2573 * @param {Number} [options.retry] The retry option. 2574 * @param {String} [options.bucket] The bucket name option. 2575 * @param {String} [options.collection] The collection name option. 2576 * @param {Object} [options.headers] The headers object option. 2577 * @param {Boolean} [options.aggregate=false] Produces an aggregated result object. 2578 * @return {Promise<Object, Error>} 2579 */ 2580 async batch(fn, options = {}) { 2581 const rootBatch = new KintoClientBase(this.remote, { 2582 events: this.events, 2583 batch: true, 2584 safe: this._getSafe(options), 2585 retry: this._getRetry(options), 2586 }); 2587 if (options.bucket && options.collection) { 2588 fn(rootBatch.bucket(options.bucket).collection(options.collection)); 2589 } else if (options.bucket) { 2590 fn(rootBatch.bucket(options.bucket)); 2591 } else { 2592 fn(rootBatch); 2593 } 2594 const responses = await this._batchRequests(rootBatch._requests, options); 2595 if (options.aggregate) { 2596 return aggregate(responses, rootBatch._requests); 2597 } 2598 return responses; 2599 } 2600 async execute(request, options = {}) { 2601 const { raw = false, stringify = true } = options; 2602 // If we're within a batch, add the request to the stack to send at once. 2603 if (this._isBatch) { 2604 this._requests.push(request); 2605 // Resolve with a message in case people attempt at consuming the result 2606 // from within a batch operation. 2607 const msg = 2608 "This result is generated from within a batch " + 2609 "operation and should not be consumed."; 2610 return raw ? { status: 0, json: msg, headers: new Headers() } : msg; 2611 } 2612 const uri = this.remote + addEndpointOptions(request.path, options); 2613 const result = await this.http.request( 2614 uri, 2615 cleanUndefinedProperties({ 2616 // Limit requests to only those parts that would be allowed in 2617 // a batch request -- don't pass through other fancy fetch() 2618 // options like integrity, redirect, mode because they will 2619 // break on a batch request. A batch request only allows 2620 // headers, method, path (above), and body. 2621 method: request.method, 2622 headers: request.headers, 2623 body: stringify ? JSON.stringify(request.body) : request.body, 2624 }), 2625 { retry: this._getRetry(options) } 2626 ); 2627 return raw ? result : result.json; 2628 } 2629 /** 2630 * Perform an operation with a given HTTP method on some pages from 2631 * a paginated list, following the `next-page` header automatically 2632 * until we have processed the requested number of pages. Return a 2633 * response with a `.next()` method that can be called to perform 2634 * the requested HTTP method on more results. 2635 * 2636 * @private 2637 * @param {String} path 2638 * The path to make the request to. 2639 * @param {Object} params 2640 * The parameters to use when making the request. 2641 * @param {String} [params.sort="-last_modified"] 2642 * The sorting order to use when doing operation on pages. 2643 * @param {Object} [params.filters={}] 2644 * The filters to send in the request. 2645 * @param {Number} [params.limit=undefined] 2646 * The limit to send in the request. Undefined means no limit. 2647 * @param {Number} [params.pages=undefined] 2648 * The number of pages to operate on. Undefined means one page. Pass 2649 * Infinity to operate on everything. 2650 * @param {String} [params.since=undefined] 2651 * The ETag from which to start doing operation on pages. 2652 * @param {Array} [params.fields] 2653 * Limit response to just some fields. 2654 * @param {Object} [options={}] 2655 * Additional request-level parameters to use in all requests. 2656 * @param {Object} [options.headers={}] 2657 * Headers to use during all requests. 2658 * @param {Number} [options.retry=0] 2659 * Number of times to retry each request if the server responds 2660 * with Retry-After. 2661 * @param {String} [options.method="GET"] 2662 * The method to use in the request. 2663 */ 2664 async paginatedOperation(path, params = {}, options = {}) { 2665 // FIXME: this is called even in batch requests, which doesn't 2666 // make any sense (since all batch requests get a "dummy" 2667 // response; see execute() above). 2668 const { sort, filters, limit, pages, since, fields } = { 2669 sort: "-last_modified", 2670 ...params, 2671 }; 2672 // Safety/Consistency check on ETag value. 2673 if (since && typeof since !== "string") { 2674 throw new Error( 2675 `Invalid value for since (${since}), should be ETag value.` 2676 ); 2677 } 2678 const query = { 2679 ...filters, 2680 _sort: sort, 2681 _limit: limit, 2682 _since: since, 2683 }; 2684 if (fields) { 2685 query._fields = fields; 2686 } 2687 const querystring = qsify(query); 2688 let results = [], 2689 current = 0; 2690 const next = async function (nextPage) { 2691 if (!nextPage) { 2692 throw new Error("Pagination exhausted."); 2693 } 2694 return processNextPage(nextPage); 2695 }; 2696 const processNextPage = async (nextPage) => { 2697 const { headers } = options; 2698 return handleResponse(await this.http.request(nextPage, { headers })); 2699 }; 2700 const pageResults = (results, nextPage, etag) => { 2701 // ETag string is supposed to be opaque and stored «as-is». 2702 // ETag header values are quoted (because of * and W/"foo"). 2703 return { 2704 last_modified: etag ? etag.replace(/"/g, "") : etag, 2705 data: results, 2706 next: next.bind(null, nextPage), 2707 hasNextPage: !!nextPage, 2708 totalRecords: -1, 2709 }; 2710 }; 2711 const handleResponse = async function ({ 2712 headers = new Headers(), 2713 json = {}, 2714 }) { 2715 const nextPage = headers.get("Next-Page"); 2716 const etag = headers.get("ETag"); 2717 if (!pages) { 2718 return pageResults(json.data, nextPage, etag); 2719 } 2720 // Aggregate new results with previous ones 2721 results = results.concat(json.data); 2722 current += 1; 2723 if (current >= pages || !nextPage) { 2724 // Pagination exhausted 2725 return pageResults(results, nextPage, etag); 2726 } 2727 // Follow next page 2728 return processNextPage(nextPage); 2729 }; 2730 return handleResponse( 2731 await this.execute( 2732 // N.B.: This doesn't use _getHeaders, because all calls to 2733 // `paginatedList` are assumed to come from calls that already 2734 // have headers merged at e.g. the bucket or collection level. 2735 { 2736 headers: options.headers ? options.headers : {}, 2737 path: path + "?" + querystring, 2738 method: options.method, 2739 }, 2740 // N.B. This doesn't use _getRetry, because all calls to 2741 // `paginatedList` are assumed to come from calls that already 2742 // used `_getRetry` at e.g. the bucket or collection level. 2743 { raw: true, retry: options.retry || 0 } 2744 ) 2745 ); 2746 } 2747 /** 2748 * Fetch some pages from a paginated list, following the `next-page` 2749 * header automatically until we have fetched the requested number 2750 * of pages. Return a response with a `.next()` method that can be 2751 * called to fetch more results. 2752 * 2753 * @private 2754 * @param {String} path 2755 * The path to make the request to. 2756 * @param {Object} params 2757 * The parameters to use when making the request. 2758 * @param {String} [params.sort="-last_modified"] 2759 * The sorting order to use when fetching. 2760 * @param {Object} [params.filters={}] 2761 * The filters to send in the request. 2762 * @param {Number} [params.limit=undefined] 2763 * The limit to send in the request. Undefined means no limit. 2764 * @param {Number} [params.pages=undefined] 2765 * The number of pages to fetch. Undefined means one page. Pass 2766 * Infinity to fetch everything. 2767 * @param {String} [params.since=undefined] 2768 * The ETag from which to start fetching. 2769 * @param {Array} [params.fields] 2770 * Limit response to just some fields. 2771 * @param {Object} [options={}] 2772 * Additional request-level parameters to use in all requests. 2773 * @param {Object} [options.headers={}] 2774 * Headers to use during all requests. 2775 * @param {Number} [options.retry=0] 2776 * Number of times to retry each request if the server responds 2777 * with Retry-After. 2778 */ 2779 async paginatedList(path, params = {}, options = {}) { 2780 return this.paginatedOperation(path, params, options); 2781 } 2782 /** 2783 * Delete multiple objects, following the pagination if the number of 2784 * objects exceeds the page limit until we have deleted the requested 2785 * number of pages. Return a response with a `.next()` method that can 2786 * be called to delete more results. 2787 * 2788 * @private 2789 * @param {String} path 2790 * The path to make the request to. 2791 * @param {Object} params 2792 * The parameters to use when making the request. 2793 * @param {String} [params.sort="-last_modified"] 2794 * The sorting order to use when deleting. 2795 * @param {Object} [params.filters={}] 2796 * The filters to send in the request. 2797 * @param {Number} [params.limit=undefined] 2798 * The limit to send in the request. Undefined means no limit. 2799 * @param {Number} [params.pages=undefined] 2800 * The number of pages to delete. Undefined means one page. Pass 2801 * Infinity to delete everything. 2802 * @param {String} [params.since=undefined] 2803 * The ETag from which to start deleting. 2804 * @param {Array} [params.fields] 2805 * Limit response to just some fields. 2806 * @param {Object} [options={}] 2807 * Additional request-level parameters to use in all requests. 2808 * @param {Object} [options.headers={}] 2809 * Headers to use during all requests. 2810 * @param {Number} [options.retry=0] 2811 * Number of times to retry each request if the server responds 2812 * with Retry-After. 2813 */ 2814 paginatedDelete(path, params = {}, options = {}) { 2815 const { headers, safe, last_modified } = options; 2816 const deleteRequest$1 = deleteRequest(path, { 2817 headers, 2818 safe: safe ? safe : false, 2819 last_modified, 2820 }); 2821 return this.paginatedOperation(path, params, { 2822 ...options, 2823 headers: deleteRequest$1.headers, 2824 method: "DELETE", 2825 }); 2826 } 2827 /** 2828 * Lists all permissions. 2829 * 2830 * @param {Object} [options={}] The options object. 2831 * @param {Object} [options.headers={}] Headers to use when making 2832 * this request. 2833 * @param {Number} [options.retry=0] Number of retries to make 2834 * when faced with transient errors. 2835 * @return {Promise<Object[], Error>} 2836 */ 2837 async listPermissions(options = {}) { 2838 const path = ENDPOINTS.permissions(); 2839 // Ensure the default sort parameter is something that exists in permissions 2840 // entries, as `last_modified` doesn't; here, we pick "id". 2841 const paginationOptions = { sort: "id", ...options }; 2842 return this.paginatedList(path, paginationOptions, { 2843 headers: this._getHeaders(options), 2844 retry: this._getRetry(options), 2845 }); 2846 } 2847 /** 2848 * Retrieves the list of buckets. 2849 * 2850 * @param {Object} [options={}] The options object. 2851 * @param {Object} [options.headers={}] Headers to use when making 2852 * this request. 2853 * @param {Number} [options.retry=0] Number of retries to make 2854 * when faced with transient errors. 2855 * @param {Object} [options.filters={}] The filters object. 2856 * @param {Array} [options.fields] Limit response to 2857 * just some fields. 2858 * @return {Promise<Object[], Error>} 2859 */ 2860 async listBuckets(options = {}) { 2861 const path = ENDPOINTS.bucket(); 2862 return this.paginatedList(path, options, { 2863 headers: this._getHeaders(options), 2864 retry: this._getRetry(options), 2865 }); 2866 } 2867 /** 2868 * Creates a new bucket on the server. 2869 * 2870 * @param {String|null} id The bucket name (optional). 2871 * @param {Object} [options={}] The options object. 2872 * @param {Boolean} [options.data] The bucket data option. 2873 * @param {Boolean} [options.safe] The safe option. 2874 * @param {Object} [options.headers] The headers object option. 2875 * @param {Number} [options.retry=0] Number of retries to make 2876 * when faced with transient errors. 2877 * @return {Promise<Object, Error>} 2878 */ 2879 async createBucket(id, options = {}) { 2880 const { data, permissions } = options; 2881 const _data = { ...data, id: id ? id : undefined }; 2882 const path = _data.id ? ENDPOINTS.bucket(_data.id) : ENDPOINTS.bucket(); 2883 return this.execute( 2884 createRequest( 2885 path, 2886 { data: _data, permissions }, 2887 { 2888 headers: this._getHeaders(options), 2889 safe: this._getSafe(options), 2890 } 2891 ), 2892 { retry: this._getRetry(options) } 2893 ); 2894 } 2895 /** 2896 * Deletes a bucket from the server. 2897 * 2898 * @ignore 2899 * @param {Object|String} bucket The bucket to delete. 2900 * @param {Object} [options={}] The options object. 2901 * @param {Boolean} [options.safe] The safe option. 2902 * @param {Object} [options.headers] The headers object option. 2903 * @param {Number} [options.retry=0] Number of retries to make 2904 * when faced with transient errors. 2905 * @param {Number} [options.last_modified] The last_modified option. 2906 * @return {Promise<Object, Error>} 2907 */ 2908 async deleteBucket(bucket, options = {}) { 2909 const bucketObj = toDataBody(bucket); 2910 if (!bucketObj.id) { 2911 throw new Error("A bucket id is required."); 2912 } 2913 const path = ENDPOINTS.bucket(bucketObj.id); 2914 const { last_modified } = { ...bucketObj, ...options }; 2915 return this.execute( 2916 deleteRequest(path, { 2917 last_modified, 2918 headers: this._getHeaders(options), 2919 safe: this._getSafe(options), 2920 }), 2921 { retry: this._getRetry(options) } 2922 ); 2923 } 2924 /** 2925 * Deletes buckets. 2926 * 2927 * @param {Object} [options={}] The options object. 2928 * @param {Boolean} [options.safe] The safe option. 2929 * @param {Object} [options.headers={}] Headers to use when making 2930 * this request. 2931 * @param {Number} [options.retry=0] Number of retries to make 2932 * when faced with transient errors. 2933 * @param {Object} [options.filters={}] The filters object. 2934 * @param {Array} [options.fields] Limit response to 2935 * just some fields. 2936 * @param {Number} [options.last_modified] The last_modified option. 2937 * @return {Promise<Object[], Error>} 2938 */ 2939 async deleteBuckets(options = {}) { 2940 const path = ENDPOINTS.bucket(); 2941 return this.paginatedDelete(path, options, { 2942 headers: this._getHeaders(options), 2943 retry: this._getRetry(options), 2944 safe: options.safe, 2945 last_modified: options.last_modified, 2946 }); 2947 } 2948 async createAccount(username, password) { 2949 return this.execute( 2950 createRequest( 2951 `/accounts/${username}`, 2952 { data: { password } }, 2953 { method: "PUT" } 2954 ) 2955 ); 2956 } 2957 } 2958 __decorate( 2959 [nobatch("This operation is not supported within a batch operation.")], 2960 KintoClientBase.prototype, 2961 "fetchServerSettings", 2962 null 2963 ); 2964 __decorate( 2965 [nobatch("This operation is not supported within a batch operation.")], 2966 KintoClientBase.prototype, 2967 "fetchServerCapabilities", 2968 null 2969 ); 2970 __decorate( 2971 [nobatch("This operation is not supported within a batch operation.")], 2972 KintoClientBase.prototype, 2973 "fetchUser", 2974 null 2975 ); 2976 __decorate( 2977 [nobatch("This operation is not supported within a batch operation.")], 2978 KintoClientBase.prototype, 2979 "fetchHTTPApiVersion", 2980 null 2981 ); 2982 __decorate( 2983 [nobatch("Can't use batch within a batch!")], 2984 KintoClientBase.prototype, 2985 "batch", 2986 null 2987 ); 2988 __decorate( 2989 [capable(["permissions_endpoint"])], 2990 KintoClientBase.prototype, 2991 "listPermissions", 2992 null 2993 ); 2994 __decorate( 2995 [support("1.4", "2.0")], 2996 KintoClientBase.prototype, 2997 "deleteBuckets", 2998 null 2999 ); 3000 __decorate( 3001 [capable(["accounts"])], 3002 KintoClientBase.prototype, 3003 "createAccount", 3004 null 3005 ); 3006 3007 /* 3008 * 3009 * Licensed under the Apache License, Version 2.0 (the "License"); 3010 * you may not use this file except in compliance with the License. 3011 * You may obtain a copy of the License at 3012 * 3013 * http://www.apache.org/licenses/LICENSE-2.0 3014 * 3015 * Unless required by applicable law or agreed to in writing, software 3016 * distributed under the License is distributed on an "AS IS" BASIS, 3017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 3018 * See the License for the specific language governing permissions and 3019 * limitations under the License. 3020 */ 3021 /* @ts-ignore */ 3022 class KintoHttpClient extends KintoClientBase { 3023 constructor(remote, options = {}) { 3024 const events = {}; 3025 EventEmitter.decorate(events); 3026 super(remote, { events: events, ...options }); 3027 } 3028 } 3029 KintoHttpClient.errors = errors; 3030 3031 export { KintoHttpClient };