Avatar

@rlesser

16 likes27 public vals
Joined January 28, 2023
Product Engineer at https://nomic.ai | MBA Candidate at Columbia Business School | Building ways to clear the fog
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
import { getMermaidDependencyGraph } from "https://esm.town/v/rlesser/getDependencyGraph?v=91";
import { getReverseDependencies } from "https://esm.town/v/rlesser/getReverseDependencies";
import { gfm } from "https://esm.town/v/rlesser/gfm";
import { Hono } from "npm:hono@3.9.2";
const app = new Hono();
const homePageMarkdown = `
# ValTown Dependency Graph Viewer
### What is this
This tool lets you view dependency graph for any public val on [valtown](www.val.town).
Add a valtown slug (\`author/name\`) to this url to see the dependency graph for that val.
Some big dependency graphs - let me know if you find any bigger ones!
* [stevekrouse/sqlite_admin](stevekrouse/sqlite_admin)
* [nbbaier/vtIdeaAggregator](nbbaier/vtIdeaAggregator)
* [rodrigotello/hnFollow](rodrigotello/hnFollow)
* [rlesser/dependency_graph](rlesser/dependency_graph) (a bit meta...)
### TODO
Some ideas for how to expand this site:
* **show the upstream dependencies** - no public API for this yet, but could probably make it work with the search api
* **search for vals** - let people search for vals right from here, using the search api and debouncing
* **toggle graph complexity** - let people choose to see all dependencies or only valtown dependencies (thru the browser)
* **cache graphs in DB** - we should be kind to valtown's API by caching graph data as exploring the
graph involves a lot of calls. maybe only update it when the val version number changes?
* **handle more module sources** - currently we only group npm, esm.sh, and valtown modules. we could add more or make
this a dynamic grouping mechanism?
* **author pages?** - a page to show all the public vals by a person. not sure if needed and dont want to just end up
wrapping every val.town page here.
* **val readme** - show the val readme at the top of each val page
`;
const valPageMarkdown = (author: string, name: string, mermaid: string, reverseDeps: string) => `
[Home](/)
# ${author}/${name}
### Dependency Graph
\`\`\`mermaid
${mermaid}
\`\`\`
### Reverse Dependencies
${reverseDeps || "*None found*"}
`;
const footerMarkdown = `
---
*Website created by [rlesser/dependency_graph](https://www.val.town/v/rlesser/dependency_graph)*
`;
async function makePage(markdown: string, title: string) {
return await gfm(markdown + footerMarkdown, { title, favicon: "🖇️" });
}
app.get("/", async (c) => {
return c.html(await makePage(homePageMarkdown, "ValTown Dependency Graph"));
});
app.get("/:author/:name", async (c) => {
const author = c.req.param("author");
const name = c.req.param("name");
try {
const mermaid = await getMermaidDependencyGraph(`${author}/${name}`);
const reverseDeps = (await getReverseDependencies(`${author}/${name}`))
.map(d => `* [${d.slug}](/${d.slug})`)
.join("\n");
return c.html(
await makePage(
valPageMarkdown(author, name, mermaid, reverseDeps),
`${author}/${name} | ValTown Dependency Graph`,
),
);
} catch (e) {
return c.html("Error!<br>" + e);
}
});
export default app.fetch;

Get Favorites on Resy

This function fetches your favorite restaurants and venues, as specified in your Hit List.

Inputs

An object containing:

Returns

A Favorites object, containing the ids of your favorite venues, defined in the type below.

See other Resy vals I've made.

Readme
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
import { fetch } from "https://esm.town/v/std/fetch";
type Favorites = {
results: {
venues: {
venue: {
id: {
resy: number;
};
name: string;
};
}[];
};
};
export const Resy_getFavorites = async (params: {
authToken: string;
}): Promise<Favorites> => {
const url = "https://api.resy.com";
const response = await fetch(`${url}/3/user/favorites`, {
headers: {
accept: "application/json, text/plain, */*",
"accept-encoding": "gzip, deflate, br",
"accept-language": "en-US,en;q=0.9",
authorization: 'ResyAPI api_key="VbWk7s3L4KiK5fzlO7JD3Q5EYolJI7n5"',
"x-origin": "https://resy.com",
origin: "https://resy.com/",
dnt: "1",
referer: "https://resy.com/",
"content-type": "application/json",
"content-type": "application/x-www-form-urlencoded",
"x-resy-auth-token": params.authToken,
},
});
const data = await response.json();
return data;
};

SQLite Table Export Utils

This allows for a val.town-hosted SQLite table to be exported as:

  • JSON (Record<string, unknown>[])
  • Arrow IPC (Uint8Array)
  • TODO: Others?

