Back to packages list

Vals using zod

Description from the NPM package:
TypeScript-first schema declaration and validation library with static type inference
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 { fetch } from "https://esm.town/v/std/fetch";
export const bookReservationOnResy = async ({
reservation,
venue,
auth,
}: {
reservation: {
// How many seats to book
seats: number;
// e.g. 2023-05-01 (interpreted as May 05, 2023)
date: string;
// e.g. [17:00, 22:00]
timeRange: [string, string];
};
venue:
| {
type: "id";
id: string;
}
| {
type: "slug";
slug: string;
city: string;
};
auth: {
email: string;
password: string;
};
}) => {
const { z } = await import("npm:zod");
const RESY_API_URL = "https://api.resy.com";
const RESY_DEFAULT_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",
};
const BookedReservationResponseSchema = z.object({
resy_token: z.string(),
reservation_id: z.number(),
});
const SlotDetailsResponseSchema = z.object({
book_token: z.object({
value: z.string(),
date_expires: z.string(),
}),
});
const FindResponseSchema = z.object({
results: z.object({
venues: z
.object({
venue: z.object({
id: z.object({
resy: z.number(),
}),
name: z.string(),
location: z.object({
time_zone: z.string(),
}),
}),
slots: z
.object({
availability: z.object({
id: z.number(),
}),
config: z.object({
id: z.number(),
// Reservations are booked with the token
token: z.string(),
type: z.string(),
}),
date: z.object({
start: z.string(),
end: z.string(),
}),
payment: z.object({
cancellation_fee: z.number().nullish(),
deposit_fee: z.number().nullish(),
is_paid: z.boolean(),
}),
})
.array(),
})
.array()
.default([]),
}),
});
const PaymentMethodSchema = z.object({
id: z.number(),
type: z.string(),
});
const AuthenticationResponseSchema = z.object({
id: z.number(),
token: z.string(),
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
import { z, ZodError } from "npm:zod";
const bodySchema = z.object({
body: z.string(),
headers: z.record(z.string(), z.string()),
}).partial();
const searchParamsSchema = z.object({
status: z.coerce.number().min(100).max(599).default(200),
statusText: z.string().default(undefined),
data: z.string(),
}).partial();
async function getBody(req: Request): Promise<Object> {
try {
return await req.json();
} catch (e) {
if (e instanceof SyntaxError) {
return {};
}
throw e;
}
}
export default async function(req: Request): Promise<Response> {
const searchParams = new URL(req.url).searchParams;
const searchParamsObj = new Map();
searchParams.forEach((v, k) => {
if (searchParamsObj.has(k)) {
searchParamsObj.set(k, [searchParamsObj[k]].concat(v));
} else {
searchParamsObj.set(k, v);
}
});
try {
const body = await getBody(req);
const bodyParsed = await bodySchema.parse(body);
const searchParamsParsed = await searchParamsSchema.parse(
Object.fromEntries(searchParamsObj.entries()),
);
const headers = new Headers(bodyParsed.headers);
const bodyResponse = bodyParsed.body ?? searchParamsParsed.data ?? undefined;
return new Response(bodyResponse, {
headers: bodyParsed.headers,
statusText: searchParamsParsed.statusText,
status: searchParamsParsed.status,
});
} catch (e) {
console.error(e);
if (e instanceof ZodError) {
return Response.json(JSON.stringify(e.message));
}
return Response.error();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { sql } from "npm:drizzle-orm@0.30.7";
import { sqliteTable, text } from "npm:drizzle-orm@0.30.7/sqlite-core";
import { z } from "npm:zod@3.22.4";
export type UserId = string & { readonly UserId: unique symbol };
export const UserId = z
.string()
.startsWith("u_")
.transform((k) => k as UserId);
export const users_table = sqliteTable("users", {
user_id: text("user_id").$type<UserId>().primaryKey(),
username: text("username").unique().notNull(),
first_name: text("first_name"),
last_name: text("last_name"),
email_address: text("email_address").notNull(),
clerk_user_id: text("clerk_user_id").unique().notNull(),
});
export type InsertUser = typeof users_table.$inferInsert;
export type SelectUser = typeof users_table.$inferSelect;
export const schema = { users_table };

SQLite - Docs ↗

SQLite is a lightweight, standard database. Every Val Town account comes with its own private SQLite database that is accessible from any of your vals via std/sqlite.

Val Town SQLite is powered by Turso.

You may find these admin viewers helpful managing your database:

Limits

You can store 10mb on the free plan and up to 1gb on the paid plan. Contact us if you need more space.

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_URL } from "https://esm.town/v/std/API_URL";
import { LibsqlError, type TransactionMode } from "npm:@libsql/client";
import { z } from "npm:zod";
/**
* Every Val Town account comes with its own private
* [SQLite database](https://www.sqlite.org/) that
* is accessible from any of your vals.
* ([Docs ↗](https://docs.val.town/std/sqlite))
*/
export const sqlite = {
/**
* Executes a SQLite statement.
*
* @param {InStatement} statement - The SQLite statement to execute.
* @example String query:
* `sqlite.execute("SELECT 1;")`
* @example Query with arguments:
* `sqlite.execute({sql: "SELECT * from books WHERE year > ?;", args: [2020]})`
*/
execute,
/**
* Executes a batch of SQLite statements.
*
* @param {InStatement[]} statements - An array of SQLite statements to execute.
* @param {TransactionMode} [mode] - The transaction mode for the batch execution.
*/
batch,
};
// ------------
// Functions
// ------------
async function execute(statement: InStatement): Promise<ResultSet> {
const res = await fetch(`${API_URL}/v1/sqlite/execute`, {
method: "POST",
headers: {
Authorization: `Bearer ${Deno.env.get("valtown")}`,
},
body: JSON.stringify({ statement }),
});
if (!res.ok) {
throw createResError(await res.text());
}
return res.json();
}
async function batch(statements: InStatement[], mode?: TransactionMode): Promise<ResultSet[]> {
const res = await fetch(`${API_URL}/v1/sqlite/batch`, {
method: "POST",
headers: {
Authorization: `Bearer ${Deno.env.get("valtown")}`,
},
body: JSON.stringify({ statements, mode }),
});
if (!res.ok) {
throw createResError(await res.text());
}
return res.json();
}
function createResError(body: string) {
try {
const e = zLibsqlError.parse(JSON.parse(body));
// e.message already contains the code, and LibsqlError adds the
// code to the beginning, so we remove it here
const msg = e.message.replace(e.code, "").replace(/^:\s+/, "");
return new LibsqlError(msg, e.code, e.rawCode);
} catch (_) {
// Failed to parse libsql error
}
return new Error(body);
}
// ------------
// Helpers
// ------------
const zLibsqlError = z.object({
message: z.string(),
code: z.string(),
rawCode: z.number().optional(),
});
// We patch these types to only support JSON values
export type InValue = null | string | number | boolean;
export type InArgs = Array<InValue> | Record<string, InValue>;
export type InStatement = {
/**
* The SQL statement to execute.
*/
sql: string;
/**
* The arguments to bind to the SQL statement.
*/
args: InArgs;
} | string;
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_URL } from "https://esm.town/v/std/API_URL";
import { LibsqlError, type TransactionMode } from "npm:@libsql/client";
import { z } from "npm:zod";
/**
* Every Val Town account comes with its own private
* [SQLite database](https://www.sqlite.org/) that
* is accessible from any of your vals.
* ([Docs ↗](https://docs.val.town/std/sqlite))
*/
export const sqlite = {
/**
* Executes a SQLite statement.
*
* @param {InStatement} statement - The SQLite statement to execute.
* @example String query:
* `sqlite.execute("SELECT 1;")`
* @example Query with arguments:
* `sqlite.execute({sql: "SELECT * from books WHERE year > ?;", args: [2020]})`
*/
execute,
/**
* Executes a batch of SQLite statements.
*
* @param {InStatement[]} statements - An array of SQLite statements to execute.
* @param {TransactionMode} [mode] - The transaction mode for the batch execution.
*/
batch,
};
// ------------
// Functions
// ------------
async function execute(statement: InStatement): Promise<ResultSet> {
const res = await fetch(`${API_URL}/v1/sqlite/execute`, {
method: "POST",
headers: {
Authorization: `Bearer ${Deno.env.get("valtown")}`,
},
body: JSON.stringify({ statement }),
});
if (!res.ok) {
throw createResError(await res.text());
}
return res.json();
}
async function batch(statements: InStatement[], mode?: TransactionMode): Promise<ResultSet[]> {
const res = await fetch(`${API_URL}/v1/sqlite/batch`, {
method: "POST",
headers: {
Authorization: `Bearer ${Deno.env.get("valtown")}`,
},
body: JSON.stringify({ statements, mode }),
});
if (!res.ok) {
throw createResError(await res.text());
}
return res.json();
}
function createResError(body: string) {
try {
const e = zLibsqlError.parse(JSON.parse(body));
// e.message already contains the code, and LibsqlError adds the
// code to the beginning, so we remove it here
const msg = e.message.replace(e.code, "").replace(/^:\s+/, "");
return new LibsqlError(msg, e.code, e.rawCode);
} catch (_) {
// Failed to parse libsql error
}
return new Error(body);
}
// ------------
// Helpers
// ------------
const zLibsqlError = z.object({
message: z.string(),
code: z.string(),
rawCode: z.number().optional(),
});
// We patch these types to only support JSON values
export type InValue = null | string | number | boolean;
export type InArgs = Array<InValue> | Record<string, InValue>;
export type InStatement = {
/**
* The SQL statement to execute.
*/
sql: string;
/**
* The arguments to bind to the SQL statement.
*/
args: InArgs;
} | string;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { zodStringToJSON } from "https://esm.town/v/freecrayon/zodStringToJSON";
import { zfd } from "npm:zod-form-data";
const $CommentFormData = zfd.formData({
commentId: zfd.text(),
content: zfd.text(zodStringToJSON()),
});
// usually this will be passed to you by your Server (i.e. SvelteKit, Remix)
const formData = new FormData();
formData.set("commentId", "abc123");
formData.set("content", `{"hello":"world","value":12}`);
const { commentId, content } = $CommentFormData.parse(formData);
console.log({ commentId, content });

Useful when working with ZodFormData and you need to parse a text field that contains JSON.

See https://www.val.town/v/freecrayon/zodStringToJSON_example for an example of how to use it.

Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
import { z } from "npm:zod";
const zodStringToJSON = () =>
z.string().transform((str, ctx): z.infer<z.ZodType<unknown>> => {
try {
return JSON.parse(str);
} catch (e) {
ctx.addIssue({ code: "custom", message: "Invalid JSON" });
return z.NEVER;
}
});
export { zodStringToJSON };

Example copied https://instructor-ai.github.io/instructor-js/#usage into val.town

You will need to fork this and properly set the apiKey and organisation for it to work.

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
import Instructor from "https://esm.sh/@instructor-ai/instructor";
import OpenAI from "https://esm.sh/openai";
import { z } from "https://esm.sh/zod";
const openAISecrets = {
apiKey: getApiKey(),
organization: getOrganisationKey(),
};
const oai = new OpenAI(openAISecrets);
const client = Instructor({
client: oai,
mode: "FUNCTIONS",
});
const UserSchema = z.object({
// Description will be used in the prompt
age: z.number().describe("The age of the user"),
name: z.string(),
});
// User will be of type z.infer<typeof UserSchema>
const user = await client.chat.completions.create({
messages: [{ role: "user", content: "Jason Liu is 30 years old" }],
model: "gpt-3.5-turbo",
response_model: {
schema: UserSchema,
name: "User",
},
});
console.log(user);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { z } from "npm:zod";
export const ROWS = 9;
export const COLS = 9;
export const Slot = z.number().gte(0).lte(9);
export type Slot = z.infer<typeof Slot>;
export const Sudoku = z.array(z.array(Slot).length(COLS)).length(ROWS);
export type Sudoku = z.infer<typeof Sudoku>;
export function print(sudoku: Sudoku) {
sudoku = Sudoku.parse(sudoku);
for (let row = 0; row < ROWS; row++) {
let line = "";
for (let col = 0; col < COLS; col++) {
line += sudoku[row][col] + " ";
}
console.log(line);
}
}
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
import { z } from "npm:zod";
const paramsSchema = z.object({
slug: z.coerce.string(),
estimatedProb: z.coerce.number().min(0).max(1),
bankroll: z.coerce.number().min(0),
deferenceFactor: z.coerce.number().min(0).max(1).optional().default(0.5),
});
// Copied from https://github.com/Will-Howard/manifolio/blob/master/manifolio-ui/lib/calculate.ts
function calculateNaiveKellyBet({
marketProb,
estimatedProb,
deferenceFactor,
bankroll,
}: { marketProb: number; estimatedProb: number; deferenceFactor: number; bankroll: number }): {
amount: number;
outcome: "YES" | "NO";
} {
const outcome = estimatedProb > marketProb ? "YES" : "NO";
const marketWinProb = outcome === "YES" ? marketProb : 1 - marketProb;
// kellyFraction * ((p - p_market) / (1 - p_market))
const fraction = deferenceFactor
* (Math.abs(estimatedProb - marketProb) / (1 - marketWinProb));
// clamp fraction between 0 and 1
const clampedFraction = Math.min(Math.max(fraction, 0), 1);
const amount = bankroll * clampedFraction;
return {
amount,
outcome,
};
}
async function getMarket(slug: string) {
const res = await fetch(`https://api.manifold.markets/v0/slug/${slug}`);
if (!res.ok) {
const body = await res.text();
throw new Error(body ?? "Error fetching market");
}
return res.json();
}
export default async function(req: Request): Promise<Response> {
const searchParams = new URL(req.url).searchParams;
const params = paramsSchema.parse(Object.fromEntries(searchParams.entries())) as z.infer<typeof paramsSchema>;
const market = await getMarket(params.slug);
const kelly = calculateNaiveKellyBet({
marketProb: market.probability,
estimatedProb: params.estimatedProb,
deferenceFactor: params.deferenceFactor,
bankroll: params.bankroll,
});
return Response.json(kelly);
}
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
if (import.meta.main) Deno.serve(translate);
export async function translate(req: Request) {
if (req.method === "POST") {
return await fetch("https://deepl.deno.dev/translate", {
method: "POST",
body: await req.text(),
});
}
return new Response(html, { headers: { "content-type": "text/html" } });
}
const html = `
<!DOCTYPE html>
<html lang="en">
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Translator</title>
<style>
textarea {
font-size: 1rem;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
input:focus,
textarea:focus {
border: none;
outline: none;
}
*:disabled {
cursor: wait !important;
}
</style>
<script src="https://cdn.tailwindcss.com/"></script>
<script type="module">
import { html, Component, render } from "https://cdn.cbd.int/htm/preact/standalone.module.js";
import { ofetch } from "https://esm.sh/ofetch/dist/index.mjs";
import { z } from "https://esm.sh/zod";
// https://github.com/developit/htm
class App extends Component {
state = {
text: "hello",
source_lang: "auto",
target_lang: "zh",
translation: "",
};
translate = async function () {
this.setState({ loading: true, translation: "" });
const { text, source_lang, target_lang } = this.state;
const translation = await translate({ text, source_lang, target_lang });
this.setState({ loading: false, translation });
}.bind(this);
swap = function () {
if (this.state.source_lang.toLowerCase() === "auto") {
this.setState({ source_lang: "en" });
}
this.setState(({ source_lang, target_lang }) => ({
source_lang: target_lang,
target_lang: source_lang,
}));
}.bind(this);
handleShortcut = function (e) {
if (this.state.loading !== true && e.ctrlKey && e.key === "Enter") {
this.translate();
}
}.bind(this);
componentDidMount() {
document.addEventListener("keydown", this.handleShortcut);
}
componentWillUnmount() {
document.removeEventListener("keydown", this.handleShortcut);
}
render({ page }, { todos = [] }) {
return html\`
<div class="container m-auto p-1" disabled=\${this.state.loading}>
<header class="my-8">
<h1 class="text-4xl">Translator</h1>
</header>
<div class="my-4">
<button
onClick=\${this.translate}
disabled=\${this.state.loading}
class="px-4 py-2 bg-cyan-100 active:bg-cyan-200 disabled:bg-cyan-50"
>
translate (Ctrl+Enter)
</button>
<input
value=\${this.state.source_lang}
onChange=\${({ target: { value } }) => this.setState({ source_lang: value })}
placeholder="source_lang"
class="bg-neutral-50 focus:bg-neutral-100 p-2 w-20"
/>
<button onClick=\${this.swap} class="bg-fuchsia-200 p-2">🔄</button>
<input
value=\${this.state.target_lang}
onChange=\${({ target: { value } }) => this.setState({ target_lang: value })}
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
import { GptApi } from "https://esm.town/v/xkonti/gptApiFramework";
import { Hono } from "npm:hono@3";
import { z } from "npm:zod";
/**
* INITIALIZE API
*/
const api = new GptApi({
url: "https://vlad-tempsimplegptexample.web.val.run",
title: "Some API",
description: "API for something",
version: "1.0.0",
});
// Example of action that doesn't accept any input, but returns something
const ResponseCommandSchema = z.object({
feedback: z.string().describe("Feedback regarding submitted action"),
data: z.string().optional().describe("Additional data related to the given command"),
}).describe("Contains feedback regarding submitted action");
export type ResponseCommand = z.infer<typeof ResponseCommandSchema>;
api.nothingToJson<ResponseCommand>({
verb: "POST",
path: "/some/endpoint",
operationId: "doSomething",
desc: "Does something to do something",
requestSchema: null,
requestDesc: null,
responseSchema: ResponseCommandSchema,
responseDesc: "Summary of executed action",
}, async (ctx) => {
// TODO: Do something / call something
return {
feedback: "Did something.",
data: "124 miles",
};
});
// Example of action that accept some data and returns something
const ThingsRequestSchema = z.object({
things: z.array(z.string().describe("A thing to do something with")).describe("A list of things "),
}).describe("Input for the action containing some things");
type ThingsRequest = z.infer<typeof ThingsRequestSchema>;
api.jsonToJson<ThingsRequest, ResponseCommand>({
verb: "POST",
path: "/submitthings",
operationId: "submit-things",
desc: "Endpoint for submitting some things",
requestSchema: ThingsRequestSchema,
requestDesc: "Things to do things with",
responseSchema: ResponseCommandSchema,
responseDesc: "Feedback on things",
}, async (ctx, dto) => {
// TODO: Do things to things
return {
feedback: "Things have been done to things.",
data: "15 things processed",
};
});
// Serve the API
export default api.serve();

Allows for automatic generation of Hono API comatible with GPTs. Endpoints' inputs and outputs need to be specified via types from which the Open API spec is generated automatically and available via /gpt/schema endpoint.

Usage example:

Create valimport { GptApi } from "https://esm.town/v/xkonti/gptApiFramework"; import { z } from "npm:zod"; /** * COMMON TYPES */ const ResponseCommandSchema = z.object({ feedback: z.string().describe("Feedback regarding submitted action"), command: z.string().describe("The command for the Mediator AI to follow strictly"), data: z.string().optional().describe("Additional data related to the given command"), }).describe("Contains feedback and further instructions to follow"); export type ResponseCommand = z.infer<typeof ResponseCommandSchema>; /** * INITIALIZE API */ const api = new GptApi({ url: "https://xkonti-planoverseerai.web.val.run", title: "Overseer AI API", description: "The API for interacting with the Overseer AI", version: "1.0.0", }); /** * REQUIREMENTS GATHERING ENDPOINTS */ api.nothingToJson<ResponseCommand>({ verb: "POST", path: "/newproblem", operationId: "new-problem", desc: "Endpoint for informing Overseer AI about a new problem presented by the User", requestSchema: null, requestDesc: null, responseSchema: ResponseCommandSchema, responseDesc: "Instruction on how to proceed with the new problem", }, async (ctx) => { return { feedback: "User input downloaded. Problem analysis is required.", command: await getPrompt("analyze-problem"), data: "", }; }); export default api.serve();
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 { EndpointDefinition, getOpenApiSpec } from "https://esm.town/v/xkonti/gptApiSchemaBuilder";
import { Hono } from "npm:hono@3";
import { z } from "npm:zod";
export interface ApiInfo {
url: string;
title: string;
description: string;
version: string;
}
export class GptApi {
app = new Hono();
info: ApiInfo;
endpoints: EndpointDefinition[] = [];
constructor(info: ApiInfo) {
this.info = info;
this.app.get("/gpt/schema", async (ctx) => {
const spec = getOpenApiSpec(
this.info.url,
this.info.title,
this.info.description,
this.info.version,
this.endpoints,
);
return ctx.text(JSON.stringify(spec, null, 2));
});
}
/**
* Register endpoint that ignores all inputs and returns a specific JSON response.
*/
nothingToJson<TResponse>(
endpointDef: EndpointDefinition,
handler: (ctx) => Promise<TResponse>,
) {
endpointDef.requestDesc = null;
endpointDef.requestSchema = null;
const handlerWrapper = async (ctx) => {
const response = await handler(ctx);
return ctx.json(response);
};
// TODO: Verify response stuff is in place
this.registerHandler(endpointDef, handlerWrapper);
}
/**
* Register endpoint that accepts a JSON DTO and returns a specific JSON response.
*/
jsonToJson<TRequest, TResponse>(
endpointDef: EndpointDefinition,
handler: (ctx, dto: TRequest) => Promise<TResponse>,
) {
// TODO: Verify request and response is in place
const handlerWrapper = async (ctx) => {
const data = await ctx.req.json() as TRequest;
// TODO: Handle invalid data format
const response = await handler(ctx, data);
return ctx.json(response);
};
this.registerHandler(endpointDef, handlerWrapper);
}
// Registers a handler for a verb + path combo.
registerHandler(
endpointDef: EndpointDefinition,
handler: (ctx) => any,
) {
this.endpoints.push(endpointDef);
const verb = endpointDef.verb;
const path = endpointDef.path;
switch (verb) {
case "GET":
this.app.get(path, handler);
break;
case "POST":
this.app.post(path, handler);
break;
case "PUT":
this.app.put(path, handler);
break;
case "DELETE":
this.app.delete(path, handler);
break;
case "PATCH":
this.app.patch(path, handler);
break;
default:
throw new Error(`HTTP verb ${verb} not supported`);
}
}
/**
* Usage: `export default gptApi.serve();`
*/

Example usage of the add_to_notion_w_ai val

Try with the money database.

Read and watch the demo run here

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
/** @jsxImportSource https://esm.sh/preact */
import { blob } from "https://esm.town/v/std/blob";
import process from "node:process";
import Instructor from "npm:@instructor-ai/instructor";
import { Client } from "npm:@notionhq/client";
import OpenAI from "npm:openai";
import { render } from "npm:preact-render-to-string";
import { z } from "npm:zod";
const dbid = "DB_ID_GOES_HERE";
const NOTION_API_KEY = process.env.NOTION_API_KEY;
const notion = new Client({
auth: NOTION_API_KEY,
});
const MAGIC_AI_FILL_SYMB = "✨";
const supported_notion_props = {
"checkbox": "boolean",
"date": "date",
"multi_select": "array_enum",
"number": "number",
"rich_text": "string",
"select": "enum",
"status": "enum",
"title": "string",
"url": "string_url",
"email": "string_email",
};
const oai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY ?? undefined,
});
const client = Instructor({
client: oai,
mode: "TOOLS",
});
function createPrompt(title, description, properties) {
let prompt =
"You are processing content into a database. Based on the title of the database, its properties, their types and options, and any existing descriptions, infer appropriate values for the fields:\n";
prompt += `Database Title: ${title}\n`;
if (description) {
prompt += `Database Description: ${description}\n\n`;
} else {
prompt += "\n";
}
prompt += "Properties (with types and options where applicable.):\n";
Object.keys(properties).forEach(key => {
const prop = properties[key];
prompt += `Name: ${prop.name}, Type: ${prop.type}`;
if (prop.description) {
prompt += `, Description: ${prop.description}`;
}
prompt += "\n";
if (prop.options) {
if (prop.type === "select" || prop.type === "status") {
prompt += "Options (choose one or none of these options):\n";
} else if (prop.type === "multi_select") {
prompt += "Options (choose one, multiple or none of these options):\n";
}
prop.options.forEach(option => {
prompt += ` - ${option.name}`;
if (option.description) {
prompt += `: ${option.description}`;
}
prompt += "\n";
});
}
});
prompt +=
"\nInfer and assign values to these properties based on the provided content and the aforementioned cotext.";
return prompt;
}
function processProperties(jsonObject) {
const properties = jsonObject.properties;
const filteredProps = {};
Object.keys(properties).forEach(key => {
const prop = properties[key];
const supportedType = supported_notion_props[prop.type];
if (supportedType && (prop.description?.startsWith(MAGIC_AI_FILL_SYMB) || prop.type === "title")) {
filteredProps[key] = {
name: prop.name,
type: prop.type,
description: prop.description ? prop.description.replace(MAGIC_AI_FILL_SYMB, "") : "",
};

Uses instructor and open ai (with gpt-4-turbo) to process any content into a notion database entry.

Use addToNotion with any database id and content.

await addToNotion(
  "DB_ID_GOES_HERE",
  "CONTENT_GOES HERE"//"for example: $43.28 ordered malai kofta and kadhi (doordash) [me and mom] jan 3 2024"
);

Prompts are created based on your database name, database description, property name, property type, property description, and if applicable, property options (and their descriptions).

Supports: checkbox, date, multi_select, number, rich_text, select, status, title, url, email

  • Uses NOTION_API_KEY, OPENAI_API_KEY stored in env variables and uses Valtown blob storage to store information about the database.
  • Use get_notion_db_info to use the stored blob if exists or create one, use get_and_save_notion_db_info to create a new blob (and replace an existing one if exists).
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 { blob } from "https://esm.town/v/std/blob";
import process from "node:process";
import Instructor from "npm:@instructor-ai/instructor";
import { Client } from "npm:@notionhq/client";
import OpenAI from "npm:openai";
import { z } from "npm:zod";
const NOTION_API_KEY = process.env.NOTION_API_KEY;
const notion = new Client({
auth: NOTION_API_KEY,
});
const MAGIC_AI_FILL_SYMB = "✨";
const supported_notion_props = {
"checkbox": "boolean",
"date": "date",
"multi_select": "array_enum",
"number": "number",
"rich_text": "string",
"select": "enum",
"status": "enum",
"title": "string",
"url": "string_url",
"email": "string_email",
};
const oai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY ?? undefined,
});
const client = Instructor({
client: oai,
mode: "TOOLS",
});
function createPrompt(title, description, properties) {
let prompt =
"You are processing content into a database. Based on the title of the database, its properties, their types and options, and any existing descriptions, infer appropriate values for the fields:\n";
prompt += `Database Title: ${title}\n`;
if (description) {
prompt += `Database Description: ${description}\n\n`;
} else {
prompt += "\n";
}
prompt += "Properties (with types and options where applicable.):\n";
Object.keys(properties).forEach(key => {
const prop = properties[key];
prompt += `Name: ${prop.name}, Type: ${prop.type}`;
if (prop.description) {
prompt += `, Description: ${prop.description}`;
}
prompt += "\n";
if (prop.options) {
if (prop.type === "select" || prop.type === "status") {
prompt += "Options (choose one or none of these options):\n";
} else if (prop.type === "multi_select") {
prompt += "Options (choose one, multiple or none of these options):\n";
}
prop.options.forEach(option => {
prompt += ` - ${option.name}`;
if (option.description) {
prompt += `: ${option.description}`;
}
prompt += "\n";
});
}
});
prompt +=
"\nInfer and assign values to these properties based on the provided content and the aforementioned cotext.";
return prompt;
}
function processProperties(jsonObject) {
const properties = jsonObject.properties;
const filteredProps = {};
Object.keys(properties).forEach(key => {
const prop = properties[key];
const supportedType = supported_notion_props[prop.type];
if (supportedType && (prop.description?.startsWith(MAGIC_AI_FILL_SYMB) || prop.type === "title")) {
filteredProps[key] = {
name: prop.name,
type: prop.type,
description: prop.description ? prop.description.replace(MAGIC_AI_FILL_SYMB, "") : "",
};
if (prop.type === "multi_select" || prop.type === "select" || prop.type === "status") {
filteredProps[key].options = prop[prop.type].options.map(option => ({
name: option.name,
description: option.description || "",