@badrap/libapp
Advanced tools
Comparing version 0.1.5 to 0.1.6
import * as v from "@badrap/valita"; | ||
export declare class HTTPError extends Error { | ||
readonly statusCode: number; | ||
readonly statusText: string; | ||
constructor(statusCode: number, statusText: string); | ||
} | ||
import { HTTPError } from "./base.js"; | ||
import { Kv } from "./kv.js"; | ||
export { HTTPError }; | ||
export declare class UpdateFailed extends Error { | ||
@@ -28,8 +26,7 @@ constructor(); | ||
export declare class API<InstallationState extends Record<string, unknown> = Record<string, unknown>> { | ||
private readonly apiToken; | ||
private readonly baseUrl; | ||
private readonly base; | ||
private readonly stateType; | ||
readonly experimentalKv: Kv; | ||
constructor(apiUrl: string, apiToken: string, stateType?: v.Type<InstallationState>); | ||
private installationUrl; | ||
private request; | ||
private installationPath; | ||
checkAuthToken(token: string): Promise<{ | ||
@@ -76,2 +73,1 @@ installationId: string; | ||
} | ||
export {}; |
@@ -25,22 +25,11 @@ "use strict"; | ||
}; | ||
var __importDefault = (this && this.__importDefault) || function (mod) { | ||
return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.API = exports.UpdateFailed = exports.HTTPError = void 0; | ||
const node_path_1 = __importDefault(require("node:path")); | ||
const v = __importStar(require("@badrap/valita")); | ||
const undici_1 = require("undici"); | ||
const base_js_1 = require("./base.js"); | ||
Object.defineProperty(exports, "HTTPError", { enumerable: true, get: function () { return base_js_1.HTTPError; } }); | ||
const kv_js_1 = require("./kv.js"); | ||
function parse(data, type) { | ||
return type.parse(data, { mode: "strip" }); | ||
} | ||
class HTTPError extends Error { | ||
constructor(statusCode, statusText) { | ||
super(`HTTP status code ${statusCode} (${statusText})`); | ||
this.statusCode = statusCode; | ||
this.statusText = statusText; | ||
Object.setPrototypeOf(this, new.target.prototype); | ||
} | ||
} | ||
exports.HTTPError = HTTPError; | ||
class UpdateFailed extends Error { | ||
@@ -55,29 +44,19 @@ constructor() { | ||
constructor(apiUrl, apiToken, stateType) { | ||
this.base = new base_js_1.APIBase(apiUrl, apiToken); | ||
this.stateType = stateType !== null && stateType !== void 0 ? stateType : v.record(); | ||
this.apiToken = apiToken; | ||
this.baseUrl = new URL(apiUrl); | ||
this.baseUrl.pathname = node_path_1.default.posix.join(this.baseUrl.pathname, "app/"); | ||
this.experimentalKv = new kv_js_1.Kv(this.base); | ||
} | ||
installationUrl(installationId, path) { | ||
installationPath(installationId, path) { | ||
if (!installationId.match(/^[a-z0-9_-]{1,}$/i)) { | ||
throw new Error("invalid installation ID"); | ||
} | ||
const url = new URL(`installations/${installationId}`, this.baseUrl); | ||
let pathname = `installations/${installationId}`; | ||
if (path) { | ||
url.pathname += `/${path}`; | ||
pathname += `/${path}`; | ||
} | ||
url.pathname = url.pathname.replace(/\/+/g, "/"); | ||
return url; | ||
return pathname.replace(/\/+/g, "/"); | ||
} | ||
async request(url, options) { | ||
const headers = new undici_1.Headers(options.headers); | ||
headers.set("Authorization", `Bearer ${this.apiToken}`); | ||
const response = await (0, undici_1.fetch)(url, { ...options, headers }); | ||
if (!response.ok) { | ||
throw new HTTPError(response.status, response.statusText); | ||
} | ||
return response; | ||
} | ||
async checkAuthToken(token) { | ||
const result = await this.request(new URL("token", this.baseUrl), { | ||
const result = await this.base | ||
.request("/token", { | ||
method: "POST", | ||
@@ -90,3 +69,4 @@ headers: { | ||
}), | ||
}).then((r) => r.json()); | ||
}) | ||
.then((r) => r.json()); | ||
const { installation_id: installationId, session_id: sessionId, expires_at: expiresAt, } = parse(result, v.object({ | ||
@@ -100,3 +80,4 @@ installation_id: v.string(), | ||
async seal(data, expiresIn) { | ||
const result = await this.request(new URL("seal", this.baseUrl), { | ||
const result = await this.base | ||
.request("/seal", { | ||
method: "POST", | ||
@@ -110,7 +91,9 @@ headers: { | ||
}), | ||
}).then((r) => r.json()); | ||
}) | ||
.then((r) => r.json()); | ||
return parse(result, v.object({ data: v.string() }).map((r) => r.data)); | ||
} | ||
async unseal(data) { | ||
const result = await this.request(new URL("unseal", this.baseUrl), { | ||
const result = await this.base | ||
.request("/unseal", { | ||
method: "POST", | ||
@@ -123,9 +106,9 @@ headers: { | ||
}), | ||
}).then((r) => r.json()); | ||
}) | ||
.then((r) => r.json()); | ||
return parse(result, v.object({ data: v.unknown() }).map((r) => r.data)); | ||
} | ||
async getInstallations() { | ||
return this.request(new URL("installations", this.baseUrl), { | ||
method: "GET", | ||
}) | ||
return this.base | ||
.request("/installations", { method: "GET" }) | ||
.then((r) => r.json()) | ||
@@ -141,3 +124,4 @@ .then((r) => parse(r, v.array(v.object({ | ||
async createInstallationCallback(installationId, sessionId, callback = {}) { | ||
return this.request(this.installationUrl(installationId, "/callbacks"), { | ||
return this.base | ||
.request(this.installationPath(installationId, "/callbacks"), { | ||
method: "POST", | ||
@@ -157,3 +141,4 @@ headers: { | ||
async getInstallation(installationId) { | ||
return this.request(this.installationUrl(installationId), { | ||
return this.base | ||
.request(this.installationPath(installationId), { | ||
method: "GET", | ||
@@ -169,5 +154,5 @@ }) | ||
const { maxRetries = Infinity } = options !== null && options !== void 0 ? options : {}; | ||
const url = this.installationUrl(installationId); | ||
const path = this.installationPath(installationId); | ||
for (let i = 0; i <= maxRetries; i++) { | ||
const response = await this.request(url, { method: "GET" }); | ||
const response = await this.base.request(path, { method: "GET" }); | ||
const etag = response.headers.get("etag"); | ||
@@ -187,3 +172,3 @@ const input = await response.json().then((r) => parse(r, v.object({ | ||
try { | ||
await this.request(url, { | ||
await this.base.request(path, { | ||
method: "PATCH", | ||
@@ -198,3 +183,3 @@ headers: { | ||
catch (err) { | ||
if (err instanceof HTTPError && err.statusCode === 412) { | ||
if (err instanceof base_js_1.HTTPError && err.statusCode === 412) { | ||
continue; | ||
@@ -209,8 +194,9 @@ } | ||
async removeInstallation(installationId) { | ||
await this.request(this.installationUrl(installationId), { | ||
method: "DELETE", | ||
}).then((r) => r.arrayBuffer()); | ||
await this.base | ||
.request(this.installationPath(installationId), { method: "DELETE" }) | ||
.then((r) => r.arrayBuffer()); | ||
} | ||
async listOwnerAssets(installationId) { | ||
return this.request(this.installationUrl(installationId, "/owner/assets"), { | ||
return this.base | ||
.request(this.installationPath(installationId, "/owner/assets"), { | ||
method: "GET", | ||
@@ -226,3 +212,3 @@ }) | ||
try { | ||
await this.request(new URL("feeds", this.baseUrl), { | ||
await this.base.request("/feeds", { | ||
method: "POST", | ||
@@ -241,6 +227,6 @@ headers: { | ||
catch (err) { | ||
if (!(err instanceof HTTPError) || err.statusCode !== 409) { | ||
if (!(err instanceof base_js_1.HTTPError) || err.statusCode !== 409) { | ||
throw err; | ||
} | ||
await this.request(new URL(`feeds/${encodeURIComponent(name)}`, this.baseUrl), { | ||
await this.base.request(`/feeds/${encodeURIComponent(name)}`, { | ||
method: "PATCH", | ||
@@ -260,3 +246,3 @@ headers: { | ||
async feedEventsForInstallation(installationId, feedName, events) { | ||
await this.request(this.installationUrl(installationId, `/feeds/${encodeURIComponent(feedName)}/events`), { | ||
await this.base.request(this.installationPath(installationId, `/feeds/${encodeURIComponent(feedName)}/events`), { | ||
method: "POST", | ||
@@ -263,0 +249,0 @@ headers: { |
{ | ||
"name": "@badrap/libapp", | ||
"version": "0.1.5", | ||
"version": "0.1.6", | ||
"description": "TypeScript helpers for creating Badrap apps", | ||
@@ -43,8 +43,8 @@ "repository": { | ||
"prettier": "^2.8.4", | ||
"typescript": "^4.9.5" | ||
"typescript": "^5.1.6" | ||
}, | ||
"dependencies": { | ||
"@badrap/valita": "^0.2.0", | ||
"undici": "^5.20.0" | ||
"@badrap/valita": "^0.2.1", | ||
"undici": "^5.22.1" | ||
} | ||
} |
@@ -1,5 +0,7 @@ | ||
import path from "node:path"; | ||
import * as v from "@badrap/valita"; | ||
import { fetch, RequestInit, Headers, Response } from "undici"; | ||
import { HTTPError, APIBase } from "./base.js"; | ||
import { Kv } from "./kv.js"; | ||
export { HTTPError }; | ||
function parse<T>(data: unknown, type: v.Type<T>): T { | ||
@@ -9,9 +11,2 @@ return type.parse(data, { mode: "strip" }); | ||
export class HTTPError extends Error { | ||
constructor(readonly statusCode: number, readonly statusText: string) { | ||
super(`HTTP status code ${statusCode} (${statusText})`); | ||
Object.setPrototypeOf(this, new.target.prototype); | ||
} | ||
} | ||
export class UpdateFailed extends Error { | ||
@@ -48,5 +43,5 @@ constructor() { | ||
> { | ||
private readonly apiToken: string; | ||
private readonly baseUrl: URL; | ||
private readonly base: APIBase; | ||
private readonly stateType: v.Type<InstallationState>; | ||
readonly experimentalKv: Kv; | ||
@@ -58,43 +53,32 @@ constructor( | ||
) { | ||
this.base = new APIBase(apiUrl, apiToken); | ||
this.stateType = stateType ?? (v.record() as v.Type<InstallationState>); | ||
this.apiToken = apiToken; | ||
this.baseUrl = new URL(apiUrl); | ||
this.baseUrl.pathname = path.posix.join(this.baseUrl.pathname, "app/"); | ||
this.experimentalKv = new Kv(this.base); | ||
} | ||
private installationUrl(installationId: string, path?: string): URL { | ||
private installationPath(installationId: string, path?: string): string { | ||
if (!installationId.match(/^[a-z0-9_-]{1,}$/i)) { | ||
throw new Error("invalid installation ID"); | ||
} | ||
const url = new URL(`installations/${installationId}`, this.baseUrl); | ||
let pathname = `installations/${installationId}`; | ||
if (path) { | ||
url.pathname += `/${path}`; | ||
pathname += `/${path}`; | ||
} | ||
url.pathname = url.pathname.replace(/\/+/g, "/"); | ||
return url; | ||
return pathname.replace(/\/+/g, "/"); | ||
} | ||
private async request(url: URL, options: RequestInit): Promise<Response> { | ||
const headers = new Headers(options.headers); | ||
headers.set("Authorization", `Bearer ${this.apiToken}`); | ||
const response = await fetch(url, { ...options, headers }); | ||
if (!response.ok) { | ||
throw new HTTPError(response.status, response.statusText); | ||
} | ||
return response; | ||
} | ||
async checkAuthToken( | ||
token: string | ||
): Promise<{ installationId: string; sessionId: string; expiresAt: number }> { | ||
const result = await this.request(new URL("token", this.baseUrl), { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify({ | ||
token, | ||
}), | ||
}).then((r) => r.json()); | ||
const result = await this.base | ||
.request("/token", { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify({ | ||
token, | ||
}), | ||
}) | ||
.then((r) => r.json()); | ||
@@ -118,12 +102,14 @@ const { | ||
async seal(data: unknown, expiresIn: number): Promise<string> { | ||
const result = await this.request(new URL("seal", this.baseUrl), { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify({ | ||
expires_in: expiresIn, | ||
data, | ||
}), | ||
}).then((r) => r.json()); | ||
const result = await this.base | ||
.request("/seal", { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify({ | ||
expires_in: expiresIn, | ||
data, | ||
}), | ||
}) | ||
.then((r) => r.json()); | ||
@@ -137,11 +123,13 @@ return parse( | ||
async unseal(data: string): Promise<unknown> { | ||
const result = await this.request(new URL("unseal", this.baseUrl), { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify({ | ||
data, | ||
}), | ||
}).then((r) => r.json()); | ||
const result = await this.base | ||
.request("/unseal", { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify({ | ||
data, | ||
}), | ||
}) | ||
.then((r) => r.json()); | ||
@@ -161,5 +149,4 @@ return parse( | ||
> { | ||
return this.request(new URL("installations", this.baseUrl), { | ||
method: "GET", | ||
}) | ||
return this.base | ||
.request("/installations", { method: "GET" }) | ||
.then((r) => r.json()) | ||
@@ -193,13 +180,14 @@ .then((r) => | ||
): Promise<string> { | ||
return this.request(this.installationUrl(installationId, "/callbacks"), { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify({ | ||
session_id: sessionId, | ||
action: callback.action, | ||
client_state: callback.clientState, | ||
}), | ||
}) | ||
return this.base | ||
.request(this.installationPath(installationId, "/callbacks"), { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify({ | ||
session_id: sessionId, | ||
action: callback.action, | ||
client_state: callback.clientState, | ||
}), | ||
}) | ||
.then((r) => r.json()) | ||
@@ -217,5 +205,6 @@ .then((r) => | ||
): Promise<Installation<InstallationState>> { | ||
return this.request(this.installationUrl(installationId), { | ||
method: "GET", | ||
}) | ||
return this.base | ||
.request(this.installationPath(installationId), { | ||
method: "GET", | ||
}) | ||
.then((r) => r.json()) | ||
@@ -245,6 +234,6 @@ .then((r) => | ||
const url = this.installationUrl(installationId); | ||
const path = this.installationPath(installationId); | ||
for (let i = 0; i <= maxRetries; i++) { | ||
const response = await this.request(url, { method: "GET" }); | ||
const response = await this.base.request(path, { method: "GET" }); | ||
const etag = response.headers.get("etag"); | ||
@@ -271,3 +260,3 @@ const input = await response.json().then((r) => | ||
try { | ||
await this.request(url, { | ||
await this.base.request(path, { | ||
method: "PATCH", | ||
@@ -293,5 +282,5 @@ headers: { | ||
async removeInstallation(installationId: string): Promise<void> { | ||
await this.request(this.installationUrl(installationId), { | ||
method: "DELETE", | ||
}).then((r) => r.arrayBuffer()); | ||
await this.base | ||
.request(this.installationPath(installationId), { method: "DELETE" }) | ||
.then((r) => r.arrayBuffer()); | ||
} | ||
@@ -302,5 +291,6 @@ | ||
): Promise<{ type: "ip" | "email" | "domain"; value: string }[]> { | ||
return this.request(this.installationUrl(installationId, "/owner/assets"), { | ||
method: "GET", | ||
}) | ||
return this.base | ||
.request(this.installationPath(installationId, "/owner/assets"), { | ||
method: "GET", | ||
}) | ||
.then((r) => r.json()) | ||
@@ -333,3 +323,3 @@ .then((r) => | ||
try { | ||
await this.request(new URL("feeds", this.baseUrl), { | ||
await this.base.request("/feeds", { | ||
method: "POST", | ||
@@ -350,17 +340,14 @@ headers: { | ||
} | ||
await this.request( | ||
new URL(`feeds/${encodeURIComponent(name)}`, this.baseUrl), | ||
{ | ||
method: "PATCH", | ||
headers: { | ||
"Content-Type": "application/json", | ||
"If-Match": "*", | ||
}, | ||
body: JSON.stringify({ | ||
title: config?.title, | ||
summary_template: config?.summaryTemplate, | ||
details_template: config?.detailsTemplate, | ||
}), | ||
} | ||
); | ||
await this.base.request(`/feeds/${encodeURIComponent(name)}`, { | ||
method: "PATCH", | ||
headers: { | ||
"Content-Type": "application/json", | ||
"If-Match": "*", | ||
}, | ||
body: JSON.stringify({ | ||
title: config?.title, | ||
summary_template: config?.summaryTemplate, | ||
details_template: config?.detailsTemplate, | ||
}), | ||
}); | ||
} | ||
@@ -374,4 +361,4 @@ } | ||
): Promise<void> { | ||
await this.request( | ||
this.installationUrl( | ||
await this.base.request( | ||
this.installationPath( | ||
installationId, | ||
@@ -378,0 +365,0 @@ `/feeds/${encodeURIComponent(feedName)}/events` |
@@ -13,5 +13,6 @@ { | ||
"forceConsistentCasingInFileNames": true, | ||
"noFallthroughCasesInSwitch": true | ||
"noFallthroughCasesInSwitch": true, | ||
"noUncheckedIndexedAccess": true | ||
}, | ||
"include": ["./src/**/*"] | ||
} |
Sorry, the diff of this file is not supported yet
61735
34
1407
Updated@badrap/valita@^0.2.1
Updatedundici@^5.22.1