diff --git a/package.json b/package.json index 1856e84b..d3242d3b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@internxt/sdk", "author": "Internxt ", - "version": "1.17.22", + "version": "1.17.23", "description": "An sdk for interacting with Internxt's services", "repository": { "type": "git", diff --git a/src/drive/storage/index.ts b/src/drive/storage/index.ts index c2f0df13..6c5819e8 100644 --- a/src/drive/storage/index.ts +++ b/src/drive/storage/index.ts @@ -16,6 +16,10 @@ import { CreateThumbnailEntryPayload, DeleteFilePayload, DriveFileData, + FavoriteFileDto, + FavoriteFolderDto, + FavoriteItemType, + FavoriteStatusResponse, FetchFolderContentResponse, FetchLimitResponse, FetchPaginatedFilesContent, @@ -31,6 +35,7 @@ import { FolderStatsResponse, FolderTreeResponse, FileLimitsResponse, + GetFavoritesPayload, MoveFilePayload, MoveFileResponse, MoveFileUuidPayload, @@ -911,4 +916,57 @@ export class Storage { public getFileVersionLimits(): Promise { return this.client.get('/files/limits', this.headers()); } + + /** + * Marks a file or folder as favorite. The operation is idempotent: marking an + * already favorited item succeeds without creating duplicates. + * + * @param {FavoriteItemType} itemType - The type of the item ('file' | 'folder'). + * @param {string} uuid - The UUID of the item. + * @returns {Promise} A promise that resolves with the favorite status. + */ + public markItemAsFavorite(itemType: FavoriteItemType, uuid: string): Promise { + return this.client.put(`/favorites/${itemType}/${uuid}`, {}, this.headers()); + } + + /** + * Unmarks a file or folder as favorite. The operation is idempotent: unmarking + * an item that is not favorited succeeds as a no-op. + * + * @param {FavoriteItemType} itemType - The type of the item ('file' | 'folder'). + * @param {string} uuid - The UUID of the item. + * @returns {Promise} A promise that resolves with the favorite status. + */ + public unmarkItemAsFavorite(itemType: FavoriteItemType, uuid: string): Promise { + return this.client.delete(`/favorites/${itemType}/${uuid}`, this.headers()); + } + + /** + * Gets the list of files or folders marked as favorite by the user. + * + * @param {FavoriteItemType} type - The type of favorite items to list ('file' | 'folder'). + * @param {GetFavoritesPayload} payload - Pagination (limit/offset required) plus optional sort and order. + * @returns {[Promise, RequestCanceler]} An array containing a promise to get the API response and a function to cancel the request. + */ + public getFavorites(type: 'file', payload: GetFavoritesPayload): [Promise, RequestCanceler]; + public getFavorites(type: 'folder', payload: GetFavoritesPayload): [Promise, RequestCanceler]; + public getFavorites( + type: FavoriteItemType, + payload: GetFavoritesPayload, + ): [Promise, RequestCanceler] { + const { limit, offset, sort, order } = payload; + const query = new URLSearchParams(); + query.set('type', type); + query.set('limit', String(limit)); + query.set('offset', String(offset)); + if (sort !== undefined) query.set('sort', sort); + if (order !== undefined) query.set('order', order); + + const { promise, requestCanceler } = this.client.getCancellable( + `/favorites?${query}`, + this.headers(), + ); + + return [promise, requestCanceler]; + } } diff --git a/src/drive/storage/types.ts b/src/drive/storage/types.ts index e86af125..7ba3c959 100644 --- a/src/drive/storage/types.ts +++ b/src/drive/storage/types.ts @@ -1,4 +1,4 @@ -import { paths } from '../../schema'; +import { components, paths } from '../../schema'; import { UserResumeData } from '../users/types'; export interface DriveFolderData { @@ -22,6 +22,7 @@ export interface DriveFolderData { user_id: number; uuid: string; user?: UserResumeData; + isFavorite?: boolean; } export interface DriveFileData { @@ -51,6 +52,7 @@ export interface DriveFileData { user?: UserResumeData; creationTime?: string; modificationTime?: string; + isFavorite?: boolean; } export interface Thumbnail { @@ -84,6 +86,7 @@ export interface FolderChild { user_id: number; uuid: string; plainName?: string; + isFavorite?: boolean; } export interface FetchFolderContentResponse { @@ -469,6 +472,25 @@ export interface CheckDuplicatedFoldersResponse { existentFolders: DriveFolderData[]; } +// Favorites + +export type FavoriteItemType = 'file' | 'folder'; + +export type FavoriteFileDto = components['schemas']['FileDto']; + +export type FavoriteFolderDto = components['schemas']['FolderDto']; + +export interface FavoriteStatusResponse { + favorited: boolean; +} + +export interface GetFavoritesPayload { + limit: number; + offset: number; + sort?: 'uuid' | 'plainName' | 'updatedAt'; + order?: 'ASC' | 'DESC'; +} + export type FileVersion = paths['/files/{uuid}/versions']['get']['responses']['200']['content']['application/json'][0]; export type RestoreFileVersionResponse = diff --git a/src/schema.ts b/src/schema.ts index 6402a9d6..e74e3563 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -2599,6 +2599,43 @@ export interface paths { patch?: never; trace?: never; }; + '/photos/devices': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all photo devices as folder */ + get: operations['PhotosController_getPhotoDevicesAsFolder']; + put?: never; + /** Create a photo device as folder */ + post: operations['PhotosController_createPhotoDeviceAsFolder']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/photos/devices/{uuid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get photo device as folder by uuid */ + get: operations['PhotosController_getPhotoDeviceAsFolder']; + put?: never; + post?: never; + /** Delete photo device as folder by uuid */ + delete: operations['PhotosController_deletePhotoDeviceAsFolder']; + options?: never; + head?: never; + /** Update photo device as folder by uuid */ + patch: operations['PhotosController_updatePhotoDeviceAsFolder']; + trace?: never; + }; '/storage/trash/paginated': { parameters: { query?: never; @@ -2717,6 +2754,44 @@ export interface paths { patch?: never; trace?: never; }; + '/favorites': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Gets favorite items + * @description Returns the favorite files or folders of the user, depending on the `type` query param. + */ + get: operations['FavoriteController_getFavorites']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/favorites/{itemType}/{itemId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Mark an item as favorite */ + put: operations['FavoriteController_markItemAsFavorite']; + post?: never; + /** Unmark an item as favorite */ + delete: operations['FavoriteController_unmarkItemAsFavorite']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; '/auth/login': { parameters: { query?: never; @@ -2872,23 +2947,6 @@ export interface paths { patch?: never; trace?: never; }; - '/device/geolocation': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Get Geolocation by ip */ - post: operations['DeviceController_getLocation']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; '/gateway/workspaces': { parameters: { query?: never; @@ -3802,6 +3860,7 @@ export interface components { status: 'EXISTS' | 'TRASHED' | 'DELETED'; removed: boolean; deleted: boolean; + isFavorite?: boolean; }; CreateBulkFoldersConflictResponseDto: { /** @example Folders already exist */ @@ -3837,6 +3896,7 @@ export interface components { plainName: string; /** @enum {string} */ status: 'EXISTS' | 'TRASHED' | 'DELETED'; + isFavorite?: boolean; }; FilesDto: { files: components['schemas']['FileDto'][]; @@ -3903,6 +3963,7 @@ export interface components { status: 'EXISTS' | 'TRASHED' | 'DELETED'; removed: boolean; deleted: boolean; + isFavorite?: boolean; children: components['schemas']['FolderDto'][]; files: components['schemas']['FileDto'][]; }; @@ -4026,6 +4087,8 @@ export interface components { GetFileLimitsDto: { versioning: components['schemas']['VersioningLimitsDto']; maxUploadFileSize: number | null; + /** @description Whether photos access is enabled for this tier */ + photosAccess: boolean; }; FileVersionDto: { id: string; @@ -4339,6 +4402,7 @@ export interface components { status: 'EXISTS' | 'TRASHED' | 'DELETED'; removed: boolean; deleted: boolean; + isFavorite?: boolean; /** @description Owner of the folder */ user: components['schemas']['SharingOwnerInfoDto'] | null; }; @@ -4399,6 +4463,7 @@ export interface components { plainName: string; /** @enum {string} */ status: 'EXISTS' | 'TRASHED' | 'DELETED'; + isFavorite?: boolean; /** @description Owner of the file */ user: components['schemas']['SharingOwnerInfoDto'] | null; }; @@ -4772,6 +4837,7 @@ export interface components { status: 'EXISTS' | 'TRASHED' | 'DELETED'; removed: boolean; deleted: boolean; + isFavorite?: boolean; hasBackups: boolean; /** Format: date-time */ lastBackupAt: string; @@ -5043,6 +5109,36 @@ export interface components { */ code?: string; }; + CliEccKeysDto: { + /** @example publicKeyExample */ + publicKey?: string; + /** @example privateKeyExample */ + privateKey?: string; + /** @example revocationKeyExample */ + revocationKey?: string; + }; + CliKeysDto: { + ecc?: components['schemas']['CliEccKeysDto']; + }; + CliLoginAccessDto: { + /** + * @description The email of the user + * @example user@internxt.com + */ + email: string; + /** + * @description User password + * @example some_hashed_pass + */ + password: string; + /** + * @description TFA + * @example two_factor_authentication_code + */ + tfa?: string; + /** @description keys */ + keys?: components['schemas']['CliKeysDto']; + }; CreateSendLinkDto: { /** * @description Emails of destinatary @@ -9302,6 +9398,113 @@ export interface operations { }; }; }; + PhotosController_getPhotoDevicesAsFolder: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['DeviceAsFolder'][]; + }; + }; + }; + }; + PhotosController_createPhotoDeviceAsFolder: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['CreateDeviceAsFolderDto']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['DeviceAsFolder']; + }; + }; + }; + }; + PhotosController_getPhotoDeviceAsFolder: { + parameters: { + query?: never; + header?: never; + path: { + uuid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['DeviceAsFolder']; + }; + }; + }; + }; + PhotosController_deletePhotoDeviceAsFolder: { + parameters: { + query?: never; + header?: never; + path: { + uuid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + PhotosController_updatePhotoDeviceAsFolder: { + parameters: { + query?: never; + header?: never; + path: { + uuid: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['CreateDeviceAsFolderDto']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['DeviceAsFolder']; + }; + }; + }; + }; TrashController_getTrashedFilesPaginated: { parameters: { query: { @@ -9450,6 +9653,83 @@ export interface operations { }; }; }; + FavoriteController_getFavorites: { + parameters: { + query: { + /** @description Items per page */ + limit: number; + /** @description Offset for pagination */ + offset: number; + /** @description Type of favorite items to list */ + type: 'file' | 'folder'; + /** @description Field to sort by */ + sort?: 'uuid' | 'plainName' | 'updatedAt'; + /** @description Sort order */ + order?: 'ASC' | 'DESC'; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Favorite files or folders, depending on `type` */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': (components['schemas']['FileDto'] | components['schemas']['FolderDto'])[]; + }; + }; + }; + }; + FavoriteController_markItemAsFavorite: { + parameters: { + query?: never; + header?: never; + path: { + /** @description file | folder */ + itemType: string; + /** @description UUID of the item to mark as favorite */ + itemId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Item marked as favorite */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + FavoriteController_unmarkItemAsFavorite: { + parameters: { + query?: never; + header?: never; + path: { + /** @description file | folder */ + itemType: string; + /** @description UUID of the item to unmark as favorite */ + itemId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Item unmarked as favorite */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; AuthController_login: { parameters: { query?: never; @@ -9608,7 +9888,7 @@ export interface operations { }; requestBody: { content: { - 'application/json': components['schemas']['LoginAccessDto']; + 'application/json': components['schemas']['CliLoginAccessDto']; }; }; responses: { @@ -9694,24 +9974,6 @@ export interface operations { }; }; }; - DeviceController_getLocation: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Get geolocation by ip */ - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; GatewayController_initializeWorkspace: { parameters: { query?: never; diff --git a/test/drive/storage/index.test.ts b/test/drive/storage/index.test.ts index f0f343e8..0e45138b 100644 --- a/test/drive/storage/index.test.ts +++ b/test/drive/storage/index.test.ts @@ -725,6 +725,116 @@ describe('# storage service tests', () => { }); }); + describe('-> favorites', () => { + describe('mark item as favorite', () => { + it('Should be called with the file route & return content when marking a file', async () => { + // Arrange + const fileUuid = crypto.randomUUID(); + const callStub = vi.spyOn(HttpClient.prototype, 'put').mockResolvedValue({ favorited: true }); + const { client, headers } = clientAndHeaders({}); + + // Act + const body = await client.markItemAsFavorite('file', fileUuid); + + // Assert + expect(callStub).toHaveBeenCalledWith(`/favorites/file/${fileUuid}`, {}, headers); + expect(body).toEqual({ favorited: true }); + }); + + it('Should be called with the folder route & return content when marking a folder', async () => { + // Arrange + const folderUuid = crypto.randomUUID(); + const callStub = vi.spyOn(HttpClient.prototype, 'put').mockResolvedValue({ favorited: true }); + const { client, headers } = clientAndHeaders({}); + + // Act + const body = await client.markItemAsFavorite('folder', folderUuid); + + // Assert + expect(callStub).toHaveBeenCalledWith(`/favorites/folder/${folderUuid}`, {}, headers); + expect(body).toEqual({ favorited: true }); + }); + }); + + describe('unmark item as favorite', () => { + it('Should be called with the file route & return content when unmarking a file', async () => { + // Arrange + const fileUuid = crypto.randomUUID(); + const callStub = vi.spyOn(HttpClient.prototype, 'delete').mockResolvedValue({ favorited: false }); + const { client, headers } = clientAndHeaders({}); + + // Act + const body = await client.unmarkItemAsFavorite('file', fileUuid); + + // Assert + expect(callStub).toHaveBeenCalledWith(`/favorites/file/${fileUuid}`, headers); + expect(body).toEqual({ favorited: false }); + }); + + it('Should be called with the folder route & return content when unmarking a folder', async () => { + // Arrange + const folderUuid = crypto.randomUUID(); + const callStub = vi.spyOn(HttpClient.prototype, 'delete').mockResolvedValue({ favorited: false }); + const { client, headers } = clientAndHeaders({}); + + // Act + const body = await client.unmarkItemAsFavorite('folder', folderUuid); + + // Assert + expect(callStub).toHaveBeenCalledWith(`/favorites/folder/${folderUuid}`, headers); + expect(body).toEqual({ favorited: false }); + }); + }); + + describe('get favorites', () => { + it('Should be called with only type and the required pagination params when no optional params are given', async () => { + // Arrange + const response: Array = []; + const callStub = vi.spyOn(HttpClient.prototype, 'getCancellable').mockReturnValue({ + promise: Promise.resolve(response), + requestCanceler: { + cancel: () => null, + }, + }); + const { client, headers } = clientAndHeaders({}); + + // Act + const [promise] = client.getFavorites('file', { limit: 50, offset: 0 }); + const body = await promise; + + // Assert + expect(callStub).toHaveBeenCalledWith('/favorites?type=file&limit=50&offset=0', headers); + expect(body).toEqual(response); + }); + + it('Should include sort and order in the query when given', async () => { + // Arrange + const callStub = vi.spyOn(HttpClient.prototype, 'getCancellable').mockReturnValue({ + promise: Promise.resolve([]), + requestCanceler: { + cancel: () => null, + }, + }); + const { client, headers } = clientAndHeaders({}); + + // Act + const [promise] = client.getFavorites('folder', { + limit: 10, + offset: 20, + sort: 'plainName', + order: 'ASC', + }); + await promise; + + // Assert + expect(callStub).toHaveBeenCalledWith( + '/favorites?type=folder&limit=10&offset=20&sort=plainName&order=ASC', + headers, + ); + }); + }); + }); + describe('-> quotas', () => { describe('space usage', () => { it('should call with right params & return response', async () => {