├── .env.example ├── .gitignore ├── .npmignore ├── .prettierrc ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── README.md ├── __tests__ ├── Context.test.ts ├── CustomActions.test.ts ├── List.test.ts ├── MMS.test.ts ├── Profiles.test.ts ├── Search.test.ts ├── Securable.test.ts ├── Web.test.ts ├── namespace.test.ts ├── queryString.test.ts └── testUtils.ts ├── docs ├── .nojekyll ├── arbitrary-requests.md ├── coverpage.md ├── getting-started.md ├── index.html ├── introduction.md ├── list-operations.md ├── mms.md ├── modifying-listitems.md ├── nodejs.md ├── profiles.md ├── search.md ├── sidebar.md └── utilities.md ├── jest.config.js ├── legacy-tests ├── nodeauthtest.js ├── test.browser.js ├── test.html ├── test.server.js └── tests │ ├── authTests.js │ ├── contextTests.js │ ├── customActionTests.js │ ├── index.js │ ├── listTests.js │ ├── permissionTests.js │ ├── profileTests.js │ ├── searchTests.js │ ├── utilsTests.js │ └── webTests.js ├── package.json ├── rollup.config.js ├── src ├── Auth.ts ├── Context.ts ├── CustomActions.ts ├── List.ts ├── MMS.ts ├── Profiles.ts ├── Search.ts ├── Securable.ts ├── Web.ts ├── index.ts ├── request.ts └── utils │ ├── dependencyManagement.ts │ ├── headers.ts │ ├── index.ts │ ├── loaders.ts │ └── queryString.ts ├── stats.html ├── tasks └── setAuthCookie.js ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | SITE_URL=https://skylinespark.sharepoint.com/sites/spscript 2 | PASSWORD=SHHH!! 3 | SP_USER=apetersen@skylinespark.onmicrosoft.com -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | .gz 9 | *.config 10 | pids 11 | logs 12 | results 13 | .c9 14 | npm-debug.log 15 | node_modules 16 | bower_components 17 | lib 18 | pkg 19 | dist 20 | coverage 21 | .env 22 | .cache 23 | 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | .gz 9 | *.config 10 | pids 11 | logs 12 | results 13 | .c9 14 | npm-debug.log 15 | node_modules 16 | bower_components 17 | coverage 18 | .env 19 | docs 20 | .vscode 21 | src 22 | .env 23 | .cache -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "printWidth": 100, 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": false, 7 | "bracketSpacing": true, 8 | "arrowParens": "always" 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "name": "vscode-jest-tests", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 12 | "args": ["--runInBand"], 13 | "cwd": "${workspaceFolder}", 14 | "console": "integratedTerminal", 15 | "internalConsoleOptions": "neverOpen" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "titleBar.activeBackground": "#0b87da", 4 | "titleBar.inactiveBackground": "#0b87da99", 5 | "titleBar.activeForeground": "#e7e7e7", 6 | "titleBar.inactiveForeground": "#e7e7e799", 7 | "activityBar.activeBackground": "#24a1f4", 8 | "activityBar.activeBorder": "#b60971", 9 | "activityBar.background": "#24a1f4", 10 | "activityBar.foreground": "#15202b", 11 | "activityBar.inactiveForeground": "#15202b99", 12 | "activityBarBadge.background": "#b60971", 13 | "activityBarBadge.foreground": "#e7e7e7", 14 | "statusBar.background": "#0b87da", 15 | "statusBar.border": "#0b87da", 16 | "statusBar.foreground": "#e7e7e7", 17 | "statusBarItem.hoverBackground": "#24a1f4", 18 | "titleBar.border": "#0b87da" 19 | }, 20 | "peacock.color": "#0b87da" 21 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "npm", 4 | "isShellCommand": true, 5 | "args": [ 6 | "run" 7 | ], 8 | "tasks": [ 9 | { 10 | "taskName": "build", 11 | "args": [], 12 | "isTestCommand": false, 13 | "isBuildCommand": true 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SPScript 2 | 3 | --- 4 | 5 | [![Join the chat at https://gitter.im/DroopyTersen/spscript](https://badges.gitter.im/DroopyTersen/spscript.svg)](https://gitter.im/DroopyTersen/spscript?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 6 | 7 | ### Visit [https://spcript.com](https://spscript.com) for full documentation. 8 | 9 | SPScript is a collection of javascript helpers for the SharePoint Rest API. Some features include... 10 | 11 | - Easy querying of list data. 12 | - Add and Update list items in 1 line of code. 13 | - Easily utilize SharePoint search 14 | - Work with the Profile Service 15 | - Check permissions on sites and lists 16 | - Work with CustomActions 17 | 18 | ## Installation 19 | 20 | Add the SPScript `npm` package to your project 21 | 22 | _NPM_ 23 | 24 | ```shell 25 | npm install spscript 26 | ``` 27 | 28 | _Yarn_ 29 | 30 | ```shell 31 | yarn add spscript 32 | ``` 33 | 34 | ## Importing 35 | 36 | You can use SPScript in your Javascript/Typescript files with: 37 | 38 | ```javascript 39 | import SPScript from "spscript"; 40 | ``` 41 | 42 | **ProTip: Dynamically/Temporarily add SPScript to a Modern page with Dev Tools** 43 | 44 | You can enter the following into a browser console to dynamically load SPScript on a page. 45 | 46 | ```javascript 47 | let script = document.createElement("script"); 48 | script.src = "https://unpkg.com/spscript@beta/dist/spscript.browser.js"; 49 | document.head.appendChild(script); 50 | ``` 51 | 52 | ## SPScript Context 53 | 54 | Almost everything in SPScript is based off an SPScript `Context` class. 55 | 56 | - An SPScript **Context** is tied to specific SharePoint site. 57 | - You get a **Context** by calling `SPScript.createContext(siteUrl)`. 58 | 59 | > You get a **Context** by calling `SPScript.createContext(siteUrl)`. 60 | 61 | _This line of code is the entry point to almost everything SPScript provides._ 62 | 63 | ```javascript 64 | let ctx = SPScript.createContext(siteUrl); 65 | ``` 66 | 67 | _Example Usage: Get the News Pages of the specified site._ 68 | 69 | ```javascript 70 | import SPScript from "spscript"; 71 | 72 | const getPublishedNews = async function (siteUrl) { 73 | let ctx = SPScript.createContext(siteUrl); 74 | let pages = await ctx.lists("Site Pages").findItems("PromotedState", 2); 75 | console.log(pages); // This will show an Array of Page List Items 76 | return pages; 77 | }; 78 | ``` 79 | 80 | Throughout the docs you'll see a variable, `ctx`, representing an instance of an SPScript `Context`. 81 | 82 | ## Troubleshooting 83 | 84 | If you are using Typescript, you may have to use the syntax: 85 | 86 | ```javascript 87 | import * as SPScript from "spscript"; 88 | ``` 89 | 90 | If you don't like that, add `"allowSyntheticDefaultImports": true` to your `tsconfig.json`. 91 | -------------------------------------------------------------------------------- /__tests__/Context.test.ts: -------------------------------------------------------------------------------- 1 | import SPScript from "../src/index"; 2 | import { getContext } from "./testUtils"; 3 | 4 | describe("SPScript.createContext(url, { headers: { FedAuthCookie }} )", () => { 5 | test("There is a FedAuth token", async () => { 6 | expect(process.env.AUTH_HEADERS).toBeTruthy(); 7 | }); 8 | 9 | test("The FedAuth token can be used to authenticate requests", async () => { 10 | let ctx = await getContext(); 11 | let webInfo = await ctx.web.getInfo(); 12 | expect(webInfo).toBeTruthy(); 13 | expect(webInfo).toHaveProperty("Title"); 14 | }); 15 | }); 16 | 17 | describe("Context Namespaces", function () { 18 | let ctx = null; 19 | beforeEach(() => { 20 | ctx = SPScript.createContext("blah blah"); 21 | }); 22 | 23 | it("Should create the primary object you use to interact with the site", function () { 24 | if (!ctx) throw new Error("Context is null"); 25 | expect(ctx).toHaveProperty("webUrl"); 26 | expect(ctx).toHaveProperty("executeRequest"); 27 | expect(ctx).toHaveProperty("get"); 28 | expect(ctx).toHaveProperty("post"); 29 | expect(ctx).toHaveProperty("lists"); 30 | expect(ctx).toHaveProperty("auth"); 31 | }); 32 | it("Should allow a url to be passed in", function () { 33 | var url = "http://blah.sharepoint.com"; 34 | var context = SPScript.createContext(url); 35 | expect(context.webUrl).toBe(url); 36 | }); 37 | 38 | describe("ctx.web", function () { 39 | test("Should have an SPScript Web object with site methods (getUser, getSubsites etc...)", function () { 40 | expect(ctx).toHaveProperty("web"); 41 | expect(ctx.web).toHaveProperty("getUser"); 42 | expect(ctx.web).toHaveProperty("getSubsites"); 43 | }); 44 | }); 45 | 46 | describe("ctx.search", function () { 47 | it("Should have an SPScript Search object with search methods (query, people, sites etc...)", function () { 48 | expect(ctx).toHaveProperty("search"); 49 | expect(ctx.search).toHaveProperty("query"); 50 | expect(ctx.search).toHaveProperty("people"); 51 | expect(ctx.search).toHaveProperty("sites"); 52 | }); 53 | }); 54 | 55 | describe("ctx.profiles", function () { 56 | it("Should have an SPScript Profiles object with methods to hit the Profile Service (current, setProperty etc...)", function () { 57 | expect(ctx).toHaveProperty("profiles"); 58 | expect(ctx.profiles).toHaveProperty("get"); 59 | expect(ctx.profiles).toHaveProperty("setProperty"); 60 | }); 61 | }); 62 | 63 | describe("ctx.auth", () => { 64 | it("Should have methods to get Request digest as well as get Graph Token", () => { 65 | expect(ctx).toHaveProperty("auth"); 66 | expect(ctx.auth).toHaveProperty("getRequestDigest"); 67 | expect(ctx.auth).toHaveProperty("ensureRequestDigest"); 68 | expect(ctx.auth).toHaveProperty("getGraphToken"); 69 | }); 70 | }); 71 | 72 | describe("ctx.lists", () => { 73 | it("Should be a method you can use to get an SPScript List object back by passing a list name", () => { 74 | expect(ctx).toHaveProperty("lists"); 75 | expect(typeof ctx.lists).toBe("function"); 76 | }); 77 | }); 78 | }); 79 | 80 | describe("Context Methods", () => { 81 | let ctx = null; 82 | beforeAll(async () => { 83 | ctx = await getContext(); 84 | }); 85 | 86 | describe("ctx.lists(name)", function () { 87 | it("Should return an SPScript List instance", function () { 88 | var list = ctx.lists("My List"); 89 | expect(list).toHaveProperty("listName"); 90 | expect(list).toHaveProperty("getInfo"); 91 | }); 92 | }); 93 | 94 | describe("ctx.get(url, [opts])", function () { 95 | var promise; 96 | beforeAll(function () { 97 | promise = ctx.get("/lists?$select=Title"); 98 | }); 99 | it("Should return a Promise", function () { 100 | if (!promise) throw new Error("Promise is null"); 101 | expect(promise).toHaveProperty("then"); 102 | }); 103 | it("Should resolve to a JS object, not a JSON string", async function () { 104 | let data = await promise; 105 | expect(data).toHaveProperty("value"); 106 | }); 107 | it("Should return valid API results: ctx.get('/lists')", async () => { 108 | let data = await promise; 109 | expect(data).toHaveProperty("value"); 110 | expect(data.value).toBeInstanceOf(Array); 111 | }); 112 | it("Should use less verbose OData header", async () => { 113 | let data = await ctx.get("/thememanager/GetTenantThemingOptions"); 114 | expect(data).toHaveProperty("themePreviews"); 115 | expect(data.themePreviews).toHaveProperty("length"); 116 | }); 117 | }); 118 | 119 | // TODO: look into JEST mocking of executeRequest 120 | describe("ctx.post(url, [body], [opts]", function () { 121 | it("Should resolve to a JS object, not a JSON string", async () => { 122 | let data = await ctx.post( 123 | "/Microsoft.Sharepoint.Utilities.WebTemplateExtensions.SiteScriptUtility.GetSiteDesigns" 124 | ); 125 | expect(data).toBeTruthy(); 126 | }); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /__tests__/CustomActions.test.ts: -------------------------------------------------------------------------------- 1 | import { getContext } from "./testUtils"; 2 | import Context from "../src/Context"; 3 | 4 | describe("Custom Actions", () => { 5 | // var scriptBlock = { 6 | // Name: "spscript-test", 7 | // Location: "ScriptLink", 8 | // ScriptBlock: "console.log('deployed from spscript tests');", 9 | // }; 10 | 11 | let topNav = { 12 | title: "TopNav", 13 | componentId: "f4d63423-0e94-4a77-bf11-c668b09e3e63", 14 | properties: { menuSiteUrl: "https://skylinespark.com/sites/devshowcase" }, 15 | }; 16 | 17 | describe("customActions.activateExtension()", () => { 18 | let ctx: Context = null; 19 | beforeAll(async () => { 20 | ctx = await getContext(); 21 | }); 22 | 23 | it("Should add a Custom Action with the given name", async () => { 24 | await ctx.customActions.activateExtension( 25 | topNav.title, 26 | topNav.componentId, 27 | topNav.properties 28 | ); 29 | let addedAction = await ctx.customActions.get(topNav.title); 30 | expect(addedAction).toBeTruthy; 31 | expect(addedAction).toHaveProperty("Name"); 32 | expect(addedAction.Name).toBe(topNav.title); 33 | }); 34 | 35 | it("Should not duplicate the CustomAction if added multiple times", async () => { 36 | await ctx.customActions.activateExtension( 37 | topNav.title, 38 | topNav.componentId, 39 | topNav.properties 40 | ); 41 | await ctx.customActions.activateExtension( 42 | topNav.title, 43 | topNav.componentId, 44 | topNav.properties 45 | ); 46 | let all = await ctx.customActions.get(); 47 | expect(all.filter((ca) => ca.Name === topNav.title).length).toBe(1); 48 | }); 49 | 50 | it("Should support extra CustomAction properties", async () => { 51 | let overrides = { Sequence: 0, Description: "Custom Global Navigation" }; 52 | await ctx.customActions.activateExtension( 53 | topNav.title, 54 | topNav.componentId, 55 | topNav.properties, 56 | overrides 57 | ); 58 | 59 | let addedAction = await ctx.customActions.get(topNav.title); 60 | expect(addedAction.Sequence).toBe(overrides.Sequence); 61 | expect(addedAction.Description).toBe(overrides.Description); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /__tests__/List.test.ts: -------------------------------------------------------------------------------- 1 | import { getContext } from "./testUtils"; 2 | 3 | describe("List", () => { 4 | let list = null; 5 | beforeAll(async () => { 6 | let ctx = await getContext(); 7 | list = ctx.lists("TestList"); 8 | }); 9 | 10 | // describe("list.addItem()", () => { 11 | // it("Should create a list item", async () => { 12 | // let newitem = await list.addItem({ Title: "new item", MyStatus: "Green" }); 13 | // expect(newitem).toHaveProperty("Title"); 14 | // }); 15 | // afterAll(async () => { 16 | // let items = await list.getItems(); 17 | // return Promise.all(items.map((item) => list.deleteItem(item.Id))); 18 | // }); 19 | // }); 20 | 21 | describe("list.info()", function () { 22 | it("Should return a promise that resolves to list info", async function () { 23 | let listInfo = await list.getInfo(); 24 | expect(listInfo).toBeTruthy(); 25 | expect(listInfo).toHaveProperty("Title"); 26 | expect(listInfo).toHaveProperty("ItemCount"); 27 | expect(listInfo).toHaveProperty("ListItemEntityTypeFullName"); 28 | }); 29 | }); 30 | 31 | describe("list.getItems()", () => { 32 | var items = null; 33 | beforeAll(async function () { 34 | items = await list.getItems(); 35 | }); 36 | 37 | it("Should return a promise that resolves to an array of items", function () { 38 | expect(items).toBeTruthy; 39 | expect(items).toHaveProperty("length"); 40 | }); 41 | 42 | it("Should return all the items in the list", async () => { 43 | let info = await list.getInfo(); 44 | expect(items.length).toEqual(info.ItemCount); 45 | }); 46 | }); 47 | 48 | describe("list.getItems(odata)", () => { 49 | var items = null; 50 | var odata = "$filter=MyStatus eq 'Green'"; 51 | beforeAll(async function () { 52 | items = await list.getItems(odata); 53 | }); 54 | 55 | it("Should return a promise that resolves to an array of items", function () { 56 | expect(items).toBeTruthy(); 57 | expect(items).toHaveProperty("length"); 58 | }); 59 | 60 | it("Should return only items that match the OData params", function () { 61 | items.forEach(function (item) { 62 | expect(item).toHaveProperty("MyStatus"); 63 | expect(item.MyStatus).toBe("Green"); 64 | }); 65 | }); 66 | }); 67 | 68 | describe("list.getItemById(id)", function () { 69 | var item = null; 70 | var validId = -1; 71 | beforeAll(function (done) { 72 | list 73 | .getItems() 74 | .then(function (allItems) { 75 | validId = allItems[0].Id; 76 | return validId; 77 | }) 78 | .then(function (id) { 79 | return list.getItemById(id); 80 | }) 81 | .then(function (result) { 82 | item = result; 83 | done(); 84 | }); 85 | }); 86 | it("Should return a promise that resolves to a single item", function () { 87 | expect(item).toBeTruthy(); 88 | expect(item).toHaveProperty("Title"); 89 | }); 90 | it("Should resolve an item with a matching ID", function () { 91 | expect(item).toHaveProperty("Id"); 92 | expect(item.Id).toBe(validId); 93 | }); 94 | it("Should be able to return attachments using the optional odata query", async () => { 95 | let item = await list.getItemById(validId, "$expand=AttachmentFiles"); 96 | expect(item).toHaveProperty("AttachmentFiles"); 97 | expect(item.AttachmentFiles).toHaveProperty("length"); 98 | }); 99 | }); 100 | 101 | describe("list.findItems(key, value)", function () { 102 | var matches = null; 103 | beforeAll(async function () { 104 | matches = await list.findItems("MyStatus", "Green"); 105 | }); 106 | 107 | it("Should return a promise that resolves to an array of list items", function () { 108 | expect(matches).toHaveProperty("length"); 109 | expect(matches.length).toBeGreaterThan(0); 110 | }); 111 | it("Should only bring back items the match the key value query", function () { 112 | matches.forEach(function (item) { 113 | expect(item).toHaveProperty("MyStatus"); 114 | expect(item.MyStatus).toBe("Green"); 115 | }); 116 | }); 117 | }); 118 | 119 | describe("list.findItem(key, value)", function () { 120 | var match = null; 121 | beforeAll(async function () { 122 | match = await list.findItem("MyStatus", "Green"); 123 | }); 124 | it("Should only bring back an item if it matches the key value query", function () { 125 | expect(match).toBeTruthy(); 126 | expect(match).toHaveProperty("MyStatus"); 127 | expect(match.MyStatus).toBe("Green"); 128 | }); 129 | }); 130 | 131 | describe("list.addItem()", function () { 132 | var newItem = { 133 | Title: "Test Created Item", 134 | MyStatus: "Red", 135 | }; 136 | var insertedItem = null; 137 | beforeAll(async function () { 138 | insertedItem = await list.addItem(newItem); 139 | }); 140 | it("Should return a promise that resolves with the new list item", function () { 141 | expect(insertedItem).toBeTruthy(); 142 | expect(insertedItem).toHaveProperty("Id"); 143 | }); 144 | it("Should save the item right away so it can be queried.", async function () { 145 | let foundItem = await list.getItemById(insertedItem.Id); 146 | expect(foundItem).toHaveProperty("Title"); 147 | expect(foundItem.Title).toBe(newItem.Title); 148 | }); 149 | // it("Should throw an error if a invalid field is set", async function () { 150 | // let invalidItem = { 151 | // ...newItem, 152 | // InvalidColumn: "test", 153 | // }; 154 | // try { 155 | // list.addItem(invalidItem); 156 | // expect("This").toBe("should have failed."); 157 | // } catch (err) { 158 | // return; 159 | // } 160 | // }); 161 | }); 162 | 163 | describe("list.deleteItem(id)", function () { 164 | var itemToDelete = null; 165 | beforeAll(async function () { 166 | await list.getItems("$orderby=Id").then(function (items) { 167 | itemToDelete = items[items.length - 1]; 168 | return list.deleteItem(itemToDelete.Id); 169 | }); 170 | }); 171 | it("Should delete that item", function (done) { 172 | list 173 | .getItemById(itemToDelete.Id) 174 | .then(function () { 175 | throw "Should have failed because item has been deleted"; 176 | }) 177 | .catch(function () { 178 | done(); 179 | }); 180 | }); 181 | it("Should reject the promise if the item id can not be found", function (done) { 182 | list 183 | .deleteItem(99999999) 184 | .then(function () { 185 | throw "Should have failed because id doesnt exist"; 186 | }) 187 | .catch(function () { 188 | done(); 189 | }); 190 | }); 191 | }); 192 | 193 | describe("list.updateItem()", function () { 194 | var itemToUpdate = null; 195 | var updates = { 196 | Title: "Updated Title", 197 | }; 198 | beforeAll(async function () { 199 | let items = await list.getItems("$orderby=Id desc"); 200 | itemToUpdate = items[items.length - 1]; 201 | }); 202 | it("Should update only the properties that were passed", async function () { 203 | let updateResult = await list.updateItem(itemToUpdate.Id, updates); 204 | let target = await list.getItemById(itemToUpdate.Id); 205 | expect(target).toHaveProperty("Title"); 206 | expect(target.Title).toBe(updates.Title); 207 | }); 208 | }); 209 | 210 | describe("list.getItemsByCaml()", () => { 211 | let items = null; 212 | const caml = ``; 213 | beforeAll(async () => { 214 | items = await list.getItemsByCaml(caml); 215 | }); 216 | it("Should return an array of list items", () => { 217 | expect(items).toHaveProperty("length"); 218 | }); 219 | }); 220 | 221 | describe("list.getItemsByView()", () => { 222 | let items = null; 223 | beforeAll(async () => { 224 | items = await list.getItemsByView("Green Status"); 225 | }); 226 | it("Should return an array of list items that match the View query", () => { 227 | expect(items).toHaveProperty("length"); 228 | items.forEach((item) => { 229 | expect(item).toHaveProperty("MyStatus"); 230 | expect(item.MyStatus).toBe("Green"); 231 | }); 232 | }); 233 | }); 234 | }); 235 | -------------------------------------------------------------------------------- /__tests__/MMS.test.ts: -------------------------------------------------------------------------------- 1 | import { getContext } from "./testUtils"; 2 | 3 | const termGroup = "_Skyline"; 4 | const termset = "Departments"; 5 | describe("MMS", () => { 6 | let ctx = null; 7 | describe("getTermset()", () => { 8 | let terms = null; 9 | beforeAll(async () => { 10 | ctx = await getContext(); 11 | terms = await ctx.mms.getTermset(termGroup, termset); 12 | }); 13 | it("Should return an array of (flat) MMS terms", () => { 14 | expect(terms).toHaveProperty("length"); 15 | expect(terms.length).toBeGreaterThan(0); 16 | expect(terms[0]).toHaveProperty("path"); 17 | expect(terms[0]).toHaveProperty("name"); 18 | expect(terms[0]).toHaveProperty("id"); 19 | expect(terms[0]).toHaveProperty("termSetName"); 20 | expect(terms[0]).toHaveProperty("description"); 21 | }); 22 | it("Should return Term sorted by path", () => { 23 | expect(terms).toHaveProperty("length"); 24 | expect(terms.length).toBeGreaterThan(1); 25 | expect(terms[0].path < terms[1].path).toBe(true); 26 | }); 27 | }); 28 | 29 | describe("getTermTree()", () => { 30 | let termTree = null; 31 | beforeAll(async () => { 32 | ctx = await getContext(); 33 | termTree = await ctx.mms.getTermTree(termGroup, termset); 34 | }); 35 | it("Should with the flattened MMS terms", () => { 36 | expect(termTree).toHaveProperty("flatTerms"); 37 | expect(termTree.flatTerms.length).toBeGreaterThan(2); 38 | expect(termTree.flatTerms[0]).toHaveProperty("path"); 39 | expect(termTree.flatTerms[0]).toHaveProperty("name"); 40 | expect(termTree.flatTerms[0].path < termTree.flatTerms[1].path).toBe(true); 41 | }); 42 | 43 | describe("termTree.getTermByName(name)", () => { 44 | it("Should return the correct term", () => { 45 | let target = termTree.flatTerms[termTree.flatTerms.length - 1]; 46 | expect(target).toHaveProperty("name"); 47 | expect(target).toHaveProperty("id"); 48 | let result = termTree.getTermByName(target.name); 49 | expect(result).toHaveProperty("id"); 50 | expect(result.id).toBe(target.id); 51 | }); 52 | it("Should return null for an invalid term", () => { 53 | let result = termTree.getTermByName("BOOGA BOOGA"); 54 | expect(result).toBe(null); 55 | }); 56 | }); 57 | 58 | describe("termTree.getTermById(termGuid)", () => { 59 | it("Should return the correct term", () => { 60 | let target = termTree.flatTerms[termTree.flatTerms.length - 1]; 61 | expect(target).toHaveProperty("id"); 62 | let result = termTree.getTermById(target.id); 63 | expect(result).toHaveProperty("id"); 64 | expect(result.id).toBe(target.id); 65 | }); 66 | it("Should return null for an invalid term", () => { 67 | let result = termTree.getTermById("BOOGA BOOGA"); 68 | expect(result).toBe(null); 69 | }); 70 | }); 71 | 72 | describe("termTree.getTermByPath(path)", () => { 73 | it("Should return the correct term", () => { 74 | let target = termTree.flatTerms[termTree.flatTerms.length - 1]; 75 | expect(target).toHaveProperty("path"); 76 | let result = termTree.getTermByPath(target.path); 77 | expect(result).toHaveProperty("id"); 78 | expect(result.id).toBe(target.id); 79 | }); 80 | it("Should return null for an invalid term", () => { 81 | let result = termTree.getTermByPath("BOOGA BOOGA"); 82 | expect(result).toBe(null); 83 | }); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /__tests__/Profiles.test.ts: -------------------------------------------------------------------------------- 1 | import SPScript from "../src/index"; 2 | import { getContext } from "./testUtils"; 3 | 4 | describe("ctx.profiles", () => { 5 | describe("ctx.profiles.current()", function () { 6 | it("Should resolve to the current user's profile", async function () { 7 | try { 8 | let ctx = await getContext(); 9 | let profile = await ctx.profiles.current(); 10 | expect(profile).toHaveProperty("AccountName"); 11 | expect(profile).toHaveProperty("Email"); 12 | expect(profile.Email).toBeTruthy(); 13 | expect(profile.Email.toLowerCase()).toBe(process.env.SP_USER.toLowerCase()); 14 | expect(profile).toHaveProperty("PreferredName"); 15 | } catch (err) { 16 | console.error("ERROR", err); 17 | } 18 | }); 19 | }); 20 | 21 | describe("ctx.profiles.get()", () => { 22 | it("Should resolve to the current user's profile if no email address is provided", async function () { 23 | try { 24 | let ctx = await getContext(); 25 | let profile = await ctx.profiles.get(); 26 | expect(profile).toHaveProperty("AccountName"); 27 | expect(profile).toHaveProperty("Email"); 28 | expect(profile.Email).toBeTruthy(); 29 | expect(profile.Email.toLowerCase()).toBe(process.env.SP_USER.toLowerCase()); 30 | expect(profile).toHaveProperty("PreferredName"); 31 | } catch (err) { 32 | console.error("ERROR", err); 33 | } 34 | }); 35 | }); 36 | 37 | describe("ctx.profiles.get(email)", () => { 38 | it("Should resolve to the profile of the user tied to the given email address", async () => { 39 | try { 40 | const EMAIL = "wspiering@skylinespark.onmicrosoft.com"; 41 | let ctx = await getContext(); 42 | let profile = await ctx.profiles.get(EMAIL); 43 | expect(profile).toHaveProperty("AccountName"); 44 | expect(profile).toHaveProperty("Email"); 45 | expect(profile.Email).toBeTruthy(); 46 | expect(profile.Email.toLowerCase()).toBe(EMAIL.toLowerCase()); 47 | expect(profile).toHaveProperty("PreferredName"); 48 | } catch (err) { 49 | console.error("ERROR", err); 50 | } 51 | }); 52 | it("Should reject the Promise for an invalid email", async () => { 53 | try { 54 | const EMAIL = "INVALIDg@skylinespark.onmicrosoft.com"; 55 | let ctx = await getContext(); 56 | return await expect(ctx.profiles.get(EMAIL)).rejects.toThrowError(); 57 | } catch (err) { 58 | console.error("ERROR", err); 59 | } 60 | }); 61 | }); 62 | 63 | describe("ctx.profiles.setProperty(key, value)", () => { 64 | it("Should update the current user's profile", async () => { 65 | try { 66 | const aboutMeValue = "Hi there. I was updated with SPScript v5"; 67 | let ctx = await getContext(); 68 | await ctx.profiles.setProperty("AboutMe", aboutMeValue); 69 | let profile = await ctx.profiles.current(); 70 | expect(profile).toHaveProperty("AboutMe"); 71 | expect(profile["AboutMe"]).toBe(aboutMeValue); 72 | } catch (err) { 73 | console.error("ERROR", err); 74 | } 75 | }); 76 | }); 77 | 78 | describe.skip("ctx.profiles.setProperty(key, value, email)", () => { 79 | const EMAIL = "wspiering@skylinespark.onmicrosoft.com"; 80 | it("Should update the targeted user's profile", async () => { 81 | const aboutMeValue = "Hi there. I was updated with SPScript #2"; 82 | let ctx = await getContext(); 83 | await ctx.profiles.setProperty("AboutMe", aboutMeValue, EMAIL); 84 | let profile = await ctx.profiles.get(EMAIL); 85 | expect(profile).toHaveProperty("AboutMe"); 86 | expect(profile["AboutMe"]).toBe(aboutMeValue); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /__tests__/Search.test.ts: -------------------------------------------------------------------------------- 1 | import { getContext } from "./testUtils"; 2 | import Context from "../src/Context"; 3 | 4 | describe("Search", () => { 5 | let ctx: Context = null; 6 | beforeAll(async () => { 7 | ctx = await getContext(); 8 | }); 9 | describe("search.query(searchText)", () => { 10 | it("Should return a Promise that resolves to a SearchResults object", async () => { 11 | let result = await ctx.search.query("Andrew"); 12 | expect(result).toBeTruthy(); 13 | expect(result).toHaveProperty("resultsCount"); 14 | expect(result).toHaveProperty("totalResults"); 15 | expect(result).toHaveProperty("items"); 16 | expect(result).toHaveProperty("refiners"); 17 | expect(result.items).toHaveProperty("length"); 18 | }); 19 | }); 20 | 21 | describe("ctx.search.query(queryText, queryOptions)", function () { 22 | it("Should obey the extra query options that were passed", async function () { 23 | var queryText = "andrew"; 24 | var options = { 25 | rowlimit: 1, 26 | }; 27 | let result = await ctx.search.query(queryText, options); 28 | expect(result).toBeTruthy(); 29 | expect(result).toHaveProperty("resultsCount"); 30 | expect(result).toHaveProperty("totalResults"); 31 | expect(result).toHaveProperty("items"); 32 | expect(result).toHaveProperty("refiners"); 33 | expect(result.items).toHaveProperty("length"); 34 | expect(result.items.length).toBe(1); 35 | }); 36 | }); 37 | 38 | describe("ctx.search.query(queryText, queryOptions) - w/ Refiners", function () { 39 | it("Should return SearchResults that include a refiners Array", async () => { 40 | var refinerName = "FileType"; 41 | var queryText = "andrew"; 42 | var options = { 43 | refiners: `${refinerName}`, 44 | }; 45 | let result = await ctx.search.query(queryText, options); 46 | expect(result).toBeTruthy(); 47 | expect(result).toHaveProperty("resultsCount"); 48 | expect(result).toHaveProperty("totalResults"); 49 | expect(result).toHaveProperty("items"); 50 | expect(result).toHaveProperty("refiners"); 51 | expect(result.items).toHaveProperty("length"); 52 | expect(result.refiners).toHaveProperty("length"); 53 | expect(result.refiners.length).toBeGreaterThan(0); 54 | var firstRefiner = result.refiners[0]; 55 | expect(firstRefiner).toHaveProperty("RefinerName"); 56 | expect(firstRefiner).toHaveProperty("RefinerOptions"); 57 | expect(firstRefiner.RefinerName).toBe(refinerName); 58 | }); 59 | }); 60 | 61 | describe("ctx.search.people(queryText)", function () { 62 | it("Should only return search results that are people", async () => { 63 | let result = await ctx.search.people("Andrew"); 64 | expect(result).toHaveProperty("items"); 65 | expect(result.items).toHaveProperty("length"); 66 | expect(result.items.length).toBeGreaterThan(0); 67 | result.items.forEach((item) => { 68 | expect(item).toHaveProperty("AccountName"); 69 | expect(item).toHaveProperty("PreferredName"); 70 | expect(item).toHaveProperty("AboutMe"); 71 | expect(item).toHaveProperty("WorkEmail"); 72 | expect(item).toHaveProperty("PictureURL"); 73 | }); 74 | }); 75 | }); 76 | 77 | describe("ctx.search.sites(queryText, scope)", function () { 78 | it("Should only return search results that are sites", async () => { 79 | let result = await ctx.search.sites(""); 80 | expect(result).toHaveProperty("items"); 81 | expect(result.items).toHaveProperty("length"); 82 | expect(result.items.length).toBeGreaterThan(0); 83 | result.items.forEach((item) => { 84 | expect(item).toHaveProperty("Path"); 85 | expect(item).toHaveProperty("contentclass"); 86 | expect(item.contentclass).toBe("STS_Web"); 87 | }); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /__tests__/Securable.test.ts: -------------------------------------------------------------------------------- 1 | import { getContext } from "./testUtils"; 2 | import Context from "../src/Context"; 3 | 4 | describe("Securable (web and List)", () => { 5 | let ctx: Context; 6 | beforeAll(async () => { 7 | ctx = await getContext(); 8 | }); 9 | 10 | describe("web.permissions.check()", () => { 11 | it("Should check the permissions of the current user", async () => { 12 | let permissions = await ctx.web.permissions.check(); 13 | expect(permissions).toBeTruthy(); 14 | expect(permissions).toHaveProperty("length"); 15 | expect(permissions.length).toBeGreaterThan(0); 16 | // console.log("ME: ", permissions); 17 | }); 18 | it("Should check the permissions of the of the specified user", async () => { 19 | let permissions = await ctx.web.permissions.check("apetersen@skylinespark.onmicrosoft.com"); 20 | expect(permissions).toBeTruthy(); 21 | expect(permissions).toHaveProperty("length"); 22 | expect(permissions.length).toBeGreaterThan(0); 23 | // console.log("Sarah", permissions); 24 | }); 25 | }); 26 | 27 | describe("web.permissions.getRoleAssignments()", () => { 28 | it("Should return an array of {member, roles} objects", async () => { 29 | let roleAssignments = await ctx.web.permissions.getRoleAssignments(); 30 | expect(roleAssignments).toHaveProperty("length"); 31 | expect(roleAssignments.length).toBeGreaterThan(0); 32 | 33 | expect(roleAssignments[0]).toHaveProperty("member"); 34 | expect(roleAssignments[0]).toHaveProperty("roles"); 35 | expect(roleAssignments[0].member).toHaveProperty("name"); 36 | expect(roleAssignments[0].member).toHaveProperty("login"); 37 | expect(roleAssignments[0].member).toHaveProperty("id"); 38 | 39 | expect(roleAssignments[0].roles).toHaveProperty("length"); 40 | expect(roleAssignments[0].roles.length).toBeGreaterThan(0); 41 | 42 | expect(roleAssignments[0].roles[0]).toHaveProperty("name"); 43 | }); 44 | }); 45 | 46 | describe("list.permissions.check()", () => { 47 | it("Should check the permissions of the current user", async () => { 48 | let permissions = await ctx.lists("Site Pages").permissions.check(); 49 | expect(permissions).toBeTruthy(); 50 | expect(permissions).toHaveProperty("length"); 51 | expect(permissions.length).toBeGreaterThan(0); 52 | // console.log("ME: ", permissions); 53 | }); 54 | it("Should check the permissions of the of the specified user", async () => { 55 | let permissions = await ctx.web.permissions.check("apetersen@skylinespark.onmicrosoft.com"); 56 | expect(permissions).toBeTruthy(); 57 | expect(permissions).toHaveProperty("length"); 58 | expect(permissions.length).toBeGreaterThan(0); 59 | // console.log("Sarah", permissions); 60 | }); 61 | }); 62 | 63 | describe("list.permissions.getRoleAssignments()", () => { 64 | it("Should return an array of {member, roles} objects", async () => { 65 | let roleAssignments = await ctx.lists("Site Pages").permissions.getRoleAssignments(); 66 | expect(roleAssignments).toHaveProperty("length"); 67 | expect(roleAssignments.length).toBeGreaterThan(0); 68 | 69 | expect(roleAssignments[0]).toHaveProperty("member"); 70 | expect(roleAssignments[0]).toHaveProperty("roles"); 71 | expect(roleAssignments[0].member).toHaveProperty("name"); 72 | expect(roleAssignments[0].member).toHaveProperty("login"); 73 | expect(roleAssignments[0].member).toHaveProperty("id"); 74 | 75 | expect(roleAssignments[0].roles).toHaveProperty("length"); 76 | expect(roleAssignments[0].roles.length).toBeGreaterThan(0); 77 | 78 | expect(roleAssignments[0].roles[0]).toHaveProperty("name"); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /__tests__/Web.test.ts: -------------------------------------------------------------------------------- 1 | import { getContext } from "./testUtils"; 2 | import Context from "../src/Context"; 3 | 4 | describe("ctx.web", () => { 5 | let ctx: Context; 6 | beforeAll(async () => { 7 | ctx = await getContext(); 8 | }); 9 | describe("ctx.web.getInfo()", function () { 10 | it("Should return a promise that resolves to web info", async () => { 11 | let webInfo = await ctx.web.getInfo(); 12 | expect(webInfo).toHaveProperty("Url"); 13 | expect(webInfo).toHaveProperty("Title"); 14 | }); 15 | }); 16 | 17 | describe("ctx.web.getSubsites()", function () { 18 | it("Should return a promise that resolves to an array of subsite web infos.", async () => { 19 | let subsites = await ctx.web.getSubsites(); 20 | console.log("subsites", subsites); 21 | expect(subsites).toBeInstanceOf(Array); 22 | }); 23 | }); 24 | 25 | describe("ctx.web.getUser()", function () { 26 | it("Should return a promise that resolves to a user", async () => { 27 | let user = await ctx.web.getUser(); 28 | expect(user).toHaveProperty("Id"); 29 | expect(user).toHaveProperty("LoginName"); 30 | expect(user).toHaveProperty("Email"); 31 | }); 32 | it("Should return the current user if no email is given", async () => { 33 | let user = await ctx.web.getUser(); 34 | expect(user.Email).toBe(process.env.SP_USER); 35 | }); 36 | }); 37 | 38 | describe("ctx.web.getUser(email)", function () { 39 | const EMAIL = "wspiering@skylinespark.onmicrosoft.com"; 40 | it("Should return a promise that resolves to a user", async () => { 41 | let user = await ctx.web.getUser(EMAIL); 42 | expect(user).toHaveProperty("Id"); 43 | expect(user).toHaveProperty("LoginName"); 44 | expect(user).toHaveProperty("Email"); 45 | }); 46 | it("Should return the current user if no email is given", async () => { 47 | let user = await ctx.web.getUser(EMAIL); 48 | expect(user.Email).toBe(EMAIL); 49 | }); 50 | }); 51 | 52 | describe("ctx.web.getFile(serverRelativeFilepath)", () => { 53 | it("Should return a promise that resolves to a file object", async () => { 54 | let fileUrl = "/sites/spscript/sitepages/home.aspx"; 55 | let file = await ctx.web.getFile(fileUrl); 56 | expect(file).toBeTruthy(); 57 | expect(file).toHaveProperty("Name"); 58 | expect(file).toHaveProperty("ETag"); 59 | expect(file).toHaveProperty("UIVersionLabel"); 60 | expect(file).toHaveProperty("Exists"); 61 | }); 62 | }); 63 | 64 | describe("ctx.web.copyFile(serverRelativeSourceUrl, serverRelativeDestUrl)", function () { 65 | let sourceUrl = "/sites/spscript/shared documents/testfile.txt"; 66 | let filename = "testfile-" + Date.now() + ".txt"; 67 | let destinationUrl = "/sites/spscript/shared documents/" + filename; 68 | it("Should return a promise, and once resolved, the new (copied) file should be retrievable.", async () => { 69 | let sourceFile = await ctx.web.getFile(sourceUrl); 70 | expect(sourceFile).toBeTruthy(); 71 | await ctx.web.copyFile(sourceUrl, destinationUrl); 72 | let newFile = await ctx.web.getFile(destinationUrl); 73 | expect(newFile).toBeTruthy(); 74 | expect(newFile).toHaveProperty("Name"); 75 | expect(newFile.Name).toBe(filename); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /__tests__/namespace.test.ts: -------------------------------------------------------------------------------- 1 | import SPScript from "../src/index"; 2 | require("dotenv").config(); 3 | 4 | describe("SPScript Namespace", () => { 5 | test("Should have a 'SPScript.createContext()' method", function() { 6 | expect(SPScript).toHaveProperty("createContext"); 7 | expect(typeof SPScript.createContext).toBe("function"); 8 | }); 9 | test("Should have a 'SPScript.utils' namespace", function() { 10 | expect(SPScript).toHaveProperty("utils"); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /__tests__/queryString.test.ts: -------------------------------------------------------------------------------- 1 | import { fromObj, toObj } from "../src/utils/queryString"; 2 | 3 | describe("Query String Utils", () => { 4 | describe("fromObj(obj)", () => { 5 | it("Should turn a basic object into a querystring", () => { 6 | let obj = { 7 | foo: "one", 8 | bar: 2, 9 | }; 10 | let qs = fromObj(obj); 11 | expect(qs).toBe("foo=one&bar=2"); 12 | }); 13 | it("Should automatically run encodeURIComponent", () => { 14 | let obj = { 15 | foo: "thing one", 16 | bar: 2, 17 | }; 18 | 19 | let qs = fromObj(obj); 20 | expect(qs).toBe("foo=thing%20one&bar=2"); 21 | }); 22 | 23 | it("Should allow passing a flag to wrap values with single quotes (used by search service calls).", () => { 24 | let obj = { 25 | foo: "thing one", 26 | bar: 2, 27 | }; 28 | 29 | let qs = fromObj(obj, true); 30 | expect(qs).toBe("foo='thing%20one'&bar='2'"); 31 | }); 32 | it("SHould handle a null object", () => { 33 | let qs = fromObj(null); 34 | expect(qs).toBe(""); 35 | }); 36 | it("SHould handle a undefined object", () => { 37 | let qs = fromObj(undefined); 38 | expect(qs).toBe(""); 39 | }); 40 | }); 41 | 42 | describe("toObj", () => { 43 | it("Should handle a basic object", () => { 44 | let str = "foo=one&bar=2"; 45 | let obj = toObj(str); 46 | expect(obj).toHaveProperty("foo"); 47 | expect(obj).toHaveProperty("bar"); 48 | expect(obj.foo).toBe("one"); 49 | expect(obj.bar).toBe("2"); 50 | }); 51 | it("Should handle decoding the values", () => { 52 | let str = "foo=thing%20one&bar=2"; 53 | let obj = toObj(str); 54 | expect(obj).toHaveProperty("foo"); 55 | expect(obj).toHaveProperty("bar"); 56 | expect(obj.foo).toBe("thing one"); 57 | expect(obj.bar).toBe("2"); 58 | }); 59 | it("Should handle a ? at the beginning of the string", () => { 60 | let str = "?foo=thing%20one&bar=2"; 61 | let obj = toObj(str); 62 | expect(obj).toHaveProperty("foo"); 63 | expect(obj).toHaveProperty("bar"); 64 | expect(obj.foo).toBe("thing one"); 65 | expect(obj.bar).toBe("2"); 66 | }); 67 | it("Should handle an empty string", () => { 68 | let obj = toObj(""); 69 | expect(obj).toBeTruthy(); 70 | }); 71 | it("Should handle just a '?'", () => { 72 | let obj = toObj(""); 73 | expect(obj).toBeTruthy(); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /__tests__/testUtils.ts: -------------------------------------------------------------------------------- 1 | import * as SPScript from "../src/index"; 2 | import "isomorphic-fetch"; 3 | require("dotenv").config(); 4 | 5 | let siteUrl = process.env.SITE_URL; 6 | 7 | export const getContext = async () => { 8 | return SPScript.createContext(siteUrl, { headers: JSON.parse(process.env.AUTH_HEADERS) }); 9 | }; 10 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DroopyTersen/spscript/282c6bfaff0e217cbdbeed6baf47ee0cd56c1749/docs/.nojekyll -------------------------------------------------------------------------------- /docs/arbitrary-requests.md: -------------------------------------------------------------------------------- 1 | # Arbitrary API Requests 2 | 3 | If SPScript doesn't have a method that specifically solves your needs, you can always use the base request helpers, `ctx.get` and `ctx.post`. These methods expect you to pass an endpoint path relative to `/_api`. 4 | 5 | > **IMPORTANT!** These methods expect you to pass an endpoint path relative to `/_api`. 6 | 7 | For example, given the API url, `https://TENANT.sharepoint.com/sites/YOURSITE/_api/web/lists/getByTitle('Site%20Pages')/items`, you'd pass `/web/lists/getByTitle('Site%20Pages')/items`. 8 | 9 | ## get 10 | 11 | Takes an API path and perform the REST API `GET` request. 12 | 13 | - `ctx.get(apiPath)` 14 | - `ctx.get(apiPath, reqOpts)` 15 | 16 | _Get all Company Themes_ 17 | 18 | ```javascript 19 | let data = await ctx.get("/thememanager/GetTenantThemingOptions"); 20 | let themes = data.themePreviews; 21 | ``` 22 | 23 | ## post 24 | 25 | - `ctx.post(apiPath, payload)` 26 | - `ctx.post(apiPath, payload, verb)` 27 | 28 | Takes an API path and a payload, and performs a `POST` request that includes the necessary headers and RequestDigest. If you pass a `verb`, it will place that in the `X-HTTP-Method` header. 29 | 30 | ```javascript 31 | let endpoint = 32 | "/Microsoft.Sharepoint.Utilities.WebTemplateExtensions.SiteScriptUtility.ApplySiteDesign"; 33 | let payload = { siteDesignId: siteDesign.Id, webUrl: siteUrl }; 34 | await ctx.post(endpoint, payload); 35 | ``` 36 | 37 | **\_post** 38 | 39 | - `ctx._post(apiPath, payload, reqOptions)` 40 | 41 | There is also a`ctx.post`which does the same thing as`authorizedPost`except that it doesn't handle ensuring the RequestDigest is included in the headers. It is rare that you want to make a`POST`without the`digest`. The only reason you'd reach for this method is if you want full control over the request headers. 42 | 43 | ## More Examples 44 | 45 | _Applying a Site Design by name_ 46 | 47 | ```javascript 48 | async function getSiteDesign(siteUrl, siteDesignName) { 49 | const ctx = SPScript.createContext(siteUrl); 50 | // GetSiteDesigns actually requires a POST 51 | let data = await ctx.post( 52 | "/Microsoft.Sharepoint.Utilities.WebTemplateExtensions.SiteScriptUtility.GetSiteDesigns" 53 | ); 54 | let siteDesigns = data.value; 55 | return siteDesigns.find((sd) => sd.Title === siteDesignName); 56 | } 57 | 58 | async function applySiteDesign(siteUrl, siteDesignName) { 59 | // Take the name and try to find a Site Design with that Title 60 | let siteDesign = await getSiteDesign(siteUrl, siteDesignName); 61 | if (!siteDesign) throw new Error("Couldn't find a site design with the name, " + siteDesignName); 62 | let ctx = SPScript.createContext(siteUrl); 63 | 64 | let endpoint = 65 | "/Microsoft.Sharepoint.Utilities.WebTemplateExtensions.SiteScriptUtility.ApplySiteDesign"; 66 | let payload = { siteDesignId: siteDesign.Id, webUrl: siteUrl }; 67 | await ctx.post(endpoint, payload); 68 | } 69 | ``` 70 | 71 | ## Source Code 72 | 73 | Github Source: [/src/context/Context.ts](https://github.com/DroopyTersen/spscript/blob/master/src/context/Context.ts#L71) 74 | -------------------------------------------------------------------------------- /docs/coverpage.md: -------------------------------------------------------------------------------- 1 | # SPScript 2 | 3 | > Making the SharePoint REST API easy 4 | 5 | - Easy querying of list items 6 | - Add and Update list items with a single line of code 7 | - No more pulling out hair with working with Search endpoints 8 | - Much more... 9 | 10 | [Get Started](introduction) 11 | [GitHub](https://github.com/droopytersen/spscript) 12 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Installation 4 | 5 | Add the SPScript `npm` package to your project 6 | 7 | _NPM_ 8 | 9 | ```shell 10 | npm install spscript 11 | ``` 12 | 13 | _Yarn_ 14 | 15 | ```shell 16 | yarn add spscript 17 | ``` 18 | 19 | ## Importing 20 | 21 | You can use SPScript in your Javascript/Typescript files with: 22 | 23 | ```javascript 24 | import SPScript from "spscript"; 25 | ``` 26 | 27 | **ProTip: Dynamically/Temporarily add SPScript to a Modern page with Dev Tools** 28 | 29 | You can enter the following into a browser console to dynamically load SPScript on a page. 30 | 31 | ```javascript 32 | let script = document.createElement("script"); 33 | script.src = "https://unpkg.com/spscript@beta/dist/spscript.browser.js"; 34 | document.head.appendChild(script); 35 | ``` 36 | 37 | ## SPScript Context 38 | 39 | Almost everything in SPScript is based off an SPScript `Context` class. 40 | 41 | - An SPScript **Context** is tied to specific SharePoint site. 42 | - You get a **Context** by calling `SPScript.createContext(siteUrl)`. 43 | 44 | > You get a **Context** by calling `SPScript.createContext(siteUrl)`. 45 | 46 | _This line of code is the entry point to almost everything SPScript provides._ 47 | 48 | ```javascript 49 | let ctx = SPScript.createContext(siteUrl); 50 | ``` 51 | 52 | _Example Usage: Get the News Pages of the specified site._ 53 | 54 | ```javascript 55 | import SPScript from "spscript"; 56 | 57 | const getPublishedNews = async function (siteUrl) { 58 | let ctx = SPScript.createContext(siteUrl); 59 | let pages = await ctx.lists("Site Pages").findItems("PromotedState", 2); 60 | console.log(pages); // This will show an Array of Page List Items 61 | return pages; 62 | }; 63 | ``` 64 | 65 | Throughout the docs you'll see a variable, `ctx`, representing an instance of an SPScript `Context`. 66 | 67 | ## Troubleshooting 68 | 69 | If you are using Typescript, you may have to use the syntax: 70 | 71 | ```javascript 72 | import * as SPScript from "spscript"; 73 | ``` 74 | 75 | If you don't like that, add `"allowSyntheticDefaultImports": true` to your `tsconfig.json`. 76 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | spscript - ShareP0oint Rest Api Wrappers 6 | 7 | 8 | 12 | 16 | 28 | 29 | 30 |
31 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | # SPScript 2 | 3 | SPScript is a JavaScript library meant to simplify working with the SharePoint REST API. 4 | 5 | - Easy querying of list items 6 | - Add and Update list items with a single line of code 7 | - No more pulling out hair with working with SharePoint Search endpoints 8 | - Profile service helpers 9 | - Generic `GET` and `POST` helpers to simplify calling arbitrary endpoints 10 | - Full intellisense in VSCode 11 | - Works server-side in Node.js 12 | 13 | For example, lets say you wanted to get all of the "Active" items in the "Tasks" list and set them to "Canceled", then add a new item. 14 | 15 | ```javascript 16 | // Create an SPScript Context targeting your site 17 | let ctx = SPScript.createContext("https://TENANT.shareoint.com/sites/YOURSITE"); 18 | let tasksList = ctx.lists("Tasks"); 19 | 20 | // Find all items in the "Tasks" list with a "Status" of "Active" 21 | let activeTasks = await tasksList.findItems("Status", "Active"); 22 | 23 | // Loop through each task and update its Status 24 | for (task of activeTasks) { 25 | await tasksList.updateItem(task.Id, { Status: "Canceled" }); 26 | } 27 | 28 | //Add a new "Active" task 29 | let newTask = await tasksList.addItem({ 30 | Title: "Hello from SPScript", 31 | Status: "Active" 32 | }); 33 | ``` 34 | 35 | **PnPJS Comparison** 36 | 37 | > _"Wait, isn't this what [PnPJS](https://pnp.github.io/pnpjs/) does too?"_ 38 | 39 | Pretty much. But, PnPJS comes with an almost 200kB hit to your bundle size. SPScript is less than 30kB. That being said, PnPJS does WAAAY more than SPScript. 40 | 41 | - If you are just trying to query some List Items or use the Search Service, then SPScript could be a way to improve performance. 42 | - If you are trying to develop an advanced client-side provisioning application, go with [PnPJS](https://pnp.github.io/pnpjs/). 43 | -------------------------------------------------------------------------------- /docs/list-operations.md: -------------------------------------------------------------------------------- 1 | # List Operations 2 | 3 | ## Setup 4 | 5 | Performing list operations you need to: 6 | 7 | 1. Create an SPScript Context 8 | 2. Get a list by Title 9 | 10 | These 2 steps are synchronous. They are really just building up the base REST API url for you. 11 | 12 | ```javascript 13 | // Create a Context and get a list by Title 14 | let ctx = SPScript.createContext("https://TENANT.sharepoint.com/sites/SITE"); 15 | let list = ctx.lists("YOUR LIST TITLE"); 16 | 17 | // This call is async, it actually makes a REST request 18 | let items = await list.getItems(); 19 | ``` 20 | 21 | ## getInfo 22 | 23 | - `getInfo() => ListProperties` 24 | 25 | Get the properties of the list. Stuff like `ItemCount`, `Hidden`, etc... 26 | 27 | ```javascript 28 | let info = await ctx.lists("Featured Links").getInfo(); 29 | console.log("List Item Count", info.ItemCount); 30 | ``` 31 | 32 | ## checkExists 33 | 34 | - `checkExists() => boolean` 35 | 36 | Check whether a specific list exists 37 | 38 | ```javascript 39 | let exists = await ctx.lists("Featured Links").checkExists(); 40 | ``` 41 | 42 | ## getItems 43 | 44 | - `getItems()` 45 | - `getItems(odata)` 46 | 47 | _Get all items in the "Shared Documents" Library_ 48 | 49 | ```javascript 50 | let items = await ctx.lists("Shared Documents").getItems(); 51 | ``` 52 | 53 | You can also pass an optional OData string, giving you full control over your `$filter`, `$orderby`, `$select`, `$expand`, and `$top`. 54 | 55 | _Get the file names of the last 5 modified "Shared Documents"_ 56 | 57 | ```javascript 58 | let items = await ctx 59 | .lists("Shared Documents") 60 | .getItems("$select=FileLeafRef&$orderby=Modified desc&$top=5"); 61 | let filenames = items.map((item) => item.FileLeafRef); 62 | ``` 63 | 64 | ## getItemById 65 | 66 | - `getItemById(id)` 67 | - `getItemById(id, odata)` 68 | 69 | If you know they ID of the SharePoint list item, you can get it directly with `getItemById`. 70 | 71 | ```javascript 72 | let item = await ctx.lists("Site Pages").getItemById(4); 73 | ``` 74 | 75 | ## findItems 76 | 77 | - `findItems(fieldName, value)` 78 | - `findItems(fieldName, value, odata)` 79 | 80 | If you want to find all items that match a specified Field value, you can use `findItems`. 81 | 82 | _Find all pages with a Category of "New Hire"_ 83 | 84 | ```javascript 85 | let newHireAnnouncements = await ctx.lists("Site Pages").findItems("Category", "New Hire"); 86 | ``` 87 | 88 | You can use the optional odata argument for more control (just don't use `$filter` because `findItems` is already taking care of that for you). 89 | 90 | _Find the 5 most recent "New Hire" pages_ 91 | 92 | ```javascript 93 | let newHireAnnouncements = await ctx 94 | .lists("Site Pages") 95 | .findItems("Category", "New Hire", "$orderby=Modified desc&$top=5"); 96 | ``` 97 | 98 | ## findItem 99 | 100 | - `findItem(fieldName, value)` 101 | - `findItem(fieldName, value, odata)` 102 | 103 | The same as `findItems` except that it returns a single item (as an `Object`) instead of an `Array` of items. 104 | 105 | _Find the Blog post with a Title of "SPScript is Awesome!"_ 106 | 107 | ```javascript 108 | let item = await ctx.lists("Site Pages").findItem("Title", "SPScript is Awesome!"); 109 | ``` 110 | 111 | ## getItemsByView 112 | 113 | - `getItemsByView(viewName)` 114 | 115 | Get items based on an existing List View. This way the user can configure the filtering and sorting. 116 | 117 | _Get tasks based on the "Prioritized" view_ 118 | 119 | ```javascript 120 | let tasks = await ctx.lists("Tasks").getItemsByView("Prioritized Tasks"); 121 | ``` 122 | 123 | ## getItemsByCaml 124 | 125 | - `getItemsByCaml(caml)` 126 | 127 | Instead of OData, pass a CAML query. The parent node should be a ``. Something like: 128 | 129 | ```xml 130 | 131 | 132 | ... 133 | 134 | 135 | ``` 136 | 137 | I've only found one scenario where I need this, querying by Calculated Fields. You can't do that via OData. 138 | 139 | ## Query Examples 140 | 141 | _Find all Events with a Category of "Birthday"_ 142 | 143 | 144 | 145 | #### ** Easiest ** 146 | 147 | **`findItems(fieldName, value)`** 148 | 149 | For when you want items based on a single field value. 150 | 151 | You can pass an optional OData string as a third argument to control things like `$orderby`, `$select`, `$expand`, and `$top`. 152 | 153 | ```javascript 154 | let ctx = SPScript.createContext("https://MYTENANT.sharepoint.com/sites/MYSITE"); 155 | let items = await ctx.lists("Events").findItems("Category", "Birthday"); 156 | ``` 157 | 158 | #### ** Easy ** 159 | 160 | **`getItems(odata)`** 161 | 162 | Takes an arbtrary OData string, giving you full control over your `$filter`, `$orderby`, `$select`, `$expand`, and `$top`. 163 | 164 | ```javascript 165 | let ctx = SPScript.createContext("https://MYTENANT.sharepoint.com/sites/MYSITE"); 166 | let items = await ctx.lists("Events").getItems("$filter=Category eq 'Birthday'"); 167 | ``` 168 | 169 | #### ** Long-winded** 170 | 171 | **`ctx.get(apiUrl)`** 172 | 173 | You can always make a generic `GET` request using SPScript's helper to setup the proper headers. 174 | 175 | ```javascript 176 | let ctx = SPScript.createContext("https://MYTENANT.sharepoint.com/sites/MYSITE"); 177 | let endpoint = "/web/lists/getByTitle('Events')/items?$filter=Category eq 'Birthday'"; 178 | let data = await ctx.get(endpoint); 179 | let items = data.d.results; 180 | ``` 181 | 182 | 183 | 184 | ## addItem 185 | 186 | - `addItem(newItem)` 187 | 188 | Allows you to pass a JavaScript object, where each property aligns with a SharePoint Field name on the target list. It is async and will give you back the new List Item which will include new properties like the SharePoint ID. 189 | 190 | ```javascript 191 | var itemToCreate = { 192 | Title: "My New Task", 193 | Status: "Not Started", 194 | RemainingHours: 12, 195 | }; 196 | let listItem = await ctx.lists("Tasks").addItem(itemToCreate); 197 | ``` 198 | 199 | If your `newItem` object has a property that **isn't** a Field on the List, the call will fail. 200 | 201 | ## updateItem 202 | 203 | - `updateItem(id, updates)` 204 | 205 | Allows you to pass a JavaScript object, where each property aligns with a SharePoint Field name on the target list. 206 | 207 | - Does a `MERGE` so you don't have to pass all the field values. 208 | - If your updates object has a property that **isn't** a Field on the List, the call will fail. 209 | 210 | ```javascript 211 | var updates = { Status: "Completed", RemainingHours: 0 }; 212 | await ctx.lists("Tasks").updateItem(29, updates); 213 | ``` 214 | 215 | ## deleteItem 216 | 217 | - `deleteItem(id)` 218 | 219 | Deletes the List Item with the specified Id. 220 | 221 | _Delete the Shared Documents item with an ID of 47_ 222 | 223 | ```javascript 224 | await ctx.lists("Shared Documents").deleteItem(47); 225 | ``` 226 | 227 | ## Source Code 228 | 229 | Github Source: [/src/list/List.ts](https://github.com/DroopyTersen/spscript/blob/master/src/list/List.ts) 230 | -------------------------------------------------------------------------------- /docs/mms.md: -------------------------------------------------------------------------------- 1 | # Managed Metadata 2 | 3 | A helper for bringing back MMS terms. Dependency free and done in a single call. Wraps the `Client.svc` service because there is currently no Rest endpoing for MMS (it's supposedly coming...). 4 | 5 | ## getTermset 6 | 7 | Returns a flat list of the terms in the specified termset. Pass in the term group and the termset name. 8 | 9 | ```javascript 10 | let terms = ctx.mms.getTermset("Events Registration", "Event Types"); 11 | ``` 12 | 13 | ## getTermsetTree 14 | 15 | Returns a tree data structure representing the specified termset. Useful when conveying nested relationships with terms. 16 | 17 | ```javascript 18 | let termTree = ctx.mms.getTermTree("Inventory Tracking", "Locations"); 19 | let term1 = termTree.getTermByName("Store 123"); 20 | let term2 = termTree.getTermByPath("Stores/Midwest/Store 123"); 21 | let term3 = termTree.getTermById(""); 22 | ``` 23 | 24 | **MMSTerm** 25 | 26 | ```javascript 27 | export interface MMSTerm { 28 | id: string; 29 | sortOrder: number; 30 | description: string; 31 | name: string; 32 | path: string; 33 | termSetName: string; 34 | properties: { 35 | [key: string]: string, 36 | }; 37 | children: MMSTerm[]; 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/modifying-listitems.md: -------------------------------------------------------------------------------- 1 | # Modifying List Items 2 | 3 | ## Setup 4 | 5 | If you need to modify list items, you need to: 6 | 7 | 1. Create an SPScript Context, `SPScript.createContext(siteUrl)` 8 | 2. Get a list by Title, `ctx.lists("LIST TITLE")` 9 | 10 | These 2 steps are synchronous,they are really just building up the base REST API url for you. 11 | 12 | ```javascript 13 | // Create a Context and get a list by Title 14 | let ctx = SPScript.createContext("https://TENANT.sharepoint.com/sites/SITE"); 15 | let list = ctx.lists("YOUR LIST TITLE"); 16 | 17 | // This call is async, it actually makes a REST request 18 | let newItem = await list.addItem({ Title: "New Thing" }); 19 | ``` 20 | 21 | ## Source Code 22 | 23 | Github Source: [/src/list/List.ts](https://github.com/DroopyTersen/spscript/blob/master/src/list/List.ts) 24 | -------------------------------------------------------------------------------- /docs/nodejs.md: -------------------------------------------------------------------------------- 1 | # Serverside w/ Node.js 2 | 3 | ## Fetch Polyfill 4 | 5 | In order to work in Node.js, you need to import `isomorphic-fetch`. 6 | 7 | _Just add this line to the top of your Node.js main file_ 8 | 9 | ```javascript 10 | require("isomorphic-fetch"); 11 | ``` 12 | 13 | This is due to some library design decisions I made: 14 | 15 | - I don't want to include an isomorphic "Request" library, SPScript is only 28kb, that could double the size. 16 | - I'm done monkeying with `XMLHttpRequest`, `fetch` is sooo much nicer to work with. 17 | - I don't want to assume SPScript's consumers will want me to include a `fetch` polyfill (for IE or Node). 18 | - If you need one, you'll know, and you'll already be polyfilling, you shouldn't want me to do that for you 19 | 20 | ## Cookie Auth (username, password) 21 | 22 | You can pass a `headers` property to the `ContextOptions` param in `createContext`. 23 | 24 | For example you could use [node-sp-auth](https://www.npmjs.com/package/node-sp-auth) to log in with username and password (only do this serverside), then pass the Fed Auth cookie you receive to SPScript: 25 | 26 | ```javascript 27 | // Use node-sp-auth to get a Fed Auth cookie. 28 | // This cookie can be include in the headers of REST calls to authorize them. 29 | const spauth = require("node-sp-auth"); 30 | 31 | let auth = await spauth.getAuth(process.env.SITE_URL, { 32 | username: process.env.SP_USER, 33 | password: process.env.PASSWORD, 34 | }); 35 | // Pass the auth headers to SPScript via the optional ContextOptions param 36 | let ctx = SPScript.createContext(siteUrl, { headers: auth.headers }); 37 | let webInfo = await ctx.web.getInfo(); 38 | console.log(webInfo); 39 | ``` 40 | 41 | ## App Context (OAuth) 42 | 43 | You can also use the SharePoint App Registration process to login via "AppContext") 44 | 45 | 1. Register a SharePoint app using "\_layouts/15/appregnew.aspx ". **Make note of your `clientId` and `clientSecret`** 46 | 2. Grant your new app permissions using "\_layouts/15/appinv.aspx". You can use the xml snippet below to grant site collection admin access. **The key part is you set `AllowAppOnlyPolicy` to true\*\***. 47 | 48 | > The key part is you set `AllowAppOnlyPolicy` to true 49 | 50 | _Site Collection Admin Permissions for you App Context_ 51 | 52 | ```xml 53 | 54 | 55 | 56 | ``` 57 | 58 | 3. Pass the Client Id and Secret to `createContext`. All actions will be performed as the App. 59 | 60 | ```javascript 61 | const spauth = require("node-sp-auth"); 62 | 63 | let auth = await spauth.getAuth(process.env.SITE_URL, { 64 | clientId: process.env.CLIENT_KEY, 65 | clientSecret: process.env.CLIENT_SECRET, 66 | }); 67 | // Pass the auth headers to SPScript via the optional ContextOptions param 68 | let ctx = SPScript.createContext(siteUrl, { headers: auth.headers }); 69 | let webInfo = await ctx.web.getInfo(); 70 | console.log(webInfo); 71 | ``` 72 | -------------------------------------------------------------------------------- /docs/profiles.md: -------------------------------------------------------------------------------- 1 | # SharePoint Profiles 2 | 3 | The SharePoint Profile service automatically syncs predefined properties from Active Directory. You can access this information with SPScript 4 | 5 | - `ctx.profiles.get(email?:string)` - Get a profile by email, or the current user if no email given. 6 | - `ctx.profiles.setProperty(key, value)` - Update the current user's profile 7 | 8 | ## Get Profile 9 | 10 | ### Current User 11 | 12 | If you don't have anything to `profiles.get()` then it will use the current user. 13 | 14 | ```javascript 15 | let ctx = SPScript.createContext(); 16 | let profile = await ctx.profiles.get(); 17 | ``` 18 | 19 | ### By Email 20 | 21 | You can passing an email address to `profiles.get(email)` to retrieve a specific user's profile. 22 | 23 | ```javascript 24 | let ctx = SPScript.createContext(); 25 | let profile = await ctx.profiles.get("andrew@droopy.onmicrosoft.com); 26 | ``` 27 | 28 | ## Update Property 29 | 30 | ```javascript 31 | let ctx = SPScript.createContext(); 32 | await ctx.profiles.setProperty("AboutMe", "I was updated by SPScript"); 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/search.md: -------------------------------------------------------------------------------- 1 | # Querying the SP Search Service 2 | 3 | ## Introduction 4 | 5 | The SharePoint Search REST API responses are notoriously difficult to work with. To get the items you'd have to say `data.PrimaryQueryResult.RelevantResults.Table.Rows`. But you're not done yet! Each Row is a `{}` with a `Cells` array which contains key value pairs of the item's managed properties and values. It's a huge mess... 6 | 7 | _This is about the shortest way you could parse the Search Rest API response._ 8 | 9 | ```javascript 10 | let items = data.PrimaryQueryResult.RelevantResults.Table.Rows.map((row) => { 11 | return row.Cells.reduce((obj, cell) => { 12 | obj[cell.Key] = cell.Value; 13 | }, {}); 14 | }); 15 | ``` 16 | 17 | With SPScript you can query the Search Service with 1 line of code (and get clean objects back). 18 | 19 | ```javascript 20 | let searchResult = await ctx.search.query("SPScript"); 21 | // searchResult.items will be an array of JS objects, one for each search result 22 | console.log(searchResult.items); 23 | ``` 24 | 25 | ## Search Methods 26 | 27 | - `ctx.search.query(text)` - query the Search Service, async resolves to a `SearchResultResponse` (see below) 28 | - `ctx.search.query(text, queryOptions)` - query the Search Service and specify `QueryOptions` 29 | - `ctx.search.people(text)` - limits the search to just people 30 | - `ctx.search.people(text, queryOptions)` - limits the search to just people with specified `QueryOptions` 31 | - `ctx.search.sites(text)` - limits the search to just sites (STS_Web) 32 | - `ctx.search.sites(text, urlScope)`- limits the search to just sites (STS_Web) that are underneath the specified `scopeUrl` 33 | - `ctx.search.sites(text, urlScope, queryOptions)` - limits the search to just sites (STS_Web) that are underneath the specified `scopeUrl` with the specified `QueryOptions` 34 | 35 | The previous search methods all take query text as the first parameter. This text can be: 36 | 37 | - An arbitrary string - `"spscript is awesome"` 38 | - A Keyword Query (KQL) - `"Title:SPScript OR Path:https://andrew.sharepoint.com/sites/spscript"` 39 | - Both `"Author:Petersen come find me"` 40 | 41 | ## Search Response 42 | 43 | Each call to `ctx.search.query(searchText)` is `async` and will resolve to a `SearchResultResponse`. 44 | 45 | ```typescript 46 | interface SearchResultResponse { 47 | elapsedTime: string; 48 | suggestion: any; 49 | resultsCount: number; 50 | totalResults: number; 51 | totalResultsIncludingDuplicates: number; 52 | /** The actual search results that you care about */ 53 | items: any[]; 54 | refiners?: Refiner[]; 55 | } 56 | ``` 57 | 58 | ## Query Options 59 | 60 | You can also pass an optional second parameter to specify query options 61 | 62 | _Use QueryOptions to only bring back 5 results_ 63 | 64 | ```javascript 65 | let searchResult = await ctx.search.query("SPScript", { rowlimit: 5 ); 66 | console.log(searchResult.items); 67 | 68 | ``` 69 | 70 | Interface 71 | 72 | ```typescript 73 | interface QueryOptions { 74 | sourceid?: string; 75 | startrow?: number; 76 | rowlimit?: number; 77 | selectproperties?: string[]; 78 | refiners?: string[]; 79 | refinementfilters?: string[]; 80 | hiddencontstraints?: any; 81 | sortlist?: any; 82 | } 83 | ``` 84 | 85 | Default Query Options 86 | 87 | ```javascript 88 | { 89 | sourceid:null, 90 | startrow:null, 91 | rowlimit:100, 92 | selectproperties:null, 93 | refiners:null, 94 | refinementfilters:null, 95 | hiddencontstraints:null, 96 | sortlist:null 97 | } 98 | ``` 99 | -------------------------------------------------------------------------------- /docs/sidebar.md: -------------------------------------------------------------------------------- 1 | - [Introduction](introduction) 2 | - [Getting Started](getting-started) 3 | - [List Operations](list-operations) 4 | - [GET / POST](arbitrary-requests) 5 | - [SharePoint Search](search) 6 | - [MMS](mms) 7 | - [SharePoint Profiles](profiles) 8 | - [Utilities](utilities) 9 | - [Serverside w/ Node.js](nodejs) 10 | -------------------------------------------------------------------------------- /docs/utilities.md: -------------------------------------------------------------------------------- 1 | # Utility Functions 2 | 3 | ```javascript 4 | import { utils } from "spscript"; 5 | ``` 6 | 7 | ## getSiteUrl 8 | 9 | Returns the server relative url of the site at the provided url. It can do things like pull out the Site Url out of full document or page url. If you don't pass a url it will use the current page's url. 10 | 11 | It only really works for SharePoint Online because it assumes a managed path of `/sites` or `/teams`. 12 | 13 | ```javascript 14 | import { utils } from "spscript"; 15 | let currentSiteUrl = utils.getSiteUrl(); 16 | let documentSiteUrl = utils.getSiteUrl(documentUrl); 17 | ``` 18 | 19 | ## getTenant 20 | 21 | Returns the tenant of the provided url. If not url is provided, it uses the current page's url. 22 | 23 | ```javascript 24 | import { utils } from "spscript"; 25 | 26 | let tenant = utils.getTenant(); 27 | ``` 28 | 29 | ## getDelveLink 30 | 31 | Take an email address and returns the url to that user's Delve profile 32 | 33 | ```javascript 34 | import { utils } from "spscript"; 35 | 36 | let delveProfileUrl = utils.getDelveLink("apetersen@droopy.onmicrosoft.com"); 37 | ``` 38 | 39 | ## getProfilePhoto 40 | 41 | Take an email address and returns the url that user's SharePoint profile photo 42 | 43 | ``` 44 | let photoUrl = utils.getProfilePhoto("apetersen@droopy.onmicrosoft.com"); 45 | ``` 46 | 47 | ## HTTP Headers 48 | 49 | SPScript comes with some utilty functions to set some special SharePoint headers. 50 | 51 | ```javascript 52 | utils.getStandardHeaders((digest = "")); 53 | ``` 54 | 55 | - Sets `Accept`, `Content-Type` to `application/json` 56 | - Sets `X-RequestDigest` if a digest was provided 57 | 58 | ```javascript 59 | utils.getUpdateHeaders(digest?:string) 60 | ``` 61 | 62 | - Sets `Accept`, `Content-Type` to `application/json` 63 | - Sets `X-RequestDigest` if a digest was provided 64 | - Sets `X-HTTP-Method` to `MERGE` 65 | - Sets `If-Match` to `*` 66 | 67 | ```javascript 68 | utils.getDeleteHeaders(digest?:string) 69 | ``` 70 | 71 | - Sets `Accept`, `Content-Type` to `application/json` 72 | - Sets `X-RequestDigest` if a digest was provided 73 | - Sets `X-HTTP-Method` to `DELETE` 74 | - Sets `If-Match` to `*` 75 | 76 | ## parseOData 77 | 78 | Helps parse the odata response to give you back what you actually want instead of something like `{ d: { results: [ ] }}` 79 | 80 | Checks for the following and automatically bubbles up what it finds as the return value. 81 | 82 | - `data.d.results` 83 | - `data.d` 84 | - `data.value` 85 | 86 | _Example Usage_ 87 | 88 | ```javascript 89 | let currentUserGroups = await ctx.get("/web/currentuser/groups").then(utils.parseOData); 90 | ``` 91 | 92 | ## parseJSON 93 | 94 | Wraps `JSON.parse` in a try/catch and returns `null` if the parse fails. 95 | 96 | ```javascript 97 | utils.parseJSON(jsonStr); 98 | ``` 99 | 100 | ## Query String 101 | 102 | Helper functions to convert between a JS object, `{ active: 'BigDate', count: 4 }` to a a query string, `active=BigDate&count=4`, and back. 103 | 104 | - utils.qs.`toObj(string) => Object` 105 | - utils.qs.`fromObj(object, wrapInSingleQuotes = false) => string` 106 | 107 | _Example Usage_ 108 | 109 | ```javascript 110 | let odata = { 111 | $top: 100, 112 | $select: "Title, Id", 113 | $orderBy: "Created desc", 114 | }; 115 | let items = await ctx.lists("Site Pages").getItems(SPScript.utils.qs.fromObj(odata)); 116 | ``` 117 | 118 | ## waitForElement 119 | 120 | Function to handle waiting for DOM elements to available on a page. 121 | 122 | ```javascript 123 | let feedbackBtn = await utils.waitForElement("#feedback_btn"); 124 | feedbackBtn.style.display = "none"; 125 | ``` 126 | 127 | ## Loaders 128 | 129 | Helpers to load external JavaScript and CSS files. 130 | 131 | ```javascript 132 | await utils.loadScript("https://domain.com/javascript.js"); 133 | ``` 134 | 135 | ```javascript 136 | await utils.loadCSS("https://domain.com/styles.css"); 137 | ``` 138 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | "^.+\\.tsx?$": "ts-jest", 4 | }, 5 | testEnvironment: "node", 6 | testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 7 | testPathIgnorePatterns: ["/lib/", "/node_modules/", "testUtils.ts"], 8 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], 9 | collectCoverage: false, 10 | collectCoverageFrom: ["src/**/*.{ts,tsx}", "!/node_modules/", "!/path/to/dir/"], 11 | }; 12 | -------------------------------------------------------------------------------- /legacy-tests/nodeauthtest.js: -------------------------------------------------------------------------------- 1 | let nodeauth = require("node-sp-auth"); 2 | require("dotenv").config(); 3 | 4 | var doIt = async () => { 5 | let auth = await nodeauth.getAuth(process.env.SITE_URL, { 6 | username: process.env.SP_USER, 7 | password: process.env.PASSWORD, 8 | online: true 9 | }); 10 | console.log(auth); 11 | }; 12 | 13 | doIt(); 14 | -------------------------------------------------------------------------------- /legacy-tests/test.browser.js: -------------------------------------------------------------------------------- 1 | require("babel-polyfill"); 2 | mocha.setup("bdd"); 3 | chai.should(); 4 | var SPScript = require("../../dist/v3/spscript"); 5 | require("./tests").run(SPScript); 6 | 7 | mocha.run(); 8 | -------------------------------------------------------------------------------- /legacy-tests/test.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 33 |

