├── .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 | [](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 |
--------------------------------------------------------------------------------