import { v4 as uuid } from "uuid" ; import { capable , toDataBody , isObject } from "./utils" ; import * as requests from "./requests" ; import KintoClientBase , { PaginatedParams , PaginationResult } from "./base" ; import Bucket from "./bucket" ; import { KintoRequest , Permission , KintoResponse , KintoIdObject , KintoObject , Attachment , OperationResponse , MappableObject , } from "./types" ; import { HttpResponse } from "./http" ; import { AggregateResponse } from "./batch" ; export interface CollectionOptions { headers ? : Record < string , string > ; safe ? : boolean ; retry ? : number ; } /**
 * Abstract representation of a selected collection.
 *
 */ export default class Collection { public client : KintoClientBase ; private bucket : Bucket ; public name : string ; private _endpoints : KintoClientBase [ "endpoints" ] ; private _retry : number ; private _safe : boolean ; private _headers : Record < string , string > ; param { KintoClient } client param { Bucket } bucket param { String } name param options param { Object } options . headers param { Boolean } options . safe param { Number } options . retry param { Boolean } options . batch constructor ( client , bucket , name , options = { } ) { ignore this . client = client ; ignore this . bucket = bucket ; type { String } this . name = name ; this . _endpoints = client . endpoints ; ignore this . _retry = options . retry || 0 ; this . _safe = ! ! options . safe ; this . _headers = { ... this . bucket . headers , ... options . headers , } ; } get execute ( ) : KintoClientBase [ "execute" ] { return this . client . execute . bind ( this . client ) ; } private private _getHeaders ( options ) { return { ... this . _headers , ... options . headers , } ; } private param { Object } options returns { Boolean } private _getSafe ( options ) { return { safe : this . _safe , ... options } . safe ; } private private _getRetry ( options ) { return { retry : this . _retry , ... options } . retry ; } param options param { Object } options . headers param { Number } options . retry return { Promise < Number , Error > } async getTotalRecords ( options = { } ) { const path = this . _endpoints . record ( this . bucket . name , this . name ) ; const request = { headers : this . _getHeaders ( options ) , path , method : "HEAD" , } ; const { headers } = await this . client . execute ( request , { raw : true , retry : this . _getRetry ( options ) , } ) ; return parseInt ( headers . get ( "Total-Records" ) , 10 ) ; } param options param { Object } options . headers param { Number } options . retry return { Promise < String , Error > } async getRecordsTimestamp ( options = { } ) { const path = this . _endpoints . record ( this . bucket . name , this . name ) ; const request = { headers : this . _getHeaders ( options ) , path , method : "HEAD" , } ; const { headers } = ( await this . client . execute ( request , { raw : true , retry : this . _getRetry ( options ) , } ) ) as HttpResponse < { } > ; return headers . get ( "ETag" ) ; } param options param { Object } options . headers param { Object } options . query param { Array } options . fields param { Number } options . retry return { Promise < Object , Error > } async getData < T > ( options = { } ) { const path = this . _endpoints . collection ( this . bucket . name , this . name ) ; const request = { headers : this . _getHeaders ( options ) , path } ; const { data } = ( await this . client . execute ( request , { retry : this . _getRetry ( options ) , query : options . query , fields : options . fields , } ) ) as { data : T ; } ; return data ; } param { Object } data param options param { Object } options . headers param { Number } options . retry param { Boolean } options . safe param { Boolean } options . patch param { Number } options . last_modified return { Promise < Object , Error > } async setData < T extends MappableObject > ( data , options = { } ) { if ( ! isObject ( data ) ) { throw new Error ( "A collection object is required." ) ; } const { patch , permissions } = options ; const { last_modified } = { ... data , ... options } ; const path = this . _endpoints . collection ( this . bucket . name , this . name ) ; const request = requests . updateRequest ( path , { data , permissions } , { last_modified , patch , headers : this . _getHeaders ( options ) , safe : this . _getSafe ( options ) , } ) ; return this . client . execute < KintoResponse < T > > ( request , { retry : this . _getRetry ( options ) , } ) as Promise < KintoResponse < T > > ; } param options param { Object } options . headers param { Number } options . retry return { Promise < Object , Error > } async getPermissions ( options = { } ) { const path = this . _endpoints . collection ( this . bucket . name , this . name ) ; const request = { headers : this . _getHeaders ( options ) , path } ; const { permissions } = ( await this . client . execute < KintoResponse > ( request , { retry : this . _getRetry ( options ) , } ) ) as KintoResponse ; return permissions ; } param { Object } permissions param options param { Object } options . headers param { Number } options . retry param { Boolean } options . safe param { Number } options . last_modified return { Promise < Object , Error > } async setPermissions ( permissions , options = { } ) { if ( ! isObject ( permissions ) ) { throw new Error ( "A permissions object is required." ) ; } const path = this . _endpoints . collection ( this . bucket . name , this . name ) ; const data = { last_modified : options . last_modified } ; const request = requests . updateRequest ( path , { data , permissions } , { headers : this . _getHeaders ( options ) , safe : this . _getSafe ( options ) , } ) ; return this . client . execute < KintoResponse < { } > > ( request , { retry : this . _getRetry ( options ) , } ) as Promise < KintoResponse < { } > > ; } param { Object } permissions param options param { Boolean } options . safe param { Object } options . headers param { Number } options . retry param { Object } options . last_modified return { Promise < Object , Error > } async addPermissions ( permissions , options = { } ) { if ( ! isObject ( permissions ) ) { throw new Error ( "A permissions object is required." ) ; } const path = this . _endpoints . collection ( this . bucket . name , this . name ) ; const { last_modified } = options ; const request = requests . jsonPatchPermissionsRequest ( path , permissions , "add" , { last_modified , headers : this . _getHeaders ( options ) , safe : this . _getSafe ( options ) , } ) ; return this . client . execute < KintoResponse < { } > > ( request , { retry : this . _getRetry ( options ) , } ) as Promise < KintoResponse < { } > > ; } param { Object } permissions param options param { Boolean } options . safe param { Object } options . headers param { Number } options . retry param { Object } options . last_modified return { Promise < Object , Error > } async removePermissions ( permissions , options = { } ) { if ( ! isObject ( permissions ) ) { throw new Error ( "A permissions object is required." ) ; } const path = this . _endpoints . collection ( this . bucket . name , this . name ) ; const { last_modified } = options ; const request = requests . jsonPatchPermissionsRequest ( path , permissions , "remove" , { last_modified , headers : this . _getHeaders ( options ) , safe : this . _getSafe ( options ) , } ) ; return this . client . execute < KintoResponse < { } > > ( request , { retry : this . _getRetry ( options ) , } ) as Promise < KintoResponse < { } > > ; } param { Object } record param options param { Object } options . headers param { Number } options . retry param { Boolean } options . safe param { Object } options . permissions return { Promise < Object , Error > } async createRecord < T extends MappableObject > ( record , options = { } ) { const { permissions } = options ; const path = this . _endpoints . record ( this . bucket . name , this . name , record . id ) ; const request = requests . createRequest ( path , { data : record , permissions } , { headers : this . _getHeaders ( options ) , safe : this . _getSafe ( options ) , } ) ; return this . client . execute < KintoResponse < T > > ( request , { retry : this . _getRetry ( options ) , } ) as Promise < KintoResponse < T > > ; } param { String } dataURL param { Object } record param options param { Object } options . headers param { Number } options . retry param { Boolean } options . safe param { Number } options . last_modified param { Object } options . permissions param { String } options . filename param { String } options . gzipped return { Promise < Object , Error > } @ capable ( [ "attachments" ] ) async addAttachment ( dataURI , record = { } , options = { } ) { const { permissions } = options ; const id = record . id || uuid ( ) ; const path = this . _endpoints . attachment ( this . bucket . name , this . name , id ) ; const { last_modified } = { ... record , ... options } ; const addAttachmentRequest = requests . addAttachmentRequest ( path , dataURI , { data : record , permissions } , { last_modified , filename : options . filename , gzipped : options . gzipped , headers : this . _getHeaders ( options ) , safe : this . _getSafe ( options ) , } ) ; await this . client . execute ( addAttachmentRequest , { stringify : false , retry : this . _getRetry ( options ) , } ) ; return this . getRecord < { attachment : Attachment ; } > ( id ) ; } param { Object } recordId param options param { Object } options . headers param { Number } options . retry param { Boolean } options . safe param { Number } options . last_modified @ capable ( [ "attachments" ] ) async removeAttachment ( recordId , options = { } ) { const { last_modified } = options ; const path = this . _endpoints . attachment ( this . bucket . name , this . name , recordId ) ; const request = requests . deleteRequest ( path , { last_modified , headers : this . _getHeaders ( options ) , safe : this . _getSafe ( options ) , } ) ; return this . client . execute < { } > ( request , { retry : this . _getRetry ( options ) , } ) as Promise < { } > ; } param { Object } record param options param { Object } options . headers param { Number } options . retry param { Boolean } options . safe param { Number } options . last_modified param { Object } options . permissions return { Promise < Object , Error > } async updateRecord < T > ( record , options = { } ) { if ( ! isObject ( record ) ) { throw new Error ( "A record object is required." ) ; } if ( ! record . id ) { throw new Error ( "A record id is required." ) ; } const { permissions } = options ; const { last_modified } = { ... record , ... options } ; const path = this . _endpoints . record ( this . bucket . name , this . name , record . id ) ; const request = requests . updateRequest ( path , { data : record , permissions } , { headers : this . _getHeaders ( options ) , safe : this . _getSafe ( options ) , last_modified , patch : ! ! options . patch , } ) ; return this . client . execute < KintoResponse < T > > ( request , { retry : this . _getRetry ( options ) , } ) as Promise < KintoResponse < T > > ; } param { Object | String } record param options param { Object } options . headers param { Number } options . retry param { Boolean } options . safe param { Number } options . last_modified return { Promise < Object , Error > } async deleteRecord ( record , options = { } ) { const recordObj = toDataBody ( record ) ; if ( ! recordObj . id ) { throw new Error ( "A record id is required." ) ; } const { id } = recordObj ; const { last_modified } = { ... recordObj , ... options } ; const path = this . _endpoints . record ( this . bucket . name , this . name , id ) ; const request = requests . deleteRequest ( path , { last_modified , headers : this . _getHeaders ( options ) , safe : this . _getSafe ( options ) , } ) ; return this . client . execute < KintoResponse < { deleted : boolean ; } > > ( request , { retry : this . _getRetry ( options ) , } ) as Promise < KintoResponse < { deleted : boolean ; } > > ; } see http see http param options param { Object } options . headers param { Number } options . retry param { Object } options . filters param { String } options . sort param { String } options . at param { String } options . limit param { String } options . pages param { Number } options . since param { Array } options . fields return { Promise < Object , Error > } async deleteRecords < T extends KintoObject > ( options = { } ) { const path = this . _endpoints . record ( this . bucket . name , this . name ) ; return this . client . paginatedDelete < T > ( path , options , { headers : this . _getHeaders ( options ) , retry : this . _getRetry ( options ) , } ) ; } param { String } id param options param { Object } options . headers param { Object } options . query param { Array } options . fields param { Number } options . retry return { Promise < Object , Error > } async getRecord < T > ( id , options = { } ) { const path = this . _endpoints . record ( this . bucket . name , this . name , id ) ; const request = { headers : this . _getHeaders ( options ) , path } ; return this . client . execute < KintoResponse < T > > ( request , { retry : this . _getRetry ( options ) , query : options . query , fields : options . fields , } ) as Promise < KintoResponse < T > > ; } see http see http param options param { Object } options . headers param { Number } options . retry param { Object } options . filters param { String } options . sort param { String } options . at param { String } options . limit param { String } options . pages param { Number } options . since param { Array } options . fields return { Promise < Object , Error > } async listRecords < T extends KintoObject > ( options = { } ) { const path = this . _endpoints . record ( this . bucket . name , this . name ) ; if ( options . at ) { return this . getSnapshot < T > ( options . at ) ; } else { return this . client . paginatedList < T > ( path , options , { headers : this . _getHeaders ( options ) , retry : this . _getRetry ( options ) , } ) ; } } private async isHistoryComplete ( ) { const { data : [ oldestHistoryEntry ] , } = await this . bucket . listHistory ( { limit : 1 , filters : { action : "create" , resource_name : "collection" , collection_id : this . name , } , } ) ; return ! ! oldestHistoryEntry ; } private @ capable ( [ "history" ] ) async getSnapshot < T extends KintoObject > ( at ) { if ( ! at || ! Number . isInteger ( at ) || at <= 0 ) { throw new Error ( "Invalid argument, expected a positive integer." ) ; } if ( ! ( await this . isHistoryComplete ( ) ) ) { throw new Error ( "Computing a snapshot is only possible when the full history for a " + "collection is available. Here, the history plugin seems to have " + "been enabled after the creation of the collection." ) ; } const { data : fullHistory } = await this . bucket . listHistory < T > ( { pages : Infinity , sort : "last_modified" , filters : { resource_name : "record" , collection_id : this . name , } , } ) ; const latestEver = new Map ( ) ; const latestInSnapshot = new Map ( ) ; for ( const entry of fullHistory ) { if ( entry . target . data . last_modified <= at ) { latestInSnapshot . set ( entry . record_id , entry ) ; } latestEver . set ( entry . record_id , entry ) ; } const { data : current } = await this . listRecords ( { pages : Infinity , fields : [ "id" ] , } ) ; const currentIds = new Set ( current . map ( ( record ) => record . id ) ) ; const deletedViaPlural = new Set ( ) ; for ( const entry of latestEver . values ( ) ) { if ( entry . action != "delete" && ! currentIds . has ( entry . record_id ) ) { deletedViaPlural . add ( entry . record_id ) ; } } const reconstructed = [ ] ; for ( const entry of latestInSnapshot . values ( ) ) { if ( entry . action != "delete" && ! deletedViaPlural . has ( entry . record_id ) ) { reconstructed . push ( entry . target . data ) ; } } return { last_modified : String ( at ) , data : Array . from ( reconstructed ) . sort ( ( a , b ) => b . last_modified - a . last_modified ) , next : ( ) => { throw new Error ( "Snapshots don't support pagination" ) ; } , hasNextPage : false , totalRecords : reconstructed . length , } as PaginationResult < T > ; } param { Function } fn param options param { Object } options . headers param { Boolean } options . safe param { Number } options . retry param { Boolean } options . aggregate return { Promise < Object , Error > } async batch ( fn , options = { } ) { return this . client . batch ( fn , { bucket : this . bucket . name , collection : this . name , headers : this . _getHeaders ( options ) , retry : this . _getRetry ( options ) , safe : this . _getSafe ( options ) , aggregate : ! ! options . aggregate , } ) ; } }