diff --git a/CHANGELOG.md b/CHANGELOG.md index aad9cf0b5..3e2b15986 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 3.20.1 - 2023-Nov-20 + +- graph + - Addresses #2833 - Revert of a previous PR #2730 that was meant for V4. + ## 3.20.0 - 2023-Nov-14 - graph @@ -567,4 +572,4 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Dropped entire package, no longer needed - config-store: - - Dropped entire package. \ No newline at end of file + - Dropped entire package. diff --git a/debug/launch/graph.ts b/debug/launch/graph.ts index f2646a551..10659d478 100644 --- a/debug/launch/graph.ts +++ b/debug/launch/graph.ts @@ -17,4 +17,4 @@ export async function Example(settings: any) { }); process.exit(0); -} \ No newline at end of file +} diff --git a/docs/graph/onedrive.md b/docs/graph/onedrive.md index fb4f968ad..0411cb349 100644 --- a/docs/graph/onedrive.md +++ b/docs/graph/onedrive.md @@ -577,16 +577,6 @@ const delta: IDeltaItems = await graph.users.getById("user@tenant.onmicrosoft.co // Get the changes for the drive items from token const delta: IDeltaItems = await graph.me.drive.root.delta("{token}")(); - -// consume all the values using async iterator -for await (const val of delta.next.paged()) { - console.log(JSON.stringify(val, null, 2)); -} - -// consume all the values using async iterator in pages of 20 -for await (const val of delta.next.top(20).paged()) { - console.log(JSON.stringify(val, null, 2)); -} ``` ## Get Drive Item Analytics diff --git a/package-lock.json b/package-lock.json index 42d7db2ce..7264d5adb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pnp/monorepo", - "version": "3.20.0", + "version": "3.20.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@pnp/monorepo", - "version": "3.20.0", + "version": "3.20.1", "license": "MIT", "devDependencies": { "@azure/identity": "3.4.1", diff --git a/package.json b/package.json index b7f3ce3c8..7d7251ae1 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@pnp/monorepo", "private": true, "type": "module", - "version": "3.20.0", + "version": "3.20.1", "description": "A JavaScript library for SharePoint & Graph development.", "devDependencies": { "@azure/identity": "3.4.1", diff --git a/packages/graph/behaviors/paged.ts b/packages/graph/behaviors/paged.ts index 94f6a259b..2d9c67ff7 100644 --- a/packages/graph/behaviors/paged.ts +++ b/packages/graph/behaviors/paged.ts @@ -5,40 +5,34 @@ import { ConsistencyLevel } from "./consistency-level.js"; export interface IPagedResult { count: number; - value: any | any[] | null; + value: any[] | null; hasNext: boolean; - nextLink: string; + next(): Promise; } /** - * A function that will take a collection defining IGraphQueryableCollection and return the count of items - * in that collection. Not all Graph collections support Count. - * - * @param col The collection to count - * @returns number representing the count - */ -export async function Count(col: IGraphQueryableCollection): Promise { - - const q = GraphQueryableCollection(col).using(Paged(), ConsistencyLevel()); - q.query.set("$count", "true"); - q.top(1); - - const y: IPagedResult = await q(); - return y.count; -} - -/** - * Configures a collection query to returned paged results via async iteration + * Configures a collection query to returned paged results * * @param col Collection forming the basis of the paged collection, this param is NOT modified * @returns A duplicate collection which will return paged results */ -export function AsAsyncIterable(col: IGraphQueryableCollection): AsyncIterable { +export function AsPaged(col: IGraphQueryableCollection, supportsCount = false): IGraphQueryableCollection { - const q = GraphQueryableCollection(col).using(Paged(), ConsistencyLevel()); + const q = GraphQueryableCollection(col).using(Paged(supportsCount), ConsistencyLevel()); const queryParams = ["$search", "$top", "$select", "$expand", "$filter", "$orderby"]; + if (supportsCount) { + + // we might be constructing our query with a next url that will already contain $count so we need + // to ensure we don't add it again, likewise if it is already in our query collection we don't add it again + if (!q.query.has("$count") && !/\$count=true/i.test(q.toUrl())) { + q.query.set("$count", "true"); + } + + queryParams.push("$count"); + } + for (let i = 0; i < queryParams.length; i++) { const param = col.query.get(queryParams[i]); if (objectDefinedNotNull(param)) { @@ -46,32 +40,7 @@ export function AsAsyncIterable(col: IGraphQueryableCollection): AsyncIter } } - return { - - [Symbol.asyncIterator]() { - return >{ - - _next: q, - - async next() { - - if (this._next === null) { - return { done: true, value: undefined }; - } - - const result: IPagedResult = await this._next(); - - if (result.hasNext) { - this._next = GraphQueryableCollection([this._next, result.nextLink]); - return { done: false, value: result.value }; - } else { - this._next = null; - return { done: false, value: result.value }; - } - }, - }; - }, - }; + return q; } /** @@ -79,7 +48,7 @@ export function AsAsyncIterable(col: IGraphQueryableCollection): AsyncIter * * @returns A TimelinePipe used to configure the queryable */ -export function Paged(): TimelinePipe { +export function Paged(supportsCount = false): TimelinePipe { return (instance: IGraphQueryable) => { @@ -90,14 +59,14 @@ export function Paged(): TimelinePipe { const json = txt.replace(/\s/ig, "").length > 0 ? JSON.parse(txt) : {}; const nextLink = json["@odata.nextLink"]; - const count = hOP(json, "@odata.count") ? parseInt(json["@odata.count"], 10) : -1; + const count = supportsCount && hOP(json, "@odata.count") ? parseInt(json["@odata.count"], 10) : 0; const hasNext = !stringIsNullOrEmpty(nextLink); result = { count, hasNext, - nextLink: hasNext ? nextLink : null, + next: () => (hasNext ? AsPaged(GraphQueryableCollection([instance, nextLink]), supportsCount)() : null), value: parseODataJSON(json), }; diff --git a/packages/graph/directory-objects/types.ts b/packages/graph/directory-objects/types.ts index c1e8b6c13..ad83019b7 100644 --- a/packages/graph/directory-objects/types.ts +++ b/packages/graph/directory-objects/types.ts @@ -3,7 +3,7 @@ import { DirectoryObject as IDirectoryObjectType } from "@microsoft/microsoft-gr import { defaultPath, getById, IGetById, deleteable, IDeleteable } from "../decorators.js"; import { body } from "@pnp/queryable"; import { graphPost } from "../operations.js"; -import { Count } from "../behaviors/paged.js"; +import { AsPaged, IPagedResult } from "../behaviors/paged.js"; /** * Represents a Directory Object entity @@ -65,7 +65,18 @@ export class _DirectoryObjects extends _GraphQ * If the resource doesn't support count, this value will always be zero */ public async count(): Promise { - return Count(this); + const q = AsPaged(this, true); + const r: IPagedResult = await q.top(1)(); + return r.count; + } + + /** + * Allows reading through a collection as pages of information whose size is determined by top or the api method's default + * + * @returns an object containing results, the ability to determine if there are more results, and request the next page of results + */ + public paged(): Promise { + return AsPaged(this, true)(); } } export interface IDirectoryObjects extends _DirectoryObjects, IGetById { } diff --git a/packages/graph/graphqueryable.ts b/packages/graph/graphqueryable.ts index 77d029776..3582a7979 100644 --- a/packages/graph/graphqueryable.ts +++ b/packages/graph/graphqueryable.ts @@ -1,7 +1,7 @@ import { isArray } from "@pnp/core"; import { IInvokable, Queryable, queryableFactory } from "@pnp/queryable"; import { ConsistencyLevel } from "./behaviors/consistency-level.js"; -import { AsAsyncIterable } from "./behaviors/paged.js"; +import { AsPaged, IPagedResult } from "./behaviors/paged.js"; export type GraphInit = string | IGraphQueryable | [IGraphQueryable, string]; @@ -167,8 +167,9 @@ export class _GraphQueryableCollection extends _GraphQueryable< * If the resource doesn't support count, this value will always be zero */ public async count(): Promise { - // TODO::do we want to do this, or just attach count to the collections that support it? we could use a decorator for countable on the few collections that support count. - return -1; + const q = AsPaged(this); + const r: IPagedResult = await q.top(1)(); + return r.count; } /** @@ -176,8 +177,8 @@ export class _GraphQueryableCollection extends _GraphQueryable< * * @returns an object containing results, the ability to determine if there are more results, and request the next page of results */ - public paged(): AsyncIterable { - return AsAsyncIterable(this); + public paged(): Promise { + return AsPaged(this)(); } } export interface IGraphQueryableCollection extends _GraphQueryableCollection { } diff --git a/packages/graph/onedrive/types.ts b/packages/graph/onedrive/types.ts index deb0e0b35..ec611a8da 100644 --- a/packages/graph/onedrive/types.ts +++ b/packages/graph/onedrive/types.ts @@ -14,6 +14,7 @@ import { defaultPath, getById, IGetById, deleteable, IDeleteable, updateable, IU import { body, BlobParse, CacheNever, errorCheck, InjectHeaders } from "@pnp/queryable"; import { graphPatch, graphPost, graphPut } from "../operations.js"; import { driveItemUpload } from "./funcs.js"; +import { AsPaged } from "../behaviors/paged.js"; /** * Describes a Drive instance @@ -149,17 +150,15 @@ export class _Root extends _GraphQueryableInstance { public delta(token?: string): IGraphQueryableCollection { const path = `delta${(token) ? `(token=${token})` : ""}`; - const query: IGraphQueryableCollection = GraphQueryableCollection(this, path); + const query = GraphQueryableCollection(this, path); query.on.parse.replace(errorCheck); query.on.parse(async (url: URL, response: Response, result: any): Promise<[URL, Response, any]> => { - const json = await response.json(); const nextLink = json["@odata.nextLink"]; const deltaLink = json["@odata.deltaLink"]; result = { - // TODO:: update docs to show how to load next with async iterator - next: () => (nextLink ? GraphQueryableCollection([this, nextLink]) : null), + next: () => (nextLink ? AsPaged(GraphQueryableCollection([this, nextLink]))() : null), delta: () => (deltaLink ? GraphQueryableCollection([query, deltaLink])() : null), values: json.value, }; @@ -351,8 +350,8 @@ export class _DriveItem extends _GraphQueryableInstance { * @returns IGraphQueryableCollection */ public analytics(analyticsOptions?: IAnalyticsOptions): IGraphQueryableCollection { - const query = `analytics/${analyticsOptions ? analyticsOptions.timeRange : "lastSevenDays"}`; - return >GraphQueryableCollection(this, query); + const query = `analytics/${analyticsOptions?analyticsOptions.timeRange:"lastSevenDays"}`; + return GraphQueryableCollection(this, query); } } export interface IDriveItem extends _DriveItem, IDeleteable, IUpdateable { } diff --git a/packages/graph/onedrive/users.ts b/packages/graph/onedrive/users.ts index 0fbdebdeb..2d21ba12f 100644 --- a/packages/graph/onedrive/users.ts +++ b/packages/graph/onedrive/users.ts @@ -43,7 +43,13 @@ _Drive.prototype.special = function special(specialFolder: SpecialFolder): IDriv return DriveItem(this, `special/${specialFolder}`); }; -export type SpecialFolder = "documents" | "photos" | "cameraroll" | "approot" | "music"; +export enum SpecialFolder { + "Documents" = "documents", + "Photos" = "photos", + "CameraRoll" = "cameraroll", + "AppRoot" = "approot", + "Music" = "music", +} _DriveItem.prototype.restore = function restore(restoreOptions: IItemOptions): Promise { return graphPost(DriveItem(this, "restore"), body(restoreOptions)); diff --git a/test/graph/paging.ts b/test/graph/paging.ts index 844a56a3c..69052fa4b 100644 --- a/test/graph/paging.ts +++ b/test/graph/paging.ts @@ -35,14 +35,29 @@ describe("Groups", function () { } }); + it("pages users 1", async function () { + + let users = await this.pnp.graph.users.top(2).paged(); + + expect(users).to.have.property("hasNext", true); + + users = await users.next(); + + expect(users).to.have.property("hasNext", true); + }); + it("pages all users", async function () { const count = await this.pnp.graph.users.count(); const allUsers = []; + let users = await this.pnp.graph.users.top(20).select("displayName").paged(); - for await (const users of this.pnp.graph.users.top(20).select("displayName").paged()) { - allUsers.push(...users); + allUsers.push(...users.value); + + while (users.hasNext) { + users = await users.next(); + allUsers.push(...users.value); } expect(allUsers.length).to.eq(count); @@ -50,17 +65,18 @@ describe("Groups", function () { it("pages groups", async function () { - const count = await this.pnp.graph.groups.count(); - - expect(count).is.gt(0); + let groups = await this.pnp.graph.groups.top(2).paged(); - const allGroups = []; + expect(groups).to.have.property("hasNext", true); + expect(groups).to.have.property("count").gt(0); + expect(groups.value.length).to.eq(2); - for await (const groups of this.pnp.graph.groups.top(20).select("displayName").paged()) { - allGroups.push(...groups); - } + groups = await groups.next(); - expect(allGroups.length).to.eq(count); + expect(groups).to.have.property("hasNext", true); + // count only returns on the first call, not subsequent paged calls + expect(groups).to.have.property("count").eq(0); + expect(groups.value.length).to.eq(2); }); it("groups count", async function () { @@ -70,15 +86,38 @@ describe("Groups", function () { expect(count).to.be.gt(0); }); - it("pages items", async function () { + it("pages all groups", async function () { - const allItems = []; + const count = await this.pnp.graph.groups.count(); + + const allGroups = []; + let groups = await this.pnp.graph.groups.top(20).select("mailNickname").paged(); - for await (const items of itemsCol.paged()) { - allItems.push(...items); + allGroups.push(...groups.value); + + while (groups.hasNext) { + groups = await groups.next(); + allGroups.push(...groups.value); } - expect(allItems.length).to.be.gt(0); + expect(allGroups.length).to.be.gt((count - 10)).and.lt((count + 10)); + }); + + it("pages items", async function () { + + let pagedResults = await itemsCol.top(5).paged(); + + expect(pagedResults.value.length).to.eq(5); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(pagedResults.hasNext).to.be.true; + expect(pagedResults.count).to.eq(0); + + pagedResults = await pagedResults.next(); + + expect(pagedResults.value.length).to.eq(5); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + expect(pagedResults.hasNext).to.be.true; + expect(pagedResults.count).to.eq(0); }); it("items count", async function () { @@ -86,6 +125,6 @@ describe("Groups", function () { const count = await itemsCol.count(); // items doesn't support count, should be zero - expect(count).to.eq(-1); + expect(count).to.eq(0); }); });