Skip to content

Commit

Permalink
Merge pull request #3085 from juliemturner/version-4
Browse files Browse the repository at this point in the history
Release 4.3.0
  • Loading branch information
juliemturner authored Jul 15, 2024
2 parents 0967a15 + efec1c4 commit b1dd32b
Show file tree
Hide file tree
Showing 14 changed files with 353 additions and 336 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ 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).

## 4.3.0 - 2024-July-15

- sp
- Addresses #3082 - Improves functionality of alias parameters

- graph
- Adds new AdvancedQuery behavior

## 4.2.0 - 2024-June-17

- Only documentation and package updates
Expand Down
15 changes: 15 additions & 0 deletions docs/graph/behaviors.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,18 @@ const graph = graphfi().using(ConsistencyLevel("{level value}"));

await graph.users();
```

## AdvancedQuery

Using this behaviour, you can enable [advanced query capabilities](https://learn.microsoft.com/en-us/graph/aad-advanced-queries?tabs=http) when filtering supported collections.

This sets the consistency level to eventual and enables the `$count` query parameter.

```TypeScript
import { graphfi, AdvancedQuery } from "@pnp/graph";
import "@pnp/graph/users";

const graph = graphfi().using(AdvancedQuery());

await graph.users.filter("companyName ne null and NOT(companyName eq 'Microsoft')")();
```
506 changes: 212 additions & 294 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@
"name": "@pnp/monorepo",
"private": true,
"type": "module",
"version": "4.2.0",
"version": "4.3.0",
"description": "A JavaScript library for SharePoint & Graph development.",
"devDependencies": {
"@azure/identity": "4.2.1",
"@azure/msal-browser": "3.17.0",
"@azure/msal-node": "2.9.2",
"@azure/identity": "4.3.0",
"@azure/msal-browser": "3.19.0",
"@azure/msal-node": "2.11.0",
"@microsoft/microsoft-graph-types": "2.40.0",
"@pnp/buildsystem": "^4.0.1",
"@pnp/logging": "^4.1.1",
"@pnp/logging": "^4.2.0",
"@types/chai": "4.3.16",
"@types/chai-as-promised": "7.1.8",
"@types/core-js": "2.5.8",
"@types/findup-sync": "4.0.5",
"@types/mocha": "10.0.6",
"@types/mocha": "10.0.7",
"@types/node": "18.11.9",
"@types/webpack": "5.28.5",
"@types/yargs": "17.0.32",
Expand All @@ -26,14 +26,14 @@
"del-cli": "5.1.0",
"eslint": "8.57.0",
"findup-sync": "5.0.0",
"globby": "^14.0.1",
"mocha": "10.4.0",
"globby": "14.0.2",
"mocha": "10.6.0",
"node-fetch": "3.3.2",
"prettyjson": "1.2.5",
"string-replace-loader": "3.1.0",
"tslib": "2.6.3",
"typescript": "4.9.5",
"webpack": "5.92.0",
"webpack": "5.93.0",
"webpack-cli": "5.1.4",
"webpack-dev-server": "5.0.4",
"yargs": "17.7.2"
Expand Down
2 changes: 1 addition & 1 deletion packages/azidjsclient/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"dependencies": {
"@pnp/core": "0.0.0-PLACEHOLDER",
"@pnp/queryable": "0.0.0-PLACEHOLDER",
"@azure/identity": "4.2.1",
"@azure/identity": "4.3.0",
"tslib": "2.6.3"
}
}
14 changes: 14 additions & 0 deletions packages/graph/behaviors/advanced-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { TimelinePipe } from "@pnp/core";
import { Queryable } from "@pnp/queryable";
import { ConsistencyLevel } from "@pnp/graph";

export function AdvancedQuery(): TimelinePipe<Queryable> {

return (instance: Queryable) => {

instance.using(ConsistencyLevel());
instance.query.set("$count", "true");

return instance;
};
}
1 change: 1 addition & 0 deletions packages/graph/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export { graphfi as graphfi, GraphFI as GraphFI } from "./fi.js";

export * from "./graphqueryable.js";

export * from "./behaviors/advanced-query.js";
export * from "./behaviors/consistency-level.js";
export * from "./behaviors/defaults.js";
export * from "./behaviors/endpoint.js";
Expand Down
2 changes: 1 addition & 1 deletion packages/msaljsclient/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "./index.js",
"typings": "./index",
"dependencies": {
"@azure/msal-browser": "3.17.0",
"@azure/msal-browser": "3.19.0",
"@pnp/queryable": "0.0.0-PLACEHOLDER",
"tslib": "2.6.3"
}
Expand Down
2 changes: 1 addition & 1 deletion packages/nodejs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "./index.js",
"typings": "./index",
"dependencies": {
"@azure/msal-node": "2.9.2",
"@azure/msal-node": "2.11.0",
"@pnp/core": "0.0.0-PLACEHOLDER",
"@pnp/logging": "0.0.0-PLACEHOLDER",
"@pnp/queryable": "0.0.0-PLACEHOLDER",
Expand Down
12 changes: 3 additions & 9 deletions packages/sp/spqueryable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,11 @@ export class _SPQueryable<GetType = any> extends Queryable<GetType> {

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)
// /something(!@p1::value, param=value)
// /something(param=value,!@p1::value)
// /something(param=value,!@p1::value,param=value)
// /something(param=!@p1::value)
// there could be spaces or not around the boundaries
let url = this.toUrl().replace(/([( *| *, *| *= *])'!(@.*?)::(.*?)'([ *)| *, *])/ig, (match, frontBoundary, labelName, value, endBoundary) => {
// this regex is designed to locate aliased parameters within url paths
let url = this.toUrl().replace(/'!(@.+?)::((?:[^']|'')+)'/ig, (match, labelName, value) => {
this.log(`Rewriting aliased parameter from match ${match} to label: ${labelName} value: ${value}`, 0);
aliasedParams.set(labelName, `'${value}'`);
return `${frontBoundary}${labelName}${endBoundary}`;
return labelName;
});

const query = aliasedParams.toString();
Expand Down
36 changes: 18 additions & 18 deletions test/graph/contacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,30 +183,30 @@ describe("Contacts", function () {
return expect(folderAfterUpdate?.displayName).equals(folderDisplayName);
});

// This logs to the console when it passes, ignore those messages
it("Delete Contact Folder", async function () {
// Add a folder that we can then delete
const testFolderName = `TestFolder_${getRandomString(4)}`;
const folder = await this.pnp.graph.users.getById(testUserName).contactFolders.add(testFolderName, rootFolderID);
await this.pnp.graph.users.getById(testUserName).contactFolders.getById(folder.id).delete();
let deletedFolderFound = false;

try {
// This passes the first time through, expecting it to fail on second pass.
// If we try to find a folder that doesn't exist this returns a 404
const deletedFolder = await this.pnp.graph.users.getById(testUserName).contactFolders.getById(folder.id)();
deletedFolderFound = (deletedFolder?.id.length> 0);
} catch (e) {
if (e?.isHttpRequestError) {
if ((<HttpRequestError>e).status === 404) {
// do nothing
}
} else {
console.log(e.message);
}
}
// await this.pnp.graph.users.getById(testUserName).contactFolders.getById(folder.id).delete()
// let deletedFolderFound = false;

// try {
// // This passes the first time through, expecting it to fail on second pass.
// // If we try to find a folder that doesn't exist this returns a 404
// const deletedFolder = await this.pnp.graph.users.getById(testUserName).contactFolders.getById(folder.id)();
// deletedFolderFound = (deletedFolder?.id.length> 0);
// } catch (e) {
// if (e?.isHttpRequestError) {
// if ((<HttpRequestError>e).status === 404) {
// // do nothing
// }
// } else {
// console.log(e.message);
// }
// }

return expect(deletedFolderFound).is.false;
return expect(this.pnp.graph.users.getById(testUserName).contactFolders.getById(folder.id).delete()).to.eventually.be.fulfilled;
});

it("Get Contacts In Folder", async function () {
Expand Down
4 changes: 2 additions & 2 deletions test/graph/querable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ describe("Queryable", function () {
const drives = await this.pnp.graph.users.getById(testUserName).drives.orderBy("lastModifiedBy/user/displayName")();
const drivesClone: Drive[] = JSON.parse(JSON.stringify(drives));
const drivesResort: Drive[] = drivesClone.sort((a, b) => {
if (a.lastModifiedBy.user.displayName.toUpperCase() < b.lastModifiedBy.user.displayName.toUpperCase()) {
if (a.lastModifiedBy?.user?.displayName?.toUpperCase() < b.lastModifiedBy?.user?.displayName?.toUpperCase()) {
return -1;
}
if (a.lastModifiedBy.user.displayName.toUpperCase() > b.lastModifiedBy.user.displayName.toUpperCase()) {
if (a.lastModifiedBy?.user?.displayName?.toUpperCase() > b.lastModifiedBy?.user?.displayName?.toUpperCase()) {
return 1;
}
return 0;
Expand Down
25 changes: 24 additions & 1 deletion test/graph/query-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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";
import { ConsistencyLevel, AdvancedQuery } from "@pnp/graph/index.js";

describe("Graph Query Params", function () {

Expand Down Expand Up @@ -41,4 +41,27 @@ describe("Graph Query Params", function () {

return expect(query()).to.eventually.be.fulfilled;
}));

describe("AdvancedQuery", () => {
it("NOT groupTypes/any(c:c eq 'Unified')", pnpTest("d24d9b36-d5dc-4a6c-81fa-2e9a73911372", async function () {

const query = this.pnp.graph.groups.using(AdvancedQuery()).filter("NOT groupTypes/any(c:c eq 'Unified')");

return expect(query()).to.eventually.be.fulfilled;
}));

it("companyName ne null and NOT(companyName eq 'Microsoft')", pnpTest("33791988-de36-4a6d-88e1-23f6838236ac", async function () {

const query = this.pnp.graph.users.using(AdvancedQuery()).filter("companyName ne null and NOT(companyName eq 'Microsoft')");

return expect(query()).to.eventually.be.fulfilled;
}));

it("not(assignedLicenses/$count eq 0)", pnpTest("fe202c37-e10e-4b1c-b410-99cf059a491b", async function () {

const query = this.pnp.graph.users.using(AdvancedQuery()).filter("not(assignedLicenses/$count eq 0)");

return expect(query()).to.eventually.be.fulfilled;
}));
});
});
44 changes: 44 additions & 0 deletions test/sp/alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "@pnp/sp/files/web";
import "@pnp/sp/files/folder";
import "@pnp/sp/lists/web";
import { combine } from "@pnp/core";
import { SPQueryable } from "@pnp/sp";

describe("Alias Parameters", function () {

Expand All @@ -29,6 +30,49 @@ describe("Alias Parameters", function () {
await list.rootFolder.files.addUsingPath("text.txt", "Some file content!");
});

it("Parameter parsing", function() {
/** Values to test */
const values = [
"value",
"value's",
"value with space",
"value with space' and apostrophe",
"ending with apostrophe'",
"'staring with apostrophe",
"'staring and ending with apostrophe'",
"with,' comma",
];
/** Aliased parameters to test */
const tests = values.reduce<Record<string, Record<string, string>>>((obj, value)=>{
// Escape apostrophe in value
value = value.replace(/'/g, "''");

obj[`something('!@p1::${value}')`] = {"@p1": `'${value}'`};
obj[`something('!@p1::${value}','!@p2::${value}2')`] = {"@p1": `'${value}'`, "@p2": `'${value}2'`};
obj[`something('!@p1::${value}', param=value)`] = {"@p1": `'${value}'`};
obj[`something('!@p1::${value}', param=value, '!@p2::${value}2')`] = {"@p1": `'${value}'`, "@p2": `'${value}2'`};
obj[`something(param=value,'!@p1::${value}')`] = {"@p1": `'${value}'`};
obj[`something(param=value,'!@p1::${value}','!@p2::${value}2')`] = {"@p1": `'${value}'`, "@p2": `'${value}2'`};
obj[`something(param=value,'!@p1::${value}',param=value)`] = {"@p1": `'${value}'`};
obj[`something(param=value,'!@p1::${value}',param=value,'!@p2::${value}2')`] = {"@p1": `'${value}'`, "@p2": `'${value}2'`};
obj[`something(param='!@p1::${value}')`] = {"@p1": `'${value}'`};
obj[`something(param='!@p1::${value}',param2='!@p2::${value}2')`] = {"@p1": `'${value}'`, "@p2": `'${value}2'`};
return obj;
}, {});

// Test all aliased parameters
for(const [alias, params] of Object.entries(tests)) {
const requestUrl = SPQueryable(this.pnp.sp.web, alias).toRequestUrl();
const searchParams = Object.fromEntries(new URL(requestUrl).searchParams.entries());

// eslint-disable-next-line guard-for-in
for(const param in params) {
expect(searchParams, `Failed to parse "${alias}"`).to.have.property(param);
expect(searchParams[param], `Failed to parse "${alias}"`).to.equal(params[param]);
}
}
});

it("Folders", function () {

return expect(this.pnp.sp.web.getFolderByServerRelativePath(`!@p1::/${combine(webRelativeUrl, "AliasTestLib/MyTestFolder")}`)()).to.eventually.be.fulfilled;
Expand Down

0 comments on commit b1dd32b

Please sign in to comment.