@browserlessapi/inferlo 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +130 -2
  3. package/dist/ai/index.d.ts +138 -0
  4. package/dist/ai/index.js +450 -0
  5. package/dist/ai/index.js.map +1 -0
  6. package/dist/ai/providers.d.ts +150 -0
  7. package/dist/ai/providers.js +294 -0
  8. package/dist/ai/providers.js.map +1 -0
  9. package/dist/app.d.ts +109 -0
  10. package/dist/app.js +419 -0
  11. package/dist/app.js.map +1 -0
  12. package/dist/auth.d.ts +29 -0
  13. package/dist/auth.js +61 -0
  14. package/dist/auth.js.map +1 -0
  15. package/dist/cli/main.d.ts +2 -0
  16. package/dist/cli/main.js +520 -0
  17. package/dist/cli/main.js.map +1 -0
  18. package/dist/client-ip.d.ts +7 -0
  19. package/dist/client-ip.js +27 -0
  20. package/dist/client-ip.js.map +1 -0
  21. package/dist/cron.d.ts +17 -0
  22. package/dist/cron.js +143 -0
  23. package/dist/cron.js.map +1 -0
  24. package/dist/db/memory.d.ts +2 -0
  25. package/dist/db/memory.js +128 -0
  26. package/dist/db/memory.js.map +1 -0
  27. package/dist/db/migrate.d.ts +87 -0
  28. package/dist/db/migrate.js +471 -0
  29. package/dist/db/migrate.js.map +1 -0
  30. package/dist/db/postgres.d.ts +37 -0
  31. package/dist/db/postgres.js +338 -0
  32. package/dist/db/postgres.js.map +1 -0
  33. package/dist/db/table.d.ts +117 -0
  34. package/dist/db/table.js +274 -0
  35. package/dist/db/table.js.map +1 -0
  36. package/dist/db/types.d.ts +36 -0
  37. package/dist/db/types.js +2 -0
  38. package/dist/db/types.js.map +1 -0
  39. package/dist/describe.d.ts +74 -0
  40. package/dist/describe.js +69 -0
  41. package/dist/describe.js.map +1 -0
  42. package/dist/env.d.ts +19 -0
  43. package/dist/env.js +72 -0
  44. package/dist/env.js.map +1 -0
  45. package/dist/errors.d.ts +40 -0
  46. package/dist/errors.js +99 -0
  47. package/dist/errors.js.map +1 -0
  48. package/dist/index.d.ts +31 -0
  49. package/dist/index.js +20 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/jobs.d.ts +177 -0
  52. package/dist/jobs.js +374 -0
  53. package/dist/jobs.js.map +1 -0
  54. package/dist/mcp.d.ts +22 -0
  55. package/dist/mcp.js +171 -0
  56. package/dist/mcp.js.map +1 -0
  57. package/dist/model.d.ts +36 -0
  58. package/dist/model.js +76 -0
  59. package/dist/model.js.map +1 -0
  60. package/dist/openapi.d.ts +6 -0
  61. package/dist/openapi.js +74 -0
  62. package/dist/openapi.js.map +1 -0
  63. package/dist/rate-limit.d.ts +45 -0
  64. package/dist/rate-limit.js +93 -0
  65. package/dist/rate-limit.js.map +1 -0
  66. package/dist/route.d.ts +131 -0
  67. package/dist/route.js +28 -0
  68. package/dist/route.js.map +1 -0
  69. package/dist/schema.d.ts +164 -0
  70. package/dist/schema.js +442 -0
  71. package/dist/schema.js.map +1 -0
  72. package/dist/serve.d.ts +11 -0
  73. package/dist/serve.js +86 -0
  74. package/dist/serve.js.map +1 -0
  75. package/dist/testing.d.ts +16 -0
  76. package/dist/testing.js +30 -0
  77. package/dist/testing.js.map +1 -0
  78. package/llms.txt +27 -0
  79. package/package.json +82 -4
  80. package/templates/app/AGENTS.md +55 -0
  81. package/templates/app/CLAUDE.md +1 -0
  82. package/templates/app/README.md +16 -0
  83. package/templates/app/_env.example +5 -0
  84. package/templates/app/_gitignore +3 -0
  85. package/templates/app/package.json.tpl +32 -0
  86. package/templates/app/src/app.ts +38 -0
  87. package/templates/app/src/models.ts +31 -0
  88. package/templates/app/src/routes/health.ts +3 -0
  89. package/templates/app/src/routes/notes.ts +56 -0
  90. package/templates/app/src/server.ts +26 -0
  91. package/templates/app/test/notes.test.ts +50 -0
  92. package/templates/app/tsconfig.json +17 -0
