AleksandrRogov / DynamicsWebApi

DynamicsWebApi is a Microsoft Dataverse Web API helper library for JavaScript & TypeScript
MIT License
268 stars 58 forks source link

Pagination Issue #164

Closed ArtashMardoyanS closed 8 months ago

ArtashMardoyanS commented 8 months ago

DynamicsWebApi version For example: ^2.1.1

Describe the bug '@odata.nextLink' when we use for the second parameter in "retrieveMultiple" function it returns an error

Actual result The URI 'https://devsi.api.crm.dynamics.com/devsi.api.crm.dynamics.com/api/data/v9.2/contacts?$select=contactid&$count=true&$skiptoken=?$select=contactid&$count=true' is not valid because it is not based on 'https://devsi.api.crm.dynamics.com/api/data/v9.2/'.

Code Snippet

async retrieveContacts() {
        try {
            this.loggerService.trace(`[${this.constructor.name}.retrieveContacts]`);

            const request = {
                collection: 'contacts',
                select: ['contactid'],
                maxPageSize: 10,
                count: true
            };

            const firstPage = await this.dynamicsWebApi.retrieveMultiple(request);

            console.log(firstPage.value[0]);
            const nextPageLink = firstPage['@odata.nextLink'];

            console.log(nextPageLink);

            const secondPage = await this.dynamicsWebApi.retrieveMultiple(request, nextPageLink);

            console.log(secondPage.value[0]);

            return firstPage;
        } catch (ex) {
            this.loggerService.error(`[${this.constructor.name}.retrieveContacts]`, ex);
            throw new InternalServerErrorException(ex);
        }
    }
AleksandrRogov commented 8 months ago

@ArtashMardoyanS the error happens because count gets added to a request twice. Once with nextPageLink and another with request.count. A workaround for now, until I decide what to do with this:

            const request = {
                collection: 'contacts',
                select: ['contactid'],
                maxPageSize: 10,
                count: true
            };

            const firstPage = await this.dynamicsWebApi.retrieveMultiple(request);

            console.log(firstPage.value[0]);
            const nextPageLink = firstPage['@odata.nextLink'];

            console.log(nextPageLink);

            //removing 'select' and 'count' from a request because they get duplicated in the result url
            const nextPagesRequest = {
                collection: request.collection,
                maxPageSize: request.maxPageSize,
            }

            const secondPage = await this.dynamicsWebApi.retrieveMultiple(nextPagesRequest , nextPageLink);

P.S. I was getting a different error when trying to execute your request though:

'Query option '$count' was specified more than once, but it must be specified at most once.'

ArtashMardoyanS commented 8 months ago

@AleksandrRogov thank you for your quick response.

Describe the bug We changed our code and tried to get the next page like your solution but we again received an error.

Actual result The URI 'https://devsi.api.crm.dynamics.com/devsi.api.crm.dynamics.com/api/data/v9.2/contacts?$select=contactid&$count=true&$skiptoken=' is not valid because it is not based on 'https://devsi.api.crm.dynamics.com/api/data/v9.2/'.

Code Snippet

 async retrieveContacts() {
        try {
            this.loggerService.trace(`[${this.constructor.name}.retrieveContacts]`);

            const request = {
                collection: 'contacts',
                select: ['contactid'],
                maxPageSize: 10,
                count: true
            };

            const firstPage = await this.dynamicsWebApi.retrieveMultiple(request);

            console.log(firstPage.value[0]);
            const nextPageLink = firstPage['@odata.nextLink'];

            console.log(nextPageLink);

            //removing 'select' and 'count' from a request because they get duplicated in the result url
            const nextPagesRequest = {
                maxPageSize: request.maxPageSize,
                collection: request.collection
            };

            const secondPage = await this.dynamicsWebApi.retrieveMultiple(nextPagesRequest, nextPageLink);

            console.log(secondPage.value[0]);

            return firstPage;
        } catch (ex) {
            this.loggerService.error(`[${this.constructor.name}.retrieveContacts]`, ex);
            throw new InternalServerErrorException(ex);
        }
    }
AleksandrRogov commented 8 months ago

@ArtashMardoyanS what's your DynamicsWebApi config? specifically server url in there? I see that for some reason your serverUrl gets duplicated. At least that's what the error shows. You can see https://devsi.api.crm.dynamics.com/devsi.api.crm.dynamics.com/ <- devsi.api.crm.dynamics.com in there twice.

ArtashMardoyanS commented 8 months ago

@AleksandrRogov Yes I also see this issue

