@air/api-rest 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -50,7 +50,11 @@ __export(index_exports, {
50
50
  Tags: () => Tags,
51
51
  TimeoutError: () => import_api_core2.TimeoutError,
52
52
  Uploads: () => Uploads,
53
- VERSION: () => import_api_core2.VERSION
53
+ VERSION: () => import_api_core2.VERSION,
54
+ Workspaces: () => Workspaces,
55
+ buildAuthorizationUrl: () => import_api_core2.buildAuthorizationUrl,
56
+ exchangeAuthorizationCode: () => import_api_core2.exchangeAuthorizationCode,
57
+ generatePKCEChallenge: () => import_api_core2.generatePKCEChallenge
54
58
  });
55
59
  module.exports = __toCommonJS(index_exports);
56
60
 
@@ -420,22 +424,23 @@ var Uploads = class {
420
424
  });
421
425
  }
422
426
  async uploadFile(params, options = {}) {
423
- const { fileData, fileName, ext, mime, size } = await resolveFileData(params);
427
+ const meta = await resolveFileMeta(params);
424
428
  const uploadResponse = await this.create({
425
- fileName,
426
- ext,
427
- size,
428
- mime,
429
+ fileName: meta.fileName,
430
+ ext: meta.ext,
431
+ size: meta.size,
432
+ mime: meta.mime,
429
433
  parentBoardId: options.parentBoardId,
430
434
  assetId: options.assetId,
431
435
  customFields: options.customFields,
432
436
  tags: options.tags
433
437
  });
434
438
  if (isSmallUpload(uploadResponse)) {
435
- await this._uploadSmallFile(uploadResponse.uploadUrl, fileData, options.onProgress, size);
439
+ const data = await materializeData(meta);
440
+ await this._uploadSmallFile(uploadResponse.uploadUrl, data, options.onProgress, meta.size);
436
441
  return { assetId: uploadResponse.assetId, versionId: uploadResponse.versionId };
437
442
  }
438
- await this._uploadLargeFile(uploadResponse, fileData, size, options.onProgress);
443
+ await this._uploadLargeFile(uploadResponse, meta, options.onProgress);
439
444
  return { assetId: uploadResponse.assetId, versionId: uploadResponse.versionId };
440
445
  }
441
446
  async _uploadSmallFile(url, data, onProgress, totalSize) {
@@ -451,42 +456,73 @@ var Uploads = class {
451
456
  onProgress({ percentage: 100, uploadedBytes: totalSize, totalBytes: totalSize });
452
457
  }
453
458
  }