package/llms.txt ADDED
@@ -0,0 +1,27 @@
1
+ # Inferlo
2
+
3
+ > Inferlo is a batteries-included TypeScript backend framework with built-in AI model support (Anthropic, OpenAI, Gemini, Ollama and OpenAI-compatible providers with fallback, structured output, streaming, embeddings and usage tracking), plus routes, schemas, database (PostgreSQL and in-memory), auth, an MCP server and OpenAPI, with zero runtime dependencies. Runs on Node.js 22.18+, Bun, Deno and edge runtimes. Package: @browserlessapi/inferlo.
4
+
5
+ Key rules: routes are private unless `auth: "public"`; validate input with `t.*` schemas; use `db.<table>` methods (never string-built SQL); every model has id, createdAt, updatedAt automatically; relative imports end in `.ts`; run `inferlo describe --json` to see an app and `inferlo check --json` after changes.
6
+
7
+ ## Docs
8
+
9
+ - [Getting started](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/getting-started.md): install, project layout, minimal app
10
+ - [Schemas](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/schema.md): t.* types, modifiers, validation, JSON Schema
11
+ - [Models and database](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/database.md): model(), queries, adapters, db push, transactions
12
+ - [Routes](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/routes.md): options, handler context, return values, errors, middleware
13
+ - [Auth](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/auth.md): auth.resolve, roles, bearer API keys, passwords
14
+ - [AI](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/ai.md): providers, aliases and fallback, structured output, streaming, embeddings, usage and budgets
15
+ - [MCP, OpenAPI and describe](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/mcp.md)
16
+ - [CLI](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/cli.md)
17
+ - [Testing](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/testing.md)
18
+ - [Deployment](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/deploy.md)
19
+ - [Error codes](https://github.com/BrowserlessAPI/Inferlo/blob/main/docs/errors.md): every KRxxxx code with its fix
20
+
21
+ ## Full text
22
+
23
+ - [llms-full.txt](https://raw.githubusercontent.com/BrowserlessAPI/Inferlo/main/llms-full.txt): all docs in one file
24
+
25
+ ## Example
26
+
27
+ - [Helpdesk with AI triage](https://github.com/BrowserlessAPI/Inferlo/tree/main/examples/helpdesk)
package/package.json CHANGED
@@ -1,6 +1,84 @@
1
1
  {
2
2
  "name": "@browserlessapi/inferlo",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.4.0",
4
+ "description": "The TypeScript backend framework with built-in AI model support: Anthropic, OpenAI, Gemini, Ollama and OpenAI-compatible models, plus routing, database, validation, auth and MCP in one package.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "BrowserlessAPI",
8
+ "homepage": "https://github.com/BrowserlessAPI/Inferlo#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/BrowserlessAPI/Inferlo.git"
12
+ },
13
+ "bugs": "https://github.com/BrowserlessAPI/Inferlo/issues",
14
+ "keywords": [
15
+ "typescript",
16
+ "backend",
17
+ "framework",
18
+ "api",
19
+ "rest",
20
+ "orm",
21
+ "validation",
22
+ "ai",
23
+ "llm",
24
+ "mcp",
25
+ "ai-agents",
26
+ "bun",
27
+ "deno",
28
+ "edge"
29
+ ],
30
+ "engines": {
31
+ "node": ">=22.18"
32
+ },
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "./postgres": {
39
+ "types": "./dist/db/postgres.d.ts",
40
+ "default": "./dist/db/postgres.js"
41
+ },
42
+ "./testing": {
43
+ "types": "./dist/testing.d.ts",
44
+ "default": "./dist/testing.js"
45
+ }
46
+ },
47
+ "bin": {
48
+ "inferlo": "./dist/cli/main.js"
49
+ },
50
+ "files": [
51
+ "dist",
52
+ "templates",
53
+ "README.md",
54
+ "LICENSE",
55
+ "llms.txt"
56
+ ],
57
+ "scripts": {
58
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && chmod +x dist/cli/main.js",
59
+ "prepare": "npm run build",
60
+ "typecheck": "tsc --noEmit && tsc --noEmit -p examples/helpdesk",
61
+ "test": "node --test --test-reporter=spec \"test/**/*.test.ts\" \"examples/*/test/*.test.ts\"",
62
+ "test:bun": "bun test test/",
63
+ "llms": "node scripts/build-llms.ts",
64
+ "check": "npm run typecheck && npm test && node scripts/build-llms.ts --check",
65
+ "bench": "node bench/run.ts"
66
+ },
67
+ "peerDependencies": {
68
+ "pg": ">=8"
69
+ },
70
+ "peerDependenciesMeta": {
71
+ "pg": {
72
+ "optional": true
73
+ }
74
+ },
75
+ "devDependencies": {
76
+ "@types/node": "^22.18.0",
77
+ "@types/pg": "^8.15.0",
78
+ "pg": "^8.16.0",
79
+ "typescript": "^5.9.3"
80
+ },
81
+ "publishConfig": {
82
+ "access": "public"
83
+ }
84
+ }
@@ -0,0 +1,55 @@
1
+ # AGENTS.md
2
+
3
+ This is a [Inferlo](https://github.com/BrowserlessAPI/Inferlo) backend. Inferlo is a TypeScript backend framework designed so AI coding agents get things right the first time. Follow these rules.
4
+
5
+ ## Before you change anything
6
+
7
+ ```bash
8
+ npx inferlo describe --json # every model, field, route, auth rule and AI alias
9
+ ```
10
+
11
+ Read that instead of searching the codebase. After every change run:
12
+
13
+ ```bash
14
+ npx inferlo check --json # typecheck + load the app + tests, machine-readable errors
15
+ ```
16
+
17
+ Do not finish until `inferlo check` passes.
18
+
19
+ ## Layout
20
+
21
+ | File | What goes there |
22
+ |---|---|
23
+ | `src/models.ts` | Every model (`model("table_name", { ... })`) and the `models` array |
24
+ | `src/routes/<feature>.ts` | Routes for one feature, each one an exported `const` |
25
+ | `src/app.ts` | `createApp({ models, routes, db, auth, ai })`. Import route modules with `import * as x` |
26
+ | `src/server.ts` | Starts the server. Keep it small |
27
+ | `test/<feature>.test.ts` | Tests using `app.request(path, { method, json, as: user })` |
28
+
29
+ ## Rules
30
+
31
+ 1. **One way to do each thing.** Routes: `route.get/post/put/patch/delete(path, options).handle(fn)`. Validation: `t.*` schemas. Database: `db.<table>.<method>`. AI: `ai.generate` / `ai.stream` / `ai.embed`.
32
+ 2. **Routes are private by default.** A route without `auth` needs a signed-in user. Use `auth: "public"` only for routes anyone may call, and `auth: "<role>"` for role checks.
33
+ 3. **Validate every input with a schema.** `body`, `query` and `params` options. Reuse model schemas: `Ticket.pick("title", "body")`, `Ticket.omit("ownerId")`, `.partial()` for PATCH.
34
+ 4. **Check ownership.** Fetch with `db.x.get(id)` (throws 404) and compare `ownerId` to `user.id`; throw `notFound()` when it does not match, so other users' ids are not revealed.
35
+ 5. **Related rows:** a `t.ref` field named `<name>Id` is a relation: `db.notes.findMany({ include: { owner: true } })` adds `note.owner` with one extra query, and `db.users.findMany({ include: { notes: true } })` adds `user.notes` (the rows that point to each user). Never load related rows in a loop.
36
+ 6. **Never write raw SQL with string concatenation.** Use the table methods. If you really need SQL, use `db.$sql(text, params)` with `$1` placeholders.
37
+ 7. **Model ids, timestamps:** every model already has `id`, `createdAt`, `updatedAt`. Never define them or send them as input.
38
+ 8. **New field?** Add it to the model in `src/models.ts`, then run `npx inferlo db push` in development, or `npx inferlo db migrate <name>` to write a migration file for production (applied with `npx inferlo db migrate --apply`). Use `.optional()` or `.default()` for new fields on existing tables. Never edit `migrations/snapshot.json`, and never edit a migration file that has been applied (`--apply` warns with IL4018 and does not run the change): put changes in a new migration. Removing a model or field needs `npx inferlo db migrate <name> --allow-drop` (it deletes that data); never use it for a rename. Changing a field's type or default, making it required or optional, or changing an enum's values needs `--allow-change`; give a newly required field a `.default()` so existing NULL rows get a value, and before removing an enum value add an `UPDATE` in the new migration file (before its `ADD CONSTRAINT`) that moves rows off it (IL4021).
39
+ 9. **New resource?** `npx inferlo add resource <plural_name>` generates the model and CRUD routes. Then edit them.
40
+ 10. **AI calls** use aliases from `src/app.ts` (`model: "fast"`), never hard-coded provider model ids in routes. Use `output: schema` for structured data and `for: user.id` so usage is tracked per user. To let the model act, pass routes as tools: `ai.generate({ prompt, tools: [createNote, getNote], for: user.id })`. Each tool call runs as the current user with that route's auth and validation. To send images: `ai.generate({ prompt, images: [{ url }] })` or `[{ base64, mediaType: "image/png" }]`. Give routes that call AI models, send email or check passwords a `rateLimit: { max, per, by }` (IL2005 is the 429 it returns).
41
+ 11. **Errors:** throw `notFound()`, `forbidden()`, `badRequest()` or `httpError(status, message)`. Every Inferlo error has a code (ILxxxx), a fix and a docs link: read the fix before changing code.
42
+ 12. **Imports:** relative imports end in `.ts` (`import { Note } from "../models.ts"`).
43
+ 13. **Expose to AI agents:** add `tool: true` to a route to make it an MCP tool at `/mcp`. Same auth applies.
44
+ 14. **Slow work after the response** (email, slow AI calls) is a background job: `export const x = job("send-email", { input: t.object({ ... }) }).handle(async ({ input, db, ai }) => { ... })` in `src/jobs.ts`, registered with `createApp({ jobs: [jobs] })`, queued with `await ctx.jobs.enqueue(x, { ... })`. Pass ids in the input, not rows. In tests, run queued jobs with `await app.jobs.runDue()`. Jobs run in the server process (`app.jobs.start()` in `src/server.ts`); with `createApp({ jobQueue: { store: "database" } })` they are kept in Postgres and `npm run worker` (`inferlo worker`) runs them in separate processes. Scheduled work is a cron next to its job: `export const nightly = cron("0 3 * * *", cleanupJob)` (UTC; the job's input must be `t.object({})`).
45
+
46
+ ## Testing
47
+
48
+ ```ts
49
+ const res = await app.request("/notes", { method: "POST", json: { title: "Hi" }, as: user });
50
+ const note = await readJson(res, 201); // from "@browserlessapi/inferlo/testing"
51
+ ```
52
+
53
+ Use `memory()` for the database and `mockProvider()` for AI in tests.
54
+
55
+ Full docs: https://github.com/BrowserlessAPI/Inferlo/tree/main/docs
@@ -0,0 +1 @@
1
+ Read AGENTS.md before making changes. Run `npx inferlo describe --json` to see the app, and `npx inferlo check --json` after every change.
@@ -0,0 +1,16 @@
1
+ # {{name}}
2
+
3
+ Built with [Inferlo](https://github.com/BrowserlessAPI/Inferlo).
4
+
5
+ ```bash
6
+ npm install
7
+ npm run dev # starts on http://localhost:3000 and prints a dev API key
8
+ npm run check # typecheck + tests
9
+ npx inferlo describe # routes, models and auth at a glance
10
+ ```
11
+
12
+ Set `DATABASE_URL` in `.env` to use PostgreSQL, then run `npx inferlo db push`.
13
+
14
+ Background jobs run in the server process. To run them in separate processes too, set `jobQueue: { store: "database" }` in `src/app.ts` (needs PostgreSQL) and start workers with `npm run worker`.
15
+
16
+ AI coding agents: read `AGENTS.md`.
@@ -0,0 +1,5 @@
1
+ # Leave DATABASE_URL empty to use the in-memory database (data resets on restart).
2
+ DATABASE_URL=
3
+ PORT=3000
4
+ ANTHROPIC_API_KEY=
5
+ OPENAI_API_KEY=
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ .env
3
+ dist
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "inferlo": {
7
+ "entry": "src/app.ts",
8
+ "server": "src/server.ts"
9
+ },
10
+ "scripts": {
11
+ "dev": "inferlo dev",
12
+ "start": "node src/server.ts",
13
+ "worker": "inferlo worker",
14
+ "check": "inferlo check",
15
+ "describe": "inferlo describe",
16
+ "db:push": "inferlo db push",
17
+ "db:migrate": "inferlo db migrate",
18
+ "test": "node --test \"test/**/*.test.ts\"",
19
+ "typecheck": "tsc --noEmit"
20
+ },
21
+ "engines": {
22
+ "node": ">=22.18"
23
+ },
24
+ "dependencies": {
25
+ "@browserlessapi/inferlo": "{{inferloDependency}}",
26
+ "pg": "^8.16.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^22.18.0",
30
+ "typescript": "^5.9.3"
31
+ }
32
+ }
@@ -0,0 +1,38 @@
1
+ import { anthropic, bearer, createApp, env, hashToken, memory, openai } from "@browserlessapi/inferlo";
2
+ import { postgres } from "@browserlessapi/inferlo/postgres";
3
+ import { models } from "./models.ts";
4
+ import * as health from "./routes/health.ts";
5
+ import * as notes from "./routes/notes.ts";
6
+
7
+ const databaseUrl = env("DATABASE_URL", { optional: true });
8
+
9
+ export const app = createApp({
10
+ name: "{{name}}",
11
+ version: "0.1.0",
12
+ models,
13
+ // inferlo:routes
14
+ routes: [health, notes],
15
+ db: databaseUrl ? postgres({ url: databaseUrl }) : memory(),
16
+
17
+ // Requests send "Authorization: Bearer <api key>". Keys are stored as SHA-256 hashes.
18
+ auth: bearer(async (token, { db }) => {
19
+ const key = await db.api_keys.findOne({ hash: await hashToken(token) });
20
+ if (!key) return null;
21
+ const user = await db.users.find(key.userId);
22
+ return user ? { id: user.id, email: user.email, name: user.name, roles: [user.role] } : null;
23
+ }),
24
+
25
+ ai: {
26
+ providers: {
27
+ anthropic: anthropic({ apiKey: env("ANTHROPIC_API_KEY", { default: "" }) }),
28
+ openai: openai({ apiKey: env("OPENAI_API_KEY", { default: "" }) }),
29
+ },
30
+ // Aliases keep model ids in one place. A list means: try in order.
31
+ models: {
32
+ fast: ["anthropic:claude-haiku-4-5", "openai:gpt-5-mini"],
33
+ smart: ["anthropic:claude-sonnet-4-5", "openai:gpt-5"],
34
+ },
35
+ },
36
+ });
37
+
38
+ export default app;
@@ -0,0 +1,31 @@
1
+ import { model, t } from "@browserlessapi/inferlo";
2
+
3
+ export const User = model("users", {
4
+ email: t.email().unique(),
5
+ name: t.string().max(80),
6
+ role: t.enum(["member", "admin"]).default("member"),
7
+ });
8
+
9
+ export const ApiKey = model("api_keys", {
10
+ userId: t.ref("users", { onDelete: "cascade" }),
11
+ hash: t.string().max(64).unique(),
12
+ label: t.string().max(80).default("default"),
13
+ });
14
+
15
+ export const Note = model("notes", {
16
+ title: t.string().min(1).max(200),
17
+ body: t.text().default(""),
18
+ ownerId: t.ref("users", { onDelete: "cascade" }),
19
+ });
20
+
21
+ // inferlo:models
22
+ export const models = [User, ApiKey, Note] as const;
23
+
24
+ export type AppUser = { id: string; email: string; name: string; roles: string[] };
25
+
26
+ declare module "@browserlessapi/inferlo" {
27
+ interface Register {
28
+ models: typeof models;
29
+ user: AppUser;
30
+ }
31
+ }
@@ -0,0 +1,3 @@
1
+ import { route } from "@browserlessapi/inferlo";
2
+
3
+ export const health = route.get("/health", { auth: "public", summary: "Health check" }).handle(() => ({ ok: true }));
@@ -0,0 +1,56 @@
1
+ import { notFound, route, t } from "@browserlessapi/inferlo";
2
+ import { Note } from "../models.ts";
3
+
4
+ const IdParams = t.object({ id: t.uuid() });
5
+
6
+ export const listNotes = route
7
+ .get("/notes", {
8
+ summary: "List my notes",
9
+ query: t.object({ limit: t.int().min(1).max(100).default(20), offset: t.int().min(0).default(0) }),
10
+ tool: true,
11
+ })
12
+ .handle(({ query, user, db }) =>
13
+ db.notes.findMany({ where: { ownerId: user.id }, orderBy: { createdAt: "desc" }, limit: query.limit, offset: query.offset }),
14
+ );
15
+
16
+ export const getNote = route
17
+ .get("/notes/:id", { params: IdParams, summary: "Get one note", tool: true })
18
+ .handle(async ({ params, user, db }) => {
19
+ const note = await db.notes.get(params.id);
20
+ if (note.ownerId !== user.id) throw notFound();
21
+ return note;
22
+ });
23
+
24
+ export const createNote = route
25
+ .post("/notes", { body: Note.pick("title", "body"), summary: "Create a note", tool: true })
26
+ .handle(({ body, user, db, status }) => {
27
+ status(201);
28
+ return db.notes.insert({ ...body, ownerId: user.id });
29
+ });
30
+
31
+ export const updateNote = route
32
+ .patch("/notes/:id", { params: IdParams, body: Note.pick("title", "body").partial(), summary: "Update a note" })
33
+ .handle(async ({ params, body, user, db }) => {
34
+ const note = await db.notes.get(params.id);
35
+ if (note.ownerId !== user.id) throw notFound();
36
+ return db.notes.update(note.id, body);
37
+ });
38
+
39
+ export const deleteNote = route
40
+ .delete("/notes/:id", { params: IdParams, summary: "Delete a note" })
41
+ .handle(async ({ params, user, db }) => {
42
+ const note = await db.notes.get(params.id);
43
+ if (note.ownerId !== user.id) throw notFound();
44
+ await db.notes.delete(note.id);
45
+ });
46
+
47
+ const Summary = t.object({ summary: t.string().max(300).describe("One or two sentences") });
48
+
49
+ export const summarizeNote = route
50
+ .post("/notes/:id/summary", { params: IdParams, summary: "Summarize a note with AI", rateLimit: { max: 20, per: "hour" } })
51
+ .handle(async ({ params, user, db, ai }) => {
52
+ const note = await db.notes.get(params.id);
53
+ if (note.ownerId !== user.id) throw notFound();
54
+ const { object } = await ai.generate({ model: "fast", prompt: `Summarize:\n\n${note.title}\n\n${note.body}`, output: Summary, for: user.id });
55
+ return object;
56
+ });
@@ -0,0 +1,26 @@
1
+ import { createToken, env, hashToken } from "@browserlessapi/inferlo";
2
+ import { app } from "./app.ts";
3
+
4
+ await app.db.$sync();
5
+
6
+ // In-memory database: create a demo user and print an API key so you can try the API.
7
+ if (app.db.$adapter.dialect === "memory") {
8
+ const user = await app.db.users.insert({ email: "dev@example.com", name: "Developer", role: "admin" });
9
+ const token = createToken("dev");
10
+ await app.db.api_keys.insert({ userId: user.id, hash: await hashToken(token) });
11
+ console.log(`Dev API key: ${token}`);
12
+ console.log(`Try: curl -H "Authorization: Bearer ${token}" http://localhost:${env("PORT", { default: "3000" })}/notes`);
13
+ }
14
+
15
+ const server = await app.listen(env.int("PORT", { default: 3000 }));
16
+ console.log(`{{name}} running on http://localhost:${server.port}`);
17
+
18
+ // Background jobs run in this process. With jobQueue: { store: "database" } in src/app.ts they can
19
+ // also run in separate processes: npm run worker.
20
+ const jobs = app.jobs.start();
21
+
22
+ process.on("SIGTERM", async () => {
23
+ await jobs.stop(); // waits for the running job
24
+ await server.close();
25
+ await app.db.$close();
26
+ });
@@ -0,0 +1,50 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { createApp, memory, mockProvider } from "@browserlessapi/inferlo";
4
+ import { readJson } from "@browserlessapi/inferlo/testing";
5
+ import { models } from "../src/models.ts";
6
+ import * as notes from "../src/routes/notes.ts";
7
+
8
+ function makeApp() {
9
+ return createApp({
10
+ models,
11
+ routes: [notes],
12
+ db: memory(),
13
+ auth: { resolve: () => null },
14
+ ai: { providers: { mock: mockProvider({ respond: () => '{"summary":"A short note."}' }) }, models: { fast: "mock:test" } },
15
+ });
16
+ }
17
+
18
+ test("create, list, update and delete notes", async () => {
19
+ const app = makeApp();
20
+ const row = await app.db.users.insert({ email: "a@example.com", name: "A" });
21
+ const user = { id: row.id, email: row.email, name: row.name, roles: [] };
22
+
23
+ const note = await readJson(await app.request("/notes", { method: "POST", json: { title: "First" }, as: user }), 201);
24
+ assert.equal(note.title, "First");
25
+
26
+ const list = await readJson(await app.request("/notes", { as: user }), 200);
27
+ assert.equal(list.length, 1);
28
+
29
+ const updated = await readJson(await app.request(`/notes/${note.id}`, { method: "PATCH", json: { body: "Hello" }, as: user }), 200);
30
+ assert.equal(updated.body, "Hello");
31
+
32
+ const summary = await readJson(await app.request(`/notes/${note.id}/summary`, { method: "POST", as: user }), 200);
33
+ assert.equal(summary.summary, "A short note.");
34
+
35
+ assert.equal((await app.request(`/notes/${note.id}`, { method: "DELETE", as: user })).status, 204);
36
+ });
37
+
38
+ test("other users cannot read my notes", async () => {
39
+ const app = makeApp();
40
+ const a = await app.db.users.insert({ email: "a@example.com", name: "A" });
41
+ const b = await app.db.users.insert({ email: "b@example.com", name: "B" });
42
+ const note = await app.db.notes.insert({ title: "Private", ownerId: a.id });
43
+ const res = await app.request(`/notes/${note.id}`, { as: { id: b.id, email: b.email, name: b.name, roles: [] } });
44
+ assert.equal(res.status, 404);
45
+ });
46
+
47
+ test("requests without a key are rejected", async () => {
48
+ const res = await makeApp().request("/notes");
49
+ assert.equal(res.status, 401);
50
+ });
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
5
+ "module": "NodeNext",
6
+ "moduleResolution": "NodeNext",
7
+ "strict": true,
8
+ "noUncheckedIndexedAccess": true,
9
+ "verbatimModuleSyntax": true,
10
+ "erasableSyntaxOnly": true,
11
+ "allowImportingTsExtensions": true,
12
+ "noEmit": true,
13
+ "skipLibCheck": true,
14
+ "types": ["node"]
15
+ },
16
+ "include": ["src", "test"]
17
+ }