From 3842c17ebe190ab5b765e1bafa8240af6338d652 Mon Sep 17 00:00:00 2001 From: jzunigax2 <125698953+jzunigax2@users.noreply.github.com> Date: Tue, 22 Sep 2026 08:25:13 -0600 Subject: [PATCH 1/2] chore: regenerate schema --- src/schema.ts | 356 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 340 insertions(+), 16 deletions(-) diff --git a/src/schema.ts b/src/schema.ts index bf8d72b3..1fe087c6 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -629,6 +629,7 @@ export interface paths { path?: never; cookie?: never; }; + /** @deprecated */ get: operations['FolderController_getFolderContentFiles']; put?: never; post?: never; @@ -638,6 +639,23 @@ export interface paths { patch?: never; trace?: never; }; + '/folders/v2/content/{uuid}/files': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get files in a folder with cursor based pagination */ + get: operations['FolderController_getFolderContentFilesV2']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; '/folders/{id}/files': { parameters: { query?: never; @@ -679,6 +697,7 @@ export interface paths { path?: never; cookie?: never; }; + /** @deprecated */ get: operations['FolderController_getFolderContentFolders']; put?: never; post?: never; @@ -688,6 +707,23 @@ export interface paths { patch?: never; trace?: never; }; + '/folders/v2/content/{uuid}/folders': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get folders in a folder with cursor based pagination */ + get: operations['FolderController_getFolderContentFoldersV2']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; '/folders/content/{uuid}/folders/existence': { parameters: { query?: never; @@ -759,6 +795,23 @@ export interface paths { patch?: never; trace?: never; }; + '/folders/sync': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get delta of folders since a date */ + get: operations['FolderController_getFoldersSync']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; '/folders/{uuid}/meta': { parameters: { query?: never; @@ -1028,6 +1081,23 @@ export interface paths { patch: operations['FileController_moveFile']; trace?: never; }; + '/files/sync': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get delta of files since a date */ + get: operations['FileController_getFilesSync']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; '/files/recents': { parameters: { query?: never; @@ -2636,6 +2706,23 @@ export interface paths { patch: operations['PhotosController_updatePhotoDeviceAsFolder']; trace?: never; }; + '/photos/folders/files/delta/search': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Get delta of files inside given folders */ + post: operations['PhotosController_getFilesInFolders']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; '/storage/trash/paginated': { parameters: { query?: never; @@ -3758,6 +3845,9 @@ export interface components { GetUserUsageDto: { drive: number; backup: number; + /** @description Mail storage charged to the shared plan counter, in bytes. Falls back to the last known cached value, or 0 if none is available, when the mail service cannot be reached. */ + mail: number; + /** @description Sum of drive, backup and mail usage. */ total: number; }; GetUserLimitDto: { @@ -3873,6 +3963,26 @@ export interface components { */ existentFolders: string[]; }; + ThumbnailDto: { + id: number; + fileId: number; + fileUuid: string; + maxWidth: number; + maxHeight: number; + type: string; + size: number; + bucketId: string; + bucketFile: string; + encryptVersion: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string; + }; + FileSharingDto: { + id: string; + type: string; + }; FileDto: { id: number; uuid: string; @@ -3897,16 +4007,26 @@ export interface components { /** @enum {string} */ status: 'EXISTS' | 'TRASHED' | 'DELETED'; isFavorite?: boolean; + thumbnails?: components['schemas']['ThumbnailDto'][]; + sharings?: components['schemas']['FileSharingDto'][]; }; FilesDto: { files: components['schemas']['FileDto'][]; }; + GetFolderContentFilesV2ResponseDto: { + files: components['schemas']['FileDto'][]; + nextCursor: string | null; + }; ResultFilesDto: { result: components['schemas']['FileDto'][]; }; FoldersDto: { folders: components['schemas']['FolderDto'][]; }; + GetFolderContentFoldersV2ResponseDto: { + folders: components['schemas']['FolderDto'][]; + nextCursor: string | null; + }; CheckFoldersExistenceDto: { /** * @description Plain name of folder or array of plain names @@ -3970,6 +4090,36 @@ export interface components { ResultFoldersDto: { result: components['schemas']['FolderDto'][]; }; + FolderSyncDto: { + type: string; + id: number; + parentId: number; + parentUuid: string; + name: string; + parent: components['schemas']['Folder']; + bucket: string; + userId: number; + encryptVersion: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string; + uuid: string; + plainName: string; + size: number; + /** Format: date-time */ + creationTime: string; + /** Format: date-time */ + modificationTime: string; + /** @enum {string} */ + status: 'EXISTS' | 'TRASHED' | 'DELETED'; + removed: boolean; + deleted: boolean; + }; + GetFoldersSyncResponseDto: { + folders: components['schemas']['FolderSyncDto'][]; + nextCursor: string | null; + }; FolderStatsDto: { /** * @description Number of files in the folder and all subfolders @@ -4143,6 +4293,35 @@ export interface components { */ type: string; }; + FileSyncDto: { + id: number; + uuid: string; + fileId: string | null; + name: string; + type: string; + size: string; + bucket: string; + folderId: number; + folderUuid: string; + encryptVersion: string; + userId: number; + /** Format: date-time */ + creationTime: string; + /** Format: date-time */ + modificationTime: string; + /** Format: date-time */ + createdAt: string; + /** Format: date-time */ + updatedAt: string; + plainName: string; + /** @enum {string} */ + status: 'EXISTS' | 'TRASHED' | 'DELETED'; + sharings?: components['schemas']['FileSharingDto'][]; + }; + GetFilesSyncResponseDto: { + files: components['schemas']['FileSyncDto'][]; + nextCursor: string | null; + }; MoveFileDto: { /** * @description New Destination Folder UUID @@ -4208,22 +4387,6 @@ export interface components { */ encryptVersion: string; }; - ThumbnailDto: { - id: number; - fileId: number; - fileUuid: string; - maxWidth: number; - maxHeight: number; - type: string; - size: number; - bucketId: string; - bucketFile: string; - encryptVersion: string; - /** Format: date-time */ - createdAt: string; - /** Format: date-time */ - updatedAt: string; - }; SetSharingPasswordDto: { /** * @description password encrypted with code generated client side @@ -4464,6 +4627,8 @@ export interface components { /** @enum {string} */ status: 'EXISTS' | 'TRASHED' | 'DELETED'; isFavorite?: boolean; + thumbnails?: components['schemas']['ThumbnailDto'][]; + sharings?: components['schemas']['FileSharingDto'][]; /** @description Owner of the file */ user: components['schemas']['SharingOwnerInfoDto'] | null; }; @@ -4972,6 +5137,20 @@ export interface components { CreateDeviceAsFolderDto: { deviceName: string; }; + GetFilesInFoldersDto: { + /** @description Folder uuids to fetch children files from */ + folderUuids: string[]; + /** @description Filter files updated after this date. Required if cursor is not provided */ + updatedAt?: string; + /** @description Cursor token to fetch the next page of results */ + cursor?: string; + /** @description Page size, max 1000 */ + limit?: number; + }; + GetFilesInFoldersResponseDto: { + files: components['schemas']['FileDto'][]; + nextCursor: string | null; + }; ItemToTrashDto: { /** * @deprecated @@ -6299,6 +6478,40 @@ export interface operations { }; }; }; + FolderController_getFolderContentFilesV2: { + parameters: { + query?: { + /** @description Sort direction */ + order?: 'ASC' | 'DESC'; + /** @description Cursor from a previous response to fetch the next page */ + cursor?: string; + /** @description Page size */ + limit?: number; + /** @description Whether to include each file favorite status */ + withFavorites?: boolean; + /** @description Whether to include each file thumbnails */ + withThumbnails?: boolean; + /** @description Whether to include each file sharing info */ + withSharings?: boolean; + }; + header?: never; + path: { + uuid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GetFolderContentFilesV2ResponseDto']; + }; + }; + }; + }; FolderController_getFolderFiles: { parameters: { query: { @@ -6381,6 +6594,38 @@ export interface operations { }; }; }; + FolderController_getFolderContentFoldersV2: { + parameters: { + query?: { + /** @description Sort direction */ + order?: 'ASC' | 'DESC'; + /** @description Cursor from a previous response to fetch the next page */ + cursor?: string; + /** @description Page size */ + limit?: number; + /** @description Whether to include each folder favorite status */ + withFavorites?: boolean; + /** @description Whether to include each folder sharing info */ + withSharings?: boolean; + }; + header?: never; + path: { + uuid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GetFolderContentFoldersV2ResponseDto']; + }; + }; + }; + }; FolderController_checkFoldersExistenceInFolderOld: { parameters: { query: { @@ -6510,6 +6755,34 @@ export interface operations { }; }; }; + FolderController_getFoldersSync: { + parameters: { + query?: { + /** @description Filter items updated after this date. Required if cursor is not provided */ + updatedAt?: string; + /** @description Cursor token to fetch the next page of results */ + cursor?: string; + /** @description Page size, max 1000 */ + limit?: number; + /** @description Folder status filter */ + status?: 'EXISTS' | 'TRASHED' | 'DELETED'; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GetFoldersSyncResponseDto']; + }; + }; + }; + }; FolderController_getFolder: { parameters: { query?: never; @@ -7017,6 +7290,34 @@ export interface operations { }; }; }; + FileController_getFilesSync: { + parameters: { + query?: { + /** @description Filter items updated after this date. Required if cursor is not provided */ + updatedAt?: string; + /** @description Cursor token to fetch the next page of results */ + cursor?: string; + /** @description Page size, max 1000 */ + limit?: number; + /** @description File status filter */ + status?: 'EXISTS' | 'TRASHED' | 'DELETED'; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GetFilesSyncResponseDto']; + }; + }; + }; + }; FileController_getRecentFiles: { parameters: { query: { @@ -9545,6 +9846,29 @@ export interface operations { }; }; }; + PhotosController_getFilesInFolders: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['GetFilesInFoldersDto']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['GetFilesInFoldersResponseDto']; + }; + }; + }; + }; TrashController_getTrashedFilesPaginated: { parameters: { query: { From 88c96d24ed740bd1d4a8cade227e89dc3ca4bdc2 Mon Sep 17 00:00:00 2001 From: jzunigax2 <125698953+jzunigax2@users.noreply.github.com> Date: Tue, 22 Sep 2026 08:27:18 -0600 Subject: [PATCH 2/2] feat: implement new cursor pagination methods --- package.json | 2 +- src/drive/storage/index.ts | 86 ++++++++++++++++++++++++++++++++ src/drive/storage/types.ts | 22 ++++++++ src/shared/http/client.ts | 4 +- test/drive/storage/index.test.ts | 77 ++++++++++++++++++++++++++++ 5 files changed, 189 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 233f6518..08589fdf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@internxt/sdk", "author": "Internxt ", - "version": "1.21.1", + "version": "1.22.0", "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 46c50f5f..d41f0559 100644 --- a/src/drive/storage/index.ts +++ b/src/drive/storage/index.ts @@ -20,7 +20,11 @@ import { FavoriteFolderDto, FavoriteItemType, FavoriteStatusResponse, + FetchFilesSyncResponse, FetchFolderContentResponse, + FetchFolderFilesCursorResponse, + FetchFolderFoldersCursorResponse, + FetchFoldersSyncResponse, FetchLimitResponse, FetchPaginatedFilesContent, FetchPaginatedFolderContentResponse, @@ -29,8 +33,12 @@ import { FileEntryByUuid, FileMeta, FileVersion, + FilesSyncQuery, FolderAncestor, FolderAncestorWorkspace, + FolderFilesCursorQuery, + FolderFoldersCursorQuery, + FoldersSyncQuery, GlobalSearchOptions, FolderMeta, FolderStatsResponse, @@ -283,6 +291,7 @@ export class Storage { * @param {string} [sort=plainName] - The reference column to sort it. * @param {string} [order=ASC] - The order to be followed. * @returns {[Promise, RequestCanceler]} An array containing a promise to get the API response and a function to cancel the request. + * @deprecated Use `getFolderFilesByUuidWithCursor` instead. */ public getFolderFilesByUuid( folderUuid: UUID, @@ -347,6 +356,7 @@ export class Storage { * @param {string} [sort=plainName] - The reference column to sort it. * @param {string} [order=ASC] - The order to be followed. * @returns {[Promise, RequestCanceler]} An array containing a promise to get the API response and a function to cancel the request. + * @deprecated Use `getFolderFoldersByUuidWithCursor` instead. */ public getFolderFoldersByUuid( folderUuid: UUID, @@ -370,6 +380,82 @@ export class Storage { return [promise, requestCanceler]; } + /** + * Gets a page of the files in a folder using cursor based pagination, sorted by plain name. + * Follow `nextCursor` until it is null to get every file. Only existing files are returned. + * + * @param {UUID} folderUuid - The UUID of the folder. + * @param {FolderFilesCursorQuery} [query] - Page size (50-1000), order, cursor and optional extra data. + * @returns {[Promise, RequestCanceler]} An array containing a promise to get the API response and a function to cancel the request. + */ + public getFolderFilesByUuidWithCursor( + folderUuid: UUID, + query: FolderFilesCursorQuery = {}, + ): [Promise, RequestCanceler] { + const { promise, requestCanceler } = this.client.getCancellable( + `/folders/v2/content/${folderUuid}/files`, + this.headers(), + query, + ); + + return [promise, requestCanceler]; + } + + /** + * Gets a page of the subfolders of a folder using cursor based pagination, sorted by plain name. + * Follow `nextCursor` until it is null to get every subfolder. Only existing folders are returned. + * + * @param {UUID} folderUuid - The UUID of the folder. + * @param {FolderFoldersCursorQuery} [query] - Page size (50-1000), order, cursor and optional extra data. + * @returns {[Promise, RequestCanceler]} An array containing a promise to get the API response and a function to cancel the request. + */ + public getFolderFoldersByUuidWithCursor( + folderUuid: UUID, + query: FolderFoldersCursorQuery = {}, + ): [Promise, RequestCanceler] { + const { promise, requestCanceler } = this.client.getCancellable( + `/folders/v2/content/${folderUuid}/folders`, + this.headers(), + query, + ); + + return [promise, requestCanceler]; + } + + /** + * Gets a page of the user's files updated after a date, using cursor based pagination. + * Pass `updatedAt` on the first request, then only `nextCursor` (with the same `status`) until it is null. + * + * @param {FilesSyncQuery} query - updatedAt or cursor, optional status filter and page size (max 1000). + * @returns {[Promise, RequestCanceler]} An array containing a promise to get the API response and a function to cancel the request. + */ + public getFilesSync(query: FilesSyncQuery): [Promise, RequestCanceler] { + const { promise, requestCanceler } = this.client.getCancellable( + '/files/sync', + this.headers(), + query, + ); + + return [promise, requestCanceler]; + } + + /** + * Gets a page of the user's folders updated after a date, using cursor based pagination. Root folders are excluded. + * Pass `updatedAt` on the first request, then only `nextCursor` (with the same `status`) until it is null. + * + * @param {FoldersSyncQuery} query - updatedAt or cursor, optional status filter and page size (max 1000). + * @returns {[Promise, RequestCanceler]} An array containing a promise to get the API response and a function to cancel the request. + */ + public getFoldersSync(query: FoldersSyncQuery): [Promise, RequestCanceler] { + const { promise, requestCanceler } = this.client.getCancellable( + '/folders/sync', + this.headers(), + query, + ); + + return [promise, requestCanceler]; + } + /** * Removes a specific folder from the centralized persistence * @param folderId diff --git a/src/drive/storage/types.ts b/src/drive/storage/types.ts index 56a49166..104dd061 100644 --- a/src/drive/storage/types.ts +++ b/src/drive/storage/types.ts @@ -179,6 +179,28 @@ export type FetchPaginatedFilesContent = export type FetchPaginatedFoldersContent = paths['/folders/content/{uuid}/folders']['get']['responses']['200']['content']['application/json']; +export type FolderFilesCursorQuery = NonNullable< + paths['/folders/v2/content/{uuid}/files']['get']['parameters']['query'] +>; + +export type FolderFoldersCursorQuery = NonNullable< + paths['/folders/v2/content/{uuid}/folders']['get']['parameters']['query'] +>; + +export type FetchFolderFilesCursorResponse = + paths['/folders/v2/content/{uuid}/files']['get']['responses']['200']['content']['application/json']; + +export type FetchFolderFoldersCursorResponse = + paths['/folders/v2/content/{uuid}/folders']['get']['responses']['200']['content']['application/json']; + +export type FilesSyncQuery = NonNullable; + +export type FoldersSyncQuery = NonNullable; + +export type FetchFilesSyncResponse = paths['/files/sync']['get']['responses']['200']['content']['application/json']; + +export type FetchFoldersSyncResponse = paths['/folders/sync']['get']['responses']['200']['content']['application/json']; + export interface FetchTrashContentResponse { result: { id: number; diff --git a/src/shared/http/client.ts b/src/shared/http/client.ts index b9c1c5d1..ccb1f5a8 100644 --- a/src/shared/http/client.ts +++ b/src/shared/http/client.ts @@ -139,10 +139,12 @@ export class HttpClient { * Requests a GET with option to cancel * @param url * @param headers + * @param params */ public getCancellable( url: URL, headers: Headers, + params?: Parameters, ): { promise: Promise; requestCanceler: RequestCanceler; @@ -153,7 +155,7 @@ export class HttpClient { const promise = this.execute(() => { const source = axios.CancelToken.source(); currentCancel = source.cancel; - return this.axios.get(url, { headers, cancelToken: source.token }); + return this.axios.get(url, { headers, params, cancelToken: source.token }); }); return { promise, requestCanceler }; diff --git a/test/drive/storage/index.test.ts b/test/drive/storage/index.test.ts index da1096d7..7f4e6298 100644 --- a/test/drive/storage/index.test.ts +++ b/test/drive/storage/index.test.ts @@ -41,6 +41,12 @@ describe('# storage service tests', () => { }); describe('-> folders', () => { + const mockGetCancellable = (response: unknown) => + vi.spyOn(HttpClient.prototype, 'getCancellable').mockReturnValue({ + promise: Promise.resolve(response), + requestCanceler: { cancel: () => null }, + }); + describe('get folder content', () => { it('Should return the expected elements, when getFolderContent is called', async () => { // Arrange @@ -253,6 +259,77 @@ describe('# storage service tests', () => { }); }); + describe('get folder content with cursor', () => { + it('When getting folder files without query, then it calls the v2 endpoint with empty params', async () => { + const folderUuid = crypto.randomUUID(); + const response = { files: randomSubfilesResponse(3).files, nextCursor: 'next' }; + const { client, headers } = clientAndHeaders({}); + const getCancellableStub = mockGetCancellable(response); + + const [promise] = client.getFolderFilesByUuidWithCursor(folderUuid); + const body = await promise; + + expect(body).toStrictEqual(response); + expect(getCancellableStub).toHaveBeenCalledWith(`/folders/v2/content/${folderUuid}/files`, headers, {}); + }); + + it('When getting folder files with a query, then it sends the query as params', async () => { + const folderUuid = crypto.randomUUID(); + const { client, headers } = clientAndHeaders({}); + const getCancellableStub = mockGetCancellable({ files: [], nextCursor: null }); + const query = { limit: 1000, order: 'ASC', cursor: 'abc=', withThumbnails: true } as const; + + const [promise] = client.getFolderFilesByUuidWithCursor(folderUuid, query); + await promise; + + expect(getCancellableStub).toHaveBeenCalledWith(`/folders/v2/content/${folderUuid}/files`, headers, query); + }); + + it('When getting folder subfolders with a query, then it calls the v2 endpoint with the params', async () => { + const folderUuid = crypto.randomUUID(); + const response = { folders: randomSubfoldersResponse(2).folders, nextCursor: null }; + const { client, headers } = clientAndHeaders({}); + const getCancellableStub = mockGetCancellable(response); + + const [promise] = client.getFolderFoldersByUuidWithCursor(folderUuid, { limit: 50, order: 'DESC' }); + const body = await promise; + + expect(body).toStrictEqual(response); + expect(getCancellableStub).toHaveBeenCalledWith(`/folders/v2/content/${folderUuid}/folders`, headers, { + limit: 50, + order: 'DESC', + }); + }); + }); + + describe('sync', () => { + it('When getting the files delta, then it calls the sync endpoint with the params', async () => { + const response = { files: [], nextCursor: 'next' }; + const { client, headers } = clientAndHeaders({}); + const getCancellableStub = mockGetCancellable(response); + + const query = { updatedAt: '2026-01-01T00:00:00.000Z', status: 'EXISTS', limit: 1000 } as const; + + const [promise] = client.getFilesSync(query); + const body = await promise; + + expect(body).toStrictEqual(response); + expect(getCancellableStub).toHaveBeenCalledWith('/files/sync', headers, query); + }); + + it('When getting the folders delta with a cursor, then it calls the sync endpoint with the cursor', async () => { + const response = { folders: [], nextCursor: null }; + const { client, headers } = clientAndHeaders({}); + const getCancellableStub = mockGetCancellable(response); + + const [promise] = client.getFoldersSync({ cursor: 'abc', status: 'TRASHED' }); + const body = await promise; + + expect(body).toStrictEqual(response); + expect(getCancellableStub).toHaveBeenCalledWith('/folders/sync', headers, { cursor: 'abc', status: 'TRASHED' }); + }); + }); + describe('create folder entry', () => { it('Should call with correct params & return content', async () => { // Arrange