-
Notifications
You must be signed in to change notification settings - Fork 222
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
Add support for using nextLink in the typescript sdk #410
Comments
welcome to the repo @waldekmastykarz !! 👋 This is very valuable feedback. Right now the way of doing that would be the following: const page1 = await apiClient.usersById('').messages.get();
const page2 = await (new MessagesRequestBuilder(page1.nextLink, apiClient)).get(); But it's not going to work at the request builder will try to append the path segment to the URL and might also mess up the query string parameters. |
It seems pretty complicated to have to do it that way. Since nextLink is a complete URL, what if we let developers to do something like |
there are a couple of different components to that approach:
|
I understand that. At the same time we need to think about how developers will use our SDK. Ideally, we should strive to make things as easy as possible for developers, even if it means that our responsibility expands to generic concepts beyond our services. I don't think that developers look at our API the same way we do and split it between Graph and OData concepts. They want to get the data they need with as little work as possible and move on to the next task. Getting out of their way and making our SDKs as simple to use as possible is key to adoption and developer satisfaction.
If the SDK gives me back a URL, I expect to be able to use that URL with the SDK. If we offer our developers only the internals requiring them to build request info manually, then I'm pretty sure that folks will come with a helper method allowing them to pass the URL and get back the the request info object. And it would be a shame for everyone to have to implement such a helper themselves if we can solve it for everyone in the SDK.
The iterator is helpful if you want to retrieve all items in the collection (eg. all groups or all channels in a Team). But if you just want to retrieve next batch of items (eg. next set of emails, where you'd almost never want to get all emails at once), it's overly complicated and being able to get just the next batch with a single line of code would be more user-friendly. I appreciate discussing the different perspectives together. I'm sure we'll find a way to solves this that works for everyone. |
Those are all valid points. To extend on the OData aspect I'd say that in my experience most graph users don't care/know enough about OData to consider its ramifications. It's usually a more transactional approach "I send you this request, give me the result". Would this be acceptable, or is it still too complicated? (should already be possible today) const page1 = await apiClient.usersById('').messages.get();
const page2Request = {URL: page1.nextLink, httpMethod: HttpMethod.GET } as RequestInfo;
const page2 = await httpCore.sendAsync(page2Request, MessagesResponse); |
I think it's odd to have to use two different entrypoints to execute a request ( |
so it'd give us something like that: const page1 = await apiClient.usersById('').messages.get();
const page2 = await apiClient.get(page1.nextLink, MessagesResponse); What do you think? (we'd probably have another sendAsync method as well for anything more complex than a get) |
I like it a lot! What is the As for the naming, isn't the |
Yep sorry, I was writing CSharp at the same time I was responding... Edited my response. |
What if we'd pass it as a generic? Wouldn't that be more intuitive? const page1 = await apiClient.usersById('').messages.get();
const page2 = await apiClient.get<MessagesResponse>(page1.nextLink); |
sure but I still need the factory for the type. Typescript doesn't have generic type constraints like : new() in dotnet. So I won't be able to new T() below during deserialization. And reflection is not really a thing, so I can't do something like typeof(T).getConstructor().newInstance(); unless I'm missing something? |
If you deserialize, wouldn't it work to do: return JSON.parse(stringResponse) as TResponse? Or do you need to know the type upfront because each type has its own deserializer. Then you could use abstract class ResponseMessage {
public abstract deserialize(stringResponse: string): void;
}
class MailResponse extends ResponseMessage {
public deserialize(stringResponse: string): void {
const o = JSON.parse(stringResponse);
// set properties on the current instance
}
}
function get<TResponse extends ResponseMessage>(url: string): TResponse {
const r: TResponse = {} as TResponse;
const stringResponse: string = '';
r.deserialize(stringResponse);
return r;
}
const mail = get<MailResponse>('/me/messages'); |
If you have a look at our deserialization process, you'll noticed we don't rely directly on JSON.parse, but instead we've introduced an abstraction layer. This has multiple advantages:
To do so, JsonParseNode instantiates the target type, so it needs to know about it. |
FYI I started working on that with #508 here is what it'll look like const page1 = await client.me.messages.get();
const page2 = await new MessagesRequestBuilder(page1.nextLink, core).get(); And of course we'll be able to add a page iterator in the graph core library like we have today. an alternative would be to add a helper method on the core service and do something like const page1 = await client.me.messages.get();
const page2 = await core.get<MessageCollectionResponse>(page1.nextLink); I'm not sure which one is more intuitive granted .messages gives you a MessagesRequestBuilder (easy to find and look at the constructor) and .get gives you MessagesCollectionResponse, but the alternative will be harder to discover granted one would have to thing about looking at the http core service, understand the generic aspect etc... |
I'm still not quite convinced about the requirement to use both Is there a way to attach |
One of the goals of Kiota was to keep all the fluent API surface (including the API client, which is really a request builder with a special constructor) super lean so the generation logic stays simple and so we can handcraft great APIs at the "manually written code". I do think wanting to "keep everything on the client" is a bias due to the existing SDKs. Sure it'd make for a shorter code path but that's about it. const page1 = await client.me.messages.get();
const page2 = await client.core.get<MessageCollectionResponse>(page1.nextLink); It'd be good if @roinochieng could provide input on this as the PM and the experience we want to deliver. const page1 = await client.me.messages.get();
const page2 = await new MessagesRequestBuilder(page1.nextLink, core).get();
// could be client.core instead of just core here const page1 = await client.me.messages.get();
const page2 = await client.get<MessagesResponse>(page1.nextLink); |
I think it's also about avoiding unnecessary complexity and exposing internals to our consumers, which are important parts of an SDK. Let's get @roinochieng's input indeed 👍 |
I would like to jump in here :) Of the two alternatives, the following seems intuitive.
@baywet I believe the point that @waldekmastykarz is making about having direct calling functions in the client should be included in the |
Thanks @nikithauc, I think having those sorts of method in the GraphServiceClient library in the service library is a good compromise. The implementation would stay simple and should look roughly something like public get<ResponseType extends Parsable>(url: string) : Promise<ResponseType> {
const requestInfo = new RequestInfo();
requestInfo.method = HttpMethod.GET;
requestInfo.setUri(url, undefined, true);
return this.httpCore.sendAsync(requestInfo);
} I'm going to go ahead and create a separate issue to track this additional method (which we can eventually transfer to the service lib repo once we have one?) as this one will get closed once the pull request gets merged. |
In the TypeScript SDK, with a GET request, you get a nextLink, but there doesn't seem to be a way to use it, other than passing it manually to fetch outside of the Graph SDK. The SDK should have a native support for retrieving data using the nextLink.
AB#10398
The text was updated successfully, but these errors were encountered: