-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add page iterator #353
Open
rkodev
wants to merge
13
commits into
main
Choose a base branch
from
feat/page-iterator
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
5a5353f
feat: add page iterator
rkodev 5abf2c6
Fix iterator
rkodev ddce5f2
Fix tests
rkodev f003af2
Adds error mapping and defensive programming
rkodev 267690a
Fix type checking
rkodev c86a9eb
Validate page iterator
rkodev 7fd2822
Adds paging state
rkodev 734a2a3
delete graph error
rkodev e12d5a3
make error factory required
rkodev 1e1b09f
Fix
rkodev 189c790
Update
rkodev 3359e5a
Fix delta link state
rkodev 6222f0c
Fix delta state
rkodev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
export * from "./adapter/index.js"; | ||
export * from "./authentication/index.js"; | ||
export * from "./http/index.js"; | ||
export * from "./middleware/index.js"; | ||
export * from "./authentication/index.js"; | ||
export * from "./tasks/index.js"; | ||
export * from "./utils/Constants.js"; | ||
export * from "./utils/Version.js"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,332 @@ | ||
/** | ||
* ------------------------------------------------------------------------------------------- | ||
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. | ||
* See License in the project root for license information. | ||
* ------------------------------------------------------------------------------------------- | ||
*/ | ||
|
||
/** | ||
* @module PageIterator | ||
*/ | ||
|
||
import { | ||
Parsable, | ||
RequestAdapter, | ||
RequestOption, | ||
RequestInformation, | ||
HttpMethod, | ||
ParsableFactory, | ||
ErrorMappings, | ||
Headers, | ||
} from "@microsoft/kiota-abstractions"; | ||
|
||
/** | ||
* Signature representing PageCollection | ||
* @property {any[]} value - The collection value | ||
* @property {string} [@odata.nextLink] - The nextLink value | ||
* @property {string} [@odata.deltaLink] - The deltaLink value | ||
* @property {any} Additional - Any number of additional properties (This is to accept the any additional data returned by in the response to the nextLink request) | ||
*/ | ||
export interface PageCollection<T> { | ||
value: T[]; | ||
odataNextLink?: string; | ||
odataDeltaLink?: string; | ||
[Key: string]: any; | ||
} | ||
|
||
/** | ||
* Signature representing callback for page iterator | ||
* @property {Function} callback - The callback function which should return boolean to continue the continue/stop the iteration. | ||
*/ | ||
export type PageIteratorCallback<T> = (data: T) => boolean; | ||
|
||
/** | ||
* Signature to define the request options to be sent during request. | ||
* The values of the GraphRequestOptions properties are passed to the Graph Request object. | ||
* @property {Headers} headers - the header options for the request | ||
* @property {RequestOption[]} options - The middleware options for the request | ||
*/ | ||
export interface PagingRequestOptions { | ||
headers?: Headers; | ||
requestOption?: RequestOption[]; | ||
} | ||
|
||
/** | ||
* @typedef {string} PagingState | ||
* Type representing the state of the iterator | ||
*/ | ||
export type PagingState = "NotStarted" | "Paused" | "IntrapageIteration" | "InterpageIteration" | "Delta" | "Complete"; | ||
|
||
/** | ||
* Class representing a PageIterator to iterate over paginated collections. | ||
* @template T - The type of the items in the collection. | ||
* | ||
* This class provides methods to iterate over a collection of items that are paginated. | ||
* It handles fetching the next set of items when the current page is exhausted. | ||
* The iteration can be paused and resumed, and the state of the iterator can be queried. | ||
* | ||
* The PageIterator uses a callback function to process each item in the collection. | ||
* The callback function should return a boolean indicating whether to continue the iteration. | ||
* | ||
* The PageIterator also supports error handling through error mappings and can be configured | ||
* with custom request options. | ||
*/ | ||
export class PageIterator<T extends Parsable> { | ||
/** | ||
* @private | ||
* Member holding the GraphClient instance | ||
*/ | ||
private readonly requestAdapter: RequestAdapter; | ||
|
||
/** | ||
* @private | ||
* Member holding the current page | ||
*/ | ||
private currentPage?: PageCollection<T>; | ||
|
||
/** | ||
* @private | ||
* Member holding a complete/incomplete status of an iterator | ||
*/ | ||
private complete: boolean; | ||
|
||
/** | ||
* @private | ||
* Member holding the current position on the collection | ||
*/ | ||
private cursor: number; | ||
|
||
/** | ||
* @private | ||
* Member holding the factory to create the parsable object | ||
*/ | ||
private readonly parsableFactory: ParsableFactory<PageCollection<T>>; | ||
|
||
/** | ||
* @private | ||
* Member holding the error mappings | ||
*/ | ||
private readonly errorMappings: ErrorMappings; | ||
|
||
/** | ||
* @private | ||
* Member holding the callback for iteration | ||
*/ | ||
private readonly callback: PageIteratorCallback<T>; | ||
|
||
/** | ||
* @private | ||
* Member holding the state of the iterator | ||
*/ | ||
private pagingState: PagingState; | ||
|
||
/** | ||
* @private | ||
* Member holding the headers for the request | ||
*/ | ||
private readonly options?: PagingRequestOptions; | ||
|
||
/** | ||
* @public | ||
* @constructor | ||
* Creates new instance for PageIterator | ||
* @returns An instance of a PageIterator | ||
* @param requestAdapter - The request adapter | ||
* @param pageResult - The page collection result of T | ||
* @param callback - The callback function to be called on each item | ||
* @param errorMappings - The error mappings | ||
* @param parsableFactory - The factory to create the parsable object collection | ||
* @param options - The request options to configure the request | ||
*/ | ||
public constructor( | ||
requestAdapter: RequestAdapter, | ||
pageResult: PageCollection<T>, | ||
callback: PageIteratorCallback<T>, | ||
parsableFactory: ParsableFactory<PageCollection<T>>, | ||
errorMappings: ErrorMappings, | ||
options?: PagingRequestOptions, | ||
) { | ||
if (!requestAdapter) { | ||
const error = new Error("Request adapter is undefined, Please provide a valid request adapter"); | ||
error.name = "Invalid Request Adapter Error"; | ||
throw error; | ||
} | ||
if (!pageResult) { | ||
const error = new Error("Page result is undefined, Please provide a valid page result"); | ||
error.name = "Invalid Page Result Error"; | ||
throw error; | ||
} | ||
if (!callback) { | ||
const error = new Error("Callback is undefined, Please provide a valid callback"); | ||
error.name = "Invalid Callback Error"; | ||
throw error; | ||
} | ||
if (!parsableFactory) { | ||
const error = new Error("Parsable factory is undefined, Please provide a valid parsable factory"); | ||
error.name = "Invalid Parsable Factory Error"; | ||
throw error; | ||
} | ||
if (!errorMappings) { | ||
const error = new Error("Error mappings is undefined, Please provide a valid error mappings"); | ||
error.name = "Invalid Error Mappings Error"; | ||
throw error; | ||
} | ||
this.requestAdapter = requestAdapter; | ||
this.currentPage = pageResult; | ||
|
||
this.cursor = 0; | ||
this.complete = false; | ||
this.errorMappings = errorMappings; | ||
this.parsableFactory = parsableFactory; | ||
this.callback = callback; | ||
|
||
if (!options) { | ||
options = {}; | ||
} | ||
if (!options.headers) { | ||
options.headers = new Headers(); | ||
} | ||
if (!options.headers.has("Content-Type")) { | ||
options.headers.add("Content-Type", "application/json"); | ||
} | ||
this.options = options; | ||
this.pagingState = "NotStarted"; | ||
} | ||
|
||
/** | ||
* @public | ||
* Getter to get the deltaLink in the current response | ||
* @returns A deltaLink which is being used to make delta requests in future | ||
*/ | ||
public getOdataDeltaLink(): string | undefined { | ||
const deltaLink = this.currentPage?.["@odata.deltaLink"] as string | undefined; | ||
return this.currentPage?.odataDeltaLink ?? deltaLink; | ||
} | ||
|
||
/** | ||
* @public | ||
* Getter to get the nextLink in the current response | ||
* @returns A nextLink which is being used to make requests in future | ||
*/ | ||
public getOdataNextLink(): string | undefined { | ||
const nextLink = this.currentPage?.["@odata.nextLink"] as string | undefined; | ||
return this.currentPage?.odataNextLink ?? nextLink; | ||
} | ||
|
||
/** | ||
* @public | ||
* @async | ||
* Iterates over the collection and kicks callback for each item on iteration. Fetches next set of data through nextLink and iterates over again | ||
* This happens until the nextLink is drained out or the user responds with a red flag to continue from callback | ||
*/ | ||
public async iterate() { | ||
const keepIterating = true; | ||
|
||
while (keepIterating) { | ||
const advance = this.enumerate(); | ||
if (!advance) { | ||
return; | ||
} | ||
|
||
const nextLink = this.getOdataNextLink(); | ||
if ( | ||
(nextLink === undefined || nextLink === null || nextLink === "") && | ||
this.cursor >= (this.currentPage?.value.length ?? 0) | ||
) { | ||
this.complete = true; | ||
this.pagingState = "Complete"; | ||
return; | ||
} | ||
|
||
const nextPage = await this.next(); | ||
if (!nextPage) { | ||
return; | ||
} | ||
if (!nextPage.odataNextLink && nextPage.odataDeltaLink) { | ||
this.pagingState = "Delta"; | ||
} | ||
this.currentPage = nextPage; | ||
} | ||
} | ||
|
||
/** | ||
* @public | ||
* Getter to get the state of the iterator | ||
*/ | ||
public getPagingState(): PagingState { | ||
return this.pagingState; | ||
} | ||
|
||
/** | ||
* @private | ||
* @async | ||
* Helper to make a get request to fetch next page with nextLink url and update the page iterator instance with the returned response | ||
* @returns A promise that resolves to a response data with next page collection | ||
*/ | ||
public async next(): Promise<PageCollection<T> | undefined> { | ||
this.pagingState = "InterpageIteration"; | ||
const requestInformation = new RequestInformation(); | ||
requestInformation.httpMethod = HttpMethod.GET; | ||
requestInformation.urlTemplate = this.getOdataNextLink(); | ||
if (this.options) { | ||
if (this.options.headers) { | ||
requestInformation.headers.addAll(this.options.headers); | ||
} | ||
if (this.options.requestOption) { | ||
requestInformation.addRequestOptions(this.options.requestOption); | ||
} | ||
} | ||
|
||
return await this.requestAdapter.send<PageCollection<T>>( | ||
requestInformation, | ||
this.parsableFactory, | ||
this.errorMappings, | ||
); | ||
} | ||
|
||
/** | ||
* @public | ||
* @async | ||
* To resume the iteration | ||
* Note: This internally calls the iterate method, It's just for more readability. | ||
*/ | ||
public async resume() { | ||
return this.iterate(); | ||
} | ||
|
||
/** | ||
* @public | ||
* To get the completeness status of the iterator | ||
* @returns Boolean indicating the completeness | ||
*/ | ||
public isComplete(): boolean { | ||
return this.complete; | ||
} | ||
|
||
/** | ||
* @private | ||
* Iterates over a collection by enqueuing entries one by one and kicking the callback with the enqueued entry | ||
* @returns A boolean indicating the continue flag to process next page | ||
*/ | ||
private enumerate() { | ||
let keepIterating = true; | ||
|
||
const pageItems = this.currentPage?.value; | ||
if (pageItems === undefined || pageItems.length === 0) { | ||
return false; | ||
} | ||
this.pagingState = "IntrapageIteration"; | ||
|
||
// continue iterating from cursor | ||
for (let i = this.cursor; i < pageItems.length; i++) { | ||
keepIterating = this.callback(pageItems[i]); | ||
this.cursor = i + 1; | ||
if (!keepIterating) { | ||
this.pagingState = "Paused"; | ||
break; | ||
} | ||
} | ||
|
||
return keepIterating; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./PageIterator.js"; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we remove the
this.headers
property now that you have this here?