Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/publish/quarto-pub/api/index.ts
6460 views
1
/*
2
* index.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*
6
*/
7
8
import { ApiError } from "../../types.ts";
9
import {
10
AccessToken,
11
AccountSite,
12
PublishDeploy,
13
Site,
14
Ticket,
15
} from "./types.ts";
16
17
// The Accept: application/json header.
18
const acceptApplicationJsonHeader = {
19
Accept: "application/json",
20
};
21
22
// The Content-Type: application/json header.
23
const contentTypeApplicationJsonHeader = {
24
"Content-Type": "application/json",
25
};
26
27
const kUrlResolveRegex = /https:\/\/quartopub\.com\/sites\/([^\/]+)\/(.*)/;
28
29
// Creates an authorization header, if a token was supplied.
30
const authorizationHeader = (
31
token?: string,
32
): HeadersInit => (!token ? {} : { Authorization: `Bearer ${token}` });
33
34
export class QuartoPubClient {
35
private readonly baseURL_: string;
36
constructor(environment: string, private readonly token_?: string) {
37
switch (environment) {
38
case "LOCAL":
39
this.baseURL_ = "http://localhost:3000/api/v1";
40
break;
41
42
case "PRODUCTION":
43
default:
44
this.baseURL_ = "https://quartopub.com/api/v1";
45
break;
46
}
47
}
48
49
// Creates a ticket.
50
public createTicket = async (client_id: string): Promise<Ticket> => {
51
const response = await fetch(
52
this.createURL(
53
`tickets?${new URLSearchParams({ application_id: client_id })}`,
54
),
55
{
56
method: "POST",
57
headers: {
58
...authorizationHeader(this.token_),
59
...acceptApplicationJsonHeader,
60
},
61
},
62
);
63
64
// If the response was not OK, throw an ApiError.
65
if (!response.ok) {
66
throw new ApiError(response.status, response.statusText);
67
}
68
69
// Return the result.
70
return <Ticket> await response.json();
71
};
72
73
// Shows a ticket.
74
public showTicket = async (id: string): Promise<Ticket> => {
75
// Perform the operation.
76
const response = await fetch(this.createURL(`tickets/${id}`), {
77
method: "GET",
78
headers: {
79
...authorizationHeader(this.token_),
80
...acceptApplicationJsonHeader,
81
},
82
});
83
84
// If the response was not OK, throw an ApiError.
85
if (!response.ok) {
86
throw new ApiError(response.status, response.statusText);
87
}
88
89
// Return the result.
90
return <Ticket> await response.json();
91
};
92
93
// Exchanges a ticket for an access token.
94
public exchangeTicket = async (id: string): Promise<AccessToken> => {
95
// Perform the operation.
96
const response = await fetch(this.createURL(`tickets/${id}/exchange`), {
97
method: "POST",
98
headers: {
99
...authorizationHeader(this.token_),
100
...acceptApplicationJsonHeader,
101
},
102
});
103
104
// If the response was not OK, throw an ApiError.
105
if (!response.ok) {
106
throw new ApiError(response.status, response.statusText);
107
}
108
109
// Return the result.
110
return <AccessToken> await response.json();
111
};
112
113
// Checks if a slug is available.
114
public slugAvailable = async (slug: string): Promise<boolean> => {
115
// Perform the operation.
116
const response = await fetch(this.createURL(`slugs/${slug}`), {
117
method: "HEAD",
118
headers: {
119
...authorizationHeader(this.token_),
120
},
121
});
122
123
// If the response was not OK, the slug is unavailable.
124
if (response.ok) {
125
return false;
126
}
127
128
// If the response was 404, the slug is available.
129
if (response.status == 404) {
130
return true;
131
}
132
133
// Any other response is an error.
134
throw new ApiError(response.status, response.statusText);
135
};
136
137
// Creates a site.
138
public createSite = async (
139
type: string,
140
title: string,
141
slug: string,
142
): Promise<Site> => {
143
// Perform the operation.
144
const response = await fetch(this.createURL("sites"), {
145
method: "POST",
146
headers: {
147
...authorizationHeader(this.token_),
148
...acceptApplicationJsonHeader,
149
},
150
body: new URLSearchParams({ type, title, slug }),
151
});
152
153
// If the response was not OK, throw an ApiError.
154
if (!response.ok) {
155
throw new ApiError(response.status, response.statusText);
156
}
157
158
// Return the result.
159
const site = <Site> await response.json();
160
site.url = this.resolveUrl(site.url);
161
return site;
162
};
163
164
// Creates a site deploy.
165
public createDeploy = async (
166
siteId: string,
167
files: Record<string, string>,
168
size: number,
169
): Promise<PublishDeploy> => {
170
// Perform the operation.
171
const response = await fetch(
172
this.createURL(`sites/${siteId}/deploys`),
173
{
174
method: "POST",
175
headers: {
176
...authorizationHeader(this.token_),
177
...acceptApplicationJsonHeader,
178
...contentTypeApplicationJsonHeader,
179
},
180
body: JSON.stringify({
181
size,
182
files,
183
}),
184
// body: JSON.stringify(files),
185
},
186
);
187
188
// If the response was not OK, throw an ApiError.
189
if (!response.ok) {
190
const description = await descriptionFromErrorResponse(response);
191
throw new ApiError(response.status, description || response.statusText);
192
}
193
194
// Return the result.
195
const deploy = <PublishDeploy> await response.json();
196
deploy.url = this.resolveUrl(deploy.url);
197
return deploy;
198
};
199
200
// Gets a deploy.
201
public getDeploy = async (deployId: string): Promise<PublishDeploy> => {
202
// Perform the operation.
203
const response = await fetch(this.createURL(`deploys/${deployId}`), {
204
method: "GET",
205
headers: {
206
...authorizationHeader(this.token_),
207
...acceptApplicationJsonHeader,
208
},
209
});
210
211
// If the response was not OK, throw an ApiError.
212
if (!response.ok) {
213
throw new ApiError(response.status, response.statusText);
214
}
215
216
// Return the result.
217
const deploy = <PublishDeploy> await response.json();
218
deploy.url = this.resolveUrl(deploy.url);
219
return deploy;
220
};
221
222
// Uploads a deploy file.
223
public uploadDeployFile = async (
224
deployId: string,
225
path: string,
226
fileBody: Blob,
227
): Promise<void> => {
228
// Perform the operation.
229
const response = await fetch(
230
this.createURL(`deploys/${deployId}/files/${path}`),
231
{
232
method: "PUT",
233
headers: {
234
...authorizationHeader(this.token_),
235
},
236
body: fileBody,
237
},
238
);
239
240
// If the response was not OK, throw an ApiError.
241
if (!response.ok) {
242
throw new ApiError(response.status, response.statusText);
243
}
244
};
245
246
// Updates the account site.
247
public updateAccountSite = async (): Promise<AccountSite> => {
248
// Perform the operation.
249
const response = await fetch(this.createURL("update-account-site"), {
250
method: "PUT",
251
headers: {
252
...authorizationHeader(this.token_),
253
...acceptApplicationJsonHeader,
254
},
255
});
256
257
// If the response was not OK, throw an ApiError.
258
if (!response.ok) {
259
throw new ApiError(response.status, response.statusText);
260
}
261
262
// Return the result.
263
return <AccountSite> await response.json();
264
};
265
266
// Creates a URL.
267
private createURL = (path: string) => `${this.baseURL_}/${path}`;
268
269
// Resolve the URL into a form that can be used to address resources
270
// (not just the root redirect). For example this form allows
271
// social metadata cards to properly form links to images, and so on.
272
private resolveUrl = (url: string) => {
273
const match = url.match(kUrlResolveRegex);
274
if (match) {
275
return `https://${match[1]}.quarto.pub/${match[2]}`;
276
} else {
277
return url;
278
}
279
};
280
}
281
282
async function descriptionFromErrorResponse(response: Response) {
283
// if there is a body, see if its a quarto pub error w/ description
284
if (response.body) {
285
try {
286
const result = await response.json();
287
if (typeof (result.description) === "string") {
288
return result.description;
289
}
290
} catch {
291
//
292
}
293
}
294
}
295
296