-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathsdk.ts
More file actions
378 lines (345 loc) · 11.2 KB
/
sdk.ts
File metadata and controls
378 lines (345 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
import ValTown from "@valtown/sdk";
import { memoize } from "@std/cache";
import manifest from "../deno.json" with { type: "json" };
import { API_KEY_KEY } from "~/consts.ts";
import { delay } from "@std/async";
const sdk = new ValTown({
// Must get set in vt.ts entrypoint if not set as an env var!
// It needs to be passed here though as *something*
bearerToken: Deno.env.get(API_KEY_KEY) ?? crypto.randomUUID(),
defaultHeaders: { "x-vt-version": String(manifest.version) },
});
/**
* Checks if a Val exists.
*
* @param projectId - The ID of the project to check
* @returns Promise resolving to whether the project exists
*/
export async function valExists(valId: string): Promise<boolean>;
/**
* Checks if a Val exists.
*
* @param options Val identification options
* @param options.username The username of the Val owner
* @param options.valName The name of the Val to check
* @returns Promise resolving to true if the Val exists, false otherwise
*/
export async function valExists(options: {
username: string;
valName: string;
}): Promise<boolean>;
export async function valExists(
valIdOrOptions: string | { username: string; valName: string },
): Promise<boolean> {
try {
if (typeof valIdOrOptions === "string") {
// Val ID provided
const valId = valIdOrOptions;
await sdk.vals.retrieve(valId);
} else {
// Username and Val name provided
const { username, valName } = valIdOrOptions;
await sdk.alias.username.valName.retrieve(username, valName);
}
return true;
} catch (error) {
if (error instanceof ValTown.APIError && error.status === 404) {
return false;
}
throw error; // Re-throw if it's not a 404 error
}
}
/**
* Checks if a branch with the given name exists in a val.
*
* @param valId The ID of the Val to check
* @param branchName The name of the branch to check for
* @returns Promise resolving to true if the branch exists, false otherwise
*/
export async function branchExists(
valId: string,
branchName: string,
): Promise<boolean> {
for await (const branch of sdk.vals.branches.list(valId, {})) {
if (branch.name == branchName) return true;
}
return false;
}
/**
* Converts a branch name to its corresponding branch ID for a given val.
*
* @param valId The ID of the Val containing the branch
* @param branchName The name of the branch to look up
* @returns Promise resolving to the branch ID
* @throws if the branch is not found or if the API request fails
*/
export async function branchNameToBranch(
valId: string,
branchName: string,
): Promise<ValTown.Vals.Branches.BranchListResponse> {
for await (const branch of sdk.vals.branches.list(valId, {})) {
if (branch.name == branchName) return branch;
}
throw new Deno.errors.NotFound(`Branch "${branchName}" not found in Val`);
}
/**
* Converts a val name to its corresponding val data.
*
* @param username The username of the Val owner
* @param valName The name of the Val to look up
* @returns Promise resolving to the Val data
*/
export async function valNameToVal(
username: string,
valName: string,
) {
return await sdk.alias.username.valName.retrieve(username, valName);
}
/**
* Checks if a file exists at the specified path in a val
*
* @param valId The ID of the Val containing the file
* @param filePath The file path to check
* @param branchId The ID of the Val branch to reference
* @param version The version of the Val to check
* @returns Promise resolving to true if the file exists, false otherwise
*/
export async function valItemExists(
valId: string,
branchId: string,
filePath: string,
version: number,
): Promise<boolean> {
try {
const item = await getValItem(valId, branchId, version, filePath);
return item !== undefined;
} catch (e) {
if (e instanceof ValTown.APIError && e.status === 404) {
return false;
} else throw e;
}
}
/**
* Converts a file path to its corresponding Val item for a given val.
*
* @param valId - The ID of the Val containing the file
* @param options - The options object
* @param options.branchId - The ID of the Val branch to reference
* @param [options.version] - The version of the Val for the file being found (optional)
* @param options.filePath - The file path to locate
* @returns Promise resolving to the file data or undefined if not found
*/
export const getValItem = memoize(async (
valId: string,
branchId: string,
version: number,
filePath: string,
): Promise<ValTown.Vals.FileRetrieveResponse | undefined> => {
const valItems = await listValItems(valId, branchId, version);
for (const filepath of valItems) {
if (filepath.path === filePath) return filepath;
}
return undefined;
});
/**
* Get the content of a Val item.
*
* @param {string} valId The ID of the Val
* @param {string} branchId The ID of the Val branch to reference
* @param {number} version The version of the Val
* @param {string} filePath The path to the file
* @returns {Promise<string>} Promise resolving to the file content
*/
export const getValItemContent = memoize(
async (
valId: string,
branchId: string,
version: number,
filePath: string,
): Promise<string> => {
return await sdk.vals.files
.getContent(valId, { path: filePath, branch_id: branchId, version })
.then((resp) => resp.text());
},
);
/**
* Lists all file paths in a Val with pagination support.
*
* @param valId ID of the val
* @param params Parameters for listing Val items
* @param params.path Path to a file or directory (e.g. 'dir/subdir/file.ts'). Pass in an empty string for root.
* @param [params.branch_id] The ID of the Val branch to reference. Defaults to main.
* @param [params.version] - The version of the val. Defaults to latest.
* @param [params.options.recursive] Whether to recursively list files in subdirectories
* @returns Promise resolving to a Set of file paths
*/
export const listValItems = memoize(async (
valId: string,
branchId: string,
version: number,
): Promise<ValTown.Vals.FileRetrieveResponse[]> => {
return await Array.fromAsync(
sdk.vals.files.retrieve(valId, {
path: "",
branch_id: branchId,
version,
recursive: true,
}),
);
});
export async function canWriteToVal(valId: string) {
// There's no way to check if we can write to the Val without actually trying
// to write to it. So we try to write a random file (a uuid, so that they
// don't already have that file) and catch any errors.
//
// In `vt push`, we could technically just wait for an error to get thrown,
// but API errors about not having permissions aren't specific, so we'd need
// to wrap specific mutation promises to rethrow the error.
//
// If we get a 403 or 401, we know we can't write to it
// If we get a 404, we know the Val doesn't exist, and may also be able to see
// in the message if it's a permissions issue
try {
const randomPath = crypto.randomUUID();
await sdk.vals.files.update(valId, { path: randomPath });
// Success means that we broke someone's file. Oops!
throw new Error(
`Got an unexpected response when trying to check write permissions. ${randomPath} may have gotten overwritten.`,
);
} catch (e) {
if (e instanceof ValTown.APIError) {
if (e.status === 403 || e.status === 401) return false;
if (e.status === 404) return !e.message.includes("Not authorized");
else throw e;
} else throw e;
}
}
/**
* Get the latest version of a branch.
*/
export async function getLatestVersion(valId: string, branchId: string) {
return (await sdk.vals.branches.retrieve(valId, branchId)).version;
}
/**
* Generate a random (valid) Val name. Useful for tests.
*/
export function randomValName(label = "") {
return `a${crypto.randomUUID().replaceAll("-", "").slice(0, 10)}_${label}`;
}
/**
* Get the owner of the API key used to auth the current ValTown instance.
*/
export const getCurrentUser = memoize(async () => {
return await sdk.me.profile.retrieve();
});
/**
* Get all organizations the current user is a member of.
*
* @returns All organizations the current user is a member of
*/
export async function getAllMemberOrgs() {
return await Array.fromAsync(sdk.orgs.list({}));
}
export async function* getTraces({
frequency = 1000,
signal,
...params
}: {
frequency?: number;
signal?: AbortSignal;
} & Partial<ValTown.Telemetry.Traces.TraceListParams>): AsyncGenerator<
ValTown.Telemetry.Traces.TraceListResponse.Data
> {
let cursor = new Date();
while (true) {
while (true) {
if (signal?.aborted) break;
const resp = await sdk.telemetry.traces.list({
...params,
limit: 50,
start: cursor.toISOString(),
order_by: "end_time",
direction: "asc",
});
yield* resp.data;
const nextUrl = resp.links.next;
if (!nextUrl) break;
const nextStart = new URL(nextUrl).searchParams.get("start");
if (!nextStart) break;
cursor = new Date(nextStart);
}
await delay(frequency);
}
}
/**
* Get all logs for a specific trace ID.
*
* @param traceId The trace ID to get logs for
* @returns AsyncGenerator yielding all log entries for the trace
*/
export async function* getLogsForTraces(
traceIds: string[],
): AsyncGenerator<ValTown.Telemetry.Logs.LogListResponse.Data> {
let nextUrl: string | undefined;
do {
const nextStart = nextUrl
? new URL(nextUrl).searchParams.get("start")
: undefined;
const response = await sdk.telemetry.logs.list({
limit: 1,
trace_ids: traceIds,
...(nextUrl && nextStart && {
start: nextStart,
}),
direction: "asc",
});
yield* response.data;
nextUrl = response.links.next;
} while (nextUrl);
}
/**
* Converts a file ID to its corresponding Val file for a given val.
*
* @param valId The ID of the Val containing the file
* @param branchId The ID of the Val branch to reference
* @param fileId The ID of the file to retrieve
* @returns Promise resolving to the Val file data
* @throws if the file is not found or if the API request fails
*/
export async function fileIdToValFile(
fileId: string,
): Promise<ValTown.Vals.FileRetrieveResponse> {
return await sdk.files.retrieve(fileId);
}
/**
* Get typeahead for Val names or organization names.
*
* @param prefix - A string in the format "org" or "org/val" to get typeahead suggestions
* @returns Promise resolving to an array of typeahead suggestions
*/
export const typeaheadValNames = memoize(async (
prefix: string,
): Promise<string[]> => {
const parts = prefix.split("/");
if (parts.length === 1) {
// Typeahead for organization name
const response = await fetch(
`https://api.val.town/v2/orgs/typeahead/${parts[0]}`,
{ headers: { "x-vt-version": String(manifest.version) } },
);
const data = await response.json() as { items: string[] };
return data.items;
} else {
// Typeahead for val name (org/val)
const [org, val] = parts;
const response = await fetch(
`https://api.val.town/v2/vals/typeahead/${org}/${val}`,
{
headers: { "x-vt-version": String(manifest.version) },
},
);
const data = await response.json() as { items: string[] };
return data.items.map((valName) => `${org}/${valName}`);
}
});
export default sdk;