but my provided nexPageLink is "https://devsi.api.crm.dynamics.com/api/data/v9.2/contacts?$select=contactid&$count=true&$skiptoken=%3Ccookie%20pagenumber=%222%22%20pagingcookie=%22%253ccookie%2520page%253d%25221%2522%253e%253ccontactid%2520last%253d%2522%257b7A3921EA-A4BC-EA11-A812-000D3A33FB82%257d%2522%2520first%253d%2522%257bCADD76EA-09B0-EA11-A812-000D3A3155C1%257d%2522%2520%252f%253e%253c%252fcookie%253e%22%20istracking=%22False%22%20/%3E"

My credentials

DYNAMICS PARAMETERS

DYNAMICS_CLIENT_ID="---" DYNAMICS_CLIENT_SECRET="---" DYNAMICS_INSTANCE_URI="https://devsi.api.crm.dynamics.com/" DYNAMICS_AUTHORITY_URL="https://login.microsoftonline.com/d0f7f33f-8d1f-4ac0-bccd-2ecda8bf422b"

Code Snippet

import * as MSAL from '@azure/msal-node';
import { ConfigService } from '@nestjs/config';
import { DynamicsWebApi } from 'dynamics-web-api';
import { InternalServerErrorException, Injectable } from '@nestjs/common';

import { LoggerService } from '@app/shared/logger';

@Injectable()
export class DynamicsService {
    private dynamicsWebApi: DynamicsWebApi;

    constructor(
        private readonly loggerService: LoggerService,
        private readonly configService: ConfigService
    ) {
        this.initializeDynamicsWebApi();
    }

    private initializeDynamicsWebApi() {
        const clientId = this.configService.get<string>('DYNAMICS.CLIENT_ID');
        const instanceUri = this.configService.get<string>('DYNAMICS.INSTANCE_URI');
        const clientSecret = this.configService.get<string>('DYNAMICS.CLIENT_SECRET');
        const authorityUrl = this.configService.get<string>('DYNAMICS.AUTHORITY_URL');

        const msalConfig = {
            auth: {
                knownAuthorities: ['login.microsoftonline.com'],
                clientSecret: clientSecret,
                authority: authorityUrl,
                clientId: clientId
            }
        };

        const cca = new MSAL.ConfidentialClientApplication(msalConfig);

        this.dynamicsWebApi = new DynamicsWebApi({
            onTokenRefresh: async () => {
                try {
                    const result = await cca.acquireTokenByClientCredential({
                        scopes: [`${instanceUri}/.default`]
                    });

                    return result.accessToken;
                } catch (ex) {
                    this.loggerService.error(`[${this.constructor.name}.initializeDynamicsWebApi]`, ex);
                    return null;
                }
            },
            serverUrl: instanceUri
        });
    }

    async retrieveContacts() {
        try {
            this.loggerService.trace(`[${this.constructor.name}.retrieveContacts]`);

            const request = {
                collection: 'contacts',
                select: ['contactid'],
                maxPageSize: 10,
                count: true
            };

            const firstPage = await this.dynamicsWebApi.retrieveMultiple(request);

            console.log(firstPage.value[0]);
            const nextPageLink = firstPage['@odata.nextLink'];

            console.log(nextPageLink);

            //removing 'select' and 'count' from a request because they get duplicated in the result url
            const nextPagesRequest = {
                maxPageSize: request.maxPageSize,
                collection: request.collection
            };

            const secondPage = await this.dynamicsWebApi.retrieveMultiple(nextPagesRequest, nextPageLink);

            console.log(secondPage.value[0]);

            return firstPage;
        } catch (ex) {
            this.loggerService.error(`[${this.constructor.name}.retrieveContacts]`, ex);
            throw new InternalServerErrorException(ex);
        }
    }
}
AleksandrRogov commented 8 months ago

@ArtashMardoyanS I will try to reproduce this locally.

Nevermind, I saw that you mentioned it in the reply above. Thanks.

AleksandrRogov commented 8 months ago

@ArtashMardoyanS please remove a slash "/" at the end of your DYNAMICS_INSTANCE_URI for now. That slash seems to cause an issue.

DYNAMICS_INSTANCE_URI="https://devsi.api.crm.dynamics.com"

I will work on the fixes. Thank you for submitting the bugs.

AleksandrRogov commented 8 months ago

@ArtashMardoyanS I fixed the issues in v.2.1.2. It can be downloaded from npm. If you have any issues with an update let me know. Thanks!

ArtashMardoyanS commented 8 months ago

@AleksandrRogov thanks, I'll check and let you know

ArtashMardoyanS commented 8 months ago

@AleksandrRogov We updated the version to "v.2.1.2" and now it is working thank you so much you helping us.