import { partition , qsify , support , nobatch , toDataBody , cleanUndefinedProperties , } from "./utils" ; import HTTP , { HttpResponse } from "./http" ; import endpoints from "./endpoints" ; import * as requests from "./requests" ; import { aggregate , AggregateResponse } from "./batch" ; import Bucket from "./bucket" ; import { addEndpointOptions , capable } from "./utils" ; import { HelloResponse , KintoRequest , BatchResponse , OperationResponse , DataResponse , Permission , KintoIdObject , MappableObject , KintoObject , PermissionData , KintoResponse , ServerSettings , ServerCapability , User , Emitter , HttpMethod , FetchFunction , } from "./types" ; import Collection from "./collection" ; type { String } export const SUPPORTED_PROTOCOL_VERSION = "v1" ; export interface KintoClientOptions { safe ? : boolean ; events ? : Emitter ; headers ? : Record < string , string > ; retry ? : number ; bucket ? : string ; requestMode ? : RequestMode ; timeout ? : number ; batch ? : boolean ; fetchFunc ? : FetchFunction ; } export interface PaginatedParams { sort ? : string ; filters ? : Record < string , string | number > ; limit ? : number ; pages ? : number ; since ? : string ; fields ? : string [ ] ; } export interface PaginationResult < T > { last_modified : string | null ; data : T [ ] ; next : ( nextPage ? : string | null ) => Promise < PaginationResult < T > > ; hasNextPage : boolean ; totalRecords : number ; } type BaseBatch = ( client : KintoClientBase ) => void ; type BucketBatch = ( client : Bucket ) => void ; type CollectionBatch = ( client : Collection ) => void ; example export default class KintoClientBase { private _backoffReleaseTime : number | null ; private _requests : KintoRequest [ ] ; private _isBatch : boolean ; private _retry : number ; private _safe : boolean ; private _headers : Record < string , string > ; public serverInfo : HelloResponse | null ; public events ? : Emitter ; public http : HTTP ; public endpoints : typeof endpoints ; private _remote ! : string ; private _version ! : string ; param { String } remote param options param { Boolean } options . safe param { EventEmitter } options . events param { Object } options . headers param { Object } options . retry param { String } options . bucket param { String } options . requestMode param { Number } options . timeout param { Function } options . fetchFunc constructor ( remote , options ) { if ( typeof remote !== "string" || ! remote . length ) { throw new Error ( "Invalid remote URL: " + remote ) ; } if ( remote [ remote . length - 1 ] === "/" ) { remote = remote . slice ( 0 , - 1 ) ; } this . _backoffReleaseTime = null ; this . _requests = [ ] ; this . _isBatch = ! ! options . batch ; this . _retry = options . retry || 0 ; this . _safe = ! ! options . safe ; this . _headers = options . headers || { } ; type { String } this . remote = remote ; ignore type { Object | null } this . serverInfo = null ; ignore type { Class } this . events = options . events ; this . endpoints = endpoints ; const { fetchFunc , requestMode , timeout } = options ; ignore type { HTTP } this . http = new HTTP ( this . events , { fetchFunc , requestMode , timeout } ) ; this . _registerHTTPEvents ( ) ; } type { String } get remote ( ) : string { return this . _remote ; } ignore set remote ( url ) { let version ; try { version = url . match ( /\/(v\d+)\/?$/ ) ! [ 1 ] ; } catch ( err ) { throw new Error ( "The remote URL must contain the version: " + url ) ; } if ( version !== SUPPORTED_PROTOCOL_VERSION ) { throw new Error ( `Unsupported protocol version: ${ version }` ) ; } this . _remote = url ; this . _version = version ; } type { String } get version ( ) : string { return this . _version ; } type { Number } get backoff ( ) : number { const currentTime = new Date ( ) . getTime ( ) ; if ( this . _backoffReleaseTime && currentTime < this . _backoffReleaseTime ) { return this . _backoffReleaseTime - currentTime ; } return 0 ; } private private _registerHTTPEvents ( ) { if ( ! this . _isBatch && this . events ) { this . events . on ( "backoff" , ( backoffMs ) => { this . _backoffReleaseTime = backoffMs ; } ) ; } } param { String } name param options param { Boolean } options . safe param { Number } options . retry param { Object } options . headers return { Bucket } bucket ( name , options = { } ) { return new Bucket ( this , name , { headers : this . _getHeaders ( options ) , safe : this . _getSafe ( options ) , retry : this . _getRetry ( options ) , } ) ; } param { Object } headers setHeaders ( headers ) { this . _headers = { ... this . _headers , ... headers , } ; this . serverInfo = null ; } private param { Object } options returns { Object } 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 ; } private param options param { Object } options . headers param { Number } options . retry return { Promise < Object , Error > } private async _getHello ( options = { } ) { const path = this . remote + endpoints . root ( ) ; const { json } = await this . http . request < HelloResponse > ( path , { headers : this . _getHeaders ( options ) } , { retry : this . _getRetry ( options ) } ) ; return json ; } param options param { Number } options . retry return { Promise < Object , Error > } async fetchServerInfo ( options = { } ) { if ( this . serverInfo ) { return this . serverInfo ; } this . serverInfo = await this . _getHello ( { retry : this . _getRetry ( options ) } ) ; return this . serverInfo ; } param options param { Number } options . retry return { Promise < Object , Error > } @ nobatch ( "This operation is not supported within a batch operation." ) async fetchServerSettings ( options = { } ) { const { settings } = await this . fetchServerInfo ( options ) ; return settings ; } param options param { Number } options . retry return { Promise < Object , Error > } @ nobatch ( "This operation is not supported within a batch operation." ) async fetchServerCapabilities ( options = { } ) { const { capabilities } = await this . fetchServerInfo ( options ) ; return capabilities ; } param options param { Object } options . headers param { Number } options . retry return { Promise < Object , Error > } @ nobatch ( "This operation is not supported within a batch operation." ) async fetchUser ( options = { } ) { const { user } = await this . _getHello ( options ) ; return user ; } param options param { Number } options . retry return { Promise < Object , Error > } @ nobatch ( "This operation is not supported within a batch operation." ) async fetchHTTPApiVersion ( options = { } ) { const { http_api_version } = await this . fetchServerInfo ( options ) ; return http_api_version ; } param { Array } requests param { Object } options return { Promise < Object , Error > } private async _batchRequests ( requests , options = { } ) { const headers = this . _getHeaders ( options ) ; if ( ! requests . length ) { return [ ] ; } const serverSettings = await this . fetchServerSettings ( { retry : this . _getRetry ( options ) , } ) ; const maxRequests = serverSettings [ "batch_max_requests" ] ; if ( maxRequests && requests . length > maxRequests ) { const chunks = partition ( requests , maxRequests ) ; const results = [ ] ; for ( const chunk of chunks ) { const result = await this . _batchRequests ( chunk , options ) ; results . push ( ... result ) ; } return results ; } const { responses } = ( await this . execute < BatchResponse > ( { headers , path : endpoints . batch ( ) , method : "POST" , body : { defaults : { headers } , requests , } , } , { retry : this . _getRetry ( options ) } ) ) as BatchResponse ; return responses ; } ignore param { Function } fn param options param { Boolean } options . safe param { Number } options . retry param { String } options . bucket param { String } options . collection param { Object } options . headers param { Boolean } options . aggregate return { Promise < Object , Error > } @ nobatch ( "Can't use batch within a batch!" ) async batch ( fn , options = { } ) { const rootBatch = new KintoClientBase ( this . remote , { events : this . events , batch : true , safe : this . _getSafe ( options ) , retry : this . _getRetry ( options ) , } ) ; if ( options . bucket && options . collection ) { ( fn as CollectionBatch ) ( rootBatch . bucket ( options . bucket ) . collection ( options . collection ) ) ; } else if ( options . bucket ) { ( fn as BucketBatch ) ( rootBatch . bucket ( options . bucket ) ) ; } else { ( fn as BaseBatch ) ( rootBatch ) ; } const responses = await this . _batchRequests ( rootBatch . _requests , options ) ; if ( options . aggregate ) { return aggregate ( responses , rootBatch . _requests ) ; } else { return responses ; } } param request param { String } request . path param { String } request . method param { Body } request . body param { Object } request . headers param options param { Boolean } options . raw param { Boolean } options . stringify param { Number } options . retry return { Promise < Object , Error > } async execute < T > ( request , options = { } ) { const { raw = false , stringify = true } = options ; if ( this . _isBatch ) { this . _requests . push ( request ) ; const msg = ( "This result is generated from within a batch " + "operation and should not be consumed." ) as unknown as T ; return raw ? ( { status : 0 , json : msg , headers : new Headers ( ) } as HttpResponse < T > ) : msg ; } const uri = this . remote + addEndpointOptions ( request . path , options ) ; const result = await this . http . request < T > ( uri , cleanUndefinedProperties ( { method : request . method , headers : request . headers , body : stringify ? JSON . stringify ( request . body ) : request . body , } ) , { retry : this . _getRetry ( options ) } ) ; return raw ? result : result . json ; } private param { String } path param params param { String } params . sort param { Object } params . filters param { Number } params . limit param { Number } params . pages param { String } params . since param { Array } params . fields param options param { Object } options . headers param { Number } options . retry param { String } options . method async paginatedOperation < T > ( path , params = { } , options = { } ) { const { sort , filters , limit , pages , since , fields } = { sort : "-last_modified" , ... params , } ; if ( since && typeof since !== "string" ) { throw new Error ( `Invalid value for since (${ since }), should be ETag value.` ) ; } const query = { ... filters , _sort : sort , _limit : limit , _since : since , } ; if ( fields ) { query . _fields = fields ; } const querystring = qsify ( query ) ; let results = [ ] , current = 0 ; const next = async function ( nextPage ) : Promise < PaginationResult < T > > { if ( ! nextPage ) { throw new Error ( "Pagination exhausted." ) ; } return processNextPage ( nextPage ) ; } ; const processNextPage = async ( nextPage ) : Promise < PaginationResult < T > > => { const { headers } = options ; return handleResponse ( await this . http . request ( nextPage , { headers } ) ) ; } ; const pageResults = ( results , nextPage , etag ) : PaginationResult < T > => { return { last_modified : etag ? etag . replace ( /"/g , "" ) : etag , data : results , next : next . bind ( null , nextPage ) , hasNextPage : ! ! nextPage , totalRecords : - 1 , } ; } ; const handleResponse = async function ( { headers = new Headers ( ) , json = { } as DataResponse < T [ ] > , } ) : Promise < PaginationResult < T > > { const nextPage = headers . get ( "Next-Page" ) ; const etag = headers . get ( "ETag" ) ; if ( ! pages ) { return pageResults ( json . data , nextPage , etag ) ; } results = results . concat ( json . data ) ; current += 1 ; if ( current >= pages || ! nextPage ) { return pageResults ( results , nextPage , etag ) ; } return processNextPage ( nextPage ) ; } ; return handleResponse ( ( await this . execute ( { headers : options . headers ? options . headers : { } , path : path + "?" + querystring , method : options . method , } , { raw : true , retry : options . retry || 0 } ) ) as HttpResponse < DataResponse < T [ ] > > ) ; } private param { String } path param params param { String } params . sort param { Object } params . filters param { Number } params . limit param { Number } params . pages param { String } params . since param { Array } params . fields param options param { Object } options . headers param { Number } options . retry async paginatedList < T > ( path , params = { } , options = { } ) { return this . paginatedOperation < T > ( path , params , options ) ; } private param { String } path param params param { String } params . sort param { Object } params . filters param { Number } params . limit param { Number } params . pages param { String } params . since param { Array } params . fields param options param { Object } options . headers param { Number } options . retry paginatedDelete < T > ( path , params = { } , options = { } ) { const { headers , safe , last_modified } = options ; const deleteRequest = requests . deleteRequest ( path , { headers , safe : safe ? safe : false , last_modified , } ) ; return this . paginatedOperation < T > ( path , params , { ... options , headers : deleteRequest . headers as Record < string , string > , method : "DELETE" , } ) ; } param options param { Object } options . headers param { Number } options . retry return { Promise < Object [ ] , Error > } @ capable ( [ "permissions_endpoint" ] ) async listPermissions ( options = { } ) { const path = endpoints . permissions ( ) ; const paginationOptions = { sort : "id" , ... options } ; return this . paginatedList < PermissionData > ( path , paginationOptions , { headers : this . _getHeaders ( options ) , retry : this . _getRetry ( options ) , } ) ; } param options param { Object } options . headers param { Number } options . retry param { Object } options . filters param { Array } options . fields return { Promise < Object [ ] , Error > } async listBuckets ( options = { } ) { const path = endpoints . bucket ( ) ; return this . paginatedList < KintoObject > ( path , options , { headers : this . _getHeaders ( options ) , retry : this . _getRetry ( options ) , } ) ; } param { String | null } id param options param { Boolean } options . data param { Boolean } options . safe param { Object } options . headers param { Number } options . retry return { Promise < Object , Error > } async createBucket < T extends MappableObject > ( id , options = { } ) { const { data , permissions } = options ; const _data = { ... data , id : id ? id : undefined } ; const path = _data . id ? endpoints . bucket ( _data . id ) : endpoints . bucket ( ) ; return this . execute < KintoResponse < T > > ( requests . createRequest ( path , { data : _data , permissions } , { headers : this . _getHeaders ( options ) , safe : this . _getSafe ( options ) , } ) , { retry : this . _getRetry ( options ) } ) as Promise < KintoResponse < T > > ; } ignore param { Object | String } bucket param options param { Boolean } options . safe param { Object } options . headers param { Number } options . retry param { Number } options . last_modified return { Promise < Object , Error > } async deleteBucket ( bucket , options = { } ) { const bucketObj = toDataBody ( bucket ) ; if ( ! bucketObj . id ) { throw new Error ( "A bucket id is required." ) ; } const path = endpoints . bucket ( bucketObj . id ) ; const { last_modified } = { ... bucketObj , ... options } ; return this . execute < KintoResponse < { deleted : boolean ; } > > ( requests . deleteRequest ( path , { last_modified , headers : this . _getHeaders ( options ) , safe : this . _getSafe ( options ) , } ) , { retry : this . _getRetry ( options ) } ) as Promise < KintoResponse < { deleted : boolean ; } > > ; } param options param { Boolean } options . safe param { Object } options . headers param { Number } options . retry param { Object } options . filters param { Array } options . fields param { Number } options . last_modified return { Promise < Object [ ] , Error > } @ support ( "1.4" , "2.0" ) async deleteBuckets ( options = { } ) { const path = endpoints . bucket ( ) ; return this . paginatedDelete < KintoObject > ( path , options , { headers : this . _getHeaders ( options ) , retry : this . _getRetry ( options ) , safe : options . safe , last_modified : options . last_modified , } ) ; } @ capable ( [ "accounts" ] ) async createAccount ( username , password ) { return this . execute < KintoResponse < { password : string ; } > > ( requests . createRequest ( `/accounts/${ username }` , { data : { password } } , { method : "PUT" } ) ) as Promise < KintoResponse < { password : string ; } > > ; } }