Epistola Node.js 客户端
节点Js 客户端库 爱皮斯托拉
epistola OpenAPI 合同生成的文档生成 API
OpenAPI 生成器 (单位:千美元)typescript-fetch,在月台上独自
fetch)和手写胶水,用于身份、认证、错误处理、结果收集,以及
客户端验证。 包含类型脚本声明.
除了股票产生的客户端之外
- 客户身份 页眉 (
User-Agent+ 键X-EP-Node-Id每一份请求都需要. - RFC 9457 问题详情 错误处理 - 输入
ProblemDetailException带有typeSlug歧视者和产生者KnownProblemSlugs常数. - 静态 API- key 通过
Authorization: ApiKey <key>,支持的方法. - 自签 JWT 认证( e)
JwtSigner, (中文(简体) ), 实验{\fn黑体\fs22\bord1\shad0\3ahbe\4ah00\fscx67\fscy66\2chffffff\3ch808080}用新鲜的短命 每个请求的代号node:crypto一个人. - 这个
Accept每个操作都用页眉声明. 生成的课从不设置一个 节点的fetch发送*/*默认的,所以没有这个请求,它永远不会说接受application/problem+json,文档中每个错误处理器都会被构建为解析. - NDJSON 结果收集 (单位:千美元)
ResultCollector具有适应性投票、压缩和 分区意识路由帮助器. - 模板数据验证 (单位:千美元)
TemplateSchemaValidator),询问服务器,带有可插件TemplateDataValidator用于想要在流程中进行检查的消费者validate<Model>帮助解决合同本身的限制.
软件包版本跟踪Epistola合同版本(info.version并锁定释放
与克特林,雅加达EE,.NET和Python客户端. 它需要22.12节或以后的节点和船舶
一个ES模块(节点的) require() 也可以装入).
它已经 没有运行时间依赖性,也没有可选的: HTTP 是该平台的 fetch,JWT签名
实值 node:crypto,减压是 node:zlib。模板数据验证询问服务器而不是
在此编译计划, 因此不再安装 JSON Schema 编译器( 见
模板数据验证) (中文(简体) ).
发布历史 : 变动情况。 。 。 .
安装
npm install @epistola.app/epistola-client
快开始
import { ClientIdentity, EpistolaClient, TemplatesApi } from '@epistola.app/epistola-client'
const identity = ClientIdentity.builder()
.nodeId('my-pod-123') // defaults to the hostname
.product('my-app', '1.0.0') // appended to User-Agent
.build()
const client = EpistolaClient.builder('https://epistola.example.com/api')
.identity(identity) // User-Agent + X-EP-Node-Id
.apiKey('epk_...') // Authorization: ApiKey <key>
.build()
const templates = new TemplatesApi(client)
const template = await templates.getTemplate({ tenantKey: 'acme', registry: '~', namespace: 'acme', catalogKey: 'invoices', release: '1.4.0', templateKey: 'invoice' })
其目录涉及 registry, (中文(简体) ), namespace 财务报告和审定财务报表 catalogKey。 。 。 . registry: '~' 地址:
服务器的默认注册; 任何其他注册都以全名命名(registry: 'acme.com') (中文(简体) ).
EpistolaClient.builder(baseUrl, apiKey) 是同一事物的简称。 一些Epistola套间
部署完全禁用 API- key 认证; 开关
e.typeSlug === KnownProblemSlugs.API_KEY_AUTH_DISABLED 并引导呼叫者前往JWT auth.
自签名 JWT 认证为 实验: 爱皮斯托拉 套房可能尚未执行
流动仍然会改变。 试试看,通过 .jwtSigner(signer) 改为 .apiKey(...)数字 :
import { JwtSigner } from '@epistola.app/epistola-client'
const signer = JwtSigner.builder()
.consumerId('invoice-service')
.privateKey(JwtSigner.loadPrivateKey('private.pem'))
.build()
EpistolaClient 实值 a 生成 Configuration,因此每个生成的API类直接取用.
一个构建者可以生产一个以上的客户端:呼叫 build() 更改后再次
requestTimeoutMs(...) 长期消费者通常需要对照
同一后端——投票、渲染和大额转账不超时(缺省),有限度
为了其他一切.
部分更新
生成的模型是平版Script接口,生成的序列器会丢出一个属性
您发送一个您设定到 null 作为 null在 API 上 PATCH 操作 a
省略或无成员保留其值,因为大多数生成的服务器无法区分两者:
updateConsumer({ ..., updateConsumerRequest: { name: 'Billing' } }) 重新命名消费者
别碰别的东西,也一样 { name: 'Billing', description: null }。要删除一个值,请发送
可选文本成员空字符串({ description: '' },空数组或对象
将列表或地图替换为一无所有,或成员的明确旗帜({ clearExpiresAt: true }, (中文(简体) ),
clearDefaultTheme, (中文(简体) ), clearCodeListBinding) (中文(简体) ).
二进制下载
合同宣布的每一项业务 format: binary —— 说 downloadDocument, (中文(简体) ), previewDocument, (中文(简体) ),
downloadImageContent - 决定 Blob· ; Buffer.from(await blob.arrayBuffer()) 给你的字节.
上传文件
多部分业务,例如: uploadImage 财务报告和审定财务报表 importCatalog 带一个 Blob通过 a File,哪个
带有名称和类型 :
import { readFile } from 'node:fs/promises'
const file = new File([await readFile('logo.png')], 'logo.png', { type: 'image/png' })
const image = await new ImagesApi(client).uploadImage({ tenantKey: 'acme-corp', registry: '~', namespace: 'acme-corp', catalogKey: 'main', release: 'k7m2x9qa', file, name: 'Logo' })
一个平地 Blob 总是随文件名一起发送 blob,然后作为 application/octet-stream 除非这
被赋予了一种类型。 服务器然后命名图像 blob。 。 。 .
更新服务器
合同的细微版本可以添加响应成员和enum值("兼容性在
API描述中的 2.x",因此这个客户端继续针对后2.x服务器工作:一个响应
它不知道的成员被忽略, 而它不知道的enum值被保存为字符串
收到,尽管生成的类型没有列出。 保持一个 default 切换时的分支
um.
客户端忽略的是什么,它不能发回. 根据答复提出的要求只涉及成员
这个客户知道。 这与书写取代所读内容有关:
getVariantContent 并写回 updateVariantContent 失去每个加入的更新目录的线条版本.
编辑至少与服务器一样新客户端的模板文档,并更改消费者的
通过只发送您更改的资源,从不发送您读取的对象.
处理错误
接着 ProblemDetailException 打开 typeSlug 反对 KnownProblemSlugs。弹片列表是
打开 ——API可以在没有客户端发布的情况下引入新的问题类型——所以始终保持一个
default 分支并返回未识别类型的 HTTP 状态.
import { KnownProblemSlugs, ProblemDetailException, ResponseError, TenantsApi } from '@epistola.app/epistola-client'
try {
await new TenantsApi(client).getTenant({ tenantKey: 'acme' })
} catch (e) {
if (e instanceof ProblemDetailException) {
switch (e.typeSlug) {
case KnownProblemSlugs.NOT_FOUND:
log.warn(`tenant not found: ${e.detail}`)
break
case KnownProblemSlugs.VALIDATION_ERROR:
for (const err of e.errors) log.warn(`${err.field}: ${err.message}`) // field-level ValidationProblemDetail
break
case KnownProblemSlugs.DATA_MODEL_VALIDATION_ERROR:
for (const [example, failures] of Object.entries(e.validationErrors)) { // 422 data-model failures
for (const f of failures) log.warn(`${example} ${f.path}: ${f.message}`)
}
break
case KnownProblemSlugs.RELEASE_DEPENDENCIES_MISSING:
for (const d of e.missingDependencies) { // 409 from deployCatalogRelease
log.warn(`${d.registry}/${d.namespace}/${d.catalogKey}@${d.version}: ${d.reason}`)
}
break
case KnownProblemSlugs.RELEASE_DEPENDENCIES_UNRELEASED:
for (const d of e.unreleasedDependencies) { // 409 from releaseCatalogDraft
log.warn(`${d.registry}/${d.namespace}/${d.catalogKey}: draft ${d.releaseName} (${d.releaseId})`)
}
break
case KnownProblemSlugs.CATALOG_READ_ONLY:
// any write to a catalog that cannot be changed here
log.warn(`catalog cannot be changed here: ${e.readOnlyReason}`)
break
case KnownProblemSlugs.MERGE_CONFLICTS_UNRESOLVED:
for (const conflict of e.unresolvedConflicts) log.warn(`needs ours or theirs: ${conflict}`) // mergeIntoCatalogDraft (alpha)
break
case KnownProblemSlugs.MERGE_PREVIEW_OUTDATED:
log.warn('the draft or the release changed since the preview: preview again') // expectedToken (alpha)
break
case KnownProblemSlugs.MERGE_BASE_MISSING:
// the merge base is not installed here: install it first, then merge again
log.warn(`install ${e.missingBaseVersion} (${e.missingBaseFingerprint}) first`)
break
case KnownProblemSlugs.CATALOG_VERSION_BEHIND:
log.warn(`the draft is behind ${e.behind.join(', ')}`) // releaseCatalogDraft: name them in acknowledgedBehind
break
case KnownProblemSlugs.CATALOG_DRAFT_IN_USE:
// deleteCatalogRelease: drafts of other catalogs name the draft, or it alone holds resources
// something outside it uses (resourceType/resourceKey); change those first
log.warn(`named by ${e.dependents.map((d) => `${d.catalogKey}: ${d.releaseName}`).join(', ')}; holds ${e.heldResources.join(', ')}`)
break
case KnownProblemSlugs.AMBIGUOUS_VARIANT:
log.warn(`several variants match: ${e.tiedVariants.join(', ')}`) // generateDocument, previewDocument: name one by variantKey
break
case KnownProblemSlugs.FORBIDDEN:
// any refused authorization: a permission in a tenant, access to a tenant, a platform role,
// or a policy (refusalReason), each read from this problem only
log.warn(`refused: missing ${e.requiredPermission} in ${e.refusedTenantKey}`)
break
case KnownProblemSlugs.CATALOG_RENDERING_DEFAULTS_TOO_NEW:
// importCatalog / installCatalogRelease: the release renders with rendering defaults this
// installation does not have
log.warn(`upgrade Epistola: the release needs ${e.extensions.renderingDefaultsVersion}, this has up to ${e.extensions.supportedVersion}`)
break
case KnownProblemSlugs.RELEASE_NUMBER_TAKEN:
// publishReleaseToRegistry (alpha): the registry has this version with other content;
// importCatalog, installCatalogRelease, installRegistryRelease: the tenant has this version
// with another fingerprint, or this release id as another release
log.warn('release the changes under a new version')
break
case KnownProblemSlugs.CATALOG_SOURCE_UNAVAILABLE:
// installCatalogRelease / listAvailableCatalogReleases: 503 when the catalog's source could
// not be reached (Retry-After may say when), 502 when it answered badly
log.warn(`catalog source unavailable (${e.problemStatus}): ${e.detail}`)
break
default:
throw e // a type this client does not know: handle it by e.problemStatus
}
} else if (e instanceof ResponseError) {
// an error response without a problem document
}
}
每个字段出错 code 说为什么球场失败(min-length, (中文(简体) ), stencil-recursion, (中文(简体) ),
catalog-resources-not-ready,或为 undefined 当服务器发送无。 比较一下
KnownValidationCodes生成自合同登记处,并保留 default 分支: 新建
代码可能会以小版本出现.
ProblemDetailException 扩展生成的 ResponseError,因此已经存在 instanceof ResponseError 网站继续工作。 它会暴露 type, (中文(简体) ), typeSlug, (中文(简体) ), title, (中文(简体) ), problemStatus, (中文(简体) ),
detail, (中文(简体) ), instance, (中文(简体) ), help (该类型带有此请求上下文的页面,例如
?operation=getTemplate,或 undefined:在类型上切换,而不是在这个上,打开它以示提示),
errors, (中文(简体) ), validationErrors, (中文(简体) ), missingFields, (中文(简体) ), invalidFields, (中文(简体) ),
missingDependencies, (中文(简体) ), unreleasedDependencies, (中文(简体) ), readOnlyReason, (中文(简体) ), unresolvedConflicts, (中文(简体) ),
isMergeConflictsUnresolvedProblem, (中文(简体) ), missingBaseVersion, (中文(简体) ), missingBaseFingerprint, (中文(简体) ),
isMergeBaseMissingProblem, (中文(简体) ), behind, (中文(简体) ), isCatalogVersionBehindProblem, (中文(简体) ), environments, (中文(简体) ),
pinnedBy, (中文(简体) ), isReleaseInUseProblem, (中文(简体) ), dependents, (中文(简体) ), heldResources, (中文(简体) ), isCatalogDraftInUseProblem, (中文(简体) ),
tiedVariants, (中文(简体) ), isAmbiguousVariantProblem, (中文(简体) ),
requiredPermission, (中文(简体) ), refusedTenantKey, (中文(简体) ), requiredPlatformRole, (中文(简体) ), refusalReason, (中文(简体) ),
isForbiddenProblem, (中文(简体) ), extensions (每个成员)
在基地成员之外,RFC 9457的五和 help, (中文(简体) ), statusCode, (中文(简体) ), rawBody, (中文(简体) ), isValidationProblem, (中文(简体) ),
isDataModelValidationProblem, (中文(简体) ), isTemplateDataProblem, (中文(简体) ), isReleaseDependenciesMissingProblem, (中文(简体) ),
isReleaseDependenciesUnreleasedProblem,以及 isCatalogReadOnlyProblem。 。 。 .
A 类 release-dependencies-unreleased 问题(409) releaseCatalogDraft 被拒绝的原因是
声明草案列出其他目录的草稿,并只列出一个发布针
释放. unreleasedDependencies 列出每一份此类草案(registry, (中文(简体) ), namespace, (中文(简体) ), catalogKey, (中文(简体) ),
releaseId, (中文(简体) ), releaseName; 先释放, 或发送 releaseDependencies: true 释放他们
与草案.
A 类 catalog-draft-in-use 问题(409) deleteCatalogRelease 被拒绝是因为什么
在草案之外,它仍然依赖它. dependents 列出其他目录的草稿
扶养声明名称registry, (中文(简体) ), namespace, (中文(简体) ), catalogKey, (中文(简体) ), releaseId, (中文(简体) ), releaseName, (中文(简体) ),
财务报告和审定财务报表 heldResources 资源只有它认为它以外的东西 仍然使用,因为
resourceType/resourceKey (单位:千美元)theme/house-style先换个吧.
A 类 release-dependencies-missing 问题(409) deployCatalogRelease 由于释放而被拒绝
在其他目录中,解禁针不能取出:租户没有,他们不能取出
用于获取或核实. missingDependencies 各列表,并附有 reason 保留为字符串
服务器已发送( 比较它与 MissingReleaseDependencyReason) (中文(简体) ). 使用依赖释放
默示; 通过 includeDependencies: true 明确部署:
await new EnvironmentsApi(client).deployCatalogRelease({
tenantKey: 'acme', environmentKey: 'production',
registry: '~', namespace: 'acme', catalogKey: 'invoices', release: '1.4.0',
includeDependencies: true,
})
结果收集
import { ResultCollector } from '@epistola.app/epistola-client'
const collector = ResultCollector.builder()
.client(client)
.tenantKey('acme')
.handler(async (result) => console.log(result.requestId, result.status))
.build()
await collector.start() // adaptive polling; resolves after collector.stop()
一次一个结果流,批次只有在结果逐一结果之后才予以确认
处理: 丢出批量的处理器未被确认, 因此它被重新交付。 gzip 总是
已解码;zstd提供并解码到Node的zlib拥有它的地方(22.15+). partitionFor, (中文(简体) ),
isMyPartition 财务报告和审定财务报表 routingKeyToMe 从任务中计算服务器报告的一个路由密钥
他们的结果会回到这个节点.
模板数据验证
模板数据由 爱皮斯托拉,通过 validateTemplateData这里没有编译,
因此,没有计划编译器可以安装,判决不能不同意哪一代人会
do ——服务器在提交任务时验证相同数据,并且它也知道哪个可选
字段所显示的释放实际上已读取.
import { ServerTemplateDataValidator, TemplateSchemaValidator, ValidatingGenerationApi, validateCreateTenantRequest } from '@epistola.app/epistola-client'
const validator = new TemplateSchemaValidator(templatesApi)
// tenant, catalog registry (`~` for the server's default, or in full: 'acme.com'), namespace, catalog key, template
await validator.validate('acme', '~', 'acme', 'invoices', 'invoice', data) // throws TemplateDataValidationException
// Against a particular release: a version range, an environment, or both together
const pinned = new TemplateSchemaValidator(
new ServerTemplateDataValidator(templatesApi, { versionRange: '^1.4.0', environmentKey: 'production' }),
)
// Or around generation, which turns the server's `template-data-invalid` problem into the same error:
const generation = new ValidatingGenerationApi(new GenerationApi(client), templatesApi)
// A request model against the constraints the contract declares on it (key patterns, lengths, ranges):
validateCreateTenantRequest({ key: 'Acme Corp', name: 'Acme' }) // throws ModelValidationException
每一次失败都带有 JSON 指针 输入数据(/customer/emailJSON斯克马 keyword
失败了,一个人能读 message ——所以一种形式可以标记错误的字段.
正在验证处理中
避免每次检查的请求——如在飞行前进行大量检查——执行 TemplateDataValidator
在您所选择的编译器之上, 而不是通过 API :
const validator = new TemplateSchemaValidator(new MyAjvValidator(templatesApi, '1.4.0'))
const generation = new ValidatingGenerationApi(new GenerationApi(client), new MyAjvValidator(templatesApi, '1.4.0'))
解答进程内声明的验证器 preflightsGeneration = true (默认),故
ValidatingGenerationApi 在提交和报告每批物品之前进行核对
路径前缀 items[<index>]。 。 。 . TtlSchemaCache 仍然导出用于缓存取来的计划;
不需要"本地"验证员 就需要每通电话的往返费 模版在发布
他们的目录,所以一个本地验证器从您命名的发布中获取其计划( ) 1.4.0 名称:
您将发布, 因为服务器验证 相对于该发布, 而不是反对草稿.
有两件事,一个是执行的,因为呼吁者都提到 TemplateDataValidator: 一个空
数组表示数据是可以接受的,而且 path 是JSON指针 编纂者不同意这一点
最后一点——Ajv's instancePath 已经是一个指针,但是要单独列出一个丢失的财产,
而其他引擎则会发出JSONPath——因此转换是适配器的工作. 一个有效的Ajv适配器,
大约四十行,在 test/validation/local/ajvTemplateDataValidator.ts; 拷贝.
注意当地的判决是自己的: format 语法、方言覆盖范围和未知关键词
处理是编译器的选择,因此它可以接受数据 Epistola 会拒绝,或者拒绝数据
埃皮斯托拉会变的.
发展
客户端由捆绑的光谱生成. 从寄存器根 :
make bundle # produce openapi.yaml
cd contracts/api/clients/nodejs-fetch
pnpm install --frozen-lockfile
./generate.sh # stock client + derived sources into src/generated/
pnpm build # dist/
pnpm test # node:test, against the compiled tree
src/generated/ (股票客户端 api/,并生成 contractVersion, (中文(简体) ),
contractIdentity, (中文(简体) ), contractMediaTypes, (中文(简体) ), contractOperations, (中文(简体) ), knownProblemSlugs 财务报告和审定财务报表
modelValidation)被从光谱中分出并重建出每次. 跨客户的合规性
套件驱动此客户端 make conformance-node。 。 。 .