Avatar

@rlesser

14 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
Fork
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
import { getMermaidDependencyGraph } from "https://esm.town/v/rlesser/getDependencyGraph?v=90";
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*"}
`;

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:

import { 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
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";
// Function to map SQLite data types to Apache Arrow data types
function sqliteTypeToArrowType(sqliteType: string): DataType {
switch (sqliteType.toUpperCase()) {
case "INTEGER":
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";
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 rows.map(r => Object.fromEntries(columns.map((c, idx) => [c.name, r[idx]])));
}
// Convert each column to an Arrow Vector
const tableData = {};
columns.forEach((column, i) => {
const data = rows.map(row => row[i]);
tableData[column.name] = vectorFromArray(data, column.type);
});
// Create an Apache Arrow Table using the dynamically determined schema and vectors

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
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[] {

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>;
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
import { api } from "https://esm.town/v/pomdtr/api";
type Module = {
slug: string;
id: string;
websiteUrl: string;
category: string | null;
};
export async function getReverseDependencies(valUrlOrSlug: string): Promise<Module[]> {
// clean the url or slug
const initialSlug = valUrlOrSlug.includes("/v/")
? valUrlOrSlug.split("/v/")[1]
: valUrlOrSlug;
// we leave the trailing quote off, in case its imported with a "?v=X" version string
// this does leave open the possibility of capturing vals that have this val as a
// prefix like (bob/getData and bob/getData). Search also returns vals where the
// search appears in the readme. We'll filter all these out later.
const searchString = ` from "https://esm.town/v/${initialSlug}`;
const { data } = await api(`/v1/search/vals?query=${encodeURI(searchString)}&offset=0&limit=100`);
console.log(data);
return data
.filter(d =>
d.code.includes(`from "https://esm.town/v/${initialSlug}"`)
|| d.code.includes(`from "https://esm.town/v/${initialSlug}?`)
)
.map(d => {
const slug = d.author.username.replace("@", "") + "/" + d.name;
return {
slug,
id: "https://esm.town/v/" + slug,
websiteUrl: "https://www.val.town/v/" + slug,
category: "valtown",
};
});
}