SPScript Tests

34 |
35 | 36 | 37 |
-------------------------------------------------------------------------------- /legacy-tests/test.server.js: -------------------------------------------------------------------------------- 1 | require('isomorphic-fetch'); 2 | var config = require("../../app.config"); 3 | var SPScript = require("../../dist/v3/spscript"); 4 | var chai = require("chai"); 5 | chai.should(); 6 | 7 | var ctx = SPScript.createContext(config.SP_SITE_URL, { 8 | clientId: config.CLIENT_KEY, 9 | clientSecret: config.CLIENT_SECRET 10 | }); 11 | 12 | require("./tests").run(SPScript, ctx); 13 | -------------------------------------------------------------------------------- /legacy-tests/tests/authTests.js: -------------------------------------------------------------------------------- 1 | var should = require("chai").should(); 2 | 3 | exports.run = function(ctx) { 4 | describe("var auth = ctx.auth", function() { 5 | this.timeout(5000); 6 | 7 | 8 | describe("auth.getRequestDigest()", function() { 9 | it("Should resolve to a string request digest", function(done) { 10 | ctx.auth.getRequestDigest().then(function(digest) { 11 | digest.should.be.a("string"); 12 | digest.should.not.be.empty; 13 | done(); 14 | }); 15 | }); 16 | }); 17 | 18 | describe("auth.ensureRequestDigest()", function() { 19 | it("Should resolve to a string request digest if no digest is given", function(done) { 20 | var initialDigest = null; 21 | ctx.auth.ensureRequestDigest(initialDigest).then(function(digest) { 22 | digest.should.be.a("string"); 23 | digest.should.not.be.empty; 24 | done(); 25 | }); 26 | }); 27 | it("Should return the same digest string if a digest value is given", function(done) { 28 | ctx.auth.getRequestDigest().then(function(initialDigest) { 29 | ctx.auth.ensureRequestDigest(initialDigest).then(function(digest) { 30 | digest.should.be.a("string"); 31 | digest.should.not.be.empty; 32 | digest.should.equal(initialDigest); 33 | done(); 34 | }); 35 | }); 36 | }) 37 | }); 38 | 39 | describe("auth.getGraphToken()", function() { 40 | it("Should resolve to a string that is the access token needed to authorize GRAPH API requests", function(done) { 41 | ctx.auth.getGraphToken().then(function(token) { 42 | token.should.not.be.null; 43 | token.should.have.property("access_token"); 44 | token.should.have.property("expires_on"); 45 | token.should.have.property("resource"); 46 | token.should.have.property("scope"); 47 | token.access_token.should.be.a("string"); 48 | token.access_token.should.not.be.empty; 49 | console.log(token); 50 | var url = "https://graph.microsoft.com/v1.0/me/" 51 | var headers = { 52 | "authorization": "Bearer " + token.access_token, 53 | "content-type": "application/json", 54 | "cache-control": "no-cache", 55 | "redirect": "follow", 56 | 57 | } 58 | fetch(url, { headers }) 59 | .then(res => res.json()) 60 | .then(profile => { 61 | profile.should.not.be.null; 62 | console.log(profile); 63 | done(); 64 | }) 65 | }) 66 | }) 67 | }) 68 | }); 69 | }; 70 | -------------------------------------------------------------------------------- /legacy-tests/tests/contextTests.js: -------------------------------------------------------------------------------- 1 | var should = require("chai").should(); 2 | 3 | exports.run = function (SPScript) { 4 | describe("SPScript Context", function () { 5 | var ctx = SPScript.createContext(); 6 | describe("var ctx = SPScript.createContext(url)", function () { 7 | it("Should create the primary object you use to interact with the site", function () { 8 | if (!ctx) throw new Error("Context is null"); 9 | ctx.should.have.property("webUrl"); 10 | ctx.should.have.property("executeRequest"); 11 | ctx.should.have.property("get"); 12 | ctx.should.have.property("post"); 13 | ctx.should.have.property("authorizedPost"); 14 | ctx.should.have.property("lists"); 15 | }); 16 | it("Should allow a url to be passed in", function () { 17 | var url = "http://blah.sharepoint.com"; 18 | var context = SPScript.createContext(url); 19 | context.webUrl.should.equal(url); 20 | }); 21 | it("Should default to the current web if no url is passed", function () { 22 | var context = SPScript.createContext(); 23 | context.webUrl.should.equal(_spPageContextInfo.webAbsoluteUrl); 24 | }); 25 | }); 26 | 27 | describe("Namespaces", function () { 28 | describe("ctx.web", function () { 29 | it("Should have an SPScript Web object with site methods (getUser, getSubsites etc...)", function () { 30 | ctx.should.have.property("web"); 31 | ctx.web.should.have.property("getUser"); 32 | ctx.web.should.have.property("getSubsites"); 33 | }); 34 | }); 35 | 36 | describe("ctx.search", function () { 37 | it("Should have an SPScript Search object with search methods (query, people, sites etc...)", function () { 38 | ctx.search.should.have.property("query"); 39 | ctx.search.should.have.property("people"); 40 | }); 41 | }); 42 | 43 | describe("ctx.profiles", function () { 44 | it("Should have an SPScript Profiles object with methods to hit the Profile Service (current, setProperty etc...)", function () { 45 | ctx.should.have.property("profiles"); 46 | ctx.profiles.should.have.property("get"); 47 | ctx.profiles.should.have.property("setProperty"); 48 | }); 49 | }); 50 | }); 51 | 52 | describe("Methods", function () { 53 | describe("ctx.list(name)", function () { 54 | it("Should return an SPScript List instance", function () { 55 | var list = ctx.lists("My List"); 56 | list.should.have.property("listName"); 57 | list.should.have.property("getInfo"); 58 | }); 59 | }); 60 | describe("ctx.get(url, [opts])", function () { 61 | var promise; 62 | before(function () { 63 | promise = ctx.get("/lists?$select=Title"); 64 | }); 65 | it("Should return a Promise", function () { 66 | if (!promise) throw new Error("Promise is null"); 67 | promise.should.have.property("then"); 68 | }); 69 | it("Should resolve to a JS object, not a JSON string", function (done) { 70 | promise 71 | .then(function (data) { 72 | data.should.have.property("d"); 73 | done(); 74 | }) 75 | .catch((err) => done(err)); 76 | }); 77 | it("Should return valid API results: ctx.get('/lists')", function (done) { 78 | promise 79 | .then((data) => { 80 | data.should.have.property("value"); 81 | done(); 82 | }) 83 | .catch((err) => done(err)); 84 | }); 85 | }); 86 | 87 | describe("ctx.post(url, [body], [opts]", function () { 88 | it("Should return a Promise"); 89 | it("Should resolve to a JS object, not a JSON string"); 90 | }); 91 | 92 | describe("ctx.authorizedPost(url, [body], [opts]", function () { 93 | it("Should include a request digest in the headers"); 94 | it("Should return a Promise"); 95 | it("Should resolve to a JS object, not a JSON string"); 96 | }); 97 | }); 98 | }); 99 | }; 100 | -------------------------------------------------------------------------------- /legacy-tests/tests/customActionTests.js: -------------------------------------------------------------------------------- 1 | exports.run = function(dao) { 2 | describe("ctx.customActions", function() { 3 | this.timeout(10000); 4 | 5 | var customAction = { 6 | Name: "spscript-test", 7 | Location: "ScriptLink", 8 | ScriptBlock: "console.log('deployed from spscript-mocha test');" 9 | }; 10 | describe("ctx.customActions.add(customAction)", function() { 11 | var beforeCount = 0; 12 | before(function(done) { 13 | dao.customActions.get().then(function(all) { 14 | beforeCount = all.length; 15 | done(); 16 | }); 17 | }); 18 | 19 | it("Should add a Custom Action with the given name", function(done) { 20 | dao.customActions.add(customAction).then(function() { 21 | dao.customActions.get().then(function(all) { 22 | all.length.should.equal(beforeCount + 1); 23 | done(); 24 | }); 25 | }); 26 | }); 27 | 28 | it("Should not add duplicate Custom Action names. It should remove old one first.", function( 29 | done 30 | ) { 31 | dao.customActions.add(customAction).then(function() { 32 | dao.customActions.get().then(function(all) { 33 | all.length.should.equal(beforeCount + 1); 34 | done(); 35 | }); 36 | }); 37 | }); 38 | }); 39 | 40 | describe("ctx.customActions.get()", function() { 41 | var results = null; 42 | before(function(done) { 43 | dao.customActions.get().then(function(data) { 44 | results = data; 45 | done(); 46 | }); 47 | }); 48 | 49 | it("Should return a promise that resolves to an array of custom actions", function() { 50 | results.should.be.an("array"); 51 | results.should.not.be.empty; 52 | }); 53 | 54 | it("Should bring back properties like Name, Url, and Location", function() { 55 | var firstItem = results[0]; 56 | firstItem.should.have.property("Name"); 57 | firstItem.should.have.property("Url"); 58 | firstItem.should.have.property("Location"); 59 | }); 60 | }); 61 | 62 | describe("ctx.customActions.get(name)", function() { 63 | var result = null; 64 | before(function(done) { 65 | dao.customActions.get().then(function(allCustomActions) { 66 | dao.customActions.get(allCustomActions[0].Name).then(function(res) { 67 | result = res; 68 | done(); 69 | }); 70 | }); 71 | }); 72 | 73 | it("Should return one object w/ properties like Name, Location, Url, Id", function() { 74 | result.should.not.be.null; 75 | result.should.have.property("Name"); 76 | result.should.have.property("Location"); 77 | result.should.have.property("Id"); 78 | }); 79 | 80 | it("Should reject the promise with a decent error if the Custom Action name is not found", function( 81 | done 82 | ) { 83 | dao.customActions 84 | .get("INVALID-NAME") 85 | .then(function() { 86 | "one".should.equal("two"); 87 | done(); 88 | }) 89 | .catch(function(err) { 90 | console.log(err); 91 | done(); 92 | }); 93 | }); 94 | }); 95 | 96 | describe("ctx.customActions.update(updates)", function() { 97 | var result = null; 98 | before(function(done) { 99 | dao.customActions.get(customAction.Name).then(function(ca) { 100 | result = ca; 101 | done(); 102 | }); 103 | }); 104 | var newTitle = "updated title - " + Date.now(); 105 | it("Should update the property", function(done) { 106 | dao.customActions.update({ Name: result.Name, Title: newTitle }).then(function() { 107 | dao.customActions.get(result.Name).then(function(i) { 108 | i.Title.should.equal(newTitle); 109 | done(); 110 | }); 111 | }); 112 | }); 113 | }); 114 | 115 | describe("dao.customActions.remove(name)", function() { 116 | var beforeCount = 0; 117 | before(function(done) { 118 | dao.customActions 119 | .get() 120 | .then(function(all) { 121 | beforeCount = all.filter(function(a) { 122 | return a.Name === customAction.Name; 123 | }).length; 124 | done(); 125 | }) 126 | .catch(err => { 127 | console.log(err); 128 | done(); 129 | }); 130 | }); 131 | 132 | it("Should remove all custom actions with that name", function(done) { 133 | dao.customActions.remove(customAction.Name).then(function() { 134 | dao.customActions.get().then(function(all) { 135 | var matches = all.filter(function(a) { 136 | return a.Name === customAction.Name; 137 | }); 138 | matches.should.be.empty(); 139 | done(); 140 | }); 141 | }); 142 | }); 143 | }); 144 | 145 | describe("ctx.customActions.addScriptLink(name, url)", function() { 146 | var jsUrl = "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"; 147 | var caName = "SPScriptJSTest-Web"; 148 | 149 | before(function(done) { 150 | dao.customActions.addScriptLink(caName, jsUrl).then(function() { 151 | done(); 152 | }); 153 | }); 154 | 155 | it("Should add a custom action with that name and ScriptBlock with specified URL", function( 156 | done 157 | ) { 158 | dao.customActions.get(caName).then(function(ca) { 159 | ca.should.have.property("Name"); 160 | ca.Name.should.equal(caName); 161 | ca.should.have.property("ScriptBlock"); 162 | ca.ScriptBlock.should.contain(jsUrl); 163 | done(); 164 | }); 165 | }); 166 | 167 | after(function(done) { 168 | dao.customActions.remove(caName).then(function() { 169 | done(); 170 | }); 171 | }); 172 | }); 173 | 174 | describe("ctx.customActions.addScriptLink(name, url, opts)", function() { 175 | var jsUrl = "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"; 176 | var caName = "SPScriptJSTest-Site"; 177 | var opts = { Sequence: 25, Group: "Custom Group" }; 178 | 179 | before(function(done) { 180 | dao.customActions.addScriptLink(caName, jsUrl, opts).then(function() { 181 | done(); 182 | }); 183 | }); 184 | 185 | it("Should add a custom action with that name and ScriptBlock with specified URL", function( 186 | done 187 | ) { 188 | dao.customActions.get(caName).then(function(ca) { 189 | ca.should.have.property("Name"); 190 | ca.Name.should.equal(caName); 191 | ca.should.have.property("ScriptBlock"); 192 | ca.ScriptBlock.should.contain(jsUrl); 193 | ca.should.have.property("Group"); 194 | ca.Group.should.equal(opts.Group); 195 | ca.should.have.property("Sequence"); 196 | ca.Sequence.should.equal(25); 197 | done(); 198 | }); 199 | }); 200 | 201 | after(function(done) { 202 | dao.customActions.remove(caName).then(function() { 203 | done(); 204 | }); 205 | }); 206 | }); 207 | 208 | describe("ctx.customActions.addCSSLink(name, url)", function() { 209 | var cssUrl = "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"; 210 | var caName = "SPScriptCSSTest-Web"; 211 | 212 | before(function(done) { 213 | dao.customActions.addCSSLink(caName, cssUrl).then(function() { 214 | done(); 215 | }); 216 | }); 217 | 218 | it("Should add a custom action with that name and ScriptBlock containing specified URL", function( 219 | done 220 | ) { 221 | dao.customActions.get(caName).then(function(ca) { 222 | ca.should.have.property("Name"); 223 | ca.Name.should.equal(caName); 224 | ca.should.have.property("ScriptBlock"); 225 | ca.ScriptBlock.should.contain(cssUrl); 226 | done(); 227 | }); 228 | }); 229 | 230 | after(function(done) { 231 | dao.customActions.remove(caName).then(function() { 232 | done(); 233 | }); 234 | }); 235 | }); 236 | 237 | describe("ctx.customActions.addCSSLink(name, url, opts)", function() { 238 | var cssUrl = "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"; 239 | var caName = "SPScriptCSSTest-Site"; 240 | var opts = { Sequence: 50, Group: "Custom Group" }; 241 | 242 | before(function(done) { 243 | dao.customActions.addCSSLink(caName, cssUrl, opts).then(function() { 244 | done(); 245 | }); 246 | }); 247 | 248 | it("Should add a custom action with that name and ScriptBlock containing specified URL with Site scope", function( 249 | done 250 | ) { 251 | dao.customActions.get(caName).then(function(ca) { 252 | ca.should.have.property("Name"); 253 | ca.Name.should.equal(caName); 254 | ca.should.have.property("ScriptBlock"); 255 | ca.ScriptBlock.should.contain(cssUrl); 256 | ca.should.have.property("Group"); 257 | ca.Group.should.equal(opts.Group); 258 | ca.should.have.property("Sequence"); 259 | ca.Sequence.should.equal(50); 260 | done(); 261 | }); 262 | }); 263 | 264 | after(function(done) { 265 | dao.customActions.remove(caName).then(function() { 266 | done(); 267 | }); 268 | }); 269 | }); 270 | }); 271 | }; 272 | -------------------------------------------------------------------------------- /legacy-tests/tests/index.js: -------------------------------------------------------------------------------- 1 | exports.run = function(SPScript, ctx) { 2 | var isServer = !!ctx; 3 | var should = require("chai").should(); 4 | 5 | describe("SPScript Global Namespace", function() { 6 | it("Should have a 'SPScript.createContext()' method", function() { 7 | SPScript.should.have.property("createContext"); 8 | SPScript.createContext.should.be.a("function"); 9 | }); 10 | it("Should have a 'SPScript.utils' namespace", function() { 11 | SPScript.should.not.be.null; 12 | SPScript.should.have.property("utils"); 13 | }); 14 | }); 15 | 16 | if (!isServer) { 17 | require("./contextTests").run(SPScript); 18 | ctx = SPScript.createContext(); 19 | } 20 | require("./authTests").run(ctx); 21 | require("./webTests").run(ctx); 22 | require("./listTests").run(ctx); 23 | require("./customActionTests").run(ctx); 24 | if (!isServer) { 25 | require("./searchTests").run(ctx); 26 | require("./profileTests").run(ctx); 27 | } 28 | require("./utilsTests").run(SPScript.utils); 29 | }; 30 | -------------------------------------------------------------------------------- /legacy-tests/tests/listTests.js: -------------------------------------------------------------------------------- 1 | var permissionsTests = require("./permissionTests.js"); 2 | var should = require("chai").should(); 3 | 4 | exports.run = function(dao) { 5 | describe("var list = ctx.lists(listname)", function() { 6 | this.timeout(4000); 7 | var list = dao.lists("TestList"); 8 | describe("list.info()", function() { 9 | var listInfo = null; 10 | before(function(done) { 11 | list.getInfo().then(function(info) { 12 | listInfo = info; 13 | done(); 14 | }); 15 | }); 16 | it("Should return a promise that resolves to list info", function() { 17 | listInfo.should.be.an("object"); 18 | }); 19 | it("Should bring back list info like Title, ItemCount, and ListItemEntityTypeFullName", function() { 20 | listInfo.should.have.property("Title"); 21 | listInfo.should.have.property("ItemCount"); 22 | listInfo.should.have.property("ListItemEntityTypeFullName"); 23 | }); 24 | }); 25 | 26 | describe("list.getItems()", function() { 27 | var items = null; 28 | before(function(done) { 29 | list.getItems().then(function(results) { 30 | items = results; 31 | done(); 32 | }); 33 | }); 34 | 35 | it("Should return a promise that resolves to an array of items", function() { 36 | items.should.be.an("array"); 37 | }); 38 | 39 | it("Should return all the items in the list", function(done) { 40 | list.getInfo().then(function(listInfo) { 41 | items.length.should.equal(listInfo.ItemCount); 42 | done(); 43 | }); 44 | }); 45 | }); 46 | 47 | describe("list.getItems(odata)", function() { 48 | //Get items where BoolColumn == TRUE 49 | var odata = "$filter=MyStatus eq 'Green'"; 50 | var items = null; 51 | before(function(done) { 52 | list.getItems(odata).then(function(results) { 53 | items = results; 54 | done(); 55 | }); 56 | }); 57 | it("Should return a promise that resolves to an array of items", function() { 58 | items.should.be.an("array"); 59 | }); 60 | it("Should return only items that match the OData params", function() { 61 | items.forEach(function(item) { 62 | item.should.have.property("MyStatus"); 63 | item.MyStatus.should.equal("Green"); 64 | }); 65 | }); 66 | }); 67 | 68 | describe("list.getItemById(id)", function() { 69 | var item = null; 70 | var validId = -1; 71 | before(function(done) { 72 | list 73 | .getItems() 74 | .then(function(allItems) { 75 | validId = allItems[0].Id; 76 | return validId; 77 | }) 78 | .then(function(id) { 79 | return list.getItemById(id); 80 | }) 81 | .then(function(result) { 82 | item = result; 83 | done(); 84 | }); 85 | }); 86 | it("Should return a promise that resolves to a single item", function() { 87 | item.should.be.an("object"); 88 | item.should.have.property("Title"); 89 | }); 90 | it("Should resolve an item with a matching ID", function() { 91 | item.should.have.property("Id"); 92 | item.Id.should.equal(validId); 93 | }); 94 | it("Should be able to return attachments using the optional odata query", function( 95 | done 96 | ) { 97 | list.getItemById(validId, "$expand=AttachmentFiles").then(function(item) { 98 | item.should.have.property("AttachmentFiles"); 99 | item.AttachmentFiles.should.have.property("results"); 100 | item.AttachmentFiles.results.should.be.an("Array"); 101 | done(); 102 | }); 103 | }); 104 | }); 105 | 106 | describe("list.findItems(key, value)", function() { 107 | var matches = null; 108 | before(function(done) { 109 | list.findItems("MyStatus", "Green").then(function(results) { 110 | matches = results; 111 | done(); 112 | }); 113 | }); 114 | it("Should return a promise that resolves to an array of list items", function() { 115 | matches.should.be.an("array"); 116 | matches.should.not.be.empty; 117 | }); 118 | it("Should only bring back items the match the key value query", function() { 119 | matches.forEach(function(item) { 120 | item.should.have.property("MyStatus"); 121 | item.MyStatus.should.equal("Green"); 122 | }); 123 | }); 124 | }); 125 | describe("list.findItem(key, value)", function() { 126 | var match = null; 127 | before(function(done) { 128 | list.findItem("MyStatus", "Green").then(function(result) { 129 | match = result; 130 | done(); 131 | }); 132 | }); 133 | it("Should resolve to one list item", function() { 134 | match.should.be.an("object"); 135 | }); 136 | it("Should only bring back an item if it matches the key value query", function() { 137 | match.should.have.property("MyStatus"); 138 | match.MyStatus.should.equal("Green"); 139 | }); 140 | }); 141 | 142 | describe("list.addItem()", function() { 143 | var newItem = { 144 | Title: "Test Created Item", 145 | MyStatus: "Red" 146 | }; 147 | var insertedItem = null; 148 | before(function(done) { 149 | list 150 | .addItem(newItem) 151 | .then(function(result) { 152 | insertedItem = result; 153 | done(); 154 | }) 155 | .catch(function(error) { 156 | console.log(error); 157 | done(); 158 | }); 159 | }); 160 | it("Should return a promise that resolves with the new list item", function() { 161 | insertedItem.should.not.be.null; 162 | insertedItem.should.be.an("object"); 163 | insertedItem.should.have.property("Id"); 164 | }); 165 | it("Should save the item right away so it can be queried.", function() { 166 | list.getItemById(insertedItem.Id).then(function(foundItem) { 167 | foundItem.should.not.be.null; 168 | foundItem.should.have.property("Title"); 169 | foundItem.Title.should.equal(newItem.Title); 170 | }); 171 | }); 172 | it("Should throw an error if a invalid field is set", function(done) { 173 | newItem.InvalidColumn = "test"; 174 | list 175 | .addItem(newItem) 176 | .then(function() { 177 | //supposed to fail 178 | "one".should.equal("two"); 179 | }) 180 | .catch(function(xhr, status, msg) { 181 | done(); 182 | }); 183 | }); 184 | }); 185 | 186 | // var itemIdWithAttachment = null; 187 | // var attachmentFilename = "testAttachment.txt"; 188 | // var attachmentContent = "test content"; 189 | 190 | // describe("list.addAttachment(id, filename, content)", function() { 191 | 192 | // before(function(done) { 193 | // list.getItems("$orderby=Id").then(function(items) { 194 | // itemIdWithAttachment = items[items.length - 1].Id; 195 | // return list.addAttachment(itemIdWithAttachment, attachmentFilename, attachmentContent); 196 | // }).then(function() { 197 | // done(); 198 | // }); 199 | // }); 200 | // it("Should add an attachment file to the list item", function(done) { 201 | // list.getItemById(itemIdWithAttachment, "$expand=AttachmentFiles").then(function(item){ 202 | // item.should.have.property('AttachmentFiles'); 203 | // item.AttachmentFiles.should.have.property('results'); 204 | // item.AttachmentFiles.results.should.be.an('Array'); 205 | // item.AttachmentFiles.results.should.not.be.empty; 206 | // done(); 207 | // }); 208 | // }) 209 | // }); 210 | 211 | // describe("list.deleteAttachment(id, filename)", function() { 212 | // var getAttachment = function(id, filename) { 213 | // return list.getItemById(itemIdWithAttachment, "$expand=AttachmentFiles").then(function(item){ 214 | // var attachments = item.AttachmentFiles.results; 215 | // return attachments.find(function(a) { return a.FileName === attachmentFilename}); 216 | // }); 217 | // }; 218 | // before(function(done) { 219 | // getAttachment(itemIdWithAttachment, attachmentFilename).then(function(attachment) { 220 | // if (attachment) { 221 | // return list.deleteAttachment(itemIdWithAttachment, attachmentFilename); 222 | // } 223 | // return false; 224 | // }).then(function(){ 225 | // done(); 226 | // }).catch(function(res) { 227 | // done(); 228 | // console.log("REQUEST ERROR") 229 | // }); 230 | // }); 231 | // it("Should delete the attachment", function(done) { 232 | // getAttachment(itemIdWithAttachment, attachmentFilename).then(function(attachment) { 233 | // if (attachment) ("attachment").should.equal("null"); 234 | // done(); 235 | // }); 236 | // }) 237 | // }); 238 | 239 | describe("list.deleteItem(id)", function() { 240 | var itemToDelete = null; 241 | before(function(done) { 242 | list 243 | .getItems("$orderby=Id") 244 | .then(function(items) { 245 | itemToDelete = items[items.length - 1]; 246 | return list.deleteItem(itemToDelete.Id); 247 | }) 248 | .then(function() { 249 | done(); 250 | }) 251 | .catch(function(err) { 252 | done(err); 253 | }); 254 | }); 255 | it("Should delete that item", function(done) { 256 | list 257 | .getItemById(itemToDelete.Id) 258 | .then(function() { 259 | throw "Should have failed because item has been deleted"; 260 | }) 261 | .catch(function() { 262 | done(); 263 | }); 264 | }); 265 | it("Should reject the promise if the item id can not be found", function(done) { 266 | list 267 | .deleteItem(99999999) 268 | .then(function() { 269 | throw "Should have failed because id doesnt exist"; 270 | }) 271 | .catch(function() { 272 | done(); 273 | }); 274 | }); 275 | }); 276 | 277 | describe("list.updateItem()", function() { 278 | var itemToUpdate = null; 279 | var updates = { 280 | Title: "Updated Title" 281 | }; 282 | before(function(done) { 283 | list.getItems("$orderby=Id desc").then(function(items) { 284 | itemToUpdate = items[items.length - 1]; 285 | done(); 286 | }); 287 | }); 288 | it("Should return a promise", function(done) { 289 | list.updateItem(itemToUpdate.Id, updates).then(function() { 290 | done(); 291 | }); 292 | }); 293 | it("Should update only the properties that were passed", function(done) { 294 | list.getItemById(itemToUpdate.Id).then(function(item) { 295 | item.should.have.property("Title"); 296 | item.Title.should.equal(updates.Title); 297 | done(); 298 | }); 299 | }); 300 | }); 301 | 302 | describe("list.permissions.getRoleAssignments()", permissionsTests.create(list)); 303 | 304 | if (isBrowser()) { 305 | describe("list.permissions.check()", permissionsTests.create(list, "check")); 306 | } 307 | 308 | describe( 309 | "list.permissions.check(email)", 310 | permissionsTests.create(list, "check", "andrew@andrewpetersen.onmicrosoft.com") 311 | ); 312 | }); 313 | }; 314 | 315 | function isBrowser() { 316 | return !(typeof window === "undefined"); 317 | } 318 | -------------------------------------------------------------------------------- /legacy-tests/tests/permissionTests.js: -------------------------------------------------------------------------------- 1 | var should = require("chai").should(); 2 | var create = (exports.create = function (securable, action, email) { 3 | if (action === "check") { 4 | return function () { 5 | var permissions = null; 6 | before(function (done) { 7 | securable.permissions 8 | .check(email) 9 | .then(function (privs) { 10 | permissions = privs; 11 | done(); 12 | }) 13 | .catch(function (err) { 14 | done(err); 15 | }); 16 | }); 17 | 18 | it("Should return a promise that resolves to an array of base permission strings", function () { 19 | permissions.should.be.an("array"); 20 | permissions.should.not.be.empty; 21 | }); 22 | 23 | it("Should reject the promise for an invalid email", function (done) { 24 | securable.permissions 25 | .check("invalid@invalid123.com") 26 | .then(function (privs) { 27 | "one".should.equal("two"); 28 | done(); 29 | }) 30 | .catch(function (error) { 31 | done(); 32 | }); 33 | }); 34 | }; 35 | } else { 36 | return function () { 37 | var permissions = null; 38 | before(function (done) { 39 | securable.permissions.getRoleAssignments().then(function (privs) { 40 | permissions = privs; 41 | done(); 42 | }); 43 | }); 44 | it("Should return a promise that resolves to an array of objects", function () { 45 | permissions.should.be.an("array"); 46 | permissions.should.not.be.empty; 47 | }); 48 | it("Should return objects that each have a member and a roles array", function () { 49 | permissions.forEach(function (permission) { 50 | permission.should.have.property("member"); 51 | permission.should.have.property("roles"); 52 | permission.roles.should.be.an("array"); 53 | }); 54 | }); 55 | it("Should return permission objects that contain member.name, member.login, and member.id", function () { 56 | permissions.forEach(function (permission) { 57 | permission.member.should.have.property("name"); 58 | permission.member.should.have.property("login"); 59 | permission.member.should.have.property("id"); 60 | }); 61 | }); 62 | it("Should return permission objects, each with a roles array that has a name and description", function () { 63 | permissions.forEach(function (permission) { 64 | permission.roles.forEach(function (role) { 65 | role.should.have.property("name"); 66 | role.should.have.property("description"); 67 | }); 68 | }); 69 | }); 70 | }; 71 | } 72 | }); 73 | -------------------------------------------------------------------------------- /legacy-tests/tests/profileTests.js: -------------------------------------------------------------------------------- 1 | var should = require("chai").should(); 2 | 3 | exports.run = function(dao) { 4 | describe("var profiles = ctx.profiles", function() { 5 | this.timeout(5000); 6 | 7 | describe("ctx.profiles.current()", function() { 8 | var profile = null; 9 | before(function(done) { 10 | dao.profiles.current().then(function(result) { 11 | profile = result; 12 | done(); 13 | }); 14 | }); 15 | 16 | it("Should return a promise that resolves to a profile properties object", function() { 17 | profile.should.be.an("object"); 18 | profile.should.have.property("AccountName"); 19 | profile.should.have.property("Email"); 20 | profile.should.have.property("PreferredName"); 21 | }); 22 | it("Should return the profile of the current user", function() { 23 | profile.should.have.property("Email"); 24 | profile.Email.should.equal(_spPageContextInfo.userEmail); 25 | }); 26 | }); 27 | 28 | describe("ctx.profiles.get()", function() { 29 | var profile = null; 30 | before(function(done) { 31 | dao.profiles.get().then(function(result) { 32 | profile = result; 33 | done(); 34 | }); 35 | }); 36 | it("Should return a promise that resolves to a profile properties object", function() { 37 | profile.should.be.an("object"); 38 | profile.should.have.property("AccountName"); 39 | profile.should.have.property("Email"); 40 | profile.should.have.property("PreferredName"); 41 | }); 42 | 43 | it("Should return the profile of the current user", function() { 44 | profile.should.have.property("Email"); 45 | profile.Email.should.equal(_spPageContextInfo.userEmail); 46 | }); 47 | }); 48 | 49 | describe("ctx.profiles.get(email)", function() { 50 | var email = "andrew@andrewpetersen.onmicrosoft.com"; 51 | var profile = null; 52 | before(function(done) { 53 | dao.profiles 54 | .get(email) 55 | .then(function(result) { 56 | profile = result; 57 | done(); 58 | }) 59 | .catch(function(err) { 60 | done(err); 61 | }); 62 | }); 63 | it("Should return a promise that resolves to a profile properties object", function() { 64 | profile.should.be.an("object"); 65 | profile.should.have.property("AccountName"); 66 | profile.should.have.property("Email"); 67 | profile.should.have.property("PreferredName"); 68 | }); 69 | 70 | it("Should give you the matching person", function() { 71 | profile.should.have.property("Email"); 72 | profile.Email.should.equal(email); 73 | }); 74 | 75 | it("Should reject the promise for an invalid email", function(done) { 76 | dao.profiles 77 | .get("invalid@invalid123.com") 78 | .then(function(result) { 79 | done("The request should have failed."); 80 | }) 81 | .catch(function() { 82 | done(); 83 | }); 84 | }); 85 | }); 86 | 87 | describe("ctx.profiles.get({ AccountName })", function() { 88 | var email = "andrew@andrewpetersen.onmicrosoft.com"; 89 | var accountName = "i:0#.f|membership|andrew@andrewpetersen.onmicrosoft.com"; 90 | var profile = null; 91 | before(function(done) { 92 | dao.profiles 93 | .get({ AccountName: accountName }) 94 | .then(function(result) { 95 | profile = result; 96 | done(); 97 | }) 98 | .catch(function(err) { 99 | done(err); 100 | }); 101 | }); 102 | it("Should return a promise that resolves to a profile properties object", function() { 103 | profile.should.be.an("object"); 104 | profile.should.have.property("AccountName"); 105 | profile.should.have.property("Email"); 106 | profile.should.have.property("PreferredName"); 107 | }); 108 | 109 | it("Should give you the matching person", function() { 110 | profile.should.have.property("Email"); 111 | profile.Email.should.equal(email); 112 | }); 113 | 114 | it("Should reject the promise for an invalid account name", function(done) { 115 | dao.profiles 116 | .get({ AccountName: "Invalid" }) 117 | .then(function(result) { 118 | done("The request should have failed."); 119 | }) 120 | .catch(function() { 121 | done(); 122 | }); 123 | }); 124 | }); 125 | 126 | describe("ctx.profiles.get({ LoginName })", function() { 127 | var email = "andrew@andrewpetersen.onmicrosoft.com"; 128 | var accountName = "i:0#.f|membership|andrew@andrewpetersen.onmicrosoft.com"; 129 | var profile = null; 130 | before(function(done) { 131 | dao.profiles 132 | .get({ LoginName: accountName }) 133 | .then(function(result) { 134 | profile = result; 135 | done(); 136 | }) 137 | .catch(function(err) { 138 | done(err); 139 | }); 140 | }); 141 | it("Should return a promise that resolves to a profile properties object", function() { 142 | profile.should.be.an("object"); 143 | profile.should.have.property("AccountName"); 144 | profile.should.have.property("Email"); 145 | profile.should.have.property("PreferredName"); 146 | }); 147 | 148 | it("Should give you the matching person", function() { 149 | profile.should.have.property("Email"); 150 | profile.Email.should.equal(email); 151 | }); 152 | 153 | it("Should reject the promise for an invalid account name", function(done) { 154 | dao.profiles 155 | .get({ LoginName: "Invalid" }) 156 | .then(function(result) { 157 | done("The request should have failed."); 158 | }) 159 | .catch(function() { 160 | done(); 161 | }); 162 | }); 163 | }); 164 | 165 | describe("ctx.profiles.setProperty(propertyName, propertyValue)", function() { 166 | it("Should update the About Me profile property of the current user", function(done) { 167 | var aboutMeValue = "Hi there. I was updated with SPScript 1"; 168 | dao.profiles 169 | .setProperty("AboutMe", aboutMeValue) 170 | .then(function() { 171 | return dao.profiles.current(); 172 | }) 173 | .then(function(profile) { 174 | profile.should.have.property("AboutMe"); 175 | profile.AboutMe.should.equal(aboutMeValue); 176 | done(); 177 | }) 178 | .catch(function(err) { 179 | done(err); 180 | }); 181 | }); 182 | }); 183 | 184 | describe("ctx.profiles.setProperty(propertyName, propertyValue, email)", function() { 185 | var email = "andrew@andrewpetersen.onmicrosoft.com"; 186 | it("Should update the About Me profile property based on the specified email", function( 187 | done 188 | ) { 189 | var aboutMeValue = "Hi there. I was updated with SPScript 2"; 190 | dao.profiles 191 | .setProperty("AboutMe", aboutMeValue, email) 192 | .then(function() { 193 | return dao.profiles.get(email); 194 | }) 195 | .then(function(profile) { 196 | profile.should.have.property("AboutMe"); 197 | profile.AboutMe.should.equal(aboutMeValue); 198 | done(); 199 | }) 200 | .catch(function(err) { 201 | done(err); 202 | }); 203 | }); 204 | }); 205 | 206 | describe("ctx.profiles.setProperty(propertyName, propertyValue, { AccountName|LoginName })", function() { 207 | var accountName = "i:0#.f|membership|andrew@andrewpetersen.onmicrosoft.com"; 208 | var email = "andrew@andrewpetersen.onmicrosoft.com"; 209 | it("Should update the About Me profile property of the passed in User object", function( 210 | done 211 | ) { 212 | var aboutMeValue = "Hi there. I was updated with SPScript 3"; 213 | dao.profiles 214 | .setProperty("AboutMe", aboutMeValue, { AccountName: accountName }) 215 | .then(function() { 216 | return dao.profiles.get({ AccountName: accountName }); 217 | }) 218 | .then(function(profile) { 219 | profile.should.have.property("AboutMe"); 220 | profile.AboutMe.should.equal(aboutMeValue); 221 | done(); 222 | }) 223 | .catch(function(err) { 224 | done(err); 225 | }); 226 | }); 227 | }); 228 | }); 229 | }; 230 | -------------------------------------------------------------------------------- /legacy-tests/tests/searchTests.js: -------------------------------------------------------------------------------- 1 | var should = require("chai").should(); 2 | 3 | exports.run = function(dao) { 4 | describe("var search = ctx.search;", function() { 5 | this.timeout(5000); 6 | describe("ctx.search.query(queryText)", function() { 7 | it("Should return promise that resolves to a SearchResults object", function(done) { 8 | var queryText = "andrew"; 9 | dao.search.query(queryText).then(function(searchResults) { 10 | searchResults.should.be.an("object"); 11 | searchResults.should.have.property("resultsCount"); 12 | searchResults.should.have.property("totalResults"); 13 | searchResults.should.have.property("items"); 14 | searchResults.should.have.property("refiners"); 15 | searchResults.items.should.be.an("array"); 16 | searchResults.items.should.not.be.empty; 17 | done(); 18 | }); 19 | }); 20 | }); 21 | 22 | describe("ctx.search.query(queryText, queryOptions)", function() { 23 | it("Should obey the extra query options that were passed", function(done) { 24 | var queryText = "andrew"; 25 | var options = { 26 | rowLimit: 1 27 | }; 28 | dao.search.query(queryText, options).then(function(searchResults) { 29 | searchResults.should.be.an("object"); 30 | searchResults.should.have.property("resultsCount"); 31 | searchResults.should.have.property("totalResults"); 32 | searchResults.should.have.property("items"); 33 | searchResults.should.have.property("refiners"); 34 | searchResults.items.should.be.an("array"); 35 | searchResults.items.should.not.be.empty; 36 | searchResults.items.length.should.equal(1); 37 | done(); 38 | }); 39 | }); 40 | }); 41 | 42 | describe("ctx.search.query(queryText, queryOptions) - w/ Refiners", function() { 43 | it("Should return SearchResults that include a refiners array", function(done) { 44 | var refinerName = "FileType"; 45 | var queryText = "andrew"; 46 | var options = { 47 | refiners: refinerName 48 | }; 49 | dao.search.query(queryText, options).then(function(searchResults) { 50 | searchResults.should.be.an("object"); 51 | searchResults.should.have.property("refiners"); 52 | searchResults.refiners.should.not.be.empty; 53 | var firstRefiner = searchResults.refiners[0]; 54 | firstRefiner.should.have.property("RefinerName"); 55 | firstRefiner.should.have.property("RefinerOptions"); 56 | firstRefiner.RefinerName.should.equal(refinerName); 57 | firstRefiner.RefinerOptions.should.be.an("array"); 58 | done(); 59 | }); 60 | }); 61 | }); 62 | describe("ctx.search.people(queryText)", function() { 63 | it("Should only return search results that are people", function(done) { 64 | var queryText = "andrew"; 65 | dao.search.people(queryText).then(function(searchResults) { 66 | searchResults.should.be.an("object"); 67 | searchResults.should.have.property("items"); 68 | searchResults.items.should.be.an("array"); 69 | searchResults.items.should.not.be.empty; 70 | 71 | var person = searchResults.items[0]; 72 | person.should.have.property("AccountName") 73 | person.should.have.property("PreferredName") 74 | person.should.have.property("AboutMe") 75 | person.should.have.property("WorkEmail") 76 | person.should.have.property("PictureURL") 77 | done(); 78 | }) 79 | }); 80 | }); 81 | 82 | describe("ctx.search.sites(queryText, scope)", function() { 83 | it("Should only return search results that are sites", function(done) { 84 | var queryText = ""; 85 | dao.search.sites(queryText).then(function(searchResults) { 86 | searchResults.should.be.an("object"); 87 | searchResults.should.have.property("items"); 88 | searchResults.items.should.be.an("array"); 89 | searchResults.items.should.not.be.empty(); 90 | 91 | var site; 92 | for(var i = 0; i < searchResults.items.length; i++) { 93 | site = searchResults.items[i]; 94 | site.should.have.property("Path"); 95 | site.should.have.property("contentclass"); 96 | site.contentclass.should.equal("STS_Web"); 97 | } 98 | 99 | done(); 100 | }) 101 | }); 102 | 103 | it("Should only return sites underneath the specified scope", function(done){ 104 | var scope = "https://andrewpetersen.sharepoint.com/sites/ep"; 105 | dao.search.sites("", scope).then(function(searchResults) { 106 | searchResults.should.be.an("object"); 107 | searchResults.should.have.property("items"); 108 | searchResults.items.should.be.an("array"); 109 | searchResults.items.should.not.be.empty(); 110 | 111 | var site; 112 | for(var i = 0; i < searchResults.items.length; i++) { 113 | site = searchResults.items[i]; 114 | site.should.have.property("Path"); 115 | site.Path.indexOf(scope).should.equal(0); 116 | site.should.have.property("contentclass"); 117 | site.contentclass.should.equal("STS_Web"); 118 | } 119 | 120 | done(); 121 | }) 122 | }) 123 | }); 124 | }) 125 | 126 | }; -------------------------------------------------------------------------------- /legacy-tests/tests/utilsTests.js: -------------------------------------------------------------------------------- 1 | var should = require("chai").should(); 2 | 3 | exports.run = function(utils) { 4 | describe("var utils = SPScript.utils", function() { 5 | describe("utils.parseJSON(data)", function() { 6 | it("Should exist on the utils object", function() { 7 | utils.should.have.property("parseJSON"); 8 | utils.parseJSON.should.be.a("function"); 9 | }) 10 | it("Should take a string or an object and return an object", function() { 11 | var obj = { one: "value of one", two: "value of two" }; 12 | var jsonStr = JSON.stringify(obj); 13 | 14 | var res1 = utils.parseJSON(obj); 15 | res1.should.not.be.null; 16 | res1.should.have.property("one"); 17 | res1.one.should.equal(obj.one); 18 | 19 | var res2 = utils.parseJSON(jsonStr); 20 | res2.should.not.be.null; 21 | res2.should.have.property("one"); 22 | res2.one.should.equal(obj.one); 23 | }) 24 | }) 25 | 26 | describe("Query String", function() { 27 | describe("utils.qs.toObj(str)", function() { 28 | it("Should take in a string in the form of key=value&key2=value and return an Object", function() { 29 | var str1 = "key1=value1"; 30 | var str2 = "key1=value1&key2=value2"; 31 | var obj1 = utils.qs.toObj(str1) 32 | obj1.should.have.property("key1"); 33 | obj1.key1.should.equal("value1"); 34 | 35 | var obj2 = utils.qs.toObj(str2); 36 | obj2.should.have.property("key1"); 37 | obj2.should.have.property("key2"); 38 | obj2.key2.should.equal("value2"); 39 | }) 40 | it("Should support an optional leading '?' ", function() { 41 | var str1 = "?key1=value1"; 42 | var obj1 = utils.qs.toObj(str1) 43 | obj1.should.have.property("key1"); 44 | obj1.key1.should.equal("value1"); 45 | }); 46 | it("Should default to 'window.location.search' if no value is passed") 47 | }) 48 | 49 | describe("utils.qs.fromObj(obj, quoteValues?)", function() { 50 | it("Should turn { key1: 'value1', key2: 'value2' } into 'key1=value1&key2=value2'", function() { 51 | var obj = { key1: "value1", key2: "value2" } 52 | var str = utils.qs.fromObj(obj); 53 | str.should.equal("key1=value1&key2=value2"); 54 | }) 55 | it("Should put single quotes around words with spaces", function() { 56 | var obj = { key1: "my value" } 57 | var str = utils.qs.fromObj(obj); 58 | str.should.equal("key1='my value'"); 59 | }) 60 | it("Should put single quotes around every value is an optional 'quoteValues' param is set to true", function() { 61 | var obj = { key1: "value1", key2: "value2" } 62 | var str = utils.qs.fromObj(obj, true); 63 | str.should.equal("key1='value1'&key2='value2'"); 64 | }) 65 | }); 66 | }) 67 | 68 | describe("Headers", function() { 69 | 70 | describe("utils.headers.getStandardHeaders(digest?)", function() { 71 | var jsonMimeType = "application/json;odata=verbose"; 72 | it("Should set the Accept header", function() { 73 | var headers = utils.headers.getStandardHeaders(); 74 | headers.should.have.property("Accept"); 75 | headers.Accept.should.equal(jsonMimeType); 76 | }) 77 | it("Should set the Request Digest if a digest is passed", function() { 78 | var digest = "123Fake" 79 | var headers = utils.headers.getStandardHeaders(digest); 80 | headers.should.have.property("Accept"); 81 | headers.Accept.should.equal(jsonMimeType); 82 | headers.should.have.property("X-RequestDigest"); 83 | headers["X-RequestDigest"].should.equal(digest); 84 | }) 85 | }) 86 | 87 | describe("utils.headers.getAddHeaders(digest)", function() { 88 | var jsonMimeType = "application/json;odata=verbose"; 89 | it("Should set the Request Digest if a digest is passed", function() { 90 | var digest = "123Fake" 91 | var headers = utils.headers.getAddHeaders(digest); 92 | headers.should.have.property("Accept"); 93 | headers.Accept.should.equal(jsonMimeType); 94 | headers.should.have.property("X-RequestDigest"); 95 | headers["X-RequestDigest"].should.equal(digest); 96 | }) 97 | }) 98 | 99 | describe("utils.headers.getUpdateHeaders(digest)", function() { 100 | var jsonMimeType = "application/json;odata=verbose"; 101 | it("Should set X-HTTP-Method to MERGE and include X-RequestDigest", function() { 102 | var digest = "123Fake" 103 | var headers = utils.headers.getUpdateHeaders(digest); 104 | headers.should.have.property("Accept"); 105 | headers.Accept.should.equal(jsonMimeType); 106 | headers.should.have.property("X-RequestDigest"); 107 | headers["X-RequestDigest"].should.equal(digest); 108 | headers.should.have.property("X-HTTP-Method"); 109 | headers["X-HTTP-Method"].should.equal("MERGE"); 110 | }) 111 | }) 112 | 113 | describe("utils.headers.getDeleteHeaders(digest)", function() { 114 | var jsonMimeType = "application/json;odata=verbose"; 115 | it("Should set X-HTTP-Method to DELETE and include X-RequestDigest", function() { 116 | var digest = "123Fake" 117 | var headers = utils.headers.getDeleteHeaders(digest); 118 | headers.should.have.property("Accept"); 119 | headers.Accept.should.equal(jsonMimeType); 120 | headers.should.have.property("X-RequestDigest"); 121 | headers["X-RequestDigest"].should.equal(digest); 122 | headers.should.have.property("X-HTTP-Method"); 123 | headers["X-HTTP-Method"].should.equal("DELETE"); 124 | }) 125 | }) 126 | }) 127 | 128 | describe("Dependency Management", function() { 129 | describe("utils.validateNamespace(namespace)", function() { 130 | it("Should return false if that namespace is not on global window") 131 | it("Should return true if that namespace is on global window") 132 | }) 133 | 134 | describe("utils.waitForLibrary(namespace)", function() { 135 | it("Should return a promise that resolves when that namespace is on the global") 136 | }) 137 | 138 | describe("utils.waitForLibraries(namespaces)", function() { 139 | it("Should return a promise that resolves when all the namespacea are on the global") 140 | }) 141 | 142 | describe("utils.waitForElement(selector)", function() { 143 | it("Should return a promise that resolves an element that matches the selector is on the page"); 144 | it("Should eventually time out") 145 | }) 146 | }) 147 | }) 148 | } -------------------------------------------------------------------------------- /legacy-tests/tests/webTests.js: -------------------------------------------------------------------------------- 1 | var permissionsTests = require("./permissionTests.js"); 2 | var should = require("chai").should(); 3 | 4 | exports.run = function(dao) { 5 | describe("var web = ctx.web", function() { 6 | this.timeout(5000); 7 | describe("ctx.web.getInfo()", function() { 8 | it("Should return a promise that resolves to web info", function(done) { 9 | dao.web 10 | .getInfo() 11 | .then(function(webInfo) { 12 | webInfo.should.have.property("Url"); 13 | webInfo.should.have.property("Title"); 14 | done(); 15 | }) 16 | .catch(function(err) { 17 | console.log(err); 18 | }); 19 | }); 20 | }); 21 | 22 | describe("ctx.web.getSubsites()", function() { 23 | it("Should return a promise that resolves to an array of subsite web infos.", function( 24 | done 25 | ) { 26 | dao.web.getSubsites().then(function(subsites) { 27 | subsites.should.be.an("array"); 28 | if (subsites.length) { 29 | subsites[0].should.have.property("Title"); 30 | subsites[0].should.have.property("ServerRelativeUrl"); 31 | } 32 | done(); 33 | }); 34 | }); 35 | }); 36 | 37 | // var folderPath = "/shared documents"; 38 | // describe("web.getFolder(serverRelativeUrl)", function() { 39 | // var folder = null; 40 | // before(function(done) { 41 | // dao.web.getFolder(folderPath).then(function(result) { 42 | // folder = result; 43 | // done(); 44 | // }); 45 | // }); 46 | // it("Should return a promise that resolves to a folder with files and folders", function() { 47 | // folder.should.be.an("object"); 48 | // folder.should.have.property("name"); 49 | // folder.should.have.property("serverRelativeUrl"); 50 | // folder.should.have.property("files"); 51 | // folder.files.should.be.an("array"); 52 | // folder.should.have.property("folders"); 53 | // folder.folders.should.be.an("array"); 54 | // }); 55 | // }); 56 | 57 | describe("ctx.web.getUser()", function() { 58 | var user = null; 59 | before(function(done) { 60 | dao.web.getUser().then(function(result) { 61 | user = result; 62 | done(); 63 | }); 64 | }); 65 | it("Should return a promise that resolves to a user object", function() { 66 | user.should.not.be.null; 67 | user.should.have.property("Id"); 68 | user.should.have.property("LoginName"); 69 | user.should.have.property("Email"); 70 | }); 71 | it("Should return the current user", function() { 72 | user.should.have.property("Id"); 73 | if (typeof window !== "undefined" && window._spPageContextInfo) { 74 | user.Id.should.equal(_spPageContextInfo.userId); 75 | } 76 | }); 77 | }); 78 | 79 | describe("ctx.web.getUser(email)", function() { 80 | var email = "andrew@andrewpetersen.onmicrosoft.com"; 81 | var user = null; 82 | before(function(done) { 83 | dao.web.getUser(email).then(function(result) { 84 | user = result; 85 | done(); 86 | }); 87 | }); 88 | 89 | it("Should return a promise that resolves to a user object", function() { 90 | user.should.not.be.null; 91 | user.should.have.property("Id"); 92 | user.should.have.property("LoginName"); 93 | user.should.have.property("Email"); 94 | }); 95 | it("Should return a user whose email matches the specified email", function() { 96 | user.should.have.property("Email"); 97 | user.Email.should.equal(email); 98 | }); 99 | }); 100 | var folderUrl = "/spscript/Shared Documents"; 101 | var filename = "testfile.txt"; 102 | var fileUrl = folderUrl + "/" + filename; 103 | 104 | // describe("web.uploadFile(fileContent, serverRelativeFolderUrl)", function() { 105 | // var fileContent = "file content"; 106 | // var fileTitle = "test title"; 107 | // var response = null; 108 | // before(function(done){ 109 | // dao.web.uploadFile(fileContent, folderUrl, { name: filename, Title: fileTitle}) 110 | // .then(function(data){ 111 | // response = data; 112 | // done(); 113 | // }) 114 | // }) 115 | // it("Should return a promise that resolves to an object with file and item", function() { 116 | // response.should.not.be.null; 117 | // response.should.have.property("file"); 118 | // response.should.have.property("item"); 119 | // response.file.should.have.property("ServerRelativeUrl"); 120 | // }); 121 | // it("Should return an item that has the parent list expanded", function() { 122 | // response.item.should.have.property("Id"); 123 | // response.item.should.have.property("ParentList"); 124 | // response.item.ParentList.should.have.property("Title"); 125 | // }) 126 | // it("Should save the file to the right location", function() { 127 | // response.file.ServerRelativeUrl.toLowerCase().should.equal(fileUrl.toLowerCase()); 128 | // }); 129 | // it("Should allow setting fields after the upload completes", function(done) { 130 | // dao.lists(response.item.ParentList.Title).getItemById(response.item.Id).then(function(retrievedItem){ 131 | // retrievedItem.should.have.property("Title"); 132 | // retrievedItem.Title.should.equal(fileTitle); 133 | // done(); 134 | // }) 135 | // }) 136 | // }) 137 | 138 | describe("ctx.web.getFile(serverRelativeFileUrl)", function() { 139 | var file = null; 140 | before(function(done) { 141 | dao.web.getFile(fileUrl).then(function(result) { 142 | file = result; 143 | done(); 144 | }); 145 | }); 146 | it("Should return a promise that resolves to a file object", function() { 147 | file.should.not.be.null; 148 | file.should.property("CheckOutType"); 149 | file.should.property("ETag"); 150 | file.should.property("Exists"); 151 | file.should.property("TimeLastModified"); 152 | file.should.property("Name"); 153 | file.should.property("UIVersionLabel"); 154 | }); 155 | }); 156 | 157 | var destinationUrl = "/spscript/Shared%20Documents/testfile2.txt"; 158 | describe("ctx.web.copyFile(serverRelativeSourceUrl, serverRelativeDestUrl)", function() { 159 | var startTestTime = new Date(); 160 | var file = null; 161 | before(function(done) { 162 | dao.web 163 | .copyFile(fileUrl, destinationUrl) 164 | .then(function() { 165 | return dao.web.getFile(destinationUrl); 166 | }) 167 | .then(function(result) { 168 | file = result; 169 | done(); 170 | }) 171 | .catch(function(resp) { 172 | "one".should.equal("two", "Error in CopyFile requst"); 173 | done(); 174 | }); 175 | }); 176 | it("Should return a promise, and once resolved, the new (copied) file should be retrievable.", function() { 177 | file.should.not.be.null; 178 | file.should.property("CheckOutType"); 179 | file.should.property("ETag"); 180 | file.should.property("Exists"); 181 | file.should.property("TimeLastModified"); 182 | file.should.property("Name"); 183 | file.should.property("UIVersionLabel"); 184 | // var modified = new Date(file["TimeLastModified"]) 185 | // modified.should.be.above(startTestTime); 186 | }); 187 | }); 188 | 189 | // describe("web.deleteFile(serverRelativeFileUrl)", function() { 190 | // var file = null; 191 | // it("Ensure there is a file to delete.", function(done){ 192 | // dao.web.getFile(destinationUrl).then(function(result){ 193 | // result.should.not.be.null; 194 | // result.should.have.property("Name"); 195 | // done(); 196 | // }); 197 | // }) 198 | 199 | // it("Should return a promise, and once resolved, the file should NOT be retrievable", function(done){ 200 | // dao.web.deleteFile(destinationUrl).then(function(result){ 201 | // dao.web.getFile(destinationUrl) 202 | // .then(function(){ 203 | // // the call to get file succeeded so for a a failure 204 | // ("one").should.equal("two"); 205 | // done(); 206 | // }) 207 | // .catch(function(){ 208 | // done(); 209 | // // call to get file failed as expected because file is gone 210 | // }) 211 | // }) 212 | // }) 213 | // }); 214 | 215 | describe("ctx.web.permissions.getRoleAssignments()", permissionsTests.create(dao.web)); 216 | 217 | if (isBrowser()) { 218 | describe("ctx.web.permissions.check()", permissionsTests.create(dao.web, "check")); 219 | } 220 | 221 | describe( 222 | "ctx.web.permissions.check(email)", 223 | permissionsTests.create(dao.web, "check", "andrew@andrewpetersen.onmicrosoft.com") 224 | ); 225 | }); 226 | }; 227 | 228 | function isBrowser() { 229 | return !(typeof window === "undefined"); 230 | } 231 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spscript", 3 | "version": "5.1.1", 4 | "description": "ShareP0oint Rest Api Wrappers", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "prebuild": "rimraf dist", 9 | "build": "tsc && rollup -c rollup.config.js", 10 | "test": "node tasks/setAuthCookie.js && jest --verbose --coverage --maxWorkers=4", 11 | "test:watch": "node tasks/setAuthCookie.js && majestic --app", 12 | "prepare": "npm run build", 13 | "docs": "docsify serve docs" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/DroopyTersen/spscript" 18 | }, 19 | "keywords": [ 20 | "sharepoint", 21 | "spfx", 22 | "o365", 23 | "microsoft", 24 | "office" 25 | ], 26 | "author": "Andrew Petersen", 27 | "bugs": { 28 | "url": "https://github.com/DroopyTersen/spscript/issues" 29 | }, 30 | "homepage": "https://github.com/DroopyTersen/spscript", 31 | "devDependencies": { 32 | "@types/jest": "^24.0.11", 33 | "chai": "^1.10.0", 34 | "concurrently": "^3.4.0", 35 | "docsify-cli": "^4.4.0", 36 | "dotenv": "^6.2.0", 37 | "isomorphic-fetch": "^2.2.1", 38 | "jest": "^25.3.0", 39 | "majestic": "^1.6.2", 40 | "node-sp-auth": "^2.5.7", 41 | "rimraf": "^2.6.3", 42 | "rollup": "^2.6.1", 43 | "rollup-plugin-terser": "^5.3.0", 44 | "rollup-plugin-typescript2": "^0.27.0", 45 | "rollup-plugin-visualizer": "^4.0.4", 46 | "ts-jest": "^25.3.1", 47 | "typescript": "^3.8.3" 48 | }, 49 | "dependencies": {} 50 | } 51 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "rollup-plugin-typescript2"; 2 | import { terser } from "rollup-plugin-terser"; 3 | import visualizer from "rollup-plugin-visualizer"; 4 | export default { 5 | input: "src/index.ts", // our source file 6 | output: [ 7 | { 8 | file: "dist/spscript.browser.js", 9 | format: "iife", 10 | name: "SPScript", // the global which can be used in a browser 11 | }, 12 | ], 13 | plugins: [ 14 | typescript({ 15 | typescript: require("typescript"), 16 | tsconfig: "tsconfig.json", 17 | tsconfigOverride: { 18 | compilerOptions: { 19 | declaration: false, 20 | }, 21 | }, 22 | }), 23 | terser(), 24 | visualizer({ title: "SPScript Bundle", open: false }), 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /src/Auth.ts: -------------------------------------------------------------------------------- 1 | import Context from "./Context"; 2 | import { parseOData } from "./utils"; 3 | 4 | export default class Auth { 5 | private ctx: Context; 6 | constructor(ctx: Context) { 7 | this.ctx = ctx; 8 | } 9 | 10 | ensureRequestDigest(digest?: string): Promise { 11 | return digest ? Promise.resolve(digest) : this.getRequestDigest(); 12 | } 13 | 14 | /** Get a Request Digest token to authorize a request */ 15 | getRequestDigest(): Promise { 16 | return this.ctx._post("/contextInfo", {}).then((data) => data.FormDigestValue); 17 | } 18 | 19 | getGraphToken(): Promise { 20 | let endpoint = "/SP.OAuth.Token/Acquire"; 21 | return this.ctx._post(endpoint, { resource: "https://graph.microsoft.com" }).then(parseOData); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Context.ts: -------------------------------------------------------------------------------- 1 | import Web from "./Web"; 2 | import CustomActions from "./CustomActions"; 3 | import Auth from "./Auth"; 4 | import request from "./request"; 5 | import { parseJSON, getActionHeaders } from "./utils"; 6 | import List from "./List"; 7 | import Search from "./Search"; 8 | import Profiles from "./Profiles"; 9 | import MMS from "./MMS"; 10 | 11 | export interface ContextOptions { 12 | token?: string; 13 | headers?: { [any: string]: string }; 14 | } 15 | 16 | export default class Context { 17 | /** The url of the SPScript data context */ 18 | webUrl: string; 19 | /** Methods to hit the SP Search Service */ 20 | search: Search; 21 | /** Methods against the SP Web object */ 22 | web: Web; 23 | /** Methods to get the SP Profile Service */ 24 | profiles: Profiles; 25 | /** Work with Site/Web scoped Custom Actions */ 26 | customActions: CustomActions; 27 | /** Request Digest and Access token helpers */ 28 | auth: Auth; 29 | /** MMS helper function for getting a termset */ 30 | mms: MMS; 31 | 32 | private request: (url: string, options: RequestInit) => Promise; 33 | private ensureToken: Promise; 34 | private accessToken: string; 35 | public headers: any; 36 | 37 | constructor(url: string, options: ContextOptions = {}) { 38 | this.webUrl = url; 39 | this.accessToken = options.token; 40 | this.headers = options.headers; 41 | 42 | this.search = new Search(this); 43 | this.customActions = new CustomActions(this); 44 | this.web = new Web(this); 45 | this.profiles = new Profiles(this); 46 | this.auth = new Auth(this); 47 | this.mms = new MMS(this); 48 | } 49 | 50 | private async executeRequest(url: string, opts: RequestInit = {}): Promise { 51 | await this.ensureToken; 52 | var fullUrl = /^http/i.test(url) ? url : this.webUrl + "/_api" + url; 53 | var defaultOptions: RequestInit = { 54 | method: "GET", 55 | headers: { 56 | Accept: "application/json", 57 | "Content-Type": "application/json", 58 | ...this.headers, 59 | }, 60 | }; 61 | var requestOptions = { 62 | ...defaultOptions, 63 | ...opts, 64 | headers: { ...defaultOptions.headers, ...opts.headers }, 65 | }; 66 | if (this.accessToken) { 67 | requestOptions.headers["Authorization"] = "Bearer " + this.accessToken; 68 | } 69 | return request(fullUrl, requestOptions); 70 | } 71 | 72 | /** Make a 'GET' request to the '/_api' relative url. */ 73 | get(url: string, opts?: RequestInit) { 74 | let options: RequestInit = Object.assign({}, { method: "GET" }, opts); 75 | return this.executeRequest(url, options).then(parseJSON); 76 | } 77 | 78 | /** Make a 'POST' request to the '/_api' relative url. */ 79 | _post(url: string, body?: any, opts?: RequestInit) { 80 | body = this._packagePostBody(body, opts); 81 | var options: RequestInit = { 82 | method: "POST", 83 | body, 84 | }; 85 | options = Object.assign({}, options, opts); 86 | // console.log("_post -> options", options); 87 | return this.executeRequest(url, options).then(parseJSON); 88 | } 89 | 90 | /** Make a 'POST' request to the '/_api' relative url. SPScript will handle authorizing the request for you.*/ 91 | post(url: string, body?: any, verb?: string) { 92 | return this.auth 93 | .getRequestDigest() 94 | .then((digest) => getActionHeaders(verb, digest)) 95 | .then((headers) => this._post(url, body, { headers })); 96 | } 97 | 98 | /** Get an SPScript List instance */ 99 | lists(name: string): List { 100 | return new List(name, this); 101 | } 102 | 103 | private _packagePostBody(body: any, opts: RequestInit) { 104 | // if its already a string just return 105 | if (typeof body === "string") return body; 106 | // if it has an explicit content-type, asssume the body is already that type 107 | if ( 108 | opts && 109 | opts.headers && 110 | opts.headers["Content-Type"] && 111 | opts.headers["Content-Type"].indexOf("json") === -1 112 | ) { 113 | return body; 114 | } 115 | //others stringify 116 | return JSON.stringify(body); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/CustomActions.ts: -------------------------------------------------------------------------------- 1 | import Context from "./Context"; 2 | import { parseOData, getUpdateHeaders, getDeleteHeaders, getAddHeaders } from "./utils"; 3 | 4 | export default class CustomActions { 5 | private ctx: Context; 6 | 7 | constructor(ctx: Context) { 8 | this.ctx = ctx; 9 | } 10 | 11 | /** Returns both Site and Web custom actions. */ 12 | get(): Promise; 13 | /** Searches both Site and Web scoped custom actions for a name match */ 14 | get(name: string): Promise; 15 | async get(name?: any): Promise { 16 | let webCustomActions = await this.ctx.get("/web/usercustomactions").then(parseOData); 17 | let siteCustomActions = await this.ctx.get("/site/usercustomactions").then(parseOData); 18 | let allCustomActions = [...webCustomActions, ...siteCustomActions]; 19 | if (name) { 20 | return allCustomActions.find((c) => c.Name === name); 21 | } 22 | return allCustomActions; 23 | } 24 | 25 | private _getUrl = async (name) => { 26 | let target = await this.get(name); 27 | if (!target || !target["odata.editLink"]) { 28 | throw new Error("Unable to find matching Custom Action: " + name); 29 | } 30 | return "/" + target["odata.editLink"]; 31 | }; 32 | /** Update an existing Custom Action. You must pass a custom action with a 'Name' property */ 33 | async update(updates: CustomAction): Promise { 34 | if (!updates || !updates.Name) throw new Error("You must at least pass a Custom Action 'Name'"); 35 | 36 | let url = await this._getUrl(updates.Name); 37 | return this.ctx.post(url, updates, "MERGE"); 38 | } 39 | 40 | /** Remove an existing Custom Action. Searches both Site and Web scoped */ 41 | async remove(name: string): Promise { 42 | if (!name) throw new Error("You must at least pass an existing Custom Action name"); 43 | let url = await this._getUrl(name); 44 | return this.ctx.post(url, {}, "DELETE"); 45 | } 46 | 47 | /** Adds a new custom action. If the custom action name already exists, it will be deleted first */ 48 | async add(customAction: CustomAction): Promise { 49 | if (!customAction || !customAction.Name) 50 | throw new Error("You must at least pass a Custom Action 'Name'"); 51 | 52 | var defaults: Partial = { 53 | Name: customAction.Name, 54 | Title: customAction.Name, 55 | Description: customAction.Name, 56 | // Group: customAction.Name, 57 | Sequence: 100, 58 | Scope: 2, 59 | }; 60 | customAction = { ...defaults, ...customAction }; 61 | 62 | // if it exists already, delete it 63 | let exists = await this.get(customAction.Name); 64 | if (exists) { 65 | await this.remove(customAction.Name); 66 | } 67 | 68 | let url = (customAction.Scope === 2 ? "/site" : "/web") + "/usercustomactions"; 69 | 70 | return this.ctx.post(url, customAction); 71 | } 72 | 73 | activateExtension = ( 74 | title: string, 75 | componentId: string, 76 | properties = {}, 77 | overrides: Partial = {} 78 | ) => { 79 | let customAction: CustomAction = { 80 | Name: title, 81 | ClientSideComponentId: componentId, 82 | Location: "ClientSideExtension.ApplicationCustomizer", 83 | ClientSideComponentProperties: JSON.stringify(properties), 84 | ...overrides, 85 | }; 86 | return this.add(customAction); 87 | }; 88 | } 89 | 90 | export type CustomActionScope = "Web" | "Site"; 91 | 92 | export interface CustomAction { 93 | Name: string; 94 | Location: string; 95 | /** Defaults to match Name */ 96 | Title?: string; 97 | /** Defaults to match Name */ 98 | Description?: string; 99 | /** Defaults to match Name */ 100 | Group?: string; 101 | /** Defaults to to 100 */ 102 | Sequence?: number; 103 | 104 | /** 3 for Web. 2 for Site */ 105 | Scope?: 3 | 2; 106 | ScriptBlock?: string; 107 | /** To activate an SPFx Extension, the Component Id*/ 108 | ClientSideComponentId?: string; 109 | /** Properties for configuring SPFx Extensions */ 110 | ClientSideComponentProperties?: string; 111 | /** The Custom Action's primary key, guid. */ 112 | Id?: string; 113 | HostProperties?: ""; 114 | } 115 | -------------------------------------------------------------------------------- /src/List.ts: -------------------------------------------------------------------------------- 1 | import Context from "./Context"; 2 | import Securable from "./Securable"; 3 | import { parseOData } from "./utils"; 4 | 5 | export default class List { 6 | /** The title of the list */ 7 | listName: string; 8 | private baseUrl: string; 9 | private _dao: Context; 10 | permissions: Securable; 11 | constructor(name: string, ctx: Context) { 12 | this.listName = name; 13 | this.baseUrl = `/web/lists/getbytitle('${this.listName}')`; 14 | this._dao = ctx; 15 | this.permissions = new Securable(this.baseUrl, ctx); 16 | } 17 | /** Get items from the list. Will return all items if no OData is passed. */ 18 | getItems(odata = "$top=5000"): Promise { 19 | return this._dao.get(this.baseUrl + "/items" + appendOData(odata)).then(parseOData); 20 | } 21 | 22 | /** Get a specific item by SharePoint ID */ 23 | getItemById(id: number, odata?: string) { 24 | var url = this.baseUrl + "/items(" + id + ")" + appendOData(odata); 25 | return this._dao.get(url).then(parseOData); 26 | } 27 | 28 | /** Gets the items returned by the specified CAML query. CAML should be something like ...*/ 29 | getItemsByCaml(caml: string, odata = "$top=4999") { 30 | var queryUrl = this.baseUrl + "/GetItems?" + odata; 31 | var postBody = { 32 | query: { 33 | ViewXml: caml, 34 | }, 35 | }; 36 | return this._dao.post(queryUrl, postBody).then(parseOData); 37 | } 38 | 39 | /** Gets the items returned by the specified View name */ 40 | async getItemsByView(viewName: string) { 41 | var viewUrl = this.baseUrl + "/Views/getByTitle('" + viewName + "')/ViewQuery"; 42 | let view = await this._dao.get(viewUrl).then(parseOData); 43 | let caml = `${view}`; 44 | return this.getItemsByCaml(caml); 45 | } 46 | 47 | /** Gets you all items whose field(key) matches the value. Currently only text and number columns are supported. */ 48 | findItems(key: string, value: any, odata = "$top=5000") { 49 | var filterValue = typeof value === "string" ? "'" + value + "'" : value; 50 | odata = "$filter=" + key + " eq " + filterValue + appendOData(odata, "&"); 51 | return this.getItems(odata); 52 | } 53 | 54 | /** Get the item whose field(key) matches the value. If multiple matches are found, the first is returned. Currently only text and number columns are supported. */ 55 | findItem(key: string, value: any, odata: string = "") { 56 | // Add a top=1 if there wasn't a specified top 57 | if (odata.indexOf("$top") === -1) { 58 | odata += odata ? "&$top=1" : "$top=1"; 59 | } 60 | return this.findItems(key, value, odata).then((items) => { 61 | if (items && items.length && items.length > 0) return items[0]; 62 | return null; 63 | }); 64 | } 65 | 66 | /** Get all the properties of the List */ 67 | getInfo(): Promise { 68 | return this._dao.get(this.baseUrl).then(parseOData); 69 | } 70 | 71 | /** Check whether the list exists */ 72 | async checkExists(): Promise { 73 | try { 74 | await this.getInfo(); 75 | return true; 76 | } catch (err) { 77 | return false; 78 | } 79 | } 80 | 81 | /** Insert a List Item */ 82 | addItem(item: any, digest?: string): Promise { 83 | return this._dao.post(this.baseUrl + "/items", item).then(parseOData); 84 | } 85 | 86 | /** Takes a SharePoint Id, and updates that item ONLY with properties that are found in the passed in updates object. */ 87 | async updateItem(itemId: number, updates: any, digest?: string) { 88 | let url = this.baseUrl + `/items(${itemId})`; 89 | return this._dao.post(url, updates, "MERGE"); 90 | } 91 | 92 | /** deletes the item with the specified SharePoint Id */ 93 | async deleteItem(itemId: number, digest?: string) { 94 | let url = this.baseUrl + `/items(${itemId})`; 95 | 96 | // digest = await this._dao.auth.ensureRequestDigest(digest); 97 | 98 | // let options = { 99 | // headers: utils.headers.getDeleteHeaders(digest, "*"), 100 | // }; 101 | return this._dao.post(url, "", "DELETE"); 102 | } 103 | 104 | //TODO: getFields 105 | //TODO: getField 106 | //TODO: updateField 107 | } 108 | 109 | var appendOData = function (odata = "", prefix?: string) { 110 | prefix = prefix || "?"; 111 | if (odata) return prefix + odata; 112 | return ""; 113 | }; 114 | -------------------------------------------------------------------------------- /src/MMS.ts: -------------------------------------------------------------------------------- 1 | import Context from "./Context"; 2 | 3 | export default class MMS { 4 | private ctx: Context; 5 | constructor(ctx: Context) { 6 | this.ctx = ctx; 7 | } 8 | 9 | getTermset = (termGroup: string, termset: string) => { 10 | return getTermSet(termGroup, termset, this.ctx); 11 | }; 12 | getTermTree = async (termGroup: string, termset: string) => { 13 | let flatTerms = await getTermSet(termGroup, termset, this.ctx); 14 | return toTermTree(flatTerms); 15 | }; 16 | } 17 | 18 | export interface MMSTerm { 19 | id: string; 20 | sortOrder: number; 21 | description: string; 22 | name: string; 23 | path: string; 24 | termSetName: string; 25 | properties: { 26 | [key: string]: string; 27 | }; 28 | children: MMSTerm[]; 29 | } 30 | 31 | export interface MMSTermTree extends MMSTerm { 32 | flatTerms: MMSTerm[]; 33 | getTermByName(termName: string): MMSTerm; 34 | getTermById(termGuid: string): MMSTerm; 35 | getTermByPath(path: string): MMSTerm; 36 | } 37 | 38 | export const toTermTree = function (flatTerms: MMSTerm[]) { 39 | try { 40 | let tree = _toTermTree(flatTerms); 41 | 42 | let termTree: MMSTermTree = { 43 | flatTerms, 44 | ...tree, 45 | getTermById(termGuid: string) { 46 | return findInTree(tree, (term) => term.id === termGuid); 47 | }, 48 | getTermByName(termName: string) { 49 | // console.log("getTermByName -> tree", tree); 50 | return findInTree(tree, (term) => term.name === termName); 51 | }, 52 | getTermByPath(path: string) { 53 | let targetPath = normalizeSlashes(path.toLowerCase()); 54 | // console.log("GETBYPATH", path, this.children); 55 | return findInTree(tree, (term) => term.path.toLowerCase() === targetPath); 56 | }, 57 | }; 58 | return termTree; 59 | } catch (err) { 60 | console.log("ERROR!!!! parsing term groupd", err); 61 | return null; 62 | } 63 | }; 64 | 65 | export const getTermSet = async ( 66 | termGroupName: string, 67 | termSetName: string, 68 | ctx: Context 69 | ): Promise => { 70 | // Create a flat array of parent termsets, and then each result of terms 71 | let flatTerms = await _getTermsetTerms(termGroupName, termSetName, ctx); 72 | // console.log("TCL: flatTerms", flatTerms); 73 | 74 | return sortByPath(flatTerms); 75 | }; 76 | 77 | const _toTermTree = function (flatTerms: MMSTerm[]): MMSTerm { 78 | let sortedTerms = sortByPath(flatTerms); 79 | // console.log("TCL: groupByPath -> sortedTerms", sortedTerms); 80 | 81 | // Requires a presort by path 82 | // Assumes the path parts match the 'name' property 83 | let result = sortedTerms.reduce((results, term) => { 84 | let pathParts = term.path.split("/"); 85 | let scope = results; 86 | // console.log("_toTermTree -> pathParts", pathParts); 87 | pathParts.forEach((key) => { 88 | let match = scope.find((t) => t.name === key); 89 | if (!match) { 90 | scope.push(term); 91 | scope = term.children; 92 | } else { 93 | // console.log("_toTermTree -> match", key, match); 94 | scope = match.children; 95 | } 96 | }); 97 | return results; 98 | }, []); 99 | // console.log("TCL: result", result); 100 | return result.length === 1 ? result[0] : result; 101 | }; 102 | 103 | const sortByPath = (terms: MMSTerm[]) => { 104 | return terms.sort((a, b) => { 105 | if (a.path === b.path) return 0; 106 | return a.path < b.path ? -1 : 1; 107 | }); 108 | }; 109 | const findInTree = function (term: MMSTerm, findFn: (term: MMSTerm) => boolean) { 110 | if (findFn(term)) return term; 111 | // console.log("term.name", term); 112 | // console.log("term.children.length", term.children.length); 113 | for (let i = 0; i < term.children.length; i++) { 114 | let childMatch = findInTree(term.children[i], findFn); 115 | if (childMatch) return childMatch; 116 | } 117 | return null; 118 | }; 119 | 120 | const processTerm = function (term: TermData, termSetName: string): MMSTerm { 121 | return { 122 | id: cleanGuid(term.Id), 123 | sortOrder: term.CustomSortOrder || 9999, 124 | children: [], 125 | description: term.Description, 126 | name: term.Name.replace(/\//g, "|"), 127 | path: (termSetName + ";" + term.PathOfTerm).replace(/\//g, "|").split(";").join("/"), 128 | properties: { 129 | ...term.CustomProperties, 130 | ...term.LocalCustomProperties, 131 | }, 132 | termSetName, 133 | }; 134 | }; 135 | 136 | function cleanGuid(guid: string): string { 137 | return guid ? guid.replace("/Guid(", "").replace("/", "").replace(")", "") : ""; 138 | } 139 | 140 | const normalizeSlashes = function (str: string) { 141 | try { 142 | if (!str) return ""; 143 | if (str[0] === "/") { 144 | str = str.substring(1); 145 | } 146 | if (str[str.length - 1] === "/") { 147 | str = str.substring(0, str.length - 1); 148 | } 149 | 150 | return str; 151 | } catch (err) { 152 | return ""; 153 | } 154 | }; 155 | 156 | const _getTermsetTerms = async ( 157 | termGroup: string, 158 | termset: string, 159 | ctx: Context 160 | ): Promise => { 161 | let digest = await ctx.auth.getRequestDigest(); 162 | var url = `${ctx.webUrl}/_vti_bin/client.svc/ProcessQuery?`; 163 | let headers = { 164 | ...ctx.headers, 165 | "content-type": "text/xml", 166 | "x-requestdigest": digest, 167 | }; 168 | let data = await fetch(url, { 169 | method: "POST", 170 | body: getRequestXml(termGroup, termset), 171 | headers, 172 | }).then((resp) => resp.json()); 173 | // console.log("_getTermsetTerms data", data); 174 | 175 | let tc: TermCollectionData = data.find((d) => d._ObjectType_ === "SP.Taxonomy.TermCollection"); 176 | if (tc && tc._Child_Items_) { 177 | return [ 178 | createTermFromTermsetName(termset), 179 | ...tc._Child_Items_.map((t) => processTerm(t, termset)), 180 | ]; 181 | } 182 | return []; 183 | }; 184 | 185 | const createTermFromTermsetName = (termset: string): MMSTerm => { 186 | return { 187 | id: "root", 188 | sortOrder: 1, 189 | children: [], 190 | description: termset, 191 | name: termset, 192 | path: termset, 193 | properties: {}, 194 | termSetName: termset, 195 | }; 196 | }; 197 | interface TermCollectionData { 198 | _Child_Items_: TermData[]; 199 | } 200 | interface TermData { 201 | Id: string; 202 | CustomSortOrder: number; 203 | Description: string; 204 | Name: string; 205 | PathOfTerm: string; 206 | LocalCustomProperties: any; 207 | CustomProperties: any; 208 | } 209 | 210 | const getRequestXml = (termGroup: string, termset: string) => { 211 | return ` 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | ${termGroup} 238 | 239 | 240 | 241 | 242 | 243 | ${termset} 244 | 245 | 246 | 247 | 248 | `; 249 | }; 250 | -------------------------------------------------------------------------------- /src/Profiles.ts: -------------------------------------------------------------------------------- 1 | import Context from "./Context"; 2 | import { parseOData } from "./utils"; 3 | 4 | export default class Profiles { 5 | private _dao: Context; 6 | private baseUrl: string; 7 | constructor(ctx: Context) { 8 | this._dao = ctx; 9 | this.baseUrl = "/SP.UserProfiles.PeopleManager"; 10 | } 11 | 12 | /** Gets the profile of the current user. */ 13 | current(): Promise { 14 | var url = this.baseUrl + "/GetMyProperties"; 15 | return this._dao.get(url).then(parseOData).then(transformPersonProperties); 16 | } 17 | 18 | /** Gets the current user's profile */ 19 | get(): Promise; 20 | /** Gets the profile of the passed in email name. */ 21 | get(email: string): Promise; 22 | /** Gets the profile of the passed in user object (AccountName or LoginName) must be set */ 23 | get(user: any): Promise; 24 | get(user?: any): Promise { 25 | if (!user) return this.current(); 26 | return this.getUserObj(user).then((user) => { 27 | var login = encodeURIComponent(user.LoginName || user.AccountName); 28 | var url = this.baseUrl + "/GetPropertiesFor(accountName=@v)?@v='" + login + "'"; 29 | return this._dao.get(url).then(parseOData).then(transformPersonProperties); 30 | }); 31 | } 32 | 33 | private getUserObj(user?: any): Promise { 34 | if (!user || typeof user === "string") { 35 | return this._dao.web.getUser(user); 36 | } else if (user.AccountName || user.LoginName) { 37 | return Promise.resolve(user); 38 | } else throw new Error("profiles.setProperty Error: Invalid user parameter"); 39 | } 40 | 41 | /** Sets a profile property on the current user */ 42 | setProperty(key: string, value: any): Promise; 43 | /** Sets a profile property on the specified email */ 44 | setProperty(key: string, value: any, email: string): Promise; 45 | /** Sets a profile property on the specified User object (needs AccountName or LoginName property) */ 46 | setProperty(key: string, value: any, userObj: any): Promise; 47 | setProperty(key: string, value: any, user?: any): Promise { 48 | return this.getUserObj(user).then((user) => { 49 | var args = { 50 | propertyName: key, 51 | propertyValue: value, 52 | accountName: user.LoginName || user.AccountName, 53 | }; 54 | var url = this.baseUrl + "/SetSingleValueProfileProperty"; 55 | return this._dao.post(url, args); 56 | }); 57 | } 58 | } 59 | 60 | var transformPersonProperties = function (profile): any[] { 61 | profile.UserProfileProperties.forEach((keyvalue) => { 62 | profile[keyvalue.Key] = keyvalue.Value; 63 | }); 64 | return profile; 65 | }; 66 | -------------------------------------------------------------------------------- /src/Search.ts: -------------------------------------------------------------------------------- 1 | import Context from "./Context"; 2 | import { qs, parseOData } from "./utils"; 3 | 4 | export interface QueryOptions { 5 | sourceid?: string; 6 | startrow?: number; 7 | rowlimit?: number; 8 | selectproperties?: string; 9 | refiners?: string; 10 | refinementfilters?: string; 11 | hiddencontstraints?: any; 12 | sortlist?: any; 13 | } 14 | 15 | export interface Refiner { 16 | RefinerName: string; 17 | RefinerOptions: any[]; 18 | } 19 | 20 | export interface SearchResultResponse { 21 | elapsedTime: string; 22 | suggestion: any; 23 | resultsCount: number; 24 | totalResults: number; 25 | totalResultsIncludingDuplicates: number; 26 | /** The actual search results that you care about */ 27 | items: any[]; 28 | refiners?: Refiner[]; 29 | } 30 | 31 | export default class Search { 32 | private _dao: Context; 33 | 34 | constructor(ctx: Context) { 35 | this._dao = ctx; 36 | } 37 | 38 | /** get default/empty QueryOptions */ 39 | get defaultQueryOptions(): QueryOptions { 40 | return { 41 | sourceid: null, 42 | startrow: null, 43 | rowlimit: 100, 44 | selectproperties: null, 45 | refiners: null, 46 | refinementfilters: null, 47 | hiddencontstraints: null, 48 | sortlist: null, 49 | }; 50 | } 51 | 52 | /** Query the SP Search Service */ 53 | query(queryTemplate: string, queryOptions: QueryOptions = {}): Promise { 54 | var optionsQueryString = qs.fromObj(queryOptions, true); 55 | console.log("Search -> optionsQueryString", optionsQueryString); 56 | var url = `/search/query?queryTemplate='${queryTemplate}'&${optionsQueryString}`; 57 | return this._dao 58 | .get(url) 59 | .then(parseOData) 60 | .then((resp) => { 61 | return mapResponse(resp); 62 | }); 63 | } 64 | 65 | /** Query for only People results */ 66 | people(queryText: string, queryOptions: QueryOptions = {}): Promise { 67 | queryOptions.sourceid = "b09a7990-05ea-4af9-81ef-edfab16c4e31"; 68 | return this.query(queryText, queryOptions); 69 | } 70 | 71 | /** Query for only sites (STS_Web). Optionally pass in a url scope. */ 72 | sites( 73 | queryText: string = "", 74 | urlScope: string = "", 75 | queryOptions: QueryOptions = {} 76 | ): Promise { 77 | urlScope = urlScope ? `Path:${urlScope}*` : ""; 78 | var query = `${queryText} contentclass:STS_Web ${urlScope}`.trim(); 79 | queryOptions.rowlimit = queryOptions.rowlimit || 499; 80 | return this.query(query, queryOptions); 81 | } 82 | } 83 | 84 | const mapResponse = function (rawResponse: any): SearchResultResponse { 85 | return { 86 | elapsedTime: rawResponse.ElapsedTime, 87 | suggestion: rawResponse.SpellingSuggestion, 88 | resultsCount: rawResponse.PrimaryQueryResult.RelevantResults.RowCount, 89 | totalResults: rawResponse.PrimaryQueryResult.RelevantResults.TotalRows, 90 | totalResultsIncludingDuplicates: 91 | rawResponse.PrimaryQueryResult.RelevantResults.TotalRowsIncludingDuplicates, 92 | items: mapItems(rawResponse.PrimaryQueryResult.RelevantResults.Table.Rows), 93 | refiners: mapRefiners(rawResponse.PrimaryQueryResult.RefinementResults), 94 | }; 95 | }; 96 | 97 | const mapRefiners = function (refinementResults) { 98 | var refiners = []; 99 | 100 | if (refinementResults && refinementResults.Refiners && refinementResults.Refiners) { 101 | refiners = refinementResults.Refiners.map((r) => { 102 | return { 103 | RefinerName: r.Name, 104 | RefinerOptions: r.Entries, 105 | }; 106 | }); 107 | } 108 | return refiners; 109 | }; 110 | 111 | const mapItems = function (itemRows: any[]): any[] { 112 | var items = []; 113 | 114 | for (var i = 0; i < itemRows.length; i++) { 115 | var row = itemRows[i], 116 | item = {}; 117 | for (var j = 0; j < row.Cells.length; j++) { 118 | item[row.Cells[j].Key] = row.Cells[j].Value; 119 | } 120 | 121 | items.push(item); 122 | } 123 | 124 | return items; 125 | }; 126 | -------------------------------------------------------------------------------- /src/Securable.ts: -------------------------------------------------------------------------------- 1 | import Context from "./Context"; 2 | import { parseOData, isBrowser } from "./utils"; 3 | 4 | declare var _spPageContextInfo; 5 | 6 | /** Allows you to check the permissions of a securable (list or site) */ 7 | export default class Securable { 8 | private _dao: Context; 9 | private baseUrl: string; 10 | 11 | constructor(baseUrl: string, ctx: Context) { 12 | this.baseUrl = baseUrl; 13 | this._dao = ctx; 14 | } 15 | 16 | /** Gets all the role assignments on that securable */ 17 | getRoleAssignments(): Promise { 18 | var url = this.baseUrl + "/RoleAssignments?$expand=Member,RoleDefinitionBindings"; 19 | 20 | return this._dao 21 | .get(url) 22 | .then(parseOData) 23 | .then((results) => results.map(transformRoleAssignment)); 24 | } 25 | 26 | private checkPrivs(user): Promise { 27 | var url = 28 | this.baseUrl + `/getusereffectivepermissions('${encodeURIComponent(user.LoginName)}')`; 29 | return this._dao.get(url).then(parseOData); 30 | } 31 | /** Gets all the role assignments on that securable. If you don't pass an email, it will use the current user. */ 32 | async check(email?: string): Promise { 33 | let user = await this._dao.web.getUser(email); 34 | return this.checkPrivs(user).then((privs) => permissionMaskToStrings(privs.Low, privs.High)); 35 | } 36 | } 37 | 38 | var transformRoleAssignment = function (raw: any): RoleAssignment { 39 | var member: RoleMember = { 40 | login: raw.Member.LoginName, 41 | name: raw.Member.Title, 42 | id: raw.Member.Id, 43 | principalType: raw.Member.PrincipalType, 44 | }; 45 | var roles: RoleDef[] = raw.RoleDefinitionBindings.map((roleDef) => { 46 | return { 47 | name: roleDef.Name, 48 | description: roleDef.Description, 49 | basePermissions: permissionMaskToStrings( 50 | roleDef.BasePermissions.Low, 51 | roleDef.BasePermissions.High 52 | ), 53 | }; 54 | }); 55 | return { member, roles }; 56 | }; 57 | 58 | var permissionMaskToStrings = function (lowMask, highMask): string[] { 59 | var permissions = []; 60 | basePermissions.forEach(function (basePermission) { 61 | if ((basePermission.low & lowMask) > 0 || (basePermission.high & highMask) > 0) { 62 | permissions.push(basePermission.name); 63 | } 64 | }); 65 | return permissions; 66 | }; 67 | 68 | export interface BasePermission { 69 | name: string; 70 | low: number; 71 | high: number; 72 | } 73 | 74 | export interface RoleMember { 75 | login: string; 76 | name: string; 77 | id: string; 78 | principalType: number; 79 | } 80 | 81 | export interface RoleDef { 82 | /** Role definition name */ 83 | name: string; 84 | description: string; 85 | /** An array of base permission names */ 86 | basePermissions: string[]; 87 | } 88 | 89 | export interface RoleAssignment { 90 | /** User or Group */ 91 | member: RoleMember; 92 | /** An array of role definitions */ 93 | roles: RoleDef[]; 94 | } 95 | 96 | export var basePermissions: BasePermission[] = [ 97 | { 98 | name: "emptyMask", 99 | low: 0, 100 | high: 0, 101 | }, 102 | { 103 | name: "viewListItems", 104 | low: 1, 105 | high: 0, 106 | }, 107 | { 108 | name: "addListItems", 109 | low: 2, 110 | high: 0, 111 | }, 112 | { 113 | name: "editListItems", 114 | low: 4, 115 | high: 0, 116 | }, 117 | { 118 | name: "deleteListItems", 119 | low: 8, 120 | high: 0, 121 | }, 122 | { 123 | name: "approveItems", 124 | low: 16, 125 | high: 0, 126 | }, 127 | { 128 | name: "openItems", 129 | low: 32, 130 | high: 0, 131 | }, 132 | { 133 | name: "viewVersions", 134 | low: 64, 135 | high: 0, 136 | }, 137 | { 138 | name: "deleteVersions", 139 | low: 128, 140 | high: 0, 141 | }, 142 | { 143 | name: "cancelCheckout", 144 | low: 256, 145 | high: 0, 146 | }, 147 | { 148 | name: "managePersonalViews", 149 | low: 512, 150 | high: 0, 151 | }, 152 | { 153 | name: "manageLists", 154 | low: 2048, 155 | high: 0, 156 | }, 157 | { 158 | name: "viewFormPages", 159 | low: 4096, 160 | high: 0, 161 | }, 162 | { 163 | name: "anonymousSearchAccessList", 164 | low: 8192, 165 | high: 0, 166 | }, 167 | { 168 | name: "open", 169 | low: 65536, 170 | high: 0, 171 | }, 172 | { 173 | name: "viewPages", 174 | low: 131072, 175 | high: 0, 176 | }, 177 | { 178 | name: "addAndCustomizePages", 179 | low: 262144, 180 | high: 0, 181 | }, 182 | { 183 | name: "applyThemeAndBorder", 184 | low: 524288, 185 | high: 0, 186 | }, 187 | { 188 | name: "applyStyleSheets", 189 | low: 1048576, 190 | high: 0, 191 | }, 192 | { 193 | name: "viewUsageData", 194 | low: 2097152, 195 | high: 0, 196 | }, 197 | { 198 | name: "createSSCSite", 199 | low: 4194304, 200 | high: 0, 201 | }, 202 | { 203 | name: "manageSubwebs", 204 | low: 8388608, 205 | high: 0, 206 | }, 207 | { 208 | name: "createGroups", 209 | low: 16777216, 210 | high: 0, 211 | }, 212 | { 213 | name: "managePermissions", 214 | low: 33554432, 215 | high: 0, 216 | }, 217 | { 218 | name: "browseDirectories", 219 | low: 67108864, 220 | high: 0, 221 | }, 222 | { 223 | name: "browseUserInfo", 224 | low: 134217728, 225 | high: 0, 226 | }, 227 | { 228 | name: "addDelPrivateWebParts", 229 | low: 268435456, 230 | high: 0, 231 | }, 232 | { 233 | name: "updatePersonalWebParts", 234 | low: 536870912, 235 | high: 0, 236 | }, 237 | { 238 | name: "manageWeb", 239 | low: 1073741824, 240 | high: 0, 241 | }, 242 | { 243 | name: "anonymousSearchAccessWebLists", 244 | low: -2147483648, 245 | high: 0, 246 | }, 247 | { 248 | name: "useClientIntegration", 249 | low: 0, 250 | high: 16, 251 | }, 252 | { 253 | name: "useRemoteAPIs", 254 | low: 0, 255 | high: 32, 256 | }, 257 | { 258 | name: "manageAlerts", 259 | low: 0, 260 | high: 64, 261 | }, 262 | { 263 | name: "createAlerts", 264 | low: 0, 265 | high: 128, 266 | }, 267 | { 268 | name: "editMyUserInfo", 269 | low: 0, 270 | high: 256, 271 | }, 272 | { 273 | name: "enumeratePermissions", 274 | low: 0, 275 | high: 1073741824, 276 | }, 277 | ]; 278 | -------------------------------------------------------------------------------- /src/Web.ts: -------------------------------------------------------------------------------- 1 | import Context from "./Context"; 2 | import Securable from "./Securable"; 3 | import { parseOData, getAddHeaders } from "./utils"; 4 | 5 | export default class Web { 6 | private baseUrl: string; 7 | private _dao: Context; 8 | permissions: Securable; 9 | 10 | constructor(ctx: Context) { 11 | this.baseUrl = `/web`; 12 | this._dao = ctx; 13 | this.permissions = new Securable(this.baseUrl, ctx); 14 | } 15 | 16 | /** Retrieves basic information about the site */ 17 | getInfo(): Promise { 18 | return this._dao.get(this.baseUrl).then(parseOData); 19 | } 20 | 21 | /** Retrieves all of the subsites */ 22 | getSubsites(): Promise { 23 | return this._dao.get(this.baseUrl + "/webinfos").then(parseOData); 24 | } 25 | 26 | /** Retrieves the current user */ 27 | getUser(): Promise; 28 | /** Retrieves a users object based on an email address */ 29 | getUser(email: string): Promise; 30 | getUser(email?: string): Promise { 31 | var url = email 32 | ? this.baseUrl + "/SiteUsers/GetByEmail('" + email + "')" 33 | : this.baseUrl + "/CurrentUser"; 34 | return this._dao.get(url).then(parseOData); 35 | } 36 | 37 | ensureUser(login: string): Promise { 38 | return this._dao.post(`/web/ensureUser('${login}')`).then(parseOData); 39 | } 40 | 41 | /** Retrieves a file by server relative url */ 42 | getFile(url: string): Promise { 43 | var url = `/web/getfilebyserverrelativeurl('${url}')`; 44 | return this._dao.get(url).then(parseOData); 45 | } 46 | 47 | private _copyFile(sourceUrl: string, destinationUrl: string, digest: string) { 48 | var url = `/web/getfilebyserverrelativeurl('${sourceUrl}')/CopyTo`; //(strnewurl='${destinationUrl}',boverwrite=true)` 49 | var options = { 50 | headers: getAddHeaders(digest), 51 | }; 52 | var body = { 53 | strNewUrl: destinationUrl, 54 | bOverWrite: true, 55 | }; 56 | return this._dao._post(url, body, options); 57 | } 58 | // TODO: getFolder 59 | // TODO: uploadFile 60 | // TODO: fileAction 61 | // TODO: deleteFile 62 | 63 | /** Copies a file from one server relative url to another */ 64 | copyFile(sourceUrl: string, destinationUrl: string, digest?: string) { 65 | return this._dao.auth 66 | .ensureRequestDigest(digest) 67 | .then((digest) => this._copyFile(sourceUrl, destinationUrl, digest)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as allUtils from "./utils"; 2 | import { isBrowser, getSiteUrl } from "./utils"; 3 | import Context, { ContextOptions } from "./Context"; 4 | 5 | declare global { 6 | interface Window { 7 | _spPageContextInfo: any; 8 | } 9 | } 10 | 11 | export function createContext(url?: string, options?: ContextOptions): Context { 12 | try { 13 | if (isBrowser()) 14 | if (!url && window._spPageContextInfo) { 15 | // TODO: use get Site url util 16 | url = window._spPageContextInfo.webAbsoluteUrl; 17 | } 18 | if (!url) url = getSiteUrl(); 19 | if (!url) throw new Error("Unable to find url to create SPScript Context"); 20 | return new Context(url, options); 21 | } catch (ex) { 22 | throw new Error("Unable to create SPScript Context: " + ex.message); 23 | } 24 | } 25 | export const utils = allUtils; 26 | 27 | export default { 28 | createContext, 29 | utils: allUtils, 30 | }; 31 | -------------------------------------------------------------------------------- /src/request.ts: -------------------------------------------------------------------------------- 1 | import { parseJSON } from "./utils"; 2 | 3 | var defaults: RequestInit = { 4 | method: "GET", 5 | credentials: "include", 6 | redirect: "follow", 7 | }; 8 | 9 | var request: any = function (url, options: RequestInit) { 10 | var opts = Object.assign({}, defaults, options); 11 | return fetch(url, opts).then((resp) => { 12 | var succeeded = resp.ok; 13 | if (!resp.ok) { 14 | return resp.text().then((err) => { 15 | throw new Error(err); 16 | }); 17 | } 18 | return resp.text().then((text) => { 19 | return parseJSON(text) || text; 20 | }); 21 | }); 22 | }; 23 | 24 | export default request; 25 | -------------------------------------------------------------------------------- /src/utils/dependencyManagement.ts: -------------------------------------------------------------------------------- 1 | export var validateNamespace = function (namespace) { 2 | var scope: any = window; 3 | var sections = namespace.split("."); 4 | var sectionsLength = sections.length; 5 | for (var i = 0; i < sectionsLength; i++) { 6 | var prop = sections[i]; 7 | if (prop in scope) { 8 | scope = scope[prop]; 9 | } else { 10 | return false; 11 | } 12 | } 13 | return true; 14 | }; 15 | 16 | var _waitForLibraries = function (namespaces, resolve) { 17 | var missing = namespaces.filter((namespace) => !validateNamespace(namespace)); 18 | 19 | if (missing.length === 0) resolve(); 20 | else setTimeout(() => _waitForLibraries(namespaces, resolve), 25); 21 | }; 22 | 23 | export var waitForLibraries = function (namespaces) { 24 | return new Promise((resolve, reject) => _waitForLibraries(namespaces, resolve)); 25 | }; 26 | 27 | export var waitForLibrary = function (namespace) { 28 | return waitForLibraries([namespace]); 29 | }; 30 | 31 | export var waitForElement = function (selector, timeout = 5000) { 32 | var counter = 0; 33 | const INTERVAL = 25; //milliseconds 34 | const MAX_ATTEMPTS = timeout / INTERVAL; // eventually give up 35 | return new Promise((resolve, reject) => { 36 | var _waitForElement = function () { 37 | if (counter > MAX_ATTEMPTS) reject("Unable to find element: " + selector); 38 | var elem = document.querySelector(selector); 39 | if (!elem) { 40 | counter++; 41 | setTimeout(_waitForElement, INTERVAL); 42 | } else resolve(elem); 43 | }; 44 | _waitForElement(); 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /src/utils/headers.ts: -------------------------------------------------------------------------------- 1 | const jsonMimeType = "application/json"; 2 | /** returns a Headers object with 'Accept', 'Content-Type' and optional 'X-RequestDigest' */ 3 | export function getStandardHeaders(digest?: string): any { 4 | var headers = { 5 | Accept: jsonMimeType, 6 | "Content-Type": jsonMimeType, 7 | }; 8 | if (digest) headers["X-RequestDigest"] = digest; 9 | return headers; 10 | } 11 | 12 | /** returns a Headers object with values configured for binary stream*/ 13 | export const getFilestreamHeaders = function (digest: string) { 14 | return { 15 | Accept: jsonMimeType, 16 | "X-RequestDigest": digest, 17 | "Content-Type": "application/octet-stream", 18 | binaryStringRequestBody: "true", 19 | }; 20 | }; 21 | 22 | /** returns a Headers object with including the X-HTTP-Method with the specified verb */ 23 | export const getActionHeaders = function (verb: string, digest?: string) { 24 | let headers = getStandardHeaders(digest); 25 | if (verb) { 26 | headers = { 27 | ...headers, 28 | ...{ 29 | "X-HTTP-Method": verb, 30 | "If-Match": "*", 31 | }, 32 | }; 33 | } 34 | return headers; 35 | }; 36 | /** returns a Headers object with values configured ADDING an item */ 37 | export const getAddHeaders = getStandardHeaders; 38 | /** returns a Headers object with values configured UPDATING an item */ 39 | export const getUpdateHeaders = (digest?: string) => getActionHeaders("MERGE", digest); 40 | /** returns a Headers object with values configured DELETING an item */ 41 | export const getDeleteHeaders = (digest?: string) => getActionHeaders("DELETE", digest); 42 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./headers"; 2 | import * as qsUtils from "./queryString"; 3 | export * from "./loaders"; 4 | export * from "./dependencyManagement"; 5 | 6 | export const qs = qsUtils; 7 | 8 | export function isBrowser(): boolean { 9 | return !(typeof window === "undefined"); 10 | } 11 | 12 | export function getProfilePhoto(email: string) { 13 | return `${getSiteUrl()}/_layouts/15/userphoto.aspx?size=L&username=${email}`; 14 | } 15 | 16 | export function getDelveLink(email: string) { 17 | return `https://${getTenant()}-my.sharepoint.com/PersonImmersive.aspx?accountname=i%3A0%23%2Ef%7Cmembership%7C${email}`; 18 | } 19 | 20 | export function parseJSON(data: any): any { 21 | if (typeof data === "string") { 22 | try { 23 | data = JSON.parse(data); 24 | } catch (e) { 25 | return null; 26 | } 27 | } 28 | return data; 29 | } 30 | 31 | export const getArrayBuffer = function (file) { 32 | if (file && file instanceof File) { 33 | return new Promise(function (resolve, reject) { 34 | var reader = new FileReader(); 35 | reader.onload = function (e: any) { 36 | resolve(e.target.result); 37 | }; 38 | reader.readAsArrayBuffer(file); 39 | }); 40 | } else { 41 | throw "SPScript.utils.getArrayBuffer: Cant get ArrayBuffer if you don't pass in a file"; 42 | } 43 | }; 44 | 45 | export function parseOData(data: any): any { 46 | data = parseJSON(data); 47 | var results = null; 48 | if (data.d && data.d.results && data.d.results.length != null) { 49 | results = data.d.results; 50 | } else if (data.d) { 51 | results = data.d; 52 | } else if (data.value) { 53 | results = data.value; 54 | } 55 | return results || data; 56 | } 57 | 58 | export function checkIsSharePointLink(url: string) { 59 | return url && url.search(/\.sharepoint\.com/i) > -1; 60 | } 61 | 62 | export function getSiteUrl(url?: string) { 63 | if (!url && !isBrowser()) throw new Error("No url given and it is not in a browser."); 64 | url = (url || window.location.href).toLowerCase(); 65 | let managedPathIndex = url.search(/\/sites\/|\/teams\//i); 66 | if (!checkIsSharePointLink(url) || managedPathIndex < 0) return null; 67 | let siteUrl = url; 68 | let trailingCharIndexes = [ 69 | url.indexOf("/", managedPathIndex + 7), 70 | url.indexOf("?", managedPathIndex + 7), 71 | url.indexOf("#", managedPathIndex + 7), 72 | ].filter((i) => i > -1); 73 | let targetIndex = Math.min(...trailingCharIndexes); 74 | if (targetIndex > -1) { 75 | siteUrl = url.substring(0, targetIndex); 76 | } 77 | return siteUrl; 78 | } 79 | 80 | export function getTenant(url?: string) { 81 | if (!url && !isBrowser()) throw new Error("No url given and it is not in a browser."); 82 | if (!url) url = window.location.href; 83 | url = url.toLowerCase(); 84 | if (!checkIsSharePointLink(url)) return null; 85 | 86 | let sharepointIndex = url.indexOf(".sharepoint"); 87 | // Substring, start after https://, and at the '.sharepoint' 88 | let subdomain = url.substring(8, sharepointIndex); 89 | // support stuff like https://mytenant-admin.sharepoint.com and https://mytenant-my.sharepoint.com 90 | 91 | return subdomain.split("-")[0]; 92 | } 93 | -------------------------------------------------------------------------------- /src/utils/loaders.ts: -------------------------------------------------------------------------------- 1 | import { isBrowser } from "."; 2 | 3 | export var loadCSS = function (url: string) { 4 | if (!isBrowser()) return Promise.reject("Not a browser env"); 5 | var link = document.createElement("link"); 6 | link.setAttribute("rel", "stylesheet"); 7 | link.setAttribute("type", "text/css"); 8 | link.setAttribute("href", url); 9 | document.querySelector("head").appendChild(link); 10 | }; 11 | 12 | export var loadScript = function (url) { 13 | if (!isBrowser()) return Promise.reject("Not a browser env"); 14 | return new Promise((resolve, reject) => { 15 | var scriptTag: any = window.document.createElement("script"); 16 | var firstScriptTag = document.getElementsByTagName("script")[0]; 17 | scriptTag.async = true; 18 | firstScriptTag.parentNode.insertBefore(scriptTag, firstScriptTag); 19 | 20 | scriptTag.onload = scriptTag.onreadystatechange = function (arg, isAbort) { 21 | // if its been aborted, readyState is gone, or readyState is in a 'done' status 22 | if (isAbort || !scriptTag.readyState || /loaded|complete/.test(scriptTag.readyState)) { 23 | //clean up 24 | scriptTag.onload = scriptTag.onreadystatechange = null; 25 | scriptTag = undefined; 26 | 27 | // resolve/reject the promise 28 | if (!isAbort) resolve(); 29 | else reject; 30 | } 31 | }; 32 | scriptTag.src = url; 33 | }); 34 | }; 35 | 36 | export var loadScripts = function (urls) { 37 | return Promise.all(urls.map(loadScript)); 38 | }; 39 | -------------------------------------------------------------------------------- /src/utils/queryString.ts: -------------------------------------------------------------------------------- 1 | import { isBrowser } from "."; 2 | 3 | export function fromObj(obj: any, singleQuoteSpacedValues = false): string { 4 | var writeParam = function (key) { 5 | var value = (obj[key] + "").trim(); 6 | if (singleQuoteSpacedValues) { 7 | value = `'${value}'`; 8 | } 9 | return encodeURIComponent(key) + "=" + encodeURIComponent(value); 10 | }; 11 | 12 | if (!obj) return ""; 13 | var str = Object.keys(obj).map(writeParam).join("&"); 14 | return str; 15 | } 16 | 17 | export function toObj(str?: string): any { 18 | //if no string is passed use window.location.search 19 | if (!str && !isBrowser()) return {}; 20 | if (str === undefined && window && window.location && window.location.search) { 21 | str = window.location.search; 22 | } 23 | if (!str) return {}; 24 | //trim off the leading '?' if its there 25 | if (str[0] === "?") str = str.substr(1); 26 | 27 | try { 28 | return JSON.parse('{"' + str.replace(/&/g, '","').replace(/=/g, '":"') + '"}', function ( 29 | key, 30 | value 31 | ) { 32 | return key === "" ? value : decodeURIComponent(value); 33 | }); 34 | } catch (err) { 35 | console.log("SPScript Error: Unable to parse querystring"); 36 | return {}; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tasks/setAuthCookie.js: -------------------------------------------------------------------------------- 1 | const spauth = require("node-sp-auth"); 2 | const dotenv = require("dotenv"); 3 | dotenv.config(); 4 | const fs = require("fs"); 5 | const path = require("path"); 6 | 7 | const envFilePath = path.join(process.cwd(), ".env"); 8 | 9 | const setAuthHeaders = async () => { 10 | console.log("Getting Auth HEaders"); 11 | let auth = await spauth.getAuth(process.env.SITE_URL, { 12 | username: process.env.SP_USER, 13 | password: process.env.PASSWORD, 14 | }); 15 | 16 | let existing = dotenv.parse(fs.readFileSync(envFilePath, "utf-8")); 17 | 18 | let updated = { 19 | ...existing, 20 | AUTH_HEADERS: JSON.stringify(auth.headers), 21 | // 15 mins 22 | AUTH_EXPIRES: Date.now() + 1000 * 60 * 15 + "", 23 | }; 24 | 25 | const contents = Object.keys(updated) 26 | .map((key) => format(key, updated[key])) 27 | .join("\n"); 28 | fs.writeFileSync(envFilePath, contents); 29 | }; 30 | 31 | try { 32 | let expires = process.env.AUTH_EXPIRES; 33 | if (expires && parseInt(expires, 10) > Date.now()) { 34 | return process.env.AUTH_HEADERS; 35 | } 36 | setAuthHeaders(); 37 | } catch (err) { 38 | console.error("Unable to set auth headers", err); 39 | } 40 | 41 | function format(key, value) { 42 | return `${key}=${escapeNewlines(value)}`; 43 | } 44 | 45 | function escapeNewlines(str) { 46 | return str.replace(/\n/g, "\\n"); 47 | } 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./lib/", 4 | "sourceMap": false, 5 | "noImplicitAny": false, 6 | "module": "esnext", 7 | "target": "es5", 8 | "lib": ["es2015", "es2016", "dom"], 9 | "declaration": true, 10 | "moduleResolution": "node", 11 | "allowSyntheticDefaultImports": true 12 | }, 13 | "include": ["./src/**/*"], 14 | "exclude": ["node_modules", "lib"] 15 | } 16 | --------------------------------------------------------------------------------