github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/webui/src/lib/api/index.js (about)

     1  export const API_ENDPOINT = '/api/v1';
     2  export const DEFAULT_LISTING_AMOUNT = 100;
     3  export const MAX_LISTING_AMOUNT = 1000;
     4  
     5  export const SETUP_STATE_INITIALIZED = "initialized";
     6  export const SETUP_STATE_NOT_INITIALIZED = "not_initialized";
     7  
     8  class LocalCache {
     9      get(key) {
    10          const value = localStorage.getItem(key);
    11          if (value !== null) {
    12              return JSON.parse(value);
    13          }
    14          return null;
    15      }
    16  
    17      set(key, value) {
    18          localStorage.setItem(key, JSON.stringify(value));
    19      }
    20  
    21      delete(key) {
    22          localStorage.removeItem(key);
    23      }
    24  }
    25  
    26  const cache = new LocalCache();
    27  
    28  export const qs = (queryParts) => {
    29      const parts = Object.keys(queryParts).map(key => [key, queryParts[key]]);
    30      return new URLSearchParams(parts).toString();
    31  };
    32  
    33  export const linkToPath = (repoId, branchId, path, presign = false) => {
    34      const query = qs({
    35          path,
    36          presign,
    37      });
    38      return `${API_ENDPOINT}/repositories/${repoId}/refs/${branchId}/objects?${query}`;
    39  };
    40  
    41  export const extractError = async (response) => {
    42      let body;
    43      if (response.headers.get('Content-Type') === 'application/json') {
    44          const jsonBody = await response.json();
    45          body = jsonBody.message;
    46      } else {
    47          body = await response.text();
    48      }
    49      return body;
    50  };
    51  
    52  export const defaultAPIHeaders = {
    53      "Accept": "application/json",
    54      "Content-Type": "application/json",
    55      "X-Lakefs-Client": "lakefs-webui/__buildVersion",
    56  };
    57  
    58  const authenticationError = "error authenticating request"
    59  
    60  const apiRequest = async (uri, requestData = {}, additionalHeaders = {}) => {
    61      const headers = new Headers({
    62          ...defaultAPIHeaders,
    63          ...additionalHeaders,
    64      });
    65      const response = await fetch(`${API_ENDPOINT}${uri}`, {headers, ...requestData});
    66  
    67      // check if we're missing credentials
    68      if (response.status === 401) {
    69          const errorMessage = await extractError(response);
    70          if (errorMessage === authenticationError) {
    71              cache.delete('user');
    72              throw new AuthenticationError('Authentication Error', response.status);
    73          }
    74          throw new AuthorizationError(errorMessage, response.status);
    75      }
    76  
    77      return response;
    78  };
    79  
    80  // helper errors
    81  export class NotFoundError extends Error {
    82      constructor(message) {
    83          super(message)
    84          this.name = this.constructor.name;
    85      }
    86  }
    87  
    88  export class BadRequestError extends Error {
    89      constructor(message) {
    90          super(message)
    91          this.name = this.constructor.name;
    92      }
    93  }
    94  
    95  export class AuthorizationError extends Error {
    96      constructor(message) {
    97          super(message);
    98          this.name = this.constructor.name;
    99      }
   100  }
   101  
   102  export class AuthenticationError extends Error {
   103      constructor(message, status) {
   104          super(message);
   105          this.status = status;
   106          this.name = this.constructor.name;
   107      }
   108  }
   109  
   110  export class MergeError extends Error {
   111      constructor(message, payload) {
   112          super(message);
   113          this.name = this.constructor.name;
   114          this.payload = payload;
   115      }
   116  }
   117  
   118  export class RepositoryDeletionError extends Error {
   119      constructor(message, repoId) {
   120          super(message);
   121          this.name = this.constructor.name;
   122          this.repoId = repoId;
   123      }
   124  }
   125  
   126  // actual actions:
   127  class Auth {
   128      async getAuthCapabilities() {
   129          const response = await apiRequest('/auth/capabilities', {
   130              method: 'GET',
   131          });
   132          switch (response.status) {
   133              case 200:
   134                  return await response.json();
   135              default:
   136                  throw new Error('Unknown');
   137          }
   138      }
   139  
   140      async login(accessKeyId, secretAccessKey) {
   141          const response = await fetch(`${API_ENDPOINT}/auth/login`, {
   142              headers: new Headers(defaultAPIHeaders),
   143              method: 'POST',
   144              body: JSON.stringify({access_key_id: accessKeyId, secret_access_key: secretAccessKey})
   145          });
   146  
   147          if (response.status === 401) {
   148              throw new AuthenticationError('invalid credentials', response.status);
   149          }
   150          if (response.status !== 200) {
   151              throw new AuthenticationError('Unknown authentication error', response.status);
   152          }
   153  
   154          this.clearCurrentUser();
   155          const user = await this.getCurrentUser();
   156  
   157          cache.set('user', user);
   158          return user;
   159      }
   160  
   161      clearCurrentUser() {
   162          cache.delete('user');
   163      }
   164  
   165      async getCurrentUserWithCache() {
   166          let user = cache.get('user')
   167          if (!user) {
   168              user = await this.getCurrentUser();
   169              cache.set('user', user);
   170          }
   171          return user
   172      }
   173  
   174      async getCurrentUser() {
   175          const userResponse = await apiRequest('/user')
   176          const body = await userResponse.json();
   177          return body.user;
   178      }
   179  
   180      async listUsers(prefix = "", after = "", amount = DEFAULT_LISTING_AMOUNT) {
   181          const query = qs({prefix, after, amount});
   182          const response = await apiRequest(`/auth/users?${query}`);
   183          if (response.status !== 200) {
   184              throw new Error(`could not list users: ${await extractError(response)}`);
   185          }
   186          return response.json();
   187      }
   188  
   189      async createUser(userId, inviteUser = false) {
   190          const response = await apiRequest(`/auth/users`,
   191              {method: 'POST', body: JSON.stringify({id: userId, invite_user: inviteUser})});
   192          if (response.status !== 201) {
   193              throw new Error(await extractError(response));
   194          }
   195          return response.json();
   196      }
   197  
   198      async listGroups(prefix = "", after = "", amount = DEFAULT_LISTING_AMOUNT) {
   199          const query = qs({prefix, after, amount});
   200          const response = await apiRequest(`/auth/groups?${query}`);
   201          if (response.status !== 200) {
   202              throw new Error(`could not list groups: ${await extractError(response)}`);
   203          }
   204          return response.json();
   205      }
   206  
   207      async listGroupMembers(groupId, after, amount = DEFAULT_LISTING_AMOUNT) {
   208          const query = qs({after, amount});
   209          const response = await apiRequest(`/auth/groups/${encodeURIComponent(groupId)}/members?` + query);
   210          if (response.status !== 200) {
   211              throw new Error(`could not list group members: ${await extractError(response)}`);
   212          }
   213          return response.json();
   214      }
   215  
   216      async getACL(groupId) {
   217          const response = await apiRequest(`/auth/groups/${groupId}/acl`);
   218          if (response.status !== 200) {
   219              throw new Error(`could not get ACL for group ${groupId}: ${await extractError(response)}`);
   220          }
   221          const ret = await response.json();
   222          if (ret.repositories === null || ret.repositories === undefined) {
   223              ret.repositories = [];
   224          }
   225          return ret;
   226      }
   227  
   228      async putACL(groupId, acl) {
   229          const response = await apiRequest(`/auth/groups/${groupId}/acl`, {
   230              method: 'POST',
   231              body: JSON.stringify(acl),
   232          });
   233          if (response.status !== 201) {
   234              throw new Error(`could not set ACL for group ${groupId}: ${await extractError(response)}`);
   235          }
   236      }
   237  
   238      async addUserToGroup(userId, groupId) {
   239          const response = await apiRequest(`/auth/groups/${encodeURIComponent(groupId)}/members/${encodeURIComponent(userId)}`, {method: 'PUT'});
   240          if (response.status !== 201) {
   241              throw new Error(await extractError(response));
   242          }
   243      }
   244  
   245      async removeUserFromGroup(userId, groupId) {
   246          const response = await apiRequest(`/auth/groups/${encodeURIComponent(groupId)}/members/${encodeURIComponent(userId)}`, {method: 'DELETE'});
   247          if (response.status !== 204) {
   248              throw new Error(await extractError(response));
   249          }
   250      }
   251  
   252      async attachPolicyToUser(userId, policyId) {
   253          const response = await apiRequest(`/auth/users/${encodeURIComponent(userId)}/policies/${encodeURIComponent(policyId)}`, {method: 'PUT'});
   254          if (response.status !== 201) {
   255              throw new Error(await extractError(response));
   256          }
   257      }
   258  
   259      async detachPolicyFromUser(userId, policyId) {
   260          const response = await apiRequest(`/auth/users/${encodeURIComponent(userId)}/policies/${encodeURIComponent(policyId)}`, {method: 'DELETE'});
   261          if (response.status !== 204) {
   262              throw new Error(await extractError(response));
   263          }
   264      }
   265  
   266      async attachPolicyToGroup(groupId, policyId) {
   267          const response = await apiRequest(`/auth/groups/${encodeURIComponent(groupId)}/policies/${encodeURIComponent(policyId)}`, {method: 'PUT'});
   268          if (response.status !== 201) {
   269              throw new Error(await extractError(response));
   270          }
   271      }
   272  
   273      async detachPolicyFromGroup(groupId, policyId) {
   274          const response = await apiRequest(`/auth/groups/${encodeURIComponent(groupId)}/policies/${encodeURIComponent(policyId)}`, {method: 'DELETE'});
   275          if (response.status !== 204) {
   276              throw new Error(await extractError(response));
   277          }
   278      }
   279  
   280      async deleteCredentials(userId, accessKeyId) {
   281          const response = await apiRequest(`/auth/users/${encodeURIComponent(userId)}/credentials/${encodeURIComponent(accessKeyId)}`, {method: 'DELETE'});
   282          if (response.status !== 204) {
   283              throw new Error(await extractError(response));
   284          }
   285      }
   286  
   287      async createGroup(groupName) {
   288          const response = await apiRequest(`/auth/groups`, {method: 'POST', body: JSON.stringify({id: groupName})});
   289          if (response.status !== 201) {
   290              throw new Error(await extractError(response));
   291          }
   292          return response.json();
   293      }
   294  
   295      async listPolicies(prefix = "", after = "", amount = DEFAULT_LISTING_AMOUNT) {
   296          const query = qs({prefix, after, amount});
   297          const response = await apiRequest(`/auth/policies?${query}`);
   298          if (response.status !== 200) {
   299              throw new Error(`could not list policies: ${await extractError(response)}`);
   300          }
   301          return response.json();
   302      }
   303  
   304      async createPolicy(policyId, policyDocument) {
   305          // keep id after policy document to override the id the user entered
   306          const policy = {...JSON.parse(policyDocument), id: policyId};
   307          const response = await apiRequest(`/auth/policies`, {
   308              method: 'POST',
   309              body: JSON.stringify(policy)
   310          });
   311          if (response.status !== 201) {
   312              throw new Error(await extractError(response));
   313          }
   314          return response.json();
   315      }
   316  
   317      async editPolicy(policyId, policyDocument) {
   318          const policy = {...JSON.parse(policyDocument), id: policyId};
   319          const response = await apiRequest(`/auth/policies/${encodeURIComponent(policyId)}`, {
   320              method: 'PUT',
   321              body: JSON.stringify(policy)
   322          });
   323          if (response.status !== 200) {
   324              throw new Error(await extractError(response));
   325          }
   326          return response.json();
   327      }
   328  
   329      async listCredentials(userId, after, amount = DEFAULT_LISTING_AMOUNT) {
   330          const query = qs({after, amount});
   331          const response = await apiRequest(`/auth/users/${encodeURIComponent(userId)}/credentials?` + query);
   332          if (response.status !== 200) {
   333              throw new Error(`could not list credentials: ${await extractError(response)}`);
   334          }
   335          return response.json();
   336      }
   337  
   338      async createCredentials(userId) {
   339          const response = await apiRequest(`/auth/users/${encodeURIComponent(userId)}/credentials`, {
   340              method: 'POST',
   341          });
   342          if (response.status !== 201) {
   343              throw new Error(await extractError(response));
   344          }
   345          return response.json();
   346      }
   347  
   348      async listUserGroups(userId, after, amount = DEFAULT_LISTING_AMOUNT) {
   349          const query = qs({after, amount});
   350          const response = await apiRequest(`/auth/users/${encodeURIComponent(userId)}/groups?` + query);
   351          if (response.status !== 200) {
   352              throw new Error(`could not list user groups: ${await extractError(response)}`);
   353          }
   354          return response.json();
   355      }
   356  
   357      async listUserPolicies(userId, effective = false, after = "", amount = DEFAULT_LISTING_AMOUNT) {
   358          const params = {after, amount};
   359          if (effective) {
   360              params.effective = 'true'
   361          }
   362          const query = qs(params);
   363          const response = await apiRequest(`/auth/users/${encodeURIComponent(userId)}/policies?` + query);
   364          if (response.status !== 200) {
   365              throw new Error(`could not list policies: ${await extractError(response)}`);
   366          }
   367          return response.json()
   368      }
   369  
   370      async getPolicy(policyId) {
   371          const response = await apiRequest(`/auth/policies/${encodeURIComponent(policyId)}`);
   372          if (response.status !== 200) {
   373              throw new Error(`could not get policy: ${await extractError(response)}`);
   374          }
   375          return response.json();
   376      }
   377  
   378      async listGroupPolicies(groupId, after, amount = DEFAULT_LISTING_AMOUNT) {
   379          const query = qs({after, amount});
   380          const response = await apiRequest(`/auth/groups/${encodeURIComponent(groupId)}/policies?` + query);
   381          if (response.status !== 200) {
   382              throw new Error(`could not list policies: ${await extractError(response)}`);
   383          }
   384          return response.json();
   385      }
   386  
   387      async deleteUser(userId) {
   388          const response = await apiRequest(`/auth/users/${encodeURIComponent(userId)}`, {method: 'DELETE'});
   389          if (response.status !== 204) {
   390              throw new Error(await extractError(response));
   391          }
   392      }
   393  
   394      async deleteUsers(userIds) {
   395          for (let i = 0; i < userIds.length; i++) {
   396              const userId = userIds[i];
   397              await this.deleteUser(userId);
   398          }
   399  
   400      }
   401  
   402      async deleteGroup(groupId) {
   403          const response = await apiRequest(`/auth/groups/${encodeURIComponent(groupId)}`, {method: 'DELETE'});
   404          if (response.status !== 204) {
   405              throw new Error(await extractError(response));
   406          }
   407      }
   408  
   409      async deleteGroups(groupIds) {
   410          for (let i = 0; i < groupIds.length; i++) {
   411              const groupId = groupIds[i]
   412              await this.deleteGroup(groupId);
   413          }
   414      }
   415  
   416      async deletePolicy(policyId) {
   417          const response = await apiRequest(`/auth/policies/${encodeURIComponent(policyId)}`, {method: 'DELETE'});
   418          if (response.status !== 204) {
   419              throw new Error(await extractError(response));
   420          }
   421      }
   422  
   423      async deletePolicies(policyIds) {
   424          for (let i = 0; i < policyIds.length; i++) {
   425              const policyId = policyIds[i];
   426              await this.deletePolicy(policyId);
   427          }
   428      }
   429  }
   430  
   431  class Repositories {
   432  
   433      async get(repoId) {
   434          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}`);
   435          if (response.status === 404) {
   436              throw new NotFoundError(`could not find repository ${repoId}`);
   437          } else if (response.status === 410) {
   438              throw new RepositoryDeletionError(`Repository in deletion`, repoId);
   439          } else if (response.status !== 200) {
   440              throw new Error(`could not get repository: ${await extractError(response)}`);
   441          }
   442          return response.json();
   443      }
   444  
   445      async list(prefix = "", after = "", amount = DEFAULT_LISTING_AMOUNT) {
   446          const query = qs({prefix, after, amount});
   447          const response = await apiRequest(`/repositories?${query}`);
   448          if (response.status !== 200) {
   449              throw new Error(`could not list repositories: ${await extractError(response)}`);
   450          }
   451          return await response.json();
   452      }
   453  
   454      async create(repo) {
   455          const response = await apiRequest('/repositories', {
   456              method: 'POST',
   457              body: JSON.stringify(repo),
   458          });
   459          if (response.status !== 201) {
   460              throw new Error(await extractError(response));
   461          }
   462          return response.json();
   463      }
   464  
   465      async delete(repoId) {
   466          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}`, {method: 'DELETE'});
   467          if (response.status !== 204) {
   468              throw new Error(await extractError(response));
   469          }
   470      }
   471  }
   472  
   473  class Branches {
   474  
   475      async get(repoId, branchId) {
   476          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/branches/${encodeURIComponent(branchId)}`);
   477          if (response.status === 400) {
   478              throw new BadRequestError('invalid get branch request');
   479          } else if (response.status === 404) {
   480              throw new NotFoundError(`could not find branch ${branchId}`);
   481          } else if (response.status !== 200) {
   482              throw new Error(`could not get branch: ${await extractError(response)}`);
   483          }
   484          return response.json();
   485      }
   486  
   487      async create(repoId, name, source) {
   488          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/branches`, {
   489              method: 'POST',
   490              body: JSON.stringify({name, source}),
   491          });
   492          if (response.status !== 201) {
   493              throw new Error(await extractError(response));
   494          }
   495          return response;
   496      }
   497  
   498      async delete(repoId, name) {
   499          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/branches/${encodeURIComponent(name)}`, {
   500              method: 'DELETE',
   501          });
   502          if (response.status !== 204) {
   503              throw new Error(await extractError(response));
   504          }
   505      }
   506  
   507      async reset(repoId, branch, options) {
   508          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/branches/${encodeURIComponent(branch)}`, {
   509              method: 'PUT',
   510              body: JSON.stringify(options),
   511          });
   512          if (response.status !== 204) {
   513              throw new Error(await extractError(response));
   514          }
   515      }
   516  
   517      async list(repoId, prefix = "", after = "", amount = DEFAULT_LISTING_AMOUNT) {
   518          const query = qs({prefix, after, amount});
   519          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/branches?` + query);
   520          if (response.status !== 200) {
   521              throw new Error(`could not list branches: ${await extractError(response)}`);
   522          }
   523          return response.json();
   524      }
   525  }
   526  
   527  
   528  class Tags {
   529      async get(repoId, tagId) {
   530          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/tags/${encodeURIComponent(tagId)}`);
   531          if (response.status === 404) {
   532              throw new NotFoundError(`could not find tag ${tagId}`);
   533          } else if (response.status !== 200) {
   534              throw new Error(`could not get tagId: ${await extractError(response)}`);
   535          }
   536          return response.json();
   537      }
   538  
   539      async list(repoId, prefix = "", after = "", amount = DEFAULT_LISTING_AMOUNT) {
   540          const query = qs({prefix, after, amount});
   541          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/tags?` + query);
   542          if (response.status !== 200) {
   543              throw new Error(`could not list tags: ${await extractError(response)}`);
   544          }
   545          return response.json();
   546      }
   547  
   548      async create(repoId, id, ref) {
   549          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/tags`, {
   550              method: 'POST',
   551              body: JSON.stringify({id, ref}),
   552          });
   553          if (response.status !== 201) {
   554              throw new Error(await extractError(response));
   555          }
   556          return response.json();
   557      }
   558  
   559      async delete(repoId, name) {
   560          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/tags/${encodeURIComponent(name)}`, {
   561              method: 'DELETE',
   562          });
   563          if (response.status !== 204) {
   564              throw new Error(await extractError(response));
   565          }
   566      }
   567  
   568  }
   569  
   570  // uploadWithProgress uses good ol' XMLHttpRequest because progress indication in fetch() is
   571  //  still not well supported across browsers (see https://stackoverflow.com/questions/35711724/upload-progress-indicators-for-fetch).
   572  export const uploadWithProgress = (url, file, method = 'POST', onProgress = null, additionalHeaders = null) => {
   573      return new Promise((resolve, reject) => {
   574          const xhr = new XMLHttpRequest();
   575          xhr.upload.addEventListener('progress', event => {
   576              if (onProgress) {
   577                  onProgress((event.loaded / event.total) * 100);
   578              }
   579          });
   580          xhr.addEventListener('load', () => {
   581            resolve({
   582                status: xhr.status,
   583                body: xhr.responseText,
   584                contentType: xhr.getResponseHeader('Content-Type'),
   585                etag: xhr.getResponseHeader('ETag'),
   586                contentMD5: xhr.getResponseHeader('Content-MD5'),
   587            })
   588          });
   589          xhr.addEventListener('error', () => reject(new Error('Upload Failed')));
   590          xhr.addEventListener('abort', () => reject(new Error('Upload Aborted')));
   591          xhr.open(method, url, true);
   592          xhr.setRequestHeader('Accept', 'application/json');
   593          xhr.setRequestHeader('X-Lakefs-Client', 'lakefs-webui/__buildVersion');
   594          if (additionalHeaders) {
   595              Object.keys(additionalHeaders).map(key => xhr.setRequestHeader(key, additionalHeaders[key]))
   596          }
   597          if (url.startsWith(API_ENDPOINT)) {
   598              // swagger API requires a form with a "content" field
   599              const data = new FormData();
   600              data.append('content', file);
   601              xhr.send(data);
   602          } else {
   603              xhr.send(file);
   604          }
   605      });
   606  };
   607  
   608  class Objects {
   609  
   610      async list(repoId, ref, tree, after = "", presign = false, amount = DEFAULT_LISTING_AMOUNT, delimiter = "/") {
   611          const query = qs({prefix: tree, amount, after, delimiter, presign});
   612          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/refs/${encodeURIComponent(ref)}/objects/ls?` + query);
   613  
   614          if (response.status === 404) {
   615              throw new NotFoundError(response.message ?? "ref not found");
   616          }
   617  
   618          if (response.status !== 200) {
   619              throw new Error(await extractError(response));
   620          }
   621          return await response.json();
   622      }
   623  
   624      listAll(repoId, ref, prefix, presign = false) {
   625          let after = "";
   626          return {
   627              next: async () => {
   628                  const query = qs({prefix, presign, after, amount: MAX_LISTING_AMOUNT});
   629                  const response = await apiRequest(
   630                    `/repositories/${encodeURIComponent(repoId)}/refs/${encodeURIComponent(ref)}/objects/ls?` + query);
   631                  if (response.status === 404) {
   632                      throw new NotFoundError(response.message ?? "ref not found");
   633                  }
   634                  if (response.status !== 200) {
   635                      throw new Error(await extractError(response));
   636                  }
   637                  const responseBody = await response.json();
   638                  const done = !responseBody.pagination.has_more;
   639                  if (!done) after = responseBody.pagination.next_offset;
   640                  return {page:responseBody.results, done}
   641              },
   642          }
   643      }
   644  
   645      async uploadPreflight(repoId, branchId, path) {
   646          const query = qs({path});
   647          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/branches/${encodeURIComponent(branchId)}/objects/stage_allowed?` + query);
   648  
   649          if (response.status === 204) {
   650              return true;
   651          }
   652          if (response.status === 401) {
   653              return false;
   654          }
   655          
   656          // This is not one of the expected responses
   657          throw new Error(await extractError(response));
   658      }
   659  
   660      async upload(repoId, branchId, path, fileObject, onProgressFn = null) {
   661          const query = qs({path});
   662          const uploadUrl = `${API_ENDPOINT}/repositories/${encodeURIComponent(repoId)}/branches/${encodeURIComponent(branchId)}/objects?` + query;
   663          const {status, body, contentType} = await uploadWithProgress(uploadUrl, fileObject, 'POST', onProgressFn)
   664          if (status !== 201) {
   665              if (contentType === "application/json" && body) {
   666                  const responseData = JSON.parse(body)
   667                  throw new Error(responseData.message)
   668              }
   669              throw new Error(body);
   670          }
   671      }
   672  
   673      async delete(repoId, branchId, path) {
   674          const query = qs({path});
   675          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/branches/${encodeURIComponent(branchId)}/objects?` + query, {
   676              method: 'DELETE',
   677          });
   678          if (response.status !== 204) {
   679              throw new Error(await extractError(response));
   680          }
   681      }
   682  
   683      async get(repoId, ref, path, presign = false) {
   684          const query = qs({path, presign});
   685          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/refs/${encodeURIComponent(ref)}/objects?` + query, {
   686              method: 'GET',
   687          });
   688          if (response.status !== 200 && response.status !== 206) {
   689              throw new Error(await extractError(response));
   690          }
   691  
   692          return response.text()
   693      }
   694  
   695      async head(repoId, ref, path) {
   696          const query = qs({path});
   697          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/refs/${encodeURIComponent(ref)}/objects?` + query, {
   698              method: 'HEAD',
   699          });
   700  
   701          if (response.status !== 200 && response.status !== 206) {
   702              throw new Error(await extractError(response));
   703          }
   704  
   705          return {
   706              headers: response.headers,
   707          }
   708      }
   709  
   710      async getStat(repoId, ref, path, presign = false) {
   711          const query = qs({path, presign});
   712          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/refs/${encodeURIComponent(ref)}/objects/stat?` + query);
   713          if (response.status !== 200) {
   714              throw new Error(await extractError(response));
   715          }
   716          return response.json()
   717      }
   718  }
   719  
   720  class Commits {
   721      async log(repoId, refId, after = "", amount = DEFAULT_LISTING_AMOUNT) {
   722          const query = qs({after, amount});
   723          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/refs/${encodeURIComponent(refId)}/commits?` + query);
   724          if (response.status !== 200) {
   725              throw new Error(await extractError(response));
   726          }
   727          return response.json();
   728      }
   729  
   730      async blame(repoId, refId, path, type) {
   731          const params = {amount: 1};
   732          if (type === 'object') {
   733              params.objects = path
   734          } else {
   735              params.prefixes = path
   736          }
   737          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/refs/${encodeURIComponent(refId)}/commits?${qs(params)}`);
   738          if (response.status !== 200) {
   739              throw new Error(await extractError(response));
   740          }
   741          const data = await response.json();
   742          if (data.results.length >= 1) {
   743              return data.results[0] // found a commit object
   744          }
   745          return null // no commit modified this
   746      }
   747  
   748      async get(repoId, commitId) {
   749          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/commits/${encodeURIComponent(commitId)}`);
   750          if (response.status === 404) {
   751              throw new NotFoundError(`could not find commit ${commitId}`);
   752          } else if (response.status !== 200) {
   753              throw new Error(`could not get commit: ${await extractError(response)}`);
   754          }
   755          return response.json();
   756      }
   757  
   758      async commit(repoId, branchId, message, metadata = {}) {
   759          const response = await apiRequest(`/repositories/${repoId}/branches/${branchId}/commits`, {
   760              method: 'POST',
   761              body: JSON.stringify({message, metadata}),
   762          });
   763          if (response.status !== 201) {
   764              throw new Error(await extractError(response));
   765          }
   766          return response.json();
   767      }
   768  }
   769  
   770  class Refs {
   771  
   772      async changes(repoId, branchId, after, prefix, delimiter, amount = DEFAULT_LISTING_AMOUNT) {
   773          const query = qs({after, prefix, delimiter, amount});
   774          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/branches/${encodeURIComponent(branchId)}/diff?` + query);
   775          if (response.status !== 200) {
   776              throw new Error(await extractError(response));
   777          }
   778          return response.json();
   779      }
   780  
   781      async diff(repoId, leftRef, rightRef, after, prefix = "", delimiter = "", amount = DEFAULT_LISTING_AMOUNT) {
   782          const query = qs({after, amount, delimiter, prefix});
   783          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/refs/${encodeURIComponent(leftRef)}/diff/${encodeURIComponent(rightRef)}?` + query);
   784          if (response.status !== 200) {
   785              throw new Error(await extractError(response));
   786          }
   787          return response.json();
   788      }
   789  
   790      async merge(repoId, sourceBranch, destinationBranch, strategy = "", message = "", metadata = {}) {
   791          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/refs/${encodeURIComponent(sourceBranch)}/merge/${encodeURIComponent(destinationBranch)}`, {
   792              method: 'POST',
   793              body: JSON.stringify({strategy, message, metadata})
   794          });
   795  
   796          let resp;
   797          switch (response.status) {
   798              case 200:
   799                  return response.json();
   800              case 409:
   801                  resp = await response.json();
   802                  throw new MergeError(response.statusText, resp.body);
   803              case 412:
   804              default:
   805                  throw new Error(await extractError(response));
   806          }
   807      }
   808  }
   809  
   810  class Actions {
   811  
   812      async listRuns(repoId, branch = "", commit = "", after = "", amount = DEFAULT_LISTING_AMOUNT) {
   813          const query = qs({branch, commit, after, amount});
   814          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/actions/runs?` + query);
   815          if (response.status !== 200) {
   816              throw new Error(`could not list actions runs: ${await extractError(response)}`);
   817          }
   818          return response.json();
   819      }
   820  
   821      async getRun(repoId, runId) {
   822          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/actions/runs/${encodeURIComponent(runId)}`);
   823          if (response.status !== 200) {
   824              throw new Error(`could not get actions run: ${await extractError(response)}`);
   825          }
   826          return response.json();
   827      }
   828  
   829      async listRunHooks(repoId, runId, after = "", amount = DEFAULT_LISTING_AMOUNT) {
   830          const query = qs({after, amount});
   831          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/actions/runs/${encodeURIComponent(runId)}/hooks?` + query);
   832          if (response.status !== 200) {
   833              throw new Error(`could not list actions run hooks: ${await extractError(response)}`)
   834          }
   835          return response.json();
   836      }
   837  
   838      async getRunHookOutput(repoId, runId, hookRunId) {
   839          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/actions/runs/${encodeURIComponent(runId)}/hooks/${encodeURIComponent(hookRunId)}/output`, {
   840              headers: {"Content-Type": "application/octet-stream"},
   841          });
   842          if (response.status !== 200) {
   843              throw new Error(`could not get actions run hook output: ${await extractError(response)}`);
   844          }
   845          return response.text();
   846      }
   847  
   848  }
   849  
   850  class Retention {
   851      async getGCPolicy(repoID) {
   852          const response = await apiRequest(`/repositories/${encodeURIComponent(repoID)}/settings/gc_rules`);
   853          if (response.status === 404) {
   854              throw new NotFoundError('policy not found')
   855          }
   856          if (response.status !== 200) {
   857              throw new Error(`could not get gc policy: ${await extractError(response)}`);
   858          }
   859          return response.json();
   860      }
   861  
   862      async setGCPolicyPreflight(repoID) {
   863          const response = await apiRequest(`/repositories/${encodeURIComponent(repoID)}/gc/rules/set_allowed`);
   864          if (response.status !== 204) {
   865              throw new Error(await extractError(response));
   866          }
   867          return response;
   868      }
   869  
   870      async setGCPolicy(repoID, policy) {
   871          const response = await apiRequest(`/repositories/${encodeURIComponent(repoID)}/settings/gc_rules`, {
   872              method: 'PUT',
   873              body: policy
   874          });
   875          if (response.status !== 204) {
   876              throw new Error(`could not set gc policy: ${await extractError(response)}`);
   877          }
   878          return response;
   879      }
   880  
   881      async deleteGCPolicy(repoID) {
   882          const response = await apiRequest(`/repositories/${encodeURIComponent(repoID)}/settings/gc_rules`, {
   883              method: 'DELETE',
   884          });
   885          if (response.status !== 204) {
   886              throw new Error(`could not delete gc policy: ${await extractError(response)}`);
   887          }
   888          return response;
   889      }
   890  }
   891  
   892  class Setup {
   893      async getState() {
   894          const response = await apiRequest('/setup_lakefs', {
   895              method: 'GET',
   896          });
   897          switch (response.status) {
   898              case 200:
   899                  return response.json();
   900              default:
   901                  throw new Error(`Could not get setup state: ${await extractError(response)}`);
   902          }
   903      }
   904  
   905      async lakeFS(username) {
   906          const response = await apiRequest('/setup_lakefs', {
   907              method: 'POST',
   908              headers: {
   909                  'Accept': 'application/json',
   910                  'Content-Type': 'application/json'
   911              },
   912              body: JSON.stringify({username: username}),
   913          });
   914          switch (response.status) {
   915              case 200:
   916                  return response.json();
   917              case 409:
   918                  throw new Error('Setup is already complete.');
   919              default:
   920                  throw new Error('Unknown');
   921          }
   922      }
   923  
   924      async commPrefs(email, updates, security) {
   925          const response = await apiRequest('/setup_comm_prefs', {
   926              method: 'POST',
   927              headers: {
   928                  'Accept': 'application/json',
   929                  'Content-Type': 'application/json',
   930              },
   931              body: JSON.stringify({
   932                  email,
   933                  featureUpdates: updates,
   934                  securityUpdates: security,
   935              }),
   936          });
   937  
   938          switch (response.status) {
   939              case 200:
   940                  return;
   941              case 409:
   942                  throw new Error('Setup is already complete.');
   943              default:
   944                  throw new Error('Unknown');
   945          }
   946      }
   947  }
   948  
   949  class Config {
   950      async getStorageConfig() {
   951          const response = await apiRequest('/config', {
   952              method: 'GET',
   953          });
   954          let cfg, storageCfg;
   955          switch (response.status) {
   956              case 200:
   957                  cfg = await response.json();
   958                  storageCfg = cfg.storage_config
   959                  storageCfg.warnings = []
   960                  if (storageCfg.blockstore_type === 'mem') {
   961                      storageCfg.warnings.push(`Block adapter ${storageCfg.blockstore_type} not usable in production`)
   962                  }
   963                  return storageCfg;
   964              case 409:
   965                  throw new Error('Conflict');
   966              default:
   967                  throw new Error('Unknown');
   968          }
   969      }
   970  
   971      async getLakeFSVersion() {
   972          const response = await apiRequest('/config', {
   973              method: 'GET',
   974          });
   975          let cfg;
   976          switch (response.status) {
   977              case 200:
   978                  cfg =  await response.json();
   979                  return cfg.version_config
   980              default:
   981                  throw new Error('Unknown');
   982          }
   983      }
   984  }
   985  
   986  class BranchProtectionRules {
   987      async getRules(repoID) {
   988          const response = await apiRequest(`/repositories/${encodeURIComponent(repoID)}/settings/branch_protection`);
   989          if (response.status === 404) {
   990              throw new NotFoundError('branch protection rules not found')
   991          }
   992          if (response.status !== 200) {
   993              throw new Error(`could not get branch protection rules: ${await extractError(response)}`);
   994          }
   995          return {
   996              'checksum': response.headers.get('ETag'),
   997              'rules': await response.json()
   998          }
   999      }
  1000  
  1001      async createRulePreflight(repoID) {
  1002          const response = await apiRequest(`/repositories/${encodeURIComponent(repoID)}/branch_protection/set_allowed`);
  1003          return response.status === 204;
  1004  
  1005      }
  1006  
  1007      async setRules(repoID, rules, lastKnownChecksum) {
  1008          const additionalHeaders = {}
  1009          if (lastKnownChecksum) {
  1010              additionalHeaders['If-Match'] = lastKnownChecksum
  1011          }
  1012          const response = await apiRequest(`/repositories/${encodeURIComponent(repoID)}/settings/branch_protection`, {
  1013              method: 'PUT',
  1014              body: JSON.stringify(rules),
  1015          }, additionalHeaders);
  1016          if (response.status !== 204) {
  1017              throw new Error(`could not create protection rule: ${await extractError(response)}`);
  1018          }
  1019      }
  1020  }
  1021  
  1022  class Statistics {
  1023      async postStatsEvents(statsEvents) {
  1024          const request = {
  1025              "events": statsEvents,
  1026          }
  1027          const response = await apiRequest(`/statistics`, {
  1028              method: 'POST',
  1029              body: JSON.stringify(request),
  1030          });
  1031          if (response.status !== 204) {
  1032              throw new Error(await extractError(response));
  1033          }
  1034      }
  1035  }
  1036  
  1037  class Staging {
  1038      async get(repoId, branchId, path, presign = false) {
  1039          const query = qs({path, presign});
  1040          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/branches/${encodeURIComponent(branchId)}/staging/backing?` + query, {
  1041              method: 'GET'
  1042          });
  1043          if (response.status !== 200) {
  1044              throw new Error(await extractError(response));
  1045          }
  1046          return response.json();
  1047      }
  1048  
  1049      async link(repoId, branchId, path, staging, checksum, sizeBytes, contentType = 'application/octet-stream') {
  1050          const query = qs({path});
  1051          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/branches/${encodeURIComponent(branchId)}/staging/backing?` + query, {
  1052              method: 'PUT',
  1053              body: JSON.stringify({staging: staging, checksum: checksum, size_bytes: sizeBytes, content_type: contentType})
  1054          });
  1055          if (response.status !== 200) {
  1056              throw new Error(await extractError(response));
  1057          }
  1058          return response.json();
  1059      }
  1060  }
  1061  
  1062  class Import {
  1063  
  1064      async get(repoId, branchId, importId) {
  1065          const query = qs({id: importId});
  1066          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/branches/${encodeURIComponent(branchId)}/import?` + query);
  1067          if (response.status === 404) {
  1068              throw new NotFoundError(`could not find import ${importId}`);
  1069          } else if (response.status !== 200) {
  1070              throw new Error(`could not get import status: ${await extractError(response)}`);
  1071          }
  1072          return response.json();
  1073      }
  1074  
  1075      async create(repoId, branchId, source, prepend, commitMessage, commitMetadata = {}) {
  1076          const body = {
  1077              "paths": [
  1078                  {
  1079                      "path": source,
  1080                      "destination": prepend,
  1081                      "type": "common_prefix",
  1082              }],
  1083              "commit": {
  1084                  "message": commitMessage
  1085              },
  1086          };
  1087          if (Object.keys(commitMetadata).length > 0) {
  1088              body.commit["metadata"] = commitMetadata
  1089          }
  1090  
  1091          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/branches/${encodeURIComponent(branchId)}/import`, {
  1092              method: 'POST',
  1093              body: JSON.stringify(body),
  1094          });
  1095          if (response.status !== 202) {
  1096              throw new Error(await extractError(response));
  1097          }
  1098          return response.json();
  1099      }
  1100  
  1101      async delete(repoId, branchId, importId) {
  1102          const query = qs({id: importId});
  1103          const response = await apiRequest(`/repositories/${encodeURIComponent(repoId)}/branches/${encodeURIComponent(branchId)}/import?` + query, {
  1104              method: 'DELETE',
  1105          });
  1106          if (response.status !== 204) {
  1107              throw new Error(await extractError(response));
  1108          }
  1109      }
  1110  }
  1111  
  1112  export const repositories = new Repositories();
  1113  export const branches = new Branches();
  1114  export const tags = new Tags();
  1115  export const objects = new Objects();
  1116  export const commits = new Commits();
  1117  export const refs = new Refs();
  1118  export const setup = new Setup();
  1119  export const auth = new Auth();
  1120  export const actions = new Actions();
  1121  export const retention = new Retention();
  1122  export const config = new Config();
  1123  export const branchProtectionRules = new BranchProtectionRules();
  1124  export const statistics = new Statistics();
  1125  export const staging = new Staging();
  1126  export const imports = new Import();