SharePoint REST 连接
一个小型、无依赖的节点客户端 默认共享点 资源
服务,带有内置的NTLMv2认证. 读取和写入列表项目,
上传并下载文档,管理文件夹. 每次呼叫返回一个输入
Result 而不是扔东西.
我厌倦了为工作项目重写这些.
所需资源
- 节点.js 20或后.
- 内置共享点 ——2013,2016,2019或订阅版-与 NTLM 认证启用 。 共享点在线是 没有 支持; 确实 不接受 NTLM.
- 没有别的事了 没有运行时间依赖性: NTLM 在顶端执行
联 合 国
node:http编号 :node:https财务报告和审定财务报表node:crypto。 。 。 .
认证
仅 NTLMv2 地图 支持。 SharePoint 2013 并随后通过谈判
默认。 如果服务器没有发送NTLMv2要求的目标信息,
请求失败 NTLM server did not send target info 而不是沉默无声
降级为 NTLMv1, 在 Node 的 OpenSSL 3 中不安全且不可用
建设.
安装
npm i @b-jones-rfd/sp-rest-connect
# or
pnpm add @b-jones-rfd/sp-rest-connect
# or
yarn add @b-jones-rfd/sp-rest-connect
快开始
import { createSiteConnection } from '@b-jones-rfd/sp-rest-connect'
const connection = createSiteConnection({
username: 'jsmith',
password: process.env.SP_PASSWORD!,
domain: 'CONTOSO', // NTLM (NetBIOS) domain, not a DNS name
site: 'sharepoint.contoso.local',
serverRelativeUrl: '/sites/finance',
})
// Read
type Invoice = { Id: number; Title: string; Amount: number }
const params = new URLSearchParams({ $select: 'Id,Title,Amount', $top: '500' })
const items = await connection.getAllListContents<Invoice>({
listName: 'Invoices',
params,
})
if (!items.success) throw new Error(items.error)
console.log(items.data.map((i) => i.Title))
// Write: the request digest and the list's entity type are handled for you
const created = await connection.addListItem({
listName: 'Invoices',
payload: { Title: 'INV-1042', Amount: 1250 },
})
if (created.success) console.log(created.data.Id)
更多实例包括文档上传 examples/basic.ts。 。 。 .
使用量
站点连接
createSiteConnection(options) 将每个动作绑在一个站点并返回 a
SiteConnection。创建连接不执行网络 I/O; 证书
在封锁中被俘获,并用于每次呼叫。 它确实检查了形状
选项并丢出 TypeError 对于明显错误( 内部协议)
site页:1 serverRelativeUrl 没有领先的斜线,所以错位
启动时失败, 而不是后来的不透明 404 .
| 选项 | 类型 | 需求 | 默认 | 说明 |
|---|---|---|---|---|
username | string | Y | Windows账户名称,没有域名. | |
password | string | Y | 账户密码。 从环境读取,而不是硬编码. | |
site | string | Y | 只使用主机名称, 例如 . 'sharepoint.contoso.local'。 。 。 . | |
serverRelativeUrl | string | Y | 站点(web)路径,例如:. '/sites/finance'全部数据 folder 参数将据此解决. | |
protocol | 'https' | 'http' | 无 | 'https' | |
domain | string | 无 | '' | NTLM (NetBIOS) 域名, 如. 'CONTOSO'。为服务器本地账户留空. |
hostname | string | 无 | os.hostname() | 工作站名传入NTLM握手. 很少需要改变. |
agentOptions | https.AgentOptions | 无 | HTTP 代理的 TLS / socket 选项 。 见 TLS 和内部证书。 。 。 . | |
retry | RetryOptions | 无 | 重试读取动作上的瞬态失败 。 见 重试瞬间失败。 。 。 . |
独立行动
单用或只导入您需要的东西, 每个动作都会导出 作为工厂。 用相同的选项调用它以获得可重复使用的动作 :
import { getListContents } from '@b-jones-rfd/sp-rest-connect'
const fetchList = getListContents(siteConnectionOptions)
const items = await fetchList({ listName: 'Invoices' })
结果
每个行动都决定 Result<T> 绝不扔出:
type Result<T> = Success<T> | Failure
type Success<T> = { success: true; data: T }
type Failure = {
success: false
error: string // "Error in getListContents: HTTP 404: List 'Tasks' does not exist"
code: FailureCode
status?: number // HTTP status, for HTTP / AUTH failures
details?: { code?: string; message?: string } // SharePoint's own error, when it sent one
retryAfterMs?: number // from a Retry-After header, when present
}
检查 success 缩小,然后在 code 而不是在留言上
文本 :
code | 含义 |
|---|---|
HTTP | SharePoint以非-2xx状态(401/403除外)回答. 见 status编号 :details。 。 。 . |
AUTH | 401或403——不良资质,NTLM被否决,或缺失/过期请求文摘. |
TIMEOUT | 没有到达字节 timeout m. |
ABORTED | 取消通过 signal。 。 。 . |
NETWORK | DNS,连接或TLS在任何响应前失败. |
PARSE | 2xx反应,其身体未达到预期状态. |
UNKNOWN | 任何别的事. |
const res = await connection.addListItem({ listName, payload })
if (!res.success) {
if (res.code === 'AUTH') await alertOps('credentials rejected')
else if (res.code === 'TIMEOUT' || res.code === 'NETWORK') await retryLater()
else log.error(res.error, res.details)
}
error 总是以动作名称为前缀的人类可读信件,用于
伐木业.
写入操作:请求摘要
SharePoint 需要一个 X-RequestDigest 每封信都写着标题 连接
处理好它:第一封信从 /_api/contextinfo, (中文(简体) ),
缓存到SharePoint说它过期前一分钟(30分钟后)
如果SharePoint仍以403拒绝,则摘要会被刷新
并重审过一次 同步写入共享一个获取 .
自己管理消化器,通过 requestDigest 明确——缓存是
然后绕行,不发生自动重试. getFormDigestValue() 获取
一个.
独立行动工厂各自保存自己的缓存. 分享一个,创建一个 上下文并当作第二个论点:
import {
createConnectionContext,
addListItem,
deleteListItem,
} from '@b-jones-rfd/sp-rest-connect'
const ctx = createConnectionContext(options)
const add = addListItem(options, ctx)
const remove = deleteListItem(options, ctx) // shares add's digest
getAuthToken() 实值 没有 这个值——它返回NTLM握手头,
这是连接的,并且只是用作证书检查.
创建和更新项目
addListItem 财务报告和审定财务报表 updateListItem 取一个普通对象。 共享点需要
列表的实体类型( n)__metadata.type)在身体上;连接在上看
与 getListItemType 它第一次看到列表并缓存它:
await connection.addListItem({
listName: 'Invoices',
payload: { Title: 'INV-1042', Amount: 1250 },
})
await connection.updateListItem({
listName: 'Invoices',
spId: 17,
patch: { Amount: 1300 },
})
包含 __metadata: { type: 'SP.Data.InvoicesListItem' } 在对象中
你自己跳过检查. addListItem 确定创建的项目为
SharePoint 将它带回了新 Id; 通过其它参数的类型
形状addListItem<Invoice>(...)) (中文(简体) ).
updateListItem 是部分更新( OData) MERGE 与 If-Match: *: 仅限
输入 patch 更改,最后的作者获胜.
大型文件
addDocumentToLibrary 接受一个 Readable 作为 payload,以及
getDocumentStreamFromLibrary 返回一个,所以文件从不需要适应
内存 :
import { createReadStream, createWriteStream } from 'node:fs'
import { stat } from 'node:fs/promises'
import { pipeline } from 'node:stream/promises'
const { size } = await stat('backup.zip')
await connection.addDocumentToLibrary({
folder: 'Shared Documents/Archive',
fileName: 'backup.zip',
payload: createReadStream('backup.zip'),
contentLength: size, // optional; without it the upload is chunked
})
const download = await connection.getDocumentStreamFromLibrary({
folder: 'Shared Documents/Archive',
fileName: 'backup.zip',
})
if (download.success)
await pipeline(download.data, createWriteStream('restored.zip'))
只能发送一次流, 所以如果管理中的文摘显示为呆滞
中上载写入代码失败 AUTH 而不是再审;拨打
再来一次 下载时的 HTTP 错误报告为 Failure 在任何时间之前
溪流返回.
文件夹
每个 folder 参数相对 serverRelativeUrl 然后从
库名称 : 'Shared Documents', (中文(简体) ), 'Shared Documents/Reports/2026'。 。 。 .
打字结果
getListContents, (中文(简体) ), getFolderContents 财务报告和审定财务报表 getListItem 选择类型参数
用于项目形状。 这是一种断言,而不是验证
但它在每一个呼叫站点都省下一个铸造者:
type Task = { Id: number; Title: string; Status: 'Open' | 'Done' }
const res = await connection.getListContents<Task>({
listName: 'Tasks',
params,
})
if (res.success) res.data.filter((t) => t.Status === 'Open')
拨号
最多共享点返回 $top 每个答复(如果未设定,100个)
和,当更多的保留,a nextLink.你有三种方法来处理:
// 1. Everything, in one array. Fails as a whole if any page fails.
const all = await connection.getAllListContents<Task>({
listName: 'Tasks',
params,
})
// 2. Page by page, as an async iterator. Ends after the last page or the first failure.
for await (const page of connection.iterateListContents<Task>({
listName: 'Tasks',
})) {
if (!page.success) throw new Error(page.error)
process(page.data)
}
// 3. Manually: a single page exposes `nextLink`, which `getNextPage` follows.
const first = await connection.getListContents<Task>({ listName: 'Tasks' })
if (first.success && first.nextLink) {
const second = await connection.getNextPage<Task>({
nextLink: first.nextLink,
})
}
getAllFolderContents 编号 : iterateFolderContents 对文档也这样做
图书馆文件夹。 两者 getAll* 财务报告和审定财务报表 iterate* 接受 maxPages 以上限
走开. getNextPage 只跟踪连接本身主机的链接, 所以您
证书从不发给别处.
重试瞬间失败
设定 retry 关于重试的连接 读取 失败的动作
NETWORK, (中文(简体) ), TIMEOUT,或HTTP 429/502/503/504。 写作从不重试
自动——重试 addListItem 可创建重复.
const connection = createSiteConnection({
...options,
retry: { retries: 3, baseDelayMs: 300, maxDelayMs: 5000 },
})
拖延随着全身的焦躁而成指数增长,如果请求出现,则提前停止
signal 流产。 当SharePoint发送一个 Retry-After 页眉(429/503),即
以等待代替,或者,如果等待时间长于 maxDelayMs,重试停止
失败又随着 retryAfterMs 设置这样你就可以决定。 过
retryOn(failure, attempt) 更改可重试的内容;
isTransientFailure 要扩展则导出(默认) .
CAML 查询和 5000 项阈值
$filter 一旦列表通过 SharePoint 的列表, 未索引列失败
视图阈值(默认为5,000项). queryListItems 发送 CAML 查询
改为 GetItems 相反, SQL 服务器对索引列的评价 :
const open = await connection.queryListItems<Task>({
listName: 'Tasks',
viewXml: `<View>
<Query><Where><Eq><FieldRef Name='Status'/><Value Type='Text'>Open</Value></Eq></Where></Query>
<RowLimit>500</RowLimit>
</View>`,
params: new URLSearchParams({ $select: 'Id,Title,Status' }),
})
通过放入页面 <RowLimit> 在风景和逝去 pagingInfo 从
上页的最后一个项目—— Paged=TRUE&p_ID=${last.Id} 当风景
由 ID (默认) 命令. GetItems 是一个 POST, 因此管理请求
文摘被使用;仍作为读取 retry 目标.
请求
batch 在一次往返中发出多个REST电话(SharePoint 2016和
后来;2013年没有 $batch 结束点). 读作个别部分;
连续的写入被组合为一个更改器, 应用SharePoint
解剖论. 每个次级请求都得到自己的回应——失败的请求不会
批量失败, 请检查 ok数字 :
const res = await connection.batch({
requests: [
{ method: 'GET', path: `_api/web/lists/GetByTitle('Tasks')/items?$top=5` },
{
method: 'POST',
path: `_api/web/lists/GetByTitle('Tasks')/items`,
body: { __metadata: { type }, Title: 'A' },
},
{
method: 'MERGE',
path: `_api/web/lists/GetByTitle('Tasks')/items(7)`,
body: { __metadata: { type }, Title: 'B' },
},
{ method: 'DELETE', path: `_api/web/lists/GetByTitle('Tasks')/items(8)` },
],
})
if (res.success) {
const [list, created, merged, deleted] = res.data
if (created.ok) console.log((created.json as { Id: number }).Id)
}
path 相对于该站点( e)_api/...)或同一上的绝对 URL
源代码。 每个子请求的默认值 : Accept: application/json;odata=nometadata, (中文(简体) ),
Content-Type: application/json;odata=verbose 当有尸体
If-Match: * 打开 MERGE编号 :PATCH编号 :DELETE; 以 headers。 。 。 .
创建或合并列表项的机构仍然需要 __metadata.type - 那个
批量不为您解析实体类型( s)getListItemType 页:1.
共同选项
每个动作都接受:
timeout- 套接字 不活动 超时数以毫秒计。 请求失败 有代码TIMEOUT当没有字节到达这么久。 这不是一个帽子 总持续时间; 缓慢但稳定的下载从未超过。 不为所取.signal- 一个AbortSignal终止生成aFailure有代码ABORTED。对于整个呼叫的艰难期限,请使用signal: AbortSignal.timeout(30_000)。 。 。 .
getDocumentFromLibrary 额外接受 binary (默认) true页:1
返回文件为 Buffer;没有理由关闭它.
TLS 和内部证书
On-premises SharePoint通常在内部CA的证书后.
而不是设定 NODE_TLS_REJECT_UNAUTHORIZED=0 整个进程,通过 CA
连接:
import { readFileSync } from 'node:fs'
const connection = createSiteConnection({
...options,
agentOptions: { ca: readFileSync('/etc/ssl/corp-root-ca.pem') },
})
agentOptions 接受任何东西 https.Agent 是否为 -- ca, (中文(简体) ), cert编号 :key 页:1
客户端证书, rejectUnauthorized, (中文(简体) ), localAddress,习惯 lookup, (中文(简体) ),
等——但 keepAlive 财务报告和审定财务报表 maxSockets,这是NTLM握手
修复。 毒剂按请求创建,后被销毁.
特殊字符
列表标题、文件夹路径和文件名称为%- 编码并有
oData 自动增加一倍,所以 "Bob's Tasks" 财务报告和审定财务报表
'Shared Documents/Q1 2026' 以书面形式工作.
API 密码
完整生成的参考 : b-jones-rfd.github.io/sp-rest-连接。 。 。 .
所有选项类型都导出( E)AddListItemOptions, (中文(简体) ), GetListContentsOptions, (中文(简体) ),
...)下面的每个成员都有记录,所以在你的编辑中徘徊
显示同样的细节。 每个写入动作也接受常见
requestDigest 选项(省略时自动管理); listName 这是
列表的显示标题.
名单
| 行动 | 所需选项 | data |
|---|---|---|
getListContents<T>({ listName, params? }) | —— 说 | T[] - 一页; nextLink 等更多的东西还剩的时候再设置 * 100个没有 $top。 。 。 . |
getAllListContents<T>({ listName, params?, maxPages? }) | —— 说 | T[] ——每页收缩. |
iterateListContents<T>({ listName, params?, maxPages? }) | —— 说 | 同步执行器 PageResult<T>。 。 。 . |
getNextPage<T>({ nextLink }) | —— 说 | T[] ——链接页;仅同源链接. |
queryListItems<T>({ listName, viewXml, pagingInfo?, params? }) | —— 说 | T[] —— CAML 查询结果通过 GetItems。 。 。 . |
getListItem<T>({ listName, spId }) | —— 说 | T ——项目对象. |
getListItemType({ listName }) | —— 说 | string - 比如说. 'SP.Data.InvoicesListItem'。 。 。 . |
addListItem<T>({ listName, payload }) | payload对象 | T & { Id: number } - 创建的项目. |
updateListItem({ listName, spId, patch }) | patch对象 | string ——序列补丁 (SharePoint return no body). |
deleteListItem({ listName, spId }) | —— 说 | '' |
addAttachmentToListItem({ listName, spId, fileName, payload }) | payload数字 : Buffer | string ——附件的服务器相对URL. |
文件库
| 行动 | 所需选项 | data |
|---|---|---|
getFolderContents<T>({ folder, params? }) | —— 说 | T[] —— 一页 SP.File 对象(Name, (中文(简体) ), Length(......) ; (......); nextLink 当更多的还剩. |
getAllFolderContents<T>({ folder, params?, maxPages? }) | —— 说 | T[] ——每页收缩. |
iterateFolderContents<T>({ folder, params?, maxPages? }) | —— 说 | 同步执行器 PageResult<T>。 。 。 . |
getDocumentFromLibrary({ folder, fileName }) | —— 说 | Buffer ——文件内容. |
getDocumentStreamFromLibrary({ folder, fileName }) | —— 说 | Readable ——文件内容,未缓存. |
checkFolderExistsInLibrary({ folder }) | —— 说 | boolean ——缺少文件夹是成功的 false。 。 。 . |
createFolderInLibrary({ folder }) | 父关系必须存在 | string ——新文件夹的服务器相对URL. |
addDocumentToLibrary({ folder, fileName, payload, contentLength? }) | payload数字 : Buffer | Readable | string ——服务器相对URL;覆盖已存在文件. |
deleteDocumentFromLibrary({ folder, fileName }) | —— 说 | string ——已删除 fileName。 。 。 . |
击打
| 行动 | 所需选项 | data |
|---|---|---|
batch({ requests }) | SharePoint 2016+ (英语) | BatchResponse[] —— 说 { status, ok, headers, body, json? } 按顺序排列. |
认证
| 行动 | data |
|---|---|
getFormDigestValue({}) | string 请求摘要,如果你想管理 requestDigest 你自己来. |
getAuthToken({}) | string - NTLM 第3型 Authorization 头曰. 仅诊断;见上文. |
错误
SpRestError 财务报告和审定财务报表 parseSharePointError(body) 导出。 前者是
内部运输在行动前给非-2xx反应投出的东西
把它变成一个 Failure;后面的摘录 { code, message } 从任何
OData 出错的身体形状。 NTLM运输本身不属于公众
API (英语). 如果您需要 SharePoint 端点, 请打开此软件包
一个问题.
迁移到 5.0
accessToken不见了. 使用requestDigest,或者省略它并让 连接管理摘要.addListItem编号 :updateListItem只取对象. 替换payload: JSON.stringify({ __metadata: { type }, ... })与payload: { ... }- 实体类型为你解决.addListItem解析创建的项目不是JSON的绳子 替换JSON.parse(res.data).Id与res.data.Id。 。 。 .- NTLM 帮助者(
get, (中文(简体) ),post, (中文(简体) ),auth, (中文(简体) ),request,信息创建者)是 不再出口.
4.x-结构中增加的任何东西 Failure 代码, signal, (中文(简体) ), agentOptions, (中文(简体) ),
retry,键入读取,呼出,管理文摘,流出——不变.
捐款
这是一个个人项目 维护我自己使用。 已经出版,所以 但我现在不接受问题 也不接受请求. 安全报告是例外;见 警卫。 。 。 .
如果你想在当地建造:
pnpm install
pnpm dev # vitest in watch mode
pnpm ci # lint, test, build, package checks
pnpm docs # generate the API reference into docs/