@browserlessapi/inferlo 0.0.0-stage → 0.3.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.
- package/LICENSE +21 -0
- package/README.md +127 -2
- package/dist/ai/index.d.ts +96 -0
- package/dist/ai/index.js +327 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/providers.d.ts +97 -0
- package/dist/ai/providers.js +204 -0
- package/dist/ai/providers.js.map +1 -0
- package/dist/app.d.ts +94 -0
- package/dist/app.js +365 -0
- package/dist/app.js.map +1 -0
- package/dist/auth.d.ts +29 -0
- package/dist/auth.js +61 -0
- package/dist/auth.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +467 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/db/memory.d.ts +2 -0
- package/dist/db/memory.js +128 -0
- package/dist/db/memory.js.map +1 -0
- package/dist/db/migrate.d.ts +84 -0
- package/dist/db/migrate.js +355 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/postgres.d.ts +37 -0
- package/dist/db/postgres.js +338 -0
- package/dist/db/postgres.js.map +1 -0
- package/dist/db/table.d.ts +72 -0
- package/dist/db/table.js +177 -0
- package/dist/db/table.js.map +1 -0
- package/dist/db/types.d.ts +36 -0
- package/dist/db/types.js +2 -0
- package/dist/db/types.js.map +1 -0
- package/dist/describe.d.ts +56 -0
- package/dist/describe.js +64 -0
- package/dist/describe.js.map +1 -0
- package/dist/env.d.ts +19 -0
- package/dist/env.js +72 -0
- package/dist/env.js.map +1 -0
- package/dist/errors.d.ts +40 -0
- package/dist/errors.js +99 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +18 -0
- package/dist/mcp.js +166 -0
- package/dist/mcp.js.map +1 -0
- package/dist/model.d.ts +36 -0
- package/dist/model.js +76 -0
- package/dist/model.js.map +1 -0
- package/dist/openapi.d.ts +6 -0
- package/dist/openapi.js +71 -0
- package/dist/openapi.js.map +1 -0
- package/dist/route.d.ts +125 -0
- package/dist/route.js +28 -0
- package/dist/route.js.map +1 -0
- package/dist/schema.d.ts +156 -0
- package/dist/schema.js +430 -0
- package/dist/schema.js.map +1 -0
- package/dist/serve.d.ts +11 -0
- package/dist/serve.js +83 -0
- package/dist/serve.js.map +1 -0
- package/dist/testing.d.ts +16 -0
- package/dist/testing.js +30 -0
- package/dist/testing.js.map +1 -0
- package/llms.txt +27 -0
- package/package.json +82 -4
- package/templates/app/AGENTS.md +53 -0
- package/templates/app/CLAUDE.md +1 -0
- package/templates/app/README.md +14 -0
- package/templates/app/_env.example +5 -0
- package/templates/app/_gitignore +3 -0
- package/templates/app/package.json.tpl +31 -0
- package/templates/app/src/app.ts +38 -0
- package/templates/app/src/models.ts +31 -0
- package/templates/app/src/routes/health.ts +3 -0
- package/templates/app/src/routes/notes.ts +56 -0
- package/templates/app/src/server.ts +16 -0
- package/templates/app/test/notes.test.ts +50 -0
- package/templates/app/tsconfig.json +17 -0
package/package.json
CHANGED
|
@@ -1,6 +1,84 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@browserlessapi/inferlo",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.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,53 @@
|
|
|
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. **Never write raw SQL with string concatenation.** Use the table methods. If you really need SQL, use `db.$sql(text, params)` with `$1` placeholders.
|
|
36
|
+
6. **Model ids, timestamps:** every model already has `id`, `createdAt`, `updatedAt`. Never define them or send them as input.
|
|
37
|
+
7. **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 default or making it required or optional needs `--allow-change`; give a newly required field a `.default()` so existing NULL rows get a value.
|
|
38
|
+
8. **New resource?** `npx inferlo add resource <plural_name>` generates the model and CRUD routes. Then edit them.
|
|
39
|
+
9. **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.
|
|
40
|
+
10. **Errors:** throw `notFound()`, `forbidden()`, `badRequest()` or `httpError(status, message)`. Every Inferlo error has a code (KRxxxx), a fix and a docs link: read the fix before changing code.
|
|
41
|
+
11. **Imports:** relative imports end in `.ts` (`import { Note } from "../models.ts"`).
|
|
42
|
+
12. **Expose to AI agents:** add `tool: true` to a route to make it an MCP tool at `/mcp`. Same auth applies.
|
|
43
|
+
|
|
44
|
+
## Testing
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const res = await app.request("/notes", { method: "POST", json: { title: "Hi" }, as: user });
|
|
48
|
+
const note = await readJson(res, 201); // from "@browserlessapi/inferlo/testing"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Use `memory()` for the database and `mockProvider()` for AI in tests.
|
|
52
|
+
|
|
53
|
+
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,14 @@
|
|
|
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
|
+
AI coding agents: read `AGENTS.md`.
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
"check": "inferlo check",
|
|
14
|
+
"describe": "inferlo describe",
|
|
15
|
+
"db:push": "inferlo db push",
|
|
16
|
+
"db:migrate": "inferlo db migrate",
|
|
17
|
+
"test": "node --test \"test/**/*.test.ts\"",
|
|
18
|
+
"typecheck": "tsc --noEmit"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=22.18"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@browserlessapi/inferlo": "{{inferloDependency}}",
|
|
25
|
+
"pg": "^8.16.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^22.18.0",
|
|
29
|
+
"typescript": "^5.9.3"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -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,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" })
|
|
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,16 @@
|
|
|
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}`);
|
|
@@ -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
|
+
}
|