454
- async _uploadLargeFile(uploadInfo, data, totalSize, onProgress) {
459
+ async _uploadLargeFile(uploadInfo, meta, onProgress) {
455
460
  const { multiPartUploadId, key } = uploadInfo;
456
- const buffer = data instanceof Buffer ? data : Buffer.from(data);
461
+ const { size: totalSize } = meta;
457
462
  const partCount = Math.ceil(totalSize / DEFAULT_PART_SIZE);
458
463
  const parts = [];
459
464
  let uploadedBytes = 0;
460
465
  const fetchFn = this.client.fetchFn;
461
- for (let i = 0; i < partCount; i++) {
462
- const partNumber = i + 1;
463
- const start = i * DEFAULT_PART_SIZE;
464
- const end = Math.min(start + DEFAULT_PART_SIZE, totalSize);
465
- const chunk = buffer.subarray(start, end);
466
- const { url } = await this.getPartUploadUrl({
467
- multiPartUploadId,
468
- key,
469
- partNumber
470
- });
471
- const response = await fetchFn(url, {
472
- method: "PUT",
473
- body: chunk
474
- });
475
- if (!response.ok) {
476
- throw new Error(`Part ${partNumber} upload failed with status ${response.status}`);
477
- }
478
- const etag = response.headers.get("etag");
479
- if (!etag) {
480
- throw new Error(`Part ${partNumber} upload did not return an ETag`);
466
+ if (meta.source.type === "path") {
467
+ const fs = await import("fs");
468
+ const fd = fs.openSync(meta.source.filePath, "r");
469
+ try {
470
+ for (let i = 0; i < partCount; i++) {
471
+ const partNumber = i + 1;
472
+ const start = i * DEFAULT_PART_SIZE;
473
+ const end = Math.min(start + DEFAULT_PART_SIZE, totalSize);
474
+ const length = end - start;
475
+ const chunk = Buffer.alloc(length);
476
+ const bytesRead = fs.readSync(fd, chunk, 0, length, start);
477
+ const body = bytesRead < length ? chunk.subarray(0, bytesRead) : chunk;
478
+ const { url } = await this.getPartUploadUrl({ multiPartUploadId, key, partNumber });
479
+ const response = await fetchFn(url, { method: "PUT", body });
480
+ if (!response.ok) {
481
+ throw new Error(`Part ${partNumber} upload failed with status ${response.status}`);
482
+ }
483
+ const etag = response.headers.get("etag");
484
+ if (!etag) {
485
+ throw new Error(`Part ${partNumber} upload did not return an ETag`);
486
+ }
487
+ parts.push({ etag, partNumber });
488
+ uploadedBytes += bytesRead;
489
+ if (onProgress) {
490
+ onProgress({
491
+ percentage: Math.round(uploadedBytes / totalSize * 100),
492
+ uploadedBytes,
493
+ totalBytes: totalSize
494
+ });
495
+ }
496
+ }
497
+ } finally {
498
+ fs.closeSync(fd);
481
499
  }
482
- parts.push({ etag, partNumber });
483
- uploadedBytes += chunk.byteLength;
484
- if (onProgress) {
485
- onProgress({
486
- percentage: Math.round(uploadedBytes / totalSize * 100),
487
- uploadedBytes,
488
- totalBytes: totalSize
489
- });
500
+ } else {
501
+ const buffer = await materializeData(meta);
502
+ const buf = buffer instanceof Buffer ? buffer : Buffer.from(buffer);
503
+ for (let i = 0; i < partCount; i++) {
504
+ const partNumber = i + 1;
505
+ const start = i * DEFAULT_PART_SIZE;
506
+ const end = Math.min(start + DEFAULT_PART_SIZE, totalSize);
507
+ const chunk = buf.subarray(start, end);
508
+ const { url } = await this.getPartUploadUrl({ multiPartUploadId, key, partNumber });
509
+ const response = await fetchFn(url, { method: "PUT", body: chunk });
510
+ if (!response.ok) {
511
+ throw new Error(`Part ${partNumber} upload failed with status ${response.status}`);
512
+ }
513
+ const etag = response.headers.get("etag");
514
+ if (!etag) {
515
+ throw new Error(`Part ${partNumber} upload did not return an ETag`);
516
+ }
517
+ parts.push({ etag, partNumber });
518
+ uploadedBytes += chunk.byteLength;
519
+ if (onProgress) {
520
+ onProgress({
521
+ percentage: Math.round(uploadedBytes / totalSize * 100),
522
+ uploadedBytes,
523
+ totalBytes: totalSize
524
+ });
525
+ }
490
526
  }
491
527
  }
492
528
  await this.completeMultipart({ multiPartUploadId, key, parts });
@@ -495,16 +531,22 @@ var Uploads = class {
495
531
  function isSmallUpload(response) {
496
532
  return "uploadUrl" in response;
497
533
  }
498
- async function resolveFileData(params) {
534
+ async function resolveFileMeta(params) {
499
535
  if (params.filePath) {
500
536
  const fs = await import("fs");
501
537
  const path = await import("path");
502
- const fileData = fs.readFileSync(params.filePath);
538
+ const stat = fs.statSync(params.filePath);
503
539
  const parsed = path.parse(params.filePath);
504
540
  const fileName = params.fileName ?? parsed.name;
505
541
  const ext = params.ext ?? parsed.ext.replace(/^\./, "");
506
542
  const mime = params.mime ?? guessMime(ext);
507
- return { fileData, fileName, ext, mime, size: fileData.byteLength };
543
+ return {
544
+ source: { type: "path", filePath: params.filePath },
545
+ fileName,
546
+ ext,
547
+ mime,
548
+ size: stat.size
549
+ };
508
550
  }
509
551
  if (params.buffer) {
510
552
  const data = params.buffer instanceof Buffer ? params.buffer : Buffer.from(params.buffer);
@@ -513,7 +555,7 @@ async function resolveFileData(params) {
513
555
  }
514
556
  const mime = params.mime ?? guessMime(params.ext);
515
557
  return {
516
- fileData: data,
558
+ source: { type: "buffer", data },
517
559
  fileName: params.fileName,
518
560
  ext: params.ext,
519
561
  mime,
@@ -521,17 +563,35 @@ async function resolveFileData(params) {
521
563
  };
522
564
  }
523
565
  if (params.file) {
524
- const arrayBuffer = await params.file.arrayBuffer();
525
- const data = new Uint8Array(arrayBuffer);
526
566
  const name = params.file.name;
527
567
  const dotIndex = name.lastIndexOf(".");
528
568
  const fileName = params.fileName ?? (dotIndex > 0 ? name.substring(0, dotIndex) : name);
529
569
  const ext = params.ext ?? (dotIndex > 0 ? name.substring(dotIndex + 1) : "");
530
570
  const mime = params.mime ?? (params.file.type || guessMime(ext));
531
- return { fileData: data, fileName, ext, mime, size: data.byteLength };
571
+ return {
572
+ source: { type: "file", file: params.file },
573
+ fileName,
574
+ ext,
575
+ mime,
576
+ size: params.file.size
577
+ };
532
578
  }
533
579
  throw new Error("One of filePath, buffer, or file must be provided");
534
580
  }
581
+ async function materializeData(meta) {
582
+ switch (meta.source.type) {
583
+ case "path": {
584
+ const fs = await import("fs");
585
+ return fs.readFileSync(meta.source.filePath);
586
+ }
587
+ case "buffer":
588
+ return meta.source.data;
589
+ case "file": {
590
+ const arrayBuffer = await meta.source.file.arrayBuffer();
591
+ return new Uint8Array(arrayBuffer);
592
+ }
593
+ }
594
+ }
535
595
  function guessMime(ext) {
536
596
  const mimeMap = {
537
597
  jpg: "image/jpeg",
@@ -564,6 +624,20 @@ function guessMime(ext) {
564
624
  return mimeMap[ext.toLowerCase()] ?? "application/octet-stream";
565
625
  }
566
626
 
627
+ // src/resources/workspaces.ts
628
+ var Workspaces = class {
629
+ constructor(client) {
630
+ this.client = client;
631
+ }
632
+ async list() {
633
+ const response = await this.client.request({
634
+ method: "GET",
635
+ path: "/workspaces"
636
+ });
637
+ return response.data;
638
+ }
639
+ };
640
+
567
641
  // src/api.ts
568
642
  var AirApi = class extends import_api_core.AirBase {
569
643
  assets;
@@ -573,6 +647,7 @@ var AirApi = class extends import_api_core.AirBase {
573
647
  roles;
574
648
  imports;
575
649
  uploads;
650
+ workspaces;
576
651
  constructor(options = {}) {
577
652
  super(options);
578
653
  this.assets = new Assets(this);
@@ -582,6 +657,7 @@ var AirApi = class extends import_api_core.AirBase {
582
657
  this.roles = new Roles(this);
583
658
  this.imports = new Imports(this);
584
659
  this.uploads = new Uploads(this);
660
+ this.workspaces = new Workspaces(this);
585
661
  }
586
662
  };
587
663
 
@@ -609,5 +685,9 @@ var import_api_core2 = require("@air/api-core");
609
685
  Tags,
610
686
  TimeoutError,
611
687
  Uploads,
612
- VERSION
688
+ VERSION,
689
+ Workspaces,
690
+ buildAuthorizationUrl,
691
+ exchangeAuthorizationCode,
692
+ generatePKCEChallenge
613
693
  });
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CursorPageParams, AirBase, PagePromise, AirBaseOptions } from '@air/api-core';
2
- export { APIError, AirBase, AirBaseOptions, AuthenticationError, BadRequestError, ConnectionError, CursorPage, CursorPageParams, CursorPageResponse, InternalServerError, NotFoundError, PagePromise, PermissionError, RateLimitError, RequestOptions, TimeoutError, VERSION } from '@air/api-core';
2
+ export { APIError, AirBase, AirBaseOptions, AuthenticationError, BadRequestError, BuildAuthorizationUrlOptions, ConnectionError, CursorPage, CursorPageParams, CursorPageResponse, ExchangeAuthorizationCodeOptions, InternalServerError, NotFoundError, OAuthAccessToken, OAuthClientAuthMethod, PKCEChallenge, PagePromise, PermissionError, RateLimitError, RequestOptions, TimeoutError, VERSION, buildAuthorizationUrl, exchangeAuthorizationCode, generatePKCEChallenge } from '@air/api-core';
3
3
 
4
4
  interface Tag {
5
5
  id: string;
@@ -341,6 +341,23 @@ interface CompleteMultipartParams {
341
341
  partNumber: number;
342
342
  }[];
343
343
  }
344
+ type FileSource = {
345
+ type: "path";
346
+ filePath: string;
347
+ } | {
348
+ type: "buffer";
349
+ data: Buffer;
350
+ } | {
351
+ type: "file";
352
+ file: File;
353
+ };
354
+ interface FileMeta {
355
+ source: FileSource;
356
+ fileName: string;
357
+ ext: string;
358
+ mime: string;
359
+ size: number;
360
+ }
344
361
  interface UploadFileParams {
345
362
  filePath?: string;
346
363
  buffer?: Buffer | ArrayBuffer;
@@ -384,6 +401,20 @@ declare class Uploads {
384
401
  private _uploadLargeFile;
385
402
  }
386
403
 
404
+ interface Workspace {
405
+ id: string;
406
+ name: string;
407
+ }
408
+ interface WorkspaceListResponse {
409
+ data: Workspace[];
410
+ }
411
+
412
+ declare class Workspaces {
413
+ private client;
414
+ constructor(client: AirBase);
415
+ list(): Promise<Workspace[]>;
416
+ }
417
+
387
418
  interface AirApiOptions extends AirBaseOptions {
388
419
  }
389
420
  declare class AirApi extends AirBase {
@@ -394,7 +425,8 @@ declare class AirApi extends AirBase {
394
425
  readonly roles: Roles;
395
426
  readonly imports: Imports;
396
427
  readonly uploads: Uploads;
428
+ readonly workspaces: Workspaces;
397
429
  constructor(options?: AirApiOptions);
398
430
  }
399
431
 
400
- export { type AddBoardAssetsParams, type AddGuestParams, type AddVersionTagParams, AirApi, type AirApiOptions, type Asset, type AssetBoardListParams, type AssetCustomFieldValue, type AssetListParams, type AssetVersion, type AssetVersionDownloadUrl, type AssetVersionListResponse, type AssetVersionUpdateParams, type AssetVersionUrls, Assets, type Board, type BoardCreateParams, type BoardListParams, type BoardUpdateParams, Boards, type CompleteMultipartParams, type CustomField, type CustomFieldCreateParams, type CustomFieldListParams, type CustomFieldType, type CustomFieldUpdateParams, type CustomFieldValue, type CustomFieldValueCreateParams, type CustomFieldValueUpdateParams, CustomFields, type GetPartUploadUrlParams, type GetPartUploadUrlResponse, type Guest, type GuestListParams, type GuestListResponse, type ImportCreateParams, type ImportCreateResponse, type ImportStatus, type ImportStatusResponse, Imports, type LargeUploadResponse, type Role, type RoleListParams, type RoleListResponse, Roles, type SetBoardCustomFieldParams, type SetCustomFieldParams, type SmallUploadResponse, type SmartTag, type Tag, type TagCreateParams, type TagListParams, type TagUpdateParams, Tags, type UpdateGuestParams, type UploadCreateParams, type UploadCreateResponse, type UploadFileOptions, type UploadFileParams, type UploadFileResponse, Uploads };
432
+ export { type AddBoardAssetsParams, type AddGuestParams, type AddVersionTagParams, AirApi, type AirApiOptions, type Asset, type AssetBoardListParams, type AssetCustomFieldValue, type AssetListParams, type AssetVersion, type AssetVersionDownloadUrl, type AssetVersionListResponse, type AssetVersionUpdateParams, type AssetVersionUrls, Assets, type Board, type BoardCreateParams, type BoardListParams, type BoardUpdateParams, Boards, type CompleteMultipartParams, type CustomField, type CustomFieldCreateParams, type CustomFieldListParams, type CustomFieldType, type CustomFieldUpdateParams, type CustomFieldValue, type CustomFieldValueCreateParams, type CustomFieldValueUpdateParams, CustomFields, type FileMeta, type FileSource, type GetPartUploadUrlParams, type GetPartUploadUrlResponse, type Guest, type GuestListParams, type GuestListResponse, type ImportCreateParams, type ImportCreateResponse, type ImportStatus, type ImportStatusResponse, Imports, type LargeUploadResponse, type Role, type RoleListParams, type RoleListResponse, Roles, type SetBoardCustomFieldParams, type SetCustomFieldParams, type SmallUploadResponse, type SmartTag, type Tag, type TagCreateParams, type TagListParams, type TagUpdateParams, Tags, type UpdateGuestParams, type UploadCreateParams, type UploadCreateResponse, type UploadFileOptions, type UploadFileParams, type UploadFileResponse, Uploads, type Workspace, type WorkspaceListResponse, Workspaces };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CursorPageParams, AirBase, PagePromise, AirBaseOptions } from '@air/api-core';
2
- export { APIError, AirBase, AirBaseOptions, AuthenticationError, BadRequestError, ConnectionError, CursorPage, CursorPageParams, CursorPageResponse, InternalServerError, NotFoundError, PagePromise, PermissionError, RateLimitError, RequestOptions, TimeoutError, VERSION } from '@air/api-core';
2
+ export { APIError, AirBase, AirBaseOptions, AuthenticationError, BadRequestError, BuildAuthorizationUrlOptions, ConnectionError, CursorPage, CursorPageParams, CursorPageResponse, ExchangeAuthorizationCodeOptions, InternalServerError, NotFoundError, OAuthAccessToken, OAuthClientAuthMethod, PKCEChallenge, PagePromise, PermissionError, RateLimitError, RequestOptions, TimeoutError, VERSION, buildAuthorizationUrl, exchangeAuthorizationCode, generatePKCEChallenge } from '@air/api-core';
3
3
 
4
4
  interface Tag {
5
5
  id: string;
@@ -341,6 +341,23 @@ interface CompleteMultipartParams {
341
341
  partNumber: number;
342
342
  }[];
343
343
  }
344
+ type FileSource = {
345
+ type: "path";
346
+ filePath: string;
347
+ } | {
348
+ type: "buffer";
349
+ data: Buffer;
350
+ } | {
351
+ type: "file";
352
+ file: File;
353
+ };
354
+ interface FileMeta {
355
+ source: FileSource;
356
+ fileName: string;
357
+ ext: string;
358
+ mime: string;
359
+ size: number;
360
+ }
344
361
  interface UploadFileParams {
345
362
  filePath?: string;
346
363
  buffer?: Buffer | ArrayBuffer;
@@ -384,6 +401,20 @@ declare class Uploads {
384
401
  private _uploadLargeFile;
385
402
  }
386
403
 
404
+ interface Workspace {
405
+ id: string;
406
+ name: string;
407
+ }
408
+ interface WorkspaceListResponse {
409
+ data: Workspace[];
410
+ }
411
+
412
+ declare class Workspaces {
413
+ private client;
414
+ constructor(client: AirBase);
415
+ list(): Promise<Workspace[]>;
416
+ }
417
+
387
418
  interface AirApiOptions extends AirBaseOptions {
388
419
  }
389
420
  declare class AirApi extends AirBase {
@@ -394,7 +425,8 @@ declare class AirApi extends AirBase {
394
425
  readonly roles: Roles;
395
426
  readonly imports: Imports;
396
427
  readonly uploads: Uploads;
428
+ readonly workspaces: Workspaces;
397
429
  constructor(options?: AirApiOptions);
398
430
  }
399
431
 
400
- export { type AddBoardAssetsParams, type AddGuestParams, type AddVersionTagParams, AirApi, type AirApiOptions, type Asset, type AssetBoardListParams, type AssetCustomFieldValue, type AssetListParams, type AssetVersion, type AssetVersionDownloadUrl, type AssetVersionListResponse, type AssetVersionUpdateParams, type AssetVersionUrls, Assets, type Board, type BoardCreateParams, type BoardListParams, type BoardUpdateParams, Boards, type CompleteMultipartParams, type CustomField, type CustomFieldCreateParams, type CustomFieldListParams, type CustomFieldType, type CustomFieldUpdateParams, type CustomFieldValue, type CustomFieldValueCreateParams, type CustomFieldValueUpdateParams, CustomFields, type GetPartUploadUrlParams, type GetPartUploadUrlResponse, type Guest, type GuestListParams, type GuestListResponse, type ImportCreateParams, type ImportCreateResponse, type ImportStatus, type ImportStatusResponse, Imports, type LargeUploadResponse, type Role, type RoleListParams, type RoleListResponse, Roles, type SetBoardCustomFieldParams, type SetCustomFieldParams, type SmallUploadResponse, type SmartTag, type Tag, type TagCreateParams, type TagListParams, type TagUpdateParams, Tags, type UpdateGuestParams, type UploadCreateParams, type UploadCreateResponse, type UploadFileOptions, type UploadFileParams, type UploadFileResponse, Uploads };
432
+ export { type AddBoardAssetsParams, type AddGuestParams, type AddVersionTagParams, AirApi, type AirApiOptions, type Asset, type AssetBoardListParams, type AssetCustomFieldValue, type AssetListParams, type AssetVersion, type AssetVersionDownloadUrl, type AssetVersionListResponse, type AssetVersionUpdateParams, type AssetVersionUrls, Assets, type Board, type BoardCreateParams, type BoardListParams, type BoardUpdateParams, Boards, type CompleteMultipartParams, type CustomField, type CustomFieldCreateParams, type CustomFieldListParams, type CustomFieldType, type CustomFieldUpdateParams, type CustomFieldValue, type CustomFieldValueCreateParams, type CustomFieldValueUpdateParams, CustomFields, type FileMeta, type FileSource, type GetPartUploadUrlParams, type GetPartUploadUrlResponse, type Guest, type GuestListParams, type GuestListResponse, type ImportCreateParams, type ImportCreateResponse, type ImportStatus, type ImportStatusResponse, Imports, type LargeUploadResponse, type Role, type RoleListParams, type RoleListResponse, Roles, type SetBoardCustomFieldParams, type SetCustomFieldParams, type SmallUploadResponse, type SmartTag, type Tag, type TagCreateParams, type TagListParams, type TagUpdateParams, Tags, type UpdateGuestParams, type UploadCreateParams, type UploadCreateResponse, type UploadFileOptions, type UploadFileParams, type UploadFileResponse, Uploads, type Workspace, type WorkspaceListResponse, Workspaces };
package/dist/index.js CHANGED
@@ -364,22 +364,23 @@ var Uploads = class {
364
364
  });
365
365
  }
366
366
  async uploadFile(params, options = {}) {
367
- const { fileData, fileName, ext, mime, size } = await resolveFileData(params);
367
+ const meta = await resolveFileMeta(params);
368
368
  const uploadResponse = await this.create({
369
- fileName,
370
- ext,
371
- size,
372
- mime,
369
+ fileName: meta.fileName,
370
+ ext: meta.ext,
371
+ size: meta.size,
372
+ mime: meta.mime,
373
373
  parentBoardId: options.parentBoardId,
374
374
  assetId: options.assetId,
375
375
  customFields: options.customFields,
376
376
  tags: options.tags
377
377
  });
378
378
  if (isSmallUpload(uploadResponse)) {
379
- await this._uploadSmallFile(uploadResponse.uploadUrl, fileData, options.onProgress, size);
379
+ const data = await materializeData(meta);
380
+ await this._uploadSmallFile(uploadResponse.uploadUrl, data, options.onProgress, meta.size);
380
381
  return { assetId: uploadResponse.assetId, versionId: uploadResponse.versionId };
381
382
  }
382
- await this._uploadLargeFile(uploadResponse, fileData, size, options.onProgress);
383
+ await this._uploadLargeFile(uploadResponse, meta, options.onProgress);
383
384
  return { assetId: uploadResponse.assetId, versionId: uploadResponse.versionId };
384
385
  }
385
386
  async _uploadSmallFile(url, data, onProgress, totalSize) {
@@ -395,42 +396,73 @@ var Uploads = class {
395
396
  onProgress({ percentage: 100, uploadedBytes: totalSize, totalBytes: totalSize });
396
397
  }
397
398
  }
398
- async _uploadLargeFile(uploadInfo, data, totalSize, onProgress) {
399
+ async _uploadLargeFile(uploadInfo, meta, onProgress) {
399
400
  const { multiPartUploadId, key } = uploadInfo;
400
- const buffer = data instanceof Buffer ? data : Buffer.from(data);
401
+ const { size: totalSize } = meta;
401
402
  const partCount = Math.ceil(totalSize / DEFAULT_PART_SIZE);
402
403
  const parts = [];
403
404
  let uploadedBytes = 0;
404
405
  const fetchFn = this.client.fetchFn;
405
- for (let i = 0; i < partCount; i++) {
406
- const partNumber = i + 1;
407
- const start = i * DEFAULT_PART_SIZE;
408
- const end = Math.min(start + DEFAULT_PART_SIZE, totalSize);
409
- const chunk = buffer.subarray(start, end);
410
- const { url } = await this.getPartUploadUrl({
411
- multiPartUploadId,
412
- key,
413
- partNumber
414
- });
415
- const response = await fetchFn(url, {
416
- method: "PUT",
417
- body: chunk
418
- });
419
- if (!response.ok) {
420
- throw new Error(`Part ${partNumber} upload failed with status ${response.status}`);
421
- }
422
- const etag = response.headers.get("etag");
423
- if (!etag) {
424
- throw new Error(`Part ${partNumber} upload did not return an ETag`);
406
+ if (meta.source.type === "path") {
407
+ const fs = await import("fs");
408
+ const fd = fs.openSync(meta.source.filePath, "r");
409
+ try {
410
+ for (let i = 0; i < partCount; i++) {
411
+ const partNumber = i + 1;
412
+ const start = i * DEFAULT_PART_SIZE;
413
+ const end = Math.min(start + DEFAULT_PART_SIZE, totalSize);
414
+ const length = end - start;
415
+ const chunk = Buffer.alloc(length);
416
+ const bytesRead = fs.readSync(fd, chunk, 0, length, start);
417
+ const body = bytesRead < length ? chunk.subarray(0, bytesRead) : chunk;
418
+ const { url } = await this.getPartUploadUrl({ multiPartUploadId, key, partNumber });
419
+ const response = await fetchFn(url, { method: "PUT", body });
420
+ if (!response.ok) {
421
+ throw new Error(`Part ${partNumber} upload failed with status ${response.status}`);
422
+ }
423
+ const etag = response.headers.get("etag");
424
+ if (!etag) {
425
+ throw new Error(`Part ${partNumber} upload did not return an ETag`);
426
+ }
427
+ parts.push({ etag, partNumber });
428
+ uploadedBytes += bytesRead;
429
+ if (onProgress) {
430
+ onProgress({
431
+ percentage: Math.round(uploadedBytes / totalSize * 100),
432
+ uploadedBytes,
433
+ totalBytes: totalSize
434
+ });
435
+ }
436
+ }
437
+ } finally {
438
+ fs.closeSync(fd);
425
439
  }
426
- parts.push({ etag, partNumber });
427
- uploadedBytes += chunk.byteLength;
428
- if (onProgress) {
429
- onProgress({
430
- percentage: Math.round(uploadedBytes / totalSize * 100),
431
- uploadedBytes,
432
- totalBytes: totalSize
433
- });
440
+ } else {
441
+ const buffer = await materializeData(meta);
442
+ const buf = buffer instanceof Buffer ? buffer : Buffer.from(buffer);
443
+ for (let i = 0; i < partCount; i++) {
444
+ const partNumber = i + 1;
445
+ const start = i * DEFAULT_PART_SIZE;
446
+ const end = Math.min(start + DEFAULT_PART_SIZE, totalSize);
447
+ const chunk = buf.subarray(start, end);
448
+ const { url } = await this.getPartUploadUrl({ multiPartUploadId, key, partNumber });
449
+ const response = await fetchFn(url, { method: "PUT", body: chunk });
450
+ if (!response.ok) {
451
+ throw new Error(`Part ${partNumber} upload failed with status ${response.status}`);
452
+ }
453
+ const etag = response.headers.get("etag");
454
+ if (!etag) {
455
+ throw new Error(`Part ${partNumber} upload did not return an ETag`);
456
+ }
457
+ parts.push({ etag, partNumber });
458
+ uploadedBytes += chunk.byteLength;
459
+ if (onProgress) {
460
+ onProgress({
461
+ percentage: Math.round(uploadedBytes / totalSize * 100),
462
+ uploadedBytes,
463
+ totalBytes: totalSize
464
+ });
465
+ }
434
466
  }
435
467
  }
436
468
  await this.completeMultipart({ multiPartUploadId, key, parts });
@@ -439,16 +471,22 @@ var Uploads = class {
439
471
  function isSmallUpload(response) {
440
472
  return "uploadUrl" in response;
441
473
  }
442
- async function resolveFileData(params) {
474
+ async function resolveFileMeta(params) {
443
475
  if (params.filePath) {
444
476
  const fs = await import("fs");
445
477
  const path = await import("path");
446
- const fileData = fs.readFileSync(params.filePath);
478
+ const stat = fs.statSync(params.filePath);
447
479
  const parsed = path.parse(params.filePath);
448
480
  const fileName = params.fileName ?? parsed.name;
449
481
  const ext = params.ext ?? parsed.ext.replace(/^\./, "");
450
482
  const mime = params.mime ?? guessMime(ext);
451
- return { fileData, fileName, ext, mime, size: fileData.byteLength };
483
+ return {
484
+ source: { type: "path", filePath: params.filePath },
485
+ fileName,
486
+ ext,
487
+ mime,
488
+ size: stat.size
489
+ };
452
490
  }
453
491
  if (params.buffer) {
454
492
  const data = params.buffer instanceof Buffer ? params.buffer : Buffer.from(params.buffer);
@@ -457,7 +495,7 @@ async function resolveFileData(params) {
457
495
  }
458
496
  const mime = params.mime ?? guessMime(params.ext);
459
497
  return {
460
- fileData: data,
498
+ source: { type: "buffer", data },
461
499
  fileName: params.fileName,
462
500
  ext: params.ext,
463
501
  mime,
@@ -465,17 +503,35 @@ async function resolveFileData(params) {
465
503
  };
466
504
  }
467
505
  if (params.file) {
468
- const arrayBuffer = await params.file.arrayBuffer();
469
- const data = new Uint8Array(arrayBuffer);
470
506
  const name = params.file.name;
471
507
  const dotIndex = name.lastIndexOf(".");
472
508
  const fileName = params.fileName ?? (dotIndex > 0 ? name.substring(0, dotIndex) : name);
473
509
  const ext = params.ext ?? (dotIndex > 0 ? name.substring(dotIndex + 1) : "");
474
510
  const mime = params.mime ?? (params.file.type || guessMime(ext));
475
- return { fileData: data, fileName, ext, mime, size: data.byteLength };
511
+ return {
512
+ source: { type: "file", file: params.file },
513
+ fileName,
514
+ ext,
515
+ mime,
516
+ size: params.file.size
517
+ };
476
518
  }
477
519
  throw new Error("One of filePath, buffer, or file must be provided");
478
520
  }
521
+ async function materializeData(meta) {
522
+ switch (meta.source.type) {
523
+ case "path": {
524
+ const fs = await import("fs");
525
+ return fs.readFileSync(meta.source.filePath);
526
+ }
527
+ case "buffer":
528
+ return meta.source.data;
529
+ case "file": {
530
+ const arrayBuffer = await meta.source.file.arrayBuffer();
531
+ return new Uint8Array(arrayBuffer);
532
+ }
533
+ }
534
+ }
479
535
  function guessMime(ext) {
480
536
  const mimeMap = {
481
537
  jpg: "image/jpeg",
@@ -508,6 +564,20 @@ function guessMime(ext) {
508
564
  return mimeMap[ext.toLowerCase()] ?? "application/octet-stream";
509
565
  }
510
566
 
567
+ // src/resources/workspaces.ts
568
+ var Workspaces = class {
569
+ constructor(client) {
570
+ this.client = client;
571
+ }
572
+ async list() {
573
+ const response = await this.client.request({
574
+ method: "GET",
575
+ path: "/workspaces"
576
+ });
577
+ return response.data;
578
+ }
579
+ };
580
+
511
581
  // src/api.ts
512
582
  var AirApi = class extends AirBase {
513
583
  assets;
@@ -517,6 +587,7 @@ var AirApi = class extends AirBase {
517
587
  roles;
518
588
  imports;
519
589
  uploads;
590
+ workspaces;
520
591
  constructor(options = {}) {
521
592
  super(options);
522
593
  this.assets = new Assets(this);
@@ -526,6 +597,7 @@ var AirApi = class extends AirBase {
526
597
  this.roles = new Roles(this);
527
598
  this.imports = new Imports(this);
528
599
  this.uploads = new Uploads(this);
600
+ this.workspaces = new Workspaces(this);
529
601
  }
530
602
  };
531
603
 
@@ -543,7 +615,10 @@ import {
543
615
  TimeoutError,
544
616
  CursorPage,
545
617
  PagePromise,
546
- VERSION
618
+ VERSION,
619
+ buildAuthorizationUrl,
620
+ exchangeAuthorizationCode,
621
+ generatePKCEChallenge
547
622
  } from "@air/api-core";
548
623
  export {
549
624
  APIError,
@@ -566,5 +641,9 @@ export {
566
641
  Tags,
567
642
  TimeoutError,
568
643
  Uploads,
569
- VERSION
644
+ VERSION,
645
+ Workspaces,
646
+ buildAuthorizationUrl,
647
+ exchangeAuthorizationCode,
648
+ generatePKCEChallenge
570
649
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@air/api-rest",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Typed REST client for Air API",
6
6
  "main": "./dist/index.cjs",
@@ -26,10 +26,10 @@
26
26
  "build": "tsup",
27
27
  "test": "vitest run",
28
28
  "typecheck": "tsc -b",
29
- "clean": "rm -rf dist tsconfig.tsbuildinfo"
29
+ "clean": "rm -rf dist .tsc-out tsconfig.tsbuildinfo"
30
30
  },
31
31
  "dependencies": {
32
- "@air/api-core": "0.1.2"
32
+ "@air/api-core": "0.2.0"
33
33
  },
34
34
  "engines": {
35
35
  "node": ">=18"