Path: blob/main/src/publish/quarto-pub/api/index.ts
6460 views
/*1* index.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*5*/67import { ApiError } from "../../types.ts";8import {9AccessToken,10AccountSite,11PublishDeploy,12Site,13Ticket,14} from "./types.ts";1516// The Accept: application/json header.17const acceptApplicationJsonHeader = {18Accept: "application/json",19};2021// The Content-Type: application/json header.22const contentTypeApplicationJsonHeader = {23"Content-Type": "application/json",24};2526const kUrlResolveRegex = /https:\/\/quartopub\.com\/sites\/([^\/]+)\/(.*)/;2728// Creates an authorization header, if a token was supplied.29const authorizationHeader = (30token?: string,31): HeadersInit => (!token ? {} : { Authorization: `Bearer ${token}` });3233export class QuartoPubClient {34private readonly baseURL_: string;35constructor(environment: string, private readonly token_?: string) {36switch (environment) {37case "LOCAL":38this.baseURL_ = "http://localhost:3000/api/v1";39break;4041case "PRODUCTION":42default:43this.baseURL_ = "https://quartopub.com/api/v1";44break;45}46}4748// Creates a ticket.49public createTicket = async (client_id: string): Promise<Ticket> => {50const response = await fetch(51this.createURL(52`tickets?${new URLSearchParams({ application_id: client_id })}`,53),54{55method: "POST",56headers: {57...authorizationHeader(this.token_),58...acceptApplicationJsonHeader,59},60},61);6263// If the response was not OK, throw an ApiError.64if (!response.ok) {65throw new ApiError(response.status, response.statusText);66}6768// Return the result.69return <Ticket> await response.json();70};7172// Shows a ticket.73public showTicket = async (id: string): Promise<Ticket> => {74// Perform the operation.75const response = await fetch(this.createURL(`tickets/${id}`), {76method: "GET",77headers: {78...authorizationHeader(this.token_),79...acceptApplicationJsonHeader,80},81});8283// If the response was not OK, throw an ApiError.84if (!response.ok) {85throw new ApiError(response.status, response.statusText);86}8788// Return the result.89return <Ticket> await response.json();90};9192// Exchanges a ticket for an access token.93public exchangeTicket = async (id: string): Promise<AccessToken> => {94// Perform the operation.95const response = await fetch(this.createURL(`tickets/${id}/exchange`), {96method: "POST",97headers: {98...authorizationHeader(this.token_),99...acceptApplicationJsonHeader,100},101});102103// If the response was not OK, throw an ApiError.104if (!response.ok) {105throw new ApiError(response.status, response.statusText);106}107108// Return the result.109return <AccessToken> await response.json();110};111112// Checks if a slug is available.113public slugAvailable = async (slug: string): Promise<boolean> => {114// Perform the operation.115const response = await fetch(this.createURL(`slugs/${slug}`), {116method: "HEAD",117headers: {118...authorizationHeader(this.token_),119},120});121122// If the response was not OK, the slug is unavailable.123if (response.ok) {124return false;125}126127// If the response was 404, the slug is available.128if (response.status == 404) {129return true;130}131132// Any other response is an error.133throw new ApiError(response.status, response.statusText);134};135136// Creates a site.137public createSite = async (138type: string,139title: string,140slug: string,141): Promise<Site> => {142// Perform the operation.143const response = await fetch(this.createURL("sites"), {144method: "POST",145headers: {146...authorizationHeader(this.token_),147...acceptApplicationJsonHeader,148},149body: new URLSearchParams({ type, title, slug }),150});151152// If the response was not OK, throw an ApiError.153if (!response.ok) {154throw new ApiError(response.status, response.statusText);155}156157// Return the result.158const site = <Site> await response.json();159site.url = this.resolveUrl(site.url);160return site;161};162163// Creates a site deploy.164public createDeploy = async (165siteId: string,166files: Record<string, string>,167size: number,168): Promise<PublishDeploy> => {169// Perform the operation.170const response = await fetch(171this.createURL(`sites/${siteId}/deploys`),172{173method: "POST",174headers: {175...authorizationHeader(this.token_),176...acceptApplicationJsonHeader,177...contentTypeApplicationJsonHeader,178},179body: JSON.stringify({180size,181files,182}),183// body: JSON.stringify(files),184},185);186187// If the response was not OK, throw an ApiError.188if (!response.ok) {189const description = await descriptionFromErrorResponse(response);190throw new ApiError(response.status, description || response.statusText);191}192193// Return the result.194const deploy = <PublishDeploy> await response.json();195deploy.url = this.resolveUrl(deploy.url);196return deploy;197};198199// Gets a deploy.200public getDeploy = async (deployId: string): Promise<PublishDeploy> => {201// Perform the operation.202const response = await fetch(this.createURL(`deploys/${deployId}`), {203method: "GET",204headers: {205...authorizationHeader(this.token_),206...acceptApplicationJsonHeader,207},208});209210// If the response was not OK, throw an ApiError.211if (!response.ok) {212throw new ApiError(response.status, response.statusText);213}214215// Return the result.216const deploy = <PublishDeploy> await response.json();217deploy.url = this.resolveUrl(deploy.url);218return deploy;219};220221// Uploads a deploy file.222public uploadDeployFile = async (223deployId: string,224path: string,225fileBody: Blob,226): Promise<void> => {227// Perform the operation.228const response = await fetch(229this.createURL(`deploys/${deployId}/files/${path}`),230{231method: "PUT",232headers: {233...authorizationHeader(this.token_),234},235body: fileBody,236},237);238239// If the response was not OK, throw an ApiError.240if (!response.ok) {241throw new ApiError(response.status, response.statusText);242}243};244245// Updates the account site.246public updateAccountSite = async (): Promise<AccountSite> => {247// Perform the operation.248const response = await fetch(this.createURL("update-account-site"), {249method: "PUT",250headers: {251...authorizationHeader(this.token_),252...acceptApplicationJsonHeader,253},254});255256// If the response was not OK, throw an ApiError.257if (!response.ok) {258throw new ApiError(response.status, response.statusText);259}260261// Return the result.262return <AccountSite> await response.json();263};264265// Creates a URL.266private createURL = (path: string) => `${this.baseURL_}/${path}`;267268// Resolve the URL into a form that can be used to address resources269// (not just the root redirect). For example this form allows270// social metadata cards to properly form links to images, and so on.271private resolveUrl = (url: string) => {272const match = url.match(kUrlResolveRegex);273if (match) {274return `https://${match[1]}.quarto.pub/${match[2]}`;275} else {276return url;277}278};279}280281async function descriptionFromErrorResponse(response: Response) {282// if there is a body, see if its a quarto pub error w/ description283if (response.body) {284try {285const result = await response.json();286if (typeof (result.description) === "string") {287return result.description;288}289} catch {290//291}292}293}294295296