From cb9d3ef0803a9d1040ff805268e540e0eb22b29a Mon Sep 17 00:00:00 2001 From: Victor Fernandez Robles Date: Mon, 13 Jul 2026 16:44:14 +0200 Subject: [PATCH 1/4] feat: add favorites support for files and folders --- src/drive/storage/index.ts | 95 ++++++++++++++++++ src/drive/storage/types.ts | 55 ++++++++-- test/drive/storage/index.test.ts | 166 +++++++++++++++++++++++++++++++ 3 files changed, 309 insertions(+), 7 deletions(-) diff --git a/src/drive/storage/index.ts b/src/drive/storage/index.ts index c2f0df13..7ca56434 100644 --- a/src/drive/storage/index.ts +++ b/src/drive/storage/index.ts @@ -16,6 +16,9 @@ import { CreateThumbnailEntryPayload, DeleteFilePayload, DriveFileData, + FavoriteFileDto, + FavoriteFolderDto, + FavoriteStatusResponse, FetchFolderContentResponse, FetchLimitResponse, FetchPaginatedFilesContent, @@ -31,6 +34,8 @@ import { FolderStatsResponse, FolderTreeResponse, FileLimitsResponse, + GetFavoriteFilesPayload, + GetFavoriteFoldersPayload, MoveFilePayload, MoveFileResponse, MoveFileUuidPayload, @@ -911,4 +916,94 @@ export class Storage { public getFileVersionLimits(): Promise { return this.client.get('/files/limits', this.headers()); } + + /** + * Marks a file as favorite. The operation is idempotent: marking an already + * favorited file succeeds without creating duplicates. + * + * @param {string} uuid - The UUID of the file. + * @returns {Promise} A promise that resolves with the favorite status. + */ + public markFileAsFavorite(uuid: string): Promise { + return this.client.put(`/files/${uuid}/favorite`, {}, this.headers()); + } + + /** + * Unmarks a file as favorite. The operation is idempotent: unmarking a file + * that is not favorited succeeds as a no-op. + * + * @param {string} uuid - The UUID of the file. + * @returns {Promise} A promise that resolves with the favorite status. + */ + public unmarkFileAsFavorite(uuid: string): Promise { + return this.client.delete(`/files/${uuid}/favorite`, this.headers()); + } + + /** + * Gets the list of files marked as favorite by the user. + * + * @param {GetFavoriteFilesPayload} payload - Pagination (limit/offset required) plus optional sort, order and updatedAt filter. + * @returns {[Promise, RequestCanceler]} An array containing a promise to get the API response and a function to cancel the request. + */ + public getFavoriteFiles(payload: GetFavoriteFilesPayload): [Promise, RequestCanceler] { + const { limit, offset, sort, order, updatedAt } = payload; + const query = new URLSearchParams(); + query.set('limit', String(limit)); + query.set('offset', String(offset)); + if (sort !== undefined) query.set('sort', sort); + if (order !== undefined) query.set('order', order); + if (updatedAt !== undefined) query.set('updatedAt', updatedAt); + + const { promise, requestCanceler } = this.client.getCancellable( + `/files/favorites?${query}`, + this.headers(), + ); + + return [promise, requestCanceler]; + } + + /** + * Marks a folder as favorite. The operation is idempotent: marking an already + * favorited folder succeeds without creating duplicates. + * + * @param {string} uuid - The UUID of the folder. + * @returns {Promise} A promise that resolves with the favorite status. + */ + public markFolderAsFavorite(uuid: string): Promise { + return this.client.put(`/folders/${uuid}/favorite`, {}, this.headers()); + } + + /** + * Unmarks a folder as favorite. The operation is idempotent: unmarking a folder + * that is not favorited succeeds as a no-op. + * + * @param {string} uuid - The UUID of the folder. + * @returns {Promise} A promise that resolves with the favorite status. + */ + public unmarkFolderAsFavorite(uuid: string): Promise { + return this.client.delete(`/folders/${uuid}/favorite`, this.headers()); + } + + /** + * Gets the list of folders marked as favorite by the user. + * + * @param {GetFavoriteFoldersPayload} payload - Pagination (limit/offset required) plus optional sort, order and updatedAt filter. + * @returns {[Promise, RequestCanceler]} An array containing a promise to get the API response and a function to cancel the request. + */ + public getFavoriteFolders(payload: GetFavoriteFoldersPayload): [Promise, RequestCanceler] { + const { limit, offset, sort, order, updatedAt } = payload; + const query = new URLSearchParams(); + query.set('limit', String(limit)); + query.set('offset', String(offset)); + if (sort !== undefined) query.set('sort', sort); + if (order !== undefined) query.set('order', order); + if (updatedAt !== undefined) query.set('updatedAt', updatedAt); + + const { promise, requestCanceler } = this.client.getCancellable( + `/folders/favorites?${query}`, + this.headers(), + ); + + return [promise, requestCanceler]; + } } diff --git a/src/drive/storage/types.ts b/src/drive/storage/types.ts index e86af125..89caa15b 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 { @@ -165,16 +168,28 @@ export enum FileStatus { } export type FetchPaginatedFile = - paths['/folders/content/{uuid}/files']['get']['responses']['200']['content']['application/json']['files'][0]; + paths['/folders/content/{uuid}/files']['get']['responses']['200']['content']['application/json']['files'][0] & { + isFavorite?: boolean; + }; export type FetchPaginatedFolder = - paths['/folders/content/{uuid}/folders']['get']['responses']['200']['content']['application/json']['folders'][0]; + paths['/folders/content/{uuid}/folders']['get']['responses']['200']['content']['application/json']['folders'][0] & { + isFavorite?: boolean; + }; -export type FetchPaginatedFilesContent = - paths['/folders/content/{uuid}/files']['get']['responses']['200']['content']['application/json']; +export type FetchPaginatedFilesContent = Omit< + paths['/folders/content/{uuid}/files']['get']['responses']['200']['content']['application/json'], + 'files' +> & { + files: FetchPaginatedFile[]; +}; -export type FetchPaginatedFoldersContent = - paths['/folders/content/{uuid}/folders']['get']['responses']['200']['content']['application/json']; +export type FetchPaginatedFoldersContent = Omit< + paths['/folders/content/{uuid}/folders']['get']['responses']['200']['content']['application/json'], + 'folders' +> & { + folders: FetchPaginatedFolder[]; +}; export interface FetchTrashContentResponse { result: { @@ -469,6 +484,32 @@ export interface CheckDuplicatedFoldersResponse { existentFolders: DriveFolderData[]; } +// Favorites + +export type FavoriteFileDto = components['schemas']['FileDto'] & { isFavorite?: boolean }; + +export type FavoriteFolderDto = components['schemas']['FolderDto'] & { isFavorite?: boolean }; + +export interface FavoriteStatusResponse { + favorited: boolean; +} + +export interface GetFavoriteFilesPayload { + limit: number; + offset: number; + sort?: 'updatedAt' | 'uuid'; + order?: 'ASC' | 'DESC'; + updatedAt?: string; +} + +export interface GetFavoriteFoldersPayload { + limit: number; + offset: number; + sort?: 'uuid' | 'plainName' | 'updatedAt'; + order?: 'ASC' | 'DESC'; + updatedAt?: string; +} + export type FileVersion = paths['/files/{uuid}/versions']['get']['responses']['200']['content']['application/json'][0]; export type RestoreFileVersionResponse = diff --git a/test/drive/storage/index.test.ts b/test/drive/storage/index.test.ts index f0f343e8..ab03f25c 100644 --- a/test/drive/storage/index.test.ts +++ b/test/drive/storage/index.test.ts @@ -725,6 +725,172 @@ describe('# storage service tests', () => { }); }); + describe('-> favorites', () => { + describe('mark file as favorite', () => { + it('Should be called with right arguments & return content', 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.markFileAsFavorite(fileUuid); + + // Assert + expect(callStub).toHaveBeenCalledWith(`/files/${fileUuid}/favorite`, {}, headers); + expect(body).toEqual({ favorited: true }); + }); + }); + + describe('unmark file as favorite', () => { + it('Should be called with right arguments & return content', 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.unmarkFileAsFavorite(fileUuid); + + // Assert + expect(callStub).toHaveBeenCalledWith(`/files/${fileUuid}/favorite`, headers); + expect(body).toEqual({ favorited: false }); + }); + }); + + describe('get favorite files', () => { + it('Should be called with only 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.getFavoriteFiles({ limit: 50, offset: 0 }); + const body = await promise; + + // Assert + expect(callStub).toHaveBeenCalledWith('/files/favorites?limit=50&offset=0', headers); + expect(body).toEqual(response); + }); + + it('Should include sort, order and updatedAt in the query when given', async () => { + // Arrange + const updatedAt = '2024-01-01T00:00:00.000Z'; + const callStub = vi.spyOn(HttpClient.prototype, 'getCancellable').mockReturnValue({ + promise: Promise.resolve([]), + requestCanceler: { + cancel: () => null, + }, + }); + const { client, headers } = clientAndHeaders({}); + + // Act + const [promise] = client.getFavoriteFiles({ + limit: 10, + offset: 20, + sort: 'updatedAt', + order: 'DESC', + updatedAt, + }); + await promise; + + // Assert + expect(callStub).toHaveBeenCalledWith( + `/files/favorites?limit=10&offset=20&sort=updatedAt&order=DESC&updatedAt=${encodeURIComponent(updatedAt)}`, + headers, + ); + }); + }); + + describe('mark folder as favorite', () => { + it('Should be called with right arguments & return content', 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.markFolderAsFavorite(folderUuid); + + // Assert + expect(callStub).toHaveBeenCalledWith(`/folders/${folderUuid}/favorite`, {}, headers); + expect(body).toEqual({ favorited: true }); + }); + }); + + describe('unmark folder as favorite', () => { + it('Should be called with right arguments & return content', 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.unmarkFolderAsFavorite(folderUuid); + + // Assert + expect(callStub).toHaveBeenCalledWith(`/folders/${folderUuid}/favorite`, headers); + expect(body).toEqual({ favorited: false }); + }); + }); + + describe('get favorite folders', () => { + it('Should be called with only 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.getFavoriteFolders({ limit: 50, offset: 0 }); + const body = await promise; + + // Assert + expect(callStub).toHaveBeenCalledWith('/folders/favorites?limit=50&offset=0', headers); + expect(body).toEqual(response); + }); + + it('Should include sort, order and updatedAt in the query when given', async () => { + // Arrange + const updatedAt = '2024-01-01T00:00:00.000Z'; + const callStub = vi.spyOn(HttpClient.prototype, 'getCancellable').mockReturnValue({ + promise: Promise.resolve([]), + requestCanceler: { + cancel: () => null, + }, + }); + const { client, headers } = clientAndHeaders({}); + + // Act + const [promise] = client.getFavoriteFolders({ + limit: 10, + offset: 20, + sort: 'plainName', + order: 'ASC', + updatedAt, + }); + await promise; + + // Assert + expect(callStub).toHaveBeenCalledWith( + `/folders/favorites?limit=10&offset=20&sort=plainName&order=ASC&updatedAt=${encodeURIComponent(updatedAt)}`, + headers, + ); + }); + }); + }); + describe('-> quotas', () => { describe('space usage', () => { it('should call with right params & return response', async () => { From 059a100d0a357ea5212c79573ec5c2793b1d5f48 Mon Sep 17 00:00:00 2001 From: Victor Fernandez Robles Date: Tue, 14 Jul 2026 19:26:12 +0200 Subject: [PATCH 2/4] refactor: align favorites API with centralized backend endpoints --- src/drive/storage/index.ts | 93 ++++++++------------------ src/drive/storage/types.ts | 13 +--- test/drive/storage/index.test.ts | 110 ++++++++----------------------- 3 files changed, 58 insertions(+), 158 deletions(-) diff --git a/src/drive/storage/index.ts b/src/drive/storage/index.ts index 7ca56434..6c5819e8 100644 --- a/src/drive/storage/index.ts +++ b/src/drive/storage/index.ts @@ -18,6 +18,7 @@ import { DriveFileData, FavoriteFileDto, FavoriteFolderDto, + FavoriteItemType, FavoriteStatusResponse, FetchFolderContentResponse, FetchLimitResponse, @@ -34,8 +35,7 @@ import { FolderStatsResponse, FolderTreeResponse, FileLimitsResponse, - GetFavoriteFilesPayload, - GetFavoriteFoldersPayload, + GetFavoritesPayload, MoveFilePayload, MoveFileResponse, MoveFileUuidPayload, @@ -918,89 +918,52 @@ export class Storage { } /** - * Marks a file as favorite. The operation is idempotent: marking an already - * favorited file succeeds without creating duplicates. + * Marks a file or folder as favorite. The operation is idempotent: marking an + * already favorited item succeeds without creating duplicates. * - * @param {string} uuid - The UUID of the file. - * @returns {Promise} A promise that resolves with the favorite status. - */ - public markFileAsFavorite(uuid: string): Promise { - return this.client.put(`/files/${uuid}/favorite`, {}, this.headers()); - } - - /** - * Unmarks a file as favorite. The operation is idempotent: unmarking a file - * that is not favorited succeeds as a no-op. - * - * @param {string} uuid - The UUID of the file. + * @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 unmarkFileAsFavorite(uuid: string): Promise { - return this.client.delete(`/files/${uuid}/favorite`, this.headers()); + public markItemAsFavorite(itemType: FavoriteItemType, uuid: string): Promise { + return this.client.put(`/favorites/${itemType}/${uuid}`, {}, this.headers()); } /** - * Gets the list of files marked as favorite by the user. + * Unmarks a file or folder as favorite. The operation is idempotent: unmarking + * an item that is not favorited succeeds as a no-op. * - * @param {GetFavoriteFilesPayload} payload - Pagination (limit/offset required) plus optional sort, order and updatedAt filter. - * @returns {[Promise, RequestCanceler]} An array containing a promise to get the API response and a function to cancel the request. - */ - public getFavoriteFiles(payload: GetFavoriteFilesPayload): [Promise, RequestCanceler] { - const { limit, offset, sort, order, updatedAt } = payload; - const query = new URLSearchParams(); - query.set('limit', String(limit)); - query.set('offset', String(offset)); - if (sort !== undefined) query.set('sort', sort); - if (order !== undefined) query.set('order', order); - if (updatedAt !== undefined) query.set('updatedAt', updatedAt); - - const { promise, requestCanceler } = this.client.getCancellable( - `/files/favorites?${query}`, - this.headers(), - ); - - return [promise, requestCanceler]; - } - - /** - * Marks a folder as favorite. The operation is idempotent: marking an already - * favorited folder succeeds without creating duplicates. - * - * @param {string} uuid - The UUID of the folder. - * @returns {Promise} A promise that resolves with the favorite status. - */ - public markFolderAsFavorite(uuid: string): Promise { - return this.client.put(`/folders/${uuid}/favorite`, {}, this.headers()); - } - - /** - * Unmarks a folder as favorite. The operation is idempotent: unmarking a folder - * that is not favorited succeeds as a no-op. - * - * @param {string} uuid - The UUID of the folder. + * @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 unmarkFolderAsFavorite(uuid: string): Promise { - return this.client.delete(`/folders/${uuid}/favorite`, this.headers()); + public unmarkItemAsFavorite(itemType: FavoriteItemType, uuid: string): Promise { + return this.client.delete(`/favorites/${itemType}/${uuid}`, this.headers()); } /** - * Gets the list of folders marked as favorite by the user. + * Gets the list of files or folders marked as favorite by the user. * - * @param {GetFavoriteFoldersPayload} payload - Pagination (limit/offset required) plus optional sort, order and updatedAt filter. - * @returns {[Promise, RequestCanceler]} An array containing a promise to get the API response and a function to cancel the request. + * @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 getFavoriteFolders(payload: GetFavoriteFoldersPayload): [Promise, RequestCanceler] { - const { limit, offset, sort, order, updatedAt } = payload; + 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); - if (updatedAt !== undefined) query.set('updatedAt', updatedAt); - const { promise, requestCanceler } = this.client.getCancellable( - `/folders/favorites?${query}`, + const { promise, requestCanceler } = this.client.getCancellable( + `/favorites?${query}`, this.headers(), ); diff --git a/src/drive/storage/types.ts b/src/drive/storage/types.ts index 89caa15b..894670eb 100644 --- a/src/drive/storage/types.ts +++ b/src/drive/storage/types.ts @@ -486,6 +486,8 @@ export interface CheckDuplicatedFoldersResponse { // Favorites +export type FavoriteItemType = 'file' | 'folder'; + export type FavoriteFileDto = components['schemas']['FileDto'] & { isFavorite?: boolean }; export type FavoriteFolderDto = components['schemas']['FolderDto'] & { isFavorite?: boolean }; @@ -494,20 +496,11 @@ export interface FavoriteStatusResponse { favorited: boolean; } -export interface GetFavoriteFilesPayload { - limit: number; - offset: number; - sort?: 'updatedAt' | 'uuid'; - order?: 'ASC' | 'DESC'; - updatedAt?: string; -} - -export interface GetFavoriteFoldersPayload { +export interface GetFavoritesPayload { limit: number; offset: number; sort?: 'uuid' | 'plainName' | 'updatedAt'; order?: 'ASC' | 'DESC'; - updatedAt?: string; } export type FileVersion = paths['/files/{uuid}/versions']['get']['responses']['200']['content']['application/json'][0]; diff --git a/test/drive/storage/index.test.ts b/test/drive/storage/index.test.ts index ab03f25c..0e45138b 100644 --- a/test/drive/storage/index.test.ts +++ b/test/drive/storage/index.test.ts @@ -726,122 +726,68 @@ describe('# storage service tests', () => { }); describe('-> favorites', () => { - describe('mark file as favorite', () => { - it('Should be called with right arguments & return content', async () => { + 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.markFileAsFavorite(fileUuid); + const body = await client.markItemAsFavorite('file', fileUuid); // Assert - expect(callStub).toHaveBeenCalledWith(`/files/${fileUuid}/favorite`, {}, headers); + expect(callStub).toHaveBeenCalledWith(`/favorites/file/${fileUuid}`, {}, headers); expect(body).toEqual({ favorited: true }); }); - }); - - describe('unmark file as favorite', () => { - it('Should be called with right arguments & return content', 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.unmarkFileAsFavorite(fileUuid); - - // Assert - expect(callStub).toHaveBeenCalledWith(`/files/${fileUuid}/favorite`, headers); - expect(body).toEqual({ favorited: false }); - }); - }); - describe('get favorite files', () => { - it('Should be called with only the required pagination params when no optional params are given', async () => { + it('Should be called with the folder route & return content when marking a folder', 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.getFavoriteFiles({ limit: 50, offset: 0 }); - const body = await promise; - - // Assert - expect(callStub).toHaveBeenCalledWith('/files/favorites?limit=50&offset=0', headers); - expect(body).toEqual(response); - }); - - it('Should include sort, order and updatedAt in the query when given', async () => { - // Arrange - const updatedAt = '2024-01-01T00:00:00.000Z'; - const callStub = vi.spyOn(HttpClient.prototype, 'getCancellable').mockReturnValue({ - promise: Promise.resolve([]), - requestCanceler: { - cancel: () => null, - }, - }); + const folderUuid = crypto.randomUUID(); + const callStub = vi.spyOn(HttpClient.prototype, 'put').mockResolvedValue({ favorited: true }); const { client, headers } = clientAndHeaders({}); // Act - const [promise] = client.getFavoriteFiles({ - limit: 10, - offset: 20, - sort: 'updatedAt', - order: 'DESC', - updatedAt, - }); - await promise; + const body = await client.markItemAsFavorite('folder', folderUuid); // Assert - expect(callStub).toHaveBeenCalledWith( - `/files/favorites?limit=10&offset=20&sort=updatedAt&order=DESC&updatedAt=${encodeURIComponent(updatedAt)}`, - headers, - ); + expect(callStub).toHaveBeenCalledWith(`/favorites/folder/${folderUuid}`, {}, headers); + expect(body).toEqual({ favorited: true }); }); }); - describe('mark folder as favorite', () => { - it('Should be called with right arguments & return content', async () => { + describe('unmark item as favorite', () => { + it('Should be called with the file route & return content when unmarking a file', async () => { // Arrange - const folderUuid = crypto.randomUUID(); - const callStub = vi.spyOn(HttpClient.prototype, 'put').mockResolvedValue({ favorited: true }); + const fileUuid = crypto.randomUUID(); + const callStub = vi.spyOn(HttpClient.prototype, 'delete').mockResolvedValue({ favorited: false }); const { client, headers } = clientAndHeaders({}); // Act - const body = await client.markFolderAsFavorite(folderUuid); + const body = await client.unmarkItemAsFavorite('file', fileUuid); // Assert - expect(callStub).toHaveBeenCalledWith(`/folders/${folderUuid}/favorite`, {}, headers); - expect(body).toEqual({ favorited: true }); + expect(callStub).toHaveBeenCalledWith(`/favorites/file/${fileUuid}`, headers); + expect(body).toEqual({ favorited: false }); }); - }); - describe('unmark folder as favorite', () => { - it('Should be called with right arguments & return content', async () => { + 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.unmarkFolderAsFavorite(folderUuid); + const body = await client.unmarkItemAsFavorite('folder', folderUuid); // Assert - expect(callStub).toHaveBeenCalledWith(`/folders/${folderUuid}/favorite`, headers); + expect(callStub).toHaveBeenCalledWith(`/favorites/folder/${folderUuid}`, headers); expect(body).toEqual({ favorited: false }); }); }); - describe('get favorite folders', () => { - it('Should be called with only the required pagination params when no optional params are given', async () => { + 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({ @@ -853,17 +799,16 @@ describe('# storage service tests', () => { const { client, headers } = clientAndHeaders({}); // Act - const [promise] = client.getFavoriteFolders({ limit: 50, offset: 0 }); + const [promise] = client.getFavorites('file', { limit: 50, offset: 0 }); const body = await promise; // Assert - expect(callStub).toHaveBeenCalledWith('/folders/favorites?limit=50&offset=0', headers); + expect(callStub).toHaveBeenCalledWith('/favorites?type=file&limit=50&offset=0', headers); expect(body).toEqual(response); }); - it('Should include sort, order and updatedAt in the query when given', async () => { + it('Should include sort and order in the query when given', async () => { // Arrange - const updatedAt = '2024-01-01T00:00:00.000Z'; const callStub = vi.spyOn(HttpClient.prototype, 'getCancellable').mockReturnValue({ promise: Promise.resolve([]), requestCanceler: { @@ -873,18 +818,17 @@ describe('# storage service tests', () => { const { client, headers } = clientAndHeaders({}); // Act - const [promise] = client.getFavoriteFolders({ + const [promise] = client.getFavorites('folder', { limit: 10, offset: 20, sort: 'plainName', order: 'ASC', - updatedAt, }); await promise; // Assert expect(callStub).toHaveBeenCalledWith( - `/folders/favorites?limit=10&offset=20&sort=plainName&order=ASC&updatedAt=${encodeURIComponent(updatedAt)}`, + '/favorites?type=folder&limit=10&offset=20&sort=plainName&order=ASC', headers, ); }); From f066c5058b03ac0448e195652336d78d619771dd Mon Sep 17 00:00:00 2001 From: Victor Fernandez Robles Date: Mon, 27 Jul 2026 11:08:15 +0200 Subject: [PATCH 3/4] refactor: regenerate schema with favorites and remove manual isFavorite intersections --- src/drive/storage/types.ts | 28 +--- src/schema.ts | 334 +++++++++++++++++++++++++++++++++---- 2 files changed, 306 insertions(+), 56 deletions(-) diff --git a/src/drive/storage/types.ts b/src/drive/storage/types.ts index 894670eb..7ba3c959 100644 --- a/src/drive/storage/types.ts +++ b/src/drive/storage/types.ts @@ -168,28 +168,16 @@ export enum FileStatus { } export type FetchPaginatedFile = - paths['/folders/content/{uuid}/files']['get']['responses']['200']['content']['application/json']['files'][0] & { - isFavorite?: boolean; - }; + paths['/folders/content/{uuid}/files']['get']['responses']['200']['content']['application/json']['files'][0]; export type FetchPaginatedFolder = - paths['/folders/content/{uuid}/folders']['get']['responses']['200']['content']['application/json']['folders'][0] & { - isFavorite?: boolean; - }; + paths['/folders/content/{uuid}/folders']['get']['responses']['200']['content']['application/json']['folders'][0]; -export type FetchPaginatedFilesContent = Omit< - paths['/folders/content/{uuid}/files']['get']['responses']['200']['content']['application/json'], - 'files' -> & { - files: FetchPaginatedFile[]; -}; +export type FetchPaginatedFilesContent = + paths['/folders/content/{uuid}/files']['get']['responses']['200']['content']['application/json']; -export type FetchPaginatedFoldersContent = Omit< - paths['/folders/content/{uuid}/folders']['get']['responses']['200']['content']['application/json'], - 'folders' -> & { - folders: FetchPaginatedFolder[]; -}; +export type FetchPaginatedFoldersContent = + paths['/folders/content/{uuid}/folders']['get']['responses']['200']['content']['application/json']; export interface FetchTrashContentResponse { result: { @@ -488,9 +476,9 @@ export interface CheckDuplicatedFoldersResponse { export type FavoriteItemType = 'file' | 'folder'; -export type FavoriteFileDto = components['schemas']['FileDto'] & { isFavorite?: boolean }; +export type FavoriteFileDto = components['schemas']['FileDto']; -export type FavoriteFolderDto = components['schemas']['FolderDto'] & { isFavorite?: boolean }; +export type FavoriteFolderDto = components['schemas']['FolderDto']; export interface FavoriteStatusResponse { favorited: boolean; 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; From 278e31ff77f42ce3aaff27d5bc70306053241a09 Mon Sep 17 00:00:00 2001 From: Victor Fernandez Robles Date: Mon, 27 Jul 2026 13:05:05 +0200 Subject: [PATCH 4/4] chore: bump SDK version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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",