mirror of
https://github.com/kennethnym/aris.git
synced 2026-03-20 17:11:17 +00:00
* feat(backend): add GET /api/context endpoint Query context values by key with exact/prefix match support. Default mode tries exact first, falls back to prefix. Co-authored-by: Ona <no-reply@ona.com> * fix(backend): validate context key element types Reject booleans, nulls, and nested arrays in the key param. Only string, number, and plain objects with primitive values are accepted. Co-authored-by: Ona <no-reply@ona.com> --------- Co-authored-by: Ona <no-reply@ona.com>
119 lines
3.4 KiB
TypeScript
119 lines
3.4 KiB
TypeScript
import type { Context, Hono } from "hono"
|
|
|
|
import { contextKey } from "@aelis/core"
|
|
import { createMiddleware } from "hono/factory"
|
|
|
|
import type { AuthSessionMiddleware } from "../auth/session-middleware.ts"
|
|
import type { UserSessionManager } from "../session/index.ts"
|
|
|
|
type Env = {
|
|
Variables: {
|
|
sessionManager: UserSessionManager
|
|
}
|
|
}
|
|
|
|
interface FeedHttpHandlersDeps {
|
|
sessionManager: UserSessionManager
|
|
authSessionMiddleware: AuthSessionMiddleware
|
|
}
|
|
|
|
export function registerFeedHttpHandlers(
|
|
app: Hono,
|
|
{ sessionManager, authSessionMiddleware }: FeedHttpHandlersDeps,
|
|
) {
|
|
const inject = createMiddleware<Env>(async (c, next) => {
|
|
c.set("sessionManager", sessionManager)
|
|
await next()
|
|
})
|
|
|
|
app.get("/api/feed", inject, authSessionMiddleware, handleGetFeed)
|
|
app.get("/api/context", inject, authSessionMiddleware, handleGetContext)
|
|
}
|
|
|
|
async function handleGetFeed(c: Context<Env>) {
|
|
const user = c.get("user")!
|
|
const sessionManager = c.get("sessionManager")
|
|
const session = sessionManager.getOrCreate(user.id)
|
|
|
|
const feed = await session.feed()
|
|
|
|
return c.json({
|
|
items: feed.items,
|
|
errors: feed.errors.map((e) => ({
|
|
sourceId: e.sourceId,
|
|
error: e.error.message,
|
|
})),
|
|
})
|
|
}
|
|
|
|
function handleGetContext(c: Context<Env>) {
|
|
const keyParam = c.req.query("key")
|
|
if (!keyParam) {
|
|
return c.json({ error: 'Invalid or missing "key" parameter: must be a JSON array' }, 400)
|
|
}
|
|
|
|
let parsed: unknown
|
|
try {
|
|
parsed = JSON.parse(keyParam)
|
|
} catch {
|
|
return c.json({ error: 'Invalid or missing "key" parameter: must be a JSON array' }, 400)
|
|
}
|
|
|
|
if (!Array.isArray(parsed) || parsed.length === 0 || !parsed.every(isContextKeyPart)) {
|
|
return c.json({ error: 'Invalid or missing "key" parameter: must be a JSON array' }, 400)
|
|
}
|
|
|
|
const matchParam = c.req.query("match")
|
|
if (matchParam !== undefined && matchParam !== "exact" && matchParam !== "prefix") {
|
|
return c.json({ error: 'Invalid "match" parameter: must be "exact" or "prefix"' }, 400)
|
|
}
|
|
|
|
const user = c.get("user")!
|
|
const sessionManager = c.get("sessionManager")
|
|
const session = sessionManager.getOrCreate(user.id)
|
|
const context = session.engine.currentContext()
|
|
const key = contextKey(...parsed)
|
|
|
|
if (matchParam === "exact") {
|
|
const value = context.get(key)
|
|
if (value === undefined) {
|
|
return c.json({ error: "Context key not found" }, 404)
|
|
}
|
|
return c.json({ match: "exact", value })
|
|
}
|
|
|
|
if (matchParam === "prefix") {
|
|
const entries = context.find(key)
|
|
if (entries.length === 0) {
|
|
return c.json({ error: "Context key not found" }, 404)
|
|
}
|
|
return c.json({ match: "prefix", entries })
|
|
}
|
|
|
|
// Default: single find() covers both exact and prefix matches
|
|
const entries = context.find(key)
|
|
if (entries.length === 0) {
|
|
return c.json({ error: "Context key not found" }, 404)
|
|
}
|
|
|
|
// If exactly one result with the same key length, treat as exact match
|
|
if (entries.length === 1 && entries[0]!.key.length === parsed.length) {
|
|
return c.json({ match: "exact", value: entries[0]!.value })
|
|
}
|
|
|
|
return c.json({ match: "prefix", entries })
|
|
}
|
|
|
|
/** Validates that a value is a valid ContextKeyPart (string, number, or plain object of primitives). */
|
|
function isContextKeyPart(value: unknown): boolean {
|
|
if (typeof value === "string" || typeof value === "number") {
|
|
return true
|
|
}
|
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
return Object.values(value).every(
|
|
(v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean",
|
|
)
|
|
}
|
|
return false
|
|
}
|