Skip to content

Commit

Permalink
Merge pull request #2982 from patrick-rodgers/version-4
Browse files Browse the repository at this point in the history
graph specific query param handling
  • Loading branch information
patrick-rodgers authored Apr 4, 2024
2 parents 0d669ef + 5bd7227 commit 8e3250c
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 40 deletions.
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 26 additions & 9 deletions packages/graph/graphqueryable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export class _GraphQueryable<GetType = any> extends Queryable<GetType> {

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;
Expand Down Expand Up @@ -162,15 +165,6 @@ export class _GraphCollection<GetType = any[]> extends _GraphQueryable<GetType>
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<number> {
// 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());
Expand Down Expand Up @@ -239,3 +233,26 @@ export const graphPatch = <T = any>(o: IGraphQueryable<any>, init?: RequestInit)
export const graphPut = <T = any>(o: IGraphQueryable<any>, init?: RequestInit): Promise<T> => {
return op(o, put, init);
};

class GraphQueryParams extends Map<string, string> {

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("&");
}
}
36 changes: 33 additions & 3 deletions packages/queryable/queryable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,41 @@ const DefaultMoments = {

export type QueryableInit = Queryable<any> | string | [Queryable<any>, string];

export type QueryParams = {

// new(init?: string[][] | Record<string, string> | 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<R> extends Timeline<typeof DefaultMoments> implements IQueryableInternal<R> {

// 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;
Expand All @@ -49,6 +78,7 @@ export class Queryable<R> extends Timeline<typeof DefaultMoments> 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
Expand Down Expand Up @@ -114,7 +144,7 @@ export class Queryable<R> extends Timeline<typeof DefaultMoments> implements IQu
/**
* Querystring key, value pairs which will be included in the request
*/
public get query(): URLSearchParams {
public get query(): QueryParams {
return this._query;
}

Expand Down Expand Up @@ -230,7 +260,7 @@ export interface Queryable<R = any> extends IInvokable<R> { }

// this interface is required to stop the class from recursively referencing itself through the DefaultBehaviors type
export interface IQueryableInternal<R = any> extends Timeline<any>, IInvokable {
readonly query: URLSearchParams;
readonly query: QueryParams;
// new(...params: any[]);
<T = R>(this: IQueryableInternal, init?: RequestInit): Promise<T>;
using(...behaviors: TimelinePipe[]): this;
Expand Down
2 changes: 1 addition & 1 deletion packages/sp/spqueryable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export class _SPQueryable<GetType = any> extends Queryable<GetType> {
*/
public toRequestUrl(): string {

const aliasedParams = new URLSearchParams(this.query);
const aliasedParams = new URLSearchParams(<any>this.query);

// this regex is designed to locate aliased parameters within url paths. These may have the form:
// /something(!@p1::value)
Expand Down
25 changes: 2 additions & 23 deletions test/graph/paging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand All @@ -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);
});
});
44 changes: 44 additions & 0 deletions test/graph/query-params.ts
Original file line number Diff line number Diff line change
@@ -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;
}));
});
2 changes: 1 addition & 1 deletion test/mocha-root-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down

0 comments on commit 8e3250c

Please sign in to comment.