This can then be used by a HTTP endpoint, like so:

Create valimport { exportSQLiteTable, SQLiteTableExportFormat } from "https://esm.town/v/rlesser/sqliteTableExportUtils"; export default async function(req: Request): Promise<Response> { const tableName = new URL(req.url).searchParams.get("table"); if (!tableName) { return new Response("Table name is required", { status: 400 }); } const format = (new URL(req.url).searchParams.get("format") || "arrowIPC") as SQLiteTableExportFormat; const data = await exportSQLiteTable(tableName, format); if (data instanceof Uint8Array) { return new Response(data, { headers: { "Content-Type": "application/octet-stream" }, }); } else { return Response.json(data); } }

TODO

  • Specify limit and offset of export, for pagination
  • Smart assessment of if the export is going to be over the val.town limit of 10MB, adjust to paginated of so.
  • Support other export formats.

PRs welcome!

Readme
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
import { sqlite } from "https://esm.town/v/std/sqlite";
import { type ResultSet } from "npm:@libsql/client";
import { DataType, Float64, Int32, Table, tableToIPC, Utf8, vectorFromArray } from "npm:apache-arrow";
import { zip } from "npm:lodash-es@4.17.21";
// Function to map SQLite data types to Apache Arrow data types
function sqliteTypeToArrowType(sqliteType: string): DataType {
switch (sqliteType.toUpperCase()) {
case "INTEGER":
case "NUMBER":
return new Int32();
case "TEXT":
return new Utf8();
case "REAL":
return new Float64();
// Add more mappings as needed
default:
return new Utf8(); // Default or throw error for unsupported types
}
}
export type SQLiteTableExportFormat = "json" | "arrowIPC";
// From https://www.val.town/v/pomdtr/sql zip
export function resultSetToJSON(res: ResultSet) {
return res.rows.map(row => Object.fromEntries(zip(res.columns, row)));
}
export function resultSetToArrowTable(res: ResultSet) {
const tableData = {};
res.columns.forEach((column, i) => {
const data = res.rows.map(row => row[i]);
tableData[column] = vectorFromArray(data, sqliteTypeToArrowType(res.columnTypes[i]));
});
return new Table(tableData);
}
export async function exportSQLiteTable(
tableName: string,
format: SQLiteTableExportFormat,
limit: number | null,
offset: number | null,
): Promise<Record<string, unknown>[] | Uint8Array> {
// Read data from the SQLite table
const data = await sqlite.execute(`
SELECT * FROM ${tableName} LIMIT ${limit || -1} OFFSET ${offset || 0}
`) as ResultSet;
const rows = data.rows;
// Dynamically read the schema of the table
const schemaInfo = await sqlite.execute(`PRAGMA table_info(${tableName})`) as ResultSet;
const columns = schemaInfo.rows.map(col => ({ name: String(col[1]), type: sqliteTypeToArrowType(String(col[2])) }));
// If json, output this right away
if (format == "json") {
return resultSetToJSON(data);
}
const arrowTable = resultSetToArrowTable(data);
return tableToIPC(arrowTable);
}

Dependency Graph Explorer

This val allows for dependency graph exploration.

getDependencyGraph

Iteratively explores the dependency graph of a val and its children, returning the results.

arguments

  • valUrlOrSlug string - the full val url or slug (author/name) of the val

returns

An object containing:

  • dependencyMap Map<string, string[]> - A map of vals (by id) to dependencies (vals or non-val id)
  • moduleMap Map<string, Module> - A map of modules (by val or non-val id) to module information. Module object contains:
    • slug string - the slug of the module, how it should be displayed visually
    • id string - the id of the module, corresponding to how it's displayed in the code, normally as a url.
    • websiteUrl string - the website link for the module, a normally a link to either valtown or npm.
    • category string|null - the category of the module, such as "valtown", "esm.sh", or "npm". Those without a predefined category are in null.
