import { AxiosError } from 'axios' ; import { IncomingMessage } from 'http' ; import mime from 'mime-types' ; import { basename } from 'path' ; import { Readable } from 'stream' ; import { createGzip } from 'zlib' ; import { Logger } from './../../service/logger/logger' ; import { inflightURL , inflightUrlWithQuery , InstanceOptions , IOResponse , RequestTracingConfig , } from '../../HttpClient' ; import { IgnoreNotFoundRequestConfig , } from '../../HttpClient/middlewares/notFound' ; import { BucketMetadata , FileListItem } from '../../responses' ; import { IOContext } from '../../service/worker/runtime/typings' ; import { InfraClient } from './InfraClient' ; const appId = process . env . VTEX_APP_ID ; const [ runningAppName ] = appId ? appId . split ( '@' ) : [ '' ] ; const routes = { Bucket : ( bucket ) => `/buckets/${ runningAppName }/${ bucket }` , Conflicts : ( bucket ) => `/buckets/${ runningAppName }/${ bucket }/conflicts` , File : ( bucket , path ) => `${ routes . Bucket ( bucket ) }/files/${ path }` , Files : ( bucket ) => `${ routes . Bucket ( bucket ) }/files` , } ; const isVBaseOptions = ( opts ? ) : opts is VBaseOptions => { return typeof opts !== 'string' && ! ( opts instanceof String ) ; } ; export class VBase extends InfraClient { constructor ( context , options ? ) { super ( 'vbase@2.x' , context , options ) ; if ( runningAppName === '' ) { throw new Error ( `Invalid path to access VBase. Variable VTEX_APP_ID is not available.` ) ; } } public getBucket = ( bucket , tracingConfig ? ) => { const inflightKey = inflightURL ; const metric = 'vbase-get-bucket' ; return this . http . get < BucketMetadata > ( routes . Bucket ( bucket ) , { inflightKey , metric , tracing : { requestSpanNameSuffix : metric , ... tracingConfig ?. tracing , } } ) ; } ; public resetBucket = ( bucket , tracingConfig ? ) => { const metric = 'vbase-reset-bucket' ; return this . http . delete ( routes . Files ( bucket ) , { metric , tracing : { requestSpanNameSuffix : metric , ... tracingConfig ?. tracing , } } ) ; } ; public listFiles = ( bucket , opts ? , tracingConfig ? ) => { let params = { } ; if ( isVBaseOptions ( opts ) ) { params = opts ; } else if ( opts ) { params = { prefix : opts } ; } const metric = 'vbase-list' ; const inflightKey = inflightUrlWithQuery ; return this . http . get < BucketFileList > ( routes . Files ( bucket ) , { inflightKey , metric , params , tracing : { requestSpanNameSuffix : metric , ... tracingConfig ?. tracing , } } ) ; } ; public getFile = ( bucket , path , tracingConfig ? ) => { const inflightKey = inflightURL ; const metric = 'vbase-get-file' ; return this . http . getBuffer ( routes . File ( bucket , path ) , { inflightKey , metric , tracing : { requestSpanNameSuffix : metric , ... tracingConfig ?. tracing , } } ) ; } ; public getJSON = < T > ( bucket , path , nullIfNotFound ? , conflictsResolver ? , tracingConfig ? ) => { return this . getRawJSON < T > ( bucket , path , nullIfNotFound , conflictsResolver , tracingConfig ) . then ( response => response . data ) ; } ; public getRawJSON = < T > ( bucket , path , nullIfNotFound ? , conflictsResolver ? , tracingConfig ? ) => { const headers = conflictsResolver ? { 'X-Vtex-Detect-Conflicts' : true } : { } ; const inflightKey = inflightURL ; const metric = 'vbase-get-json' ; return this . http . getRaw < T > ( routes . File ( bucket , path ) , { headers , inflightKey , metric , nullIfNotFound , tracing : { requestSpanNameSuffix : metric , ... tracingConfig ?. tracing , } } as IgnoreNotFoundRequestConfig ) . catch ( async ( error ) => { const { response } = error ; if ( response && response . status === 409 && conflictsResolver ) { const conflictsMergedData = await conflictsResolver . resolve ( this . context . logger ) ; return { ... response , data : conflictsMergedData } as IOResponse < T > ; } throw error ; } ) ; } ; public getFileStream = ( bucket , path , tracingConfig ? ) : Promise < IncomingMessage > => { const metric = 'vbase-get-file-s' ; return this . http . getStream ( routes . File ( bucket , path ) , { metric , tracing : { requestSpanNameSuffix : metric , ... tracingConfig ?. tracing , } } ) ; } ; public saveFile = ( bucket , path , stream , gzip = true , ttl ? , tracingConfig ? , ifMatch ? ) => { return this . saveContent ( bucket , path , stream , { gzip , ttl } , tracingConfig , ifMatch ) ; } ; public getFileMetadata = ( bucket , path , tracingConfig ? ) => { const metric = 'vbase-get-file-metadata' ; return this . http . head ( routes . File ( bucket , path ) , { metric , tracing : { requestSpanNameSuffix : metric , ... tracingConfig ?. tracing , } } ) ; } ; public saveJSON = < T > ( bucket , path , data , tracingConfig ? , ifMatch ? ) => { const headers = { 'Content-Type' : 'application/json' } ; if ( ifMatch ) { headers [ 'If-Match' ] = ifMatch ; } const metric = 'vbase-save-json' ; return this . http . put ( routes . File ( bucket , path ) , data , { headers , metric , tracing : { requestSpanNameSuffix : metric , ... tracingConfig ?. tracing , } } ) ; } ; public saveZippedContent = ( bucket , path , stream , tracingConfig ? , ifMatch ? ) => { return this . saveContent ( bucket , path , stream , { unzip : true } , tracingConfig , ifMatch ) ; } ; public deleteFile = ( bucket , path , tracingConfig ? , ifMatch ? ) => { const headers = ifMatch ? { 'If-Match' : ifMatch } : null ; const metric = 'vbase-delete-file' ; return this . http . delete ( routes . File ( bucket , path ) , { headers , metric , tracing : { requestSpanNameSuffix : metric , ... tracingConfig ?. tracing , } } ) ; } ; public getConflicts = < T > ( bucket , tracingConfig ? ) => { const metric = 'vbase-get-conflicts' ; return this . http . get < T > ( routes . Conflicts ( bucket ) , { metric , tracing : { requestSpanNameSuffix : metric , ... tracingConfig ?. tracing , } } ) ; } ; public resolveConflict = < T > ( bucket , path , content , tracingConfig ? ) => { const data = [ { op : 'replace' , path , value : content , } ] ; const metric = 'vbase-resolve-conflicts' ; return this . http . patch < T > ( routes . Conflicts ( bucket ) , data , { metric , tracing : { requestSpanNameSuffix : metric , ... tracingConfig ?. tracing , } } ) ; } ; private saveContent = ( bucket , path , stream , opts = { } , tracingConfig ? , ifMatch ? ) => { if ( ! stream . pipe || ! stream . on ) { throw new Error ( `Argument stream must be a readable stream` ) ; } const params = opts . unzip ? { unzip : opts . unzip } : { } ; const headers = { } ; let finalStream = stream ; headers [ 'Content-Type' ] = mime . contentType ( basename ( path ) ) || 'application/octet-stream' ; if ( opts . gzip ) { headers [ 'Content-Encoding' ] = 'gzip' ; finalStream = stream . pipe ( createGzip ( ) ) ; } if ( opts . ttl && Number . isInteger ( opts . ttl ) ) { headers [ 'X-VTEX-TTL' ] = opts . ttl ; } if ( ifMatch ) { headers [ 'If-Match' ] = ifMatch ; } const metric = 'vbase-save-blob' ; return this . http . put ( routes . File ( bucket , path ) , finalStream , { headers , metric , params , tracing : { requestSpanNameSuffix : metric , ... tracingConfig ?. tracing , } } ) ; } ; } interface Headers { [ key ] : string | number ; } export interface BucketFileList { data : FileListItem [ ] ; next : string ; smartCacheHeaders : any ; } export interface VBaseOptions { prefix ? : string ; next ? : string ; limit ? : number ; } export interface VBaseSaveOptions { gzip ? : boolean ; unzip ? : boolean ; ttl ? : number ; } export interface VBaseConflictData { path : string ; base : VBaseConflict ; master : VBaseConflict ; mine : VBaseConflict ; } export interface VBaseConflict { contentOmitted : boolean ; deleted : boolean ; mimeType : string ; parsedContent ? : any ; content : string ; } export interface ConflictsResolver < T > { resolve : ( logger ? : Logger ) => T | Promise < T > ; }