diff --git a/package-lock.json b/package-lock.json index ef882f70e..5553ac5e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2876,9 +2876,9 @@ } }, "node_modules/express": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.1.tgz", - "integrity": "sha512-K4w1/Bp7y8iSiVObmCrtq8Cs79XjJc/RU2YYkZQ7wpUu5ZyZ7MtPHkqoMz4pf+mgXfNvo2qft8D9OnrH2ABk9w==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dev": true, "dependencies": { "accepts": "~1.3.8", diff --git a/packages/graph/graphqueryable.ts b/packages/graph/graphqueryable.ts index 8b9599a1e..324640e83 100644 --- a/packages/graph/graphqueryable.ts +++ b/packages/graph/graphqueryable.ts @@ -34,6 +34,9 @@ export class _GraphQueryable extends Queryable { super(base, path); + // we need to use the graph implementation to handle our special encoding + this._query = new GraphQueryParams(); + if (typeof base === "string") { this.parentUrl = base; @@ -162,15 +165,6 @@ export class _GraphCollection extends _GraphQueryable return this; } - /** - * Retrieves the total count of matching resources - * 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; - } - public [Symbol.asyncIterator]() { const q = GraphCollection(this).using(Paged(), ConsistencyLevel()); @@ -239,3 +233,26 @@ export const graphPatch = (o: IGraphQueryable, init?: RequestInit) export const graphPut = (o: IGraphQueryable, init?: RequestInit): Promise => { return op(o, put, init); }; + +class GraphQueryParams extends Map { + + public toString(): string { + + const params = new URLSearchParams(); + const literals: string[] = []; + + for (const item of this) { + + // and here is where we add some "enhanced" parsing as we get issues. + if (/\/any\(.*?\)/i.test(item[1])) { + literals.push(`${item[0]}=${item[1]}`); + } else { + params.append(item[0], item[1]); + } + } + + literals.push(params.toString()); + + return literals.join("&"); + } +} diff --git a/packages/queryable/queryable.ts b/packages/queryable/queryable.ts index 276bdb3ab..4e933fb51 100644 --- a/packages/queryable/queryable.ts +++ b/packages/queryable/queryable.ts @@ -28,12 +28,41 @@ const DefaultMoments = { export type QueryableInit = Queryable | string | [Queryable, string]; +export type QueryParams = { + + // new(init?: string[][] | Record | string | QueryParams): QueryParams; + + /** + * Sets the value associated to a given search parameter to the given value. If there were several values, delete the others. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/set) + */ + set(name: string, value: string): void; + + /** + * Returns the first value associated to the given search parameter. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/get) + */ + get(name: string): string | null; + + /** + * Returns a Boolean indicating if such a search parameter exists. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/URLSearchParams/has) + */ + has(name: string, value?: string): boolean; + + /** Returns a string containing a query string suitable for use in a URL. Does not include the question mark. */ + toString(): string; +}; + @invokable() // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export class Queryable extends Timeline implements IQueryableInternal { // tracks any query parameters which will be appended to the request url - private _query: URLSearchParams; + protected _query: QueryParams; // tracks the current url for a given Queryable protected _url: string; @@ -49,6 +78,7 @@ export class Queryable extends Timeline implements IQu super(DefaultMoments); + // default to use the included URL search params to parse the query string this._query = new URLSearchParams(); // add an internal moment with specific implementation for promise creation @@ -114,7 +144,7 @@ export class Queryable extends Timeline implements IQu /** * Querystring key, value pairs which will be included in the request */ - public get query(): URLSearchParams { + public get query(): QueryParams { return this._query; } @@ -230,7 +260,7 @@ export interface Queryable extends IInvokable { } // this interface is required to stop the class from recursively referencing itself through the DefaultBehaviors type export interface IQueryableInternal extends Timeline, IInvokable { - readonly query: URLSearchParams; + readonly query: QueryParams; // new(...params: any[]); (this: IQueryableInternal, init?: RequestInit): Promise; using(...behaviors: TimelinePipe[]): this; diff --git a/packages/sp/spqueryable.ts b/packages/sp/spqueryable.ts index eff41c793..3e864c1d8 100644 --- a/packages/sp/spqueryable.ts +++ b/packages/sp/spqueryable.ts @@ -92,7 +92,7 @@ export class _SPQueryable extends Queryable { */ public toRequestUrl(): string { - const aliasedParams = new URLSearchParams(this.query); + const aliasedParams = new URLSearchParams(this.query); // this regex is designed to locate aliased parameters within url paths. These may have the form: // /something(!@p1::value) diff --git a/test/graph/paging.ts b/test/graph/paging.ts index 49b3b601b..c9696b783 100644 --- a/test/graph/paging.ts +++ b/test/graph/paging.ts @@ -37,37 +37,24 @@ describe("Groups", function () { it("pages all users", async function () { - const count = await this.pnp.graph.users.count(); - const allUsers = []; for await (const users of this.pnp.graph.users.top(20).select("displayName")) { allUsers.push(...users); } - expect(allUsers.length).to.eq(count); + expect(allUsers.length).to.be.greaterThan(0); }); it("pages groups", async function () { - const count = await this.pnp.graph.groups.count(); - - expect(count).is.gt(0); - const allGroups = []; for await (const groups of this.pnp.graph.groups.top(20).select("displayName")) { allGroups.push(...groups); } - expect(allGroups.length).to.eq(count); - }); - - it("groups count", async function () { - - const count = await this.pnp.graph.groups.count(); - - expect(count).to.be.gt(0); + expect(allGroups.length).to.be.greaterThan(0); }); it("pages items", async function () { @@ -80,12 +67,4 @@ describe("Groups", function () { expect(allItems.length).to.be.gt(0); }); - - it("items count", async function () { - - const count = await itemsCol.count(); - - // items doesn't support count, should be zero - expect(count).to.eq(-1); - }); }); diff --git a/test/graph/query-params.ts b/test/graph/query-params.ts new file mode 100644 index 000000000..1c2b0e824 --- /dev/null +++ b/test/graph/query-params.ts @@ -0,0 +1,44 @@ +import { expect } from "chai"; +import { pnpTest } from "../pnp-test.js"; +import "@pnp/graph/groups"; +import "@pnp/graph/users"; +import { ConsistencyLevel } from "@pnp/graph/index.js"; + +describe("Graph Query Params", function () { + + before(async function () { + + if ((!this.pnp.settings.enableWebTests)) { + this.skip(); + } + }); + + it("groupTypes/any(c:c eq 'Unified')", pnpTest("158a6aa2-3d0e-4435-88e0-11a146db133e", async function () { + + return expect(this.pnp.graph.groups.filter("groupTypes/any(c:c eq 'Unified')")()).to.eventually.be.fulfilled; + })); + + it("NOT groupTypes/any(c:c eq 'Unified')", pnpTest("b26626fc-d5ee-4a46-afc1-1ae210d1a739", async function () { + + const query = this.pnp.graph.groups.using(ConsistencyLevel()).filter("NOT groupTypes/any(c:c eq 'Unified')"); + query.query.set("$count", "true"); + + return expect(query()).to.eventually.be.fulfilled; + })); + + it("companyName ne null and NOT(companyName eq 'Microsoft')", pnpTest("bbca7a4d-6fce-4c1b-904f-e295919ea25e", async function () { + + const query = this.pnp.graph.users.using(ConsistencyLevel()).filter("companyName ne null and NOT(companyName eq 'Microsoft')"); + query.query.set("$count", "true"); + + return expect(query()).to.eventually.be.fulfilled; + })); + + it("not(assignedLicenses/$count eq 0)", pnpTest("1b25afc7-771e-43be-a549-a6b2c326072b", async function () { + + const query = this.pnp.graph.users.using(ConsistencyLevel()).filter("not(assignedLicenses/$count eq 0)"); + query.query.set("$count", "true"); + + return expect(query()).to.eventually.be.fulfilled; + })); +}); diff --git a/test/mocha-root-hooks.ts b/test/mocha-root-hooks.ts index c7620536a..ba09f8ef4 100644 --- a/test/mocha-root-hooks.ts +++ b/test/mocha-root-hooks.ts @@ -70,7 +70,7 @@ export const mochaHooks = { }, }; - if (this.pnp.args.logging > LogLevel.Off) { + if (this.pnp.args.logging < LogLevel.Off) { // add a listener for logging if we are enabled at any level Logger.subscribe(ConsoleListener()); }