Readme
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
import { api } from "https://esm.town/v/pomdtr/api";
import { groups } from "npm:d3-array";
type Module = {
slug: string;
id: string;
websiteUrl: string;
category: string | null;
};
function getModuleInfoFromUrl(url: string): Module {
if (url.startsWith("https://esm.town")) {
const slug = url.split("/v/")[1].split("?v")[0];
return {
// TODO: Should be able to get specific version of val, but for now we remove versions
slug,
id: url.split("?v")[0],
websiteUrl: "https://www.val.town/v/" + slug,
category: "valtown",
};
}
if (url.startsWith("https://esm.sh")) {
const slug = url.split(".sh/")[1].split("@")[0];
return {
slug,
id: url.split("@")[0],
websiteUrl: "https://www.npmjs.com/package/" + slug,
category: "esm.sh",
};
}
if (url.startsWith("npm:")) {
const slug = url.startsWith("npm:@")
? "@" + url.split("@")[1]
: url.replace("npm:", "").split("@")[0];
return {
slug,
id: "npm:" + slug.replace("@", ""),
websiteUrl: "https://www.npmjs.com/package/" + slug,
category: "npm",
};
}
return {
slug: url,
id: url.replaceAll("@", ""),
websiteUrl: url,
category: null,
};
}
function getModulesFromCode(valCode: string): Module[] {
const importRegex = new RegExp(`^import.*from ["'](.*?)["'];`, "gm");
let match;
const imports: string[] = [];
while ((match = importRegex.exec(valCode)) !== null) {
imports.push(match[1]);
}
return imports.map(d => getModuleInfoFromUrl(d));
}
export async function getDependencyGraph(valUrlOrSlug: string) {
// clean the url or slug
const initialSlug = valUrlOrSlug.includes("/v/")
? valUrlOrSlug.split("/v/")[1]
: valUrlOrSlug;
const initialModule = getModuleInfoFromUrl("https://esm.town/v/" + initialSlug);
// queue of unexplored modules
const moduleQueue = [initialModule];
// map from ids to module object
const moduleMap = new Map<string, Module>();
// map from ids to their dependency ids
const dependencyMap = new Map<string, string[]>();
while (moduleQueue.length > 0) {
const module = moduleQueue.shift();
if (moduleMap.has(module.id)) {
continue;
}
if (module.category == "valtown") {
let code: string | null = null;
try {
code = (await api(`/v1/alias/${module.slug}`)).code;
} catch (e) {
console.warn(e, module);
}
if (code) {
const dependencyModules = getModulesFromCode(code);
dependencyMap.set(module.id, dependencyModules.map(m => m.id));
moduleQueue.push(...dependencyModules);
}
}
moduleMap.set(module.id, module);
}
return { dependencyMap, moduleMap };
}
export async function getMermaidDependencyGraph(valUrlOrSlug: string) {
const { dependencyMap, moduleMap } = await getDependencyGraph(valUrlOrSlug);

Convert markdown to Html with Github styling

Forked to add remark-mermaid

Readme
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
import { matches, select, selectAll } from "https://esm.sh/hast-util-select@6";
import { h } from "https://esm.sh/hastscript";
import rehypeDocument from "https://esm.sh/rehype-document";
import rehypeStringify from "https://esm.sh/rehype-stringify";
import remarkGfm from "https://esm.sh/remark-gfm";
import remarkParse from "https://esm.sh/remark-parse";
import remarkRehype from "https://esm.sh/remark-rehype@6";
import { unified } from "https://esm.sh/unified";
// This is kind of a pain, but the existing remark/rehype mermaid plugins
// use playwright internally for some reason, which is a non-starter here
// in good ol' valtown!
const addMermaidScript = () => (tree) => {
const body = select("body", tree);
console.log(body);
const script = `
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: false });
await mermaid.run({
querySelector: '.language-mermaid',
});
`;
body.children.push(h("script", { type: "module" }, script), { type: "text", value: "\n" });
};
export async function gfm(markdown: string, options?: { title?: string; favicon?: string }) {
const html = await unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype)
.use(rehypeDocument, {
title: options?.title,
link: [
{ href: `https://fav.farm/${options?.favicon || "📝"}`, rel: "icon" },
],
style: `
.markdown-body {
box-sizing: border-box;
min-width: 200px;
max-width: 980px;
margin: 0 auto;
padding: 45px;
}
@media (max-width: 767px) {
.markdown-body {
padding: 15px;
}
}`,
css: "https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.4.0/github-markdown.min.css",
script: `document.body.classList.add('markdown-body')`,
})
.use(addMermaidScript)
.use(rehypeStringify)
.process(markdown);
return String(html);
}

Get a Venue's Calendar on Resy

This function fetches the calendar for a given venue on Resy, which gives information on which days the venue is available, sold-out, or closed.

Inputs

An object containing:

  • venue_id - The Resy venue id, either fetched from the website's network data or from @rlesser_getFavorites (Todo: add venue id from url function).
  • num_seats - The number of seats you are checking for availability for. Use 2 as a default if you just want to know when the restaurant is closed.

Returns

A VenueCalendar object, containing:

  • last_calendar_day - A string representing the last day the restaurant has made their calendar available for (normally a few weeks out from today).
  • scheduled - An object containing a list of dates and restaurant statuses. See type below.

See other Resy vals I've made.

