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();