@saptools/sharepoint-excel 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -99
- package/dist/cli.js +35 -11
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +34 -10
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Create `.xlsx` files, read workbook content, append records, update cells, and a
|
|
|
11
11
|
[](https://packagephobia.com/result?p=@saptools/sharepoint-excel)
|
|
12
12
|
[](https://www.typescriptlang.org)
|
|
13
13
|
|
|
14
|
-
[Install](#-install) • [Quick Start](#-quick-start) • [CLI](#-cli) • [Security](#-credential-security)
|
|
14
|
+
[Install](#-install) • [Quick Start](#-quick-start) • [CLI](#-cli) • [Security](#-credential-security)
|
|
15
15
|
|
|
16
16
|
</div>
|
|
17
17
|
|
|
@@ -29,24 +29,13 @@ Create `.xlsx` files, read workbook content, append records, update cells, and a
|
|
|
29
29
|
- 🧪 **Fake-backed e2e tests**: package tests do not call real Microsoft Graph or SharePoint
|
|
30
30
|
- 🧰 **CLI and typed API**: every CLI action is backed by exported TypeScript functions
|
|
31
31
|
|
|
32
|
-
> [!NOTE]
|
|
33
|
-
> Microsoft Graph's direct Excel workbook APIs are excellent for delegated user flows, but several workbook mutation endpoints do not support application permissions. This package intentionally treats SharePoint as file storage, edits the workbook locally, and uploads the changed `.xlsx` with conflict protection.
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
32
|
## 📦 Install
|
|
38
33
|
|
|
39
34
|
```bash
|
|
40
|
-
# Global CLI
|
|
41
35
|
npm install -g @saptools/sharepoint-excel
|
|
42
|
-
|
|
43
|
-
# Or as a dependency
|
|
44
|
-
npm install @saptools/sharepoint-excel
|
|
45
|
-
# pnpm add @saptools/sharepoint-excel
|
|
46
|
-
# yarn add @saptools/sharepoint-excel
|
|
47
36
|
```
|
|
48
37
|
|
|
49
|
-
Requires **Node.js >= 20**. The CLI binary is `
|
|
38
|
+
Requires **Node.js >= 20**. The CLI binary is `sharepoint-excel`.
|
|
50
39
|
|
|
51
40
|
---
|
|
52
41
|
|
|
@@ -54,7 +43,7 @@ Requires **Node.js >= 20**. The CLI binary is `saptools-sharepoint-excel`.
|
|
|
54
43
|
|
|
55
44
|
```bash
|
|
56
45
|
# 1. Store an app-only SharePoint profile
|
|
57
|
-
|
|
46
|
+
sharepoint-excel config set \
|
|
58
47
|
--tenant "11111111-1111-1111-1111-111111111111" \
|
|
59
48
|
--client-id "22222222-2222-2222-2222-222222222222" \
|
|
60
49
|
--client-secret "<your-client-secret>" \
|
|
@@ -62,30 +51,30 @@ saptools-sharepoint-excel config set \
|
|
|
62
51
|
--drive "Documents"
|
|
63
52
|
|
|
64
53
|
# 2. Prove auth and target resolution
|
|
65
|
-
|
|
54
|
+
sharepoint-excel test
|
|
66
55
|
|
|
67
56
|
# 3. Create a workbook without overwriting an existing file
|
|
68
|
-
|
|
57
|
+
sharepoint-excel create \
|
|
69
58
|
--path "Reports/orders.xlsx" \
|
|
70
59
|
--sheet "Orders" \
|
|
71
60
|
--headers "Name,Amount,Status" \
|
|
72
61
|
--rows '[{"Name":"Coffee","Amount":3,"Status":"open"}]'
|
|
73
62
|
|
|
74
63
|
# 4. Append one object by matching row 1 headers
|
|
75
|
-
|
|
64
|
+
sharepoint-excel append \
|
|
76
65
|
--path "Reports/orders.xlsx" \
|
|
77
66
|
--sheet "Orders" \
|
|
78
67
|
--record '{"Name":"Tea","Amount":8,"Status":"open"}'
|
|
79
68
|
|
|
80
69
|
# 5. Update one cell
|
|
81
|
-
|
|
70
|
+
sharepoint-excel update-cell \
|
|
82
71
|
--path "Reports/orders.xlsx" \
|
|
83
72
|
--sheet "Orders" \
|
|
84
73
|
--cell "C2" \
|
|
85
74
|
--value '"closed"'
|
|
86
75
|
|
|
87
76
|
# 6. Read workbook JSON
|
|
88
|
-
|
|
77
|
+
sharepoint-excel read --path "Reports/orders.xlsx" --json
|
|
89
78
|
```
|
|
90
79
|
|
|
91
80
|
For CI, every command can also read credentials from environment variables:
|
|
@@ -121,7 +110,7 @@ The CLI also accepts the shorter `SHAREPOINT_TENANT_ID`, `SHAREPOINT_CLIENT_ID`,
|
|
|
121
110
|
Store a reusable local profile.
|
|
122
111
|
|
|
123
112
|
```bash
|
|
124
|
-
|
|
113
|
+
sharepoint-excel config set \
|
|
125
114
|
--profile finance \
|
|
126
115
|
--tenant "$SHAREPOINT_EXCEL_TENANT_ID" \
|
|
127
116
|
--client-id "$SHAREPOINT_EXCEL_CLIENT_ID" \
|
|
@@ -140,7 +129,7 @@ For headless CI containers where an OS keyring is unavailable, an explicit plain
|
|
|
140
129
|
|
|
141
130
|
```bash
|
|
142
131
|
SAPTOOLS_SHAREPOINT_EXCEL_ALLOW_PLAINTEXT=1 \
|
|
143
|
-
|
|
132
|
+
sharepoint-excel config set --store file --allow-plaintext-secret ...
|
|
144
133
|
```
|
|
145
134
|
|
|
146
135
|
Use that only in controlled CI environments. The file is written with `0600` permissions under `~/.saptools/sharepoint-excel/secrets.json`.
|
|
@@ -148,8 +137,8 @@ Use that only in controlled CI environments. The file is written with `0600` per
|
|
|
148
137
|
### 👀 `config get`
|
|
149
138
|
|
|
150
139
|
```bash
|
|
151
|
-
|
|
152
|
-
|
|
140
|
+
sharepoint-excel config get --profile finance
|
|
141
|
+
sharepoint-excel config get --profile finance --json
|
|
153
142
|
```
|
|
154
143
|
|
|
155
144
|
Secrets are never printed.
|
|
@@ -157,7 +146,7 @@ Secrets are never printed.
|
|
|
157
146
|
### 🧹 `config remove`
|
|
158
147
|
|
|
159
148
|
```bash
|
|
160
|
-
|
|
149
|
+
sharepoint-excel config remove --profile finance
|
|
161
150
|
```
|
|
162
151
|
|
|
163
152
|
Removes both profile metadata and the stored secret.
|
|
@@ -167,15 +156,15 @@ Removes both profile metadata and the stored secret.
|
|
|
167
156
|
Authenticate, resolve the site, and list document libraries.
|
|
168
157
|
|
|
169
158
|
```bash
|
|
170
|
-
|
|
171
|
-
|
|
159
|
+
sharepoint-excel test
|
|
160
|
+
sharepoint-excel test --json
|
|
172
161
|
```
|
|
173
162
|
|
|
174
163
|
### 🗂️ `drives`
|
|
175
164
|
|
|
176
165
|
```bash
|
|
177
|
-
|
|
178
|
-
|
|
166
|
+
sharepoint-excel drives
|
|
167
|
+
sharepoint-excel drives --json
|
|
179
168
|
```
|
|
180
169
|
|
|
181
170
|
Use this when you are unsure whether the document library is named `Documents`, `Shared Documents`, or something custom.
|
|
@@ -183,7 +172,7 @@ Use this when you are unsure whether the document library is named `Documents`,
|
|
|
183
172
|
### 🆕 `create`
|
|
184
173
|
|
|
185
174
|
```bash
|
|
186
|
-
|
|
175
|
+
sharepoint-excel create \
|
|
187
176
|
--path "Reports/orders.xlsx" \
|
|
188
177
|
--sheet "Orders" \
|
|
189
178
|
--headers "Name,Amount,Status" \
|
|
@@ -196,14 +185,14 @@ saptools-sharepoint-excel create \
|
|
|
196
185
|
### 📖 `read`
|
|
197
186
|
|
|
198
187
|
```bash
|
|
199
|
-
|
|
200
|
-
|
|
188
|
+
sharepoint-excel read --path "Reports/orders.xlsx"
|
|
189
|
+
sharepoint-excel read --path "Reports/orders.xlsx" --sheet "Orders" --range "A1:C10" --json
|
|
201
190
|
```
|
|
202
191
|
|
|
203
192
|
### ➕ `append`
|
|
204
193
|
|
|
205
194
|
```bash
|
|
206
|
-
|
|
195
|
+
sharepoint-excel append \
|
|
207
196
|
--path "Reports/orders.xlsx" \
|
|
208
197
|
--sheet "Orders" \
|
|
209
198
|
--record '{"Name":"Tea","Amount":8,"Status":"open"}'
|
|
@@ -214,7 +203,7 @@ Objects are mapped by the first row's headers by default. Use `--no-match-header
|
|
|
214
203
|
### 🎯 `update-cell`
|
|
215
204
|
|
|
216
205
|
```bash
|
|
217
|
-
|
|
206
|
+
sharepoint-excel update-cell \
|
|
218
207
|
--path "Reports/orders.xlsx" \
|
|
219
208
|
--sheet "Orders" \
|
|
220
209
|
--cell "B2" \
|
|
@@ -226,7 +215,7 @@ saptools-sharepoint-excel update-cell \
|
|
|
226
215
|
### 📄 `add-sheet`
|
|
227
216
|
|
|
228
217
|
```bash
|
|
229
|
-
|
|
218
|
+
sharepoint-excel add-sheet \
|
|
230
219
|
--path "Reports/orders.xlsx" \
|
|
231
220
|
--sheet "Audit" \
|
|
232
221
|
--headers "At,Action,Actor"
|
|
@@ -250,71 +239,6 @@ Required Graph application permissions depend on your tenant model. Typical setu
|
|
|
250
239
|
|
|
251
240
|
---
|
|
252
241
|
|
|
253
|
-
## 🧑💻 Programmatic Usage
|
|
254
|
-
|
|
255
|
-
```ts
|
|
256
|
-
import {
|
|
257
|
-
appendRemoteWorkbookRows,
|
|
258
|
-
createRemoteWorkbook,
|
|
259
|
-
openSession,
|
|
260
|
-
parseSiteRef,
|
|
261
|
-
} from "@saptools/sharepoint-excel";
|
|
262
|
-
|
|
263
|
-
const session = await openSession({
|
|
264
|
-
credentials: {
|
|
265
|
-
tenantId: process.env.SHAREPOINT_EXCEL_TENANT_ID ?? "",
|
|
266
|
-
clientId: process.env.SHAREPOINT_EXCEL_CLIENT_ID ?? "",
|
|
267
|
-
clientSecret: process.env.SHAREPOINT_EXCEL_CLIENT_SECRET ?? "",
|
|
268
|
-
},
|
|
269
|
-
site: parseSiteRef("contoso.sharepoint.com/sites/demo"),
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
await createRemoteWorkbook(
|
|
273
|
-
{ session, driveHint: "Documents" },
|
|
274
|
-
"Reports/orders.xlsx",
|
|
275
|
-
{
|
|
276
|
-
sheetName: "Orders",
|
|
277
|
-
headers: ["Name", "Amount"],
|
|
278
|
-
rows: [{ Name: "Coffee", Amount: 3 }],
|
|
279
|
-
},
|
|
280
|
-
);
|
|
281
|
-
|
|
282
|
-
await appendRemoteWorkbookRows(
|
|
283
|
-
{ session, driveHint: "Documents" },
|
|
284
|
-
"Reports/orders.xlsx",
|
|
285
|
-
"Orders",
|
|
286
|
-
[{ Name: "Tea", Amount: 8 }],
|
|
287
|
-
true,
|
|
288
|
-
);
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
<details>
|
|
292
|
-
<summary><b>📚 Main exports</b></summary>
|
|
293
|
-
|
|
294
|
-
| Export | Description |
|
|
295
|
-
| --- | --- |
|
|
296
|
-
| `resolveRuntime()` | Resolve flags/env/profile into a SharePoint target |
|
|
297
|
-
| `createProfileStore()` | Read/write redacted local profile metadata |
|
|
298
|
-
| `createKeyringSecretVault()` | OS credential vault adapter |
|
|
299
|
-
| `acquireAppToken()` | Request an app-only Graph token |
|
|
300
|
-
| `createGraphClient()` | Minimal Graph JSON/binary client |
|
|
301
|
-
| `parseSiteRef()` / `resolveSite()` | Parse and resolve SharePoint site references |
|
|
302
|
-
| `listDrives()` / `selectDrive()` | Discover and select document libraries |
|
|
303
|
-
| `createWorkbookBytes()` | Build a local `.xlsx` workbook |
|
|
304
|
-
| `readWorkbookBytes()` | Read sheet rows from workbook bytes |
|
|
305
|
-
| `appendWorkbookRows()` | Append rows in workbook bytes |
|
|
306
|
-
| `updateWorkbookCell()` | Update an A1 cell in workbook bytes |
|
|
307
|
-
| `addWorkbookSheet()` | Add a new worksheet |
|
|
308
|
-
| `createRemoteWorkbook()` | Create SharePoint workbook without overwriting |
|
|
309
|
-
| `readRemoteWorkbook()` | Download and read SharePoint workbook |
|
|
310
|
-
| `appendRemoteWorkbookRows()` | Append rows and upload with ETag protection |
|
|
311
|
-
| `updateRemoteWorkbookCell()` | Update one cell and upload with ETag protection |
|
|
312
|
-
| `addRemoteWorkbookSheet()` | Add a worksheet and upload with ETag protection |
|
|
313
|
-
|
|
314
|
-
</details>
|
|
315
|
-
|
|
316
|
-
---
|
|
317
|
-
|
|
318
242
|
## 🛠️ Development
|
|
319
243
|
|
|
320
244
|
From the monorepo root:
|
package/dist/cli.js
CHANGED
|
@@ -172,7 +172,8 @@ async function redactProfile(profile, vault) {
|
|
|
172
172
|
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
173
173
|
import { dirname as dirname2 } from "path";
|
|
174
174
|
import { Entry } from "@napi-rs/keyring";
|
|
175
|
-
var SERVICE_NAME = "
|
|
175
|
+
var SERVICE_NAME = "sharepoint-excel";
|
|
176
|
+
var LEGACY_SERVICE_NAME = `saptools-${SERVICE_NAME}`;
|
|
176
177
|
var SECRET_FILE_MODE = 384;
|
|
177
178
|
var EMPTY_SECRET_FILE = { version: 1, entries: {} };
|
|
178
179
|
function isMissingFileError2(err) {
|
|
@@ -208,11 +209,29 @@ async function writeSecretFile(path, value) {
|
|
|
208
209
|
});
|
|
209
210
|
await chmod2(path, SECRET_FILE_MODE);
|
|
210
211
|
}
|
|
212
|
+
function getKeyringPassword(serviceName, profileName) {
|
|
213
|
+
const password = new Entry(serviceName, profileName).getPassword();
|
|
214
|
+
return password === null || password.length === 0 ? void 0 : password;
|
|
215
|
+
}
|
|
216
|
+
function deleteKeyringPassword(serviceName, profileName) {
|
|
217
|
+
try {
|
|
218
|
+
new Entry(serviceName, profileName).deletePassword();
|
|
219
|
+
} catch (err) {
|
|
220
|
+
if (err instanceof Error && /not found|no entry|missing/i.test(err.message)) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
211
226
|
function createKeyringSecretVault(serviceName = SERVICE_NAME) {
|
|
227
|
+
const legacyServiceName = serviceName === SERVICE_NAME ? LEGACY_SERVICE_NAME : void 0;
|
|
212
228
|
return {
|
|
213
229
|
getSecret(profileName) {
|
|
214
|
-
const password =
|
|
215
|
-
|
|
230
|
+
const password = getKeyringPassword(serviceName, profileName);
|
|
231
|
+
if (password !== void 0 || legacyServiceName === void 0) {
|
|
232
|
+
return Promise.resolve(password);
|
|
233
|
+
}
|
|
234
|
+
return Promise.resolve(getKeyringPassword(legacyServiceName, profileName));
|
|
216
235
|
},
|
|
217
236
|
setSecret(profileName, secret) {
|
|
218
237
|
new Entry(serviceName, profileName).setPassword(secret);
|
|
@@ -220,11 +239,11 @@ function createKeyringSecretVault(serviceName = SERVICE_NAME) {
|
|
|
220
239
|
},
|
|
221
240
|
deleteSecret(profileName) {
|
|
222
241
|
try {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return Promise.resolve();
|
|
242
|
+
deleteKeyringPassword(serviceName, profileName);
|
|
243
|
+
if (legacyServiceName !== void 0) {
|
|
244
|
+
deleteKeyringPassword(legacyServiceName, profileName);
|
|
227
245
|
}
|
|
246
|
+
} catch (err) {
|
|
228
247
|
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
229
248
|
}
|
|
230
249
|
return Promise.resolve();
|
|
@@ -282,8 +301,11 @@ function resolveBaseUrl(options) {
|
|
|
282
301
|
const fromEnv = (options.env ?? process2.env)[ENV_GRAPH_BASE];
|
|
283
302
|
return fromEnv === void 0 || fromEnv.length === 0 ? DEFAULT_GRAPH_BASE : fromEnv.replace(/\/+$/, "");
|
|
284
303
|
}
|
|
285
|
-
function resolveUrl(base, path) {
|
|
304
|
+
function resolveUrl(base, path, includeAuthorization) {
|
|
286
305
|
if (/^https?:\/\//i.test(path)) {
|
|
306
|
+
if (includeAuthorization && new URL(path).origin !== new URL(base).origin) {
|
|
307
|
+
throw new Error("Refusing to send a Graph bearer token to a different origin");
|
|
308
|
+
}
|
|
287
309
|
return path;
|
|
288
310
|
}
|
|
289
311
|
return `${base}${path.startsWith("/") ? path : `/${path}`}`;
|
|
@@ -318,8 +340,8 @@ async function extractErrorDetail(response) {
|
|
|
318
340
|
}
|
|
319
341
|
function buildInit(accessToken, options) {
|
|
320
342
|
const headers = {
|
|
321
|
-
Authorization: `Bearer ${accessToken}`,
|
|
322
343
|
Accept: "application/json",
|
|
344
|
+
...options.includeAuthorization === false ? {} : { Authorization: `Bearer ${accessToken}` },
|
|
323
345
|
...options.headers
|
|
324
346
|
};
|
|
325
347
|
const init = { method: options.method ?? "GET", headers };
|
|
@@ -355,7 +377,8 @@ function createGraphClient(options) {
|
|
|
355
377
|
const baseDelayMs = options.retry?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
356
378
|
const sleepFn = options.retry?.sleepFn ?? defaultSleep;
|
|
357
379
|
async function execute(path, requestOptions) {
|
|
358
|
-
const
|
|
380
|
+
const includeAuthorization = requestOptions.includeAuthorization !== false;
|
|
381
|
+
const url = resolveUrl(baseUrl, path, includeAuthorization);
|
|
359
382
|
const init = buildInit(options.accessToken, requestOptions);
|
|
360
383
|
let attempt = 0;
|
|
361
384
|
let response = await fetchFn(url, init);
|
|
@@ -755,6 +778,7 @@ async function uploadNewDriveFile(client, driveId, relativePath, bytes) {
|
|
|
755
778
|
const raw = await client.requestJson(uploadUrl, {
|
|
756
779
|
method: "PUT",
|
|
757
780
|
rawBody: bytes,
|
|
781
|
+
includeAuthorization: false,
|
|
758
782
|
headers: {
|
|
759
783
|
"Content-Length": bytes.byteLength.toString(),
|
|
760
784
|
"Content-Range": `bytes 0-${lastByte.toString()}/${bytes.byteLength.toString()}`,
|
|
@@ -1347,7 +1371,7 @@ function registerCommands(program) {
|
|
|
1347
1371
|
// src/cli/index.ts
|
|
1348
1372
|
async function main(argv) {
|
|
1349
1373
|
const program = new Command();
|
|
1350
|
-
program.name("
|
|
1374
|
+
program.name("sharepoint-excel").description("Create, read, and update SharePoint-hosted Excel workbooks");
|
|
1351
1375
|
registerCommands(program);
|
|
1352
1376
|
await program.parseAsync([...argv]);
|
|
1353
1377
|
}
|