Readme
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
import { fetch } from "https://esm.town/v/std/fetch";
type VenueCalendar = {
last_calendar_day: string;
scheduled: {
date: string;
inventory: {
reservation: "sold-out" | "available" | "closed";
};
}[];
};
export const Resy_getVenueCalendar = async (params: {
venue_id: number;
num_seats: number;
}): Promise<VenueCalendar> => {
const url = "https://api.resy.com";
const query = new URLSearchParams({
venue_id: String(params.venue_id),
num_seats: String(params.num_seats),
start_date: (new Date()).toISOString().slice(0, 10),
end_date: new Date(new Date().setFullYear(new Date().getFullYear() + 1))
.toISOString().slice(0, 10),
});
const response = await fetch(`${url}/4/venue/calendar?${query.toString()}`, {
headers: {
accept: "application/json, text/plain, */*",
"accept-encoding": "gzip, deflate, br",
"accept-language": "en-US,en;q=0.9",
authorization: 'ResyAPI api_key="VbWk7s3L4KiK5fzlO7JD3Q5EYolJI7n5"',
"x-origin": "https://resy.com",
origin: "https://resy.com/",
dnt: "1",
referer: "https://resy.com/",
"content-type": "application/json",
"content-type": "application/x-www-form-urlencoded",
},
});
const data = await response.json();
return data;
};
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
import { fetch } from "https://esm.town/v/std/fetch";
export const Resy_setNotify = async (params: {
authToken: string;
notifies: {
venue_id: number;
day: string;
time_preferred_start: string;
time_preferred_end: string;
num_seats: number;
service_type_id: number;
}[];
}) => {
const url = "https://api.resy.com";
const response = await fetch(`${url}/3/notify`, {
method: "POST",
body: `struct_data=${
encodeURIComponent(JSON.stringify({ notifies: params.notifies }))
}`,
headers: {
accept: "application/json, text/plain, */*",
"accept-encoding": "gzip, deflate, br",
"accept-language": "en-US,en;q=0.9",
authorization: 'ResyAPI api_key="VbWk7s3L4KiK5fzlO7JD3Q5EYolJI7n5"',
"x-origin": "https://resy.com",
origin: "https://resy.com/",
dnt: "1",
referer: "https://resy.com/",
"content-type": "application/json",
"content-type": "application/x-www-form-urlencoded",
"x-resy-auth-token": params.authToken,
},
});
const data = await response.json();
return data;
};

Authenticate with Resy

This function can be used to generate an authentication token for Resy, which can be used to view favorites, set notifies, or even make reservations.

Inputs

An AuthParams object containing:

  • email - The email address for your Resy account. Probably best to keep private.
  • password - The password for your Resy account. Definitely keep this private, and place it in a secret.

Returns

An AuthResponse object containing:

  • id - Your Resy account id.
  • token - Your Resy account token, used to authenticate you in subsequent API calls.

See other Resy vals I've made.

Readme
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
import { fetch } from "https://esm.town/v/std/fetch";
type AuthParams = {
email: string;
password: string;
};
type AuthResponse = {
id: string;
token: string;
[props: string]: unknown;
};
export const Resy_authenticate = async (params: AuthParams): Promise<{
AuthResponse;
}> => {
const url = "https://api.resy.com";
const body = `email=${encodeURIComponent(params.email)}&password=${
encodeURIComponent(params.password)
}`;
const response = await fetch(`${url}/3/auth/password`, {
method: "POST",
body: body,
headers: {
accept: "application/json, text/plain, */*",
"accept-encoding": "gzip, deflate, br",
"accept-language": "en-US,en;q=0.9",
authorization: 'ResyAPI api_key="VbWk7s3L4KiK5fzlO7JD3Q5EYolJI7n5"',
"x-origin": "https://resy.com",
origin: "https://resy.com/",
dnt: "1",
referer: "https://resy.com/",
"content-type": "application/json",
"content-type": "application/x-www-form-urlencoded",
},
});
const data = await response.json();
return data;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { fetch } from "https://esm.town/v/std/fetch";
// Send a pushover message.
// token, user, and other opts are as specified at https://pushover.net/api
export async function pushover({ token, user, message, title, url, ...opts }) {
return await fetch("https://api.pushover.net/1/messages.json", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
token,
user,
message,
title,
url,
...opts,
}),
});
}
// Forked from @meatcar.pushover - added content type header
1
2
3
4
5
6
7
8
interface Flavoring<FlavorT> {
_type?: FlavorT;
}
// Based on https://spin.atomicobject.com/typescript-flexible-nominal-typing/
// Prevents one flavored type from being used as another.
// Use like: Flavor<number, 'UserId'>
export type Flavor<T, FlavorT> = T & Flavoring<FlavorT>;