@hexis-ai/engram-server 0.1.7 → 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/dist/adapters/memory-key-store.d.ts +1 -1
- package/dist/adapters/memory-key-store.js +1 -1
- package/dist/adapters/postgres-key-store.d.ts +1 -1
- package/dist/adapters/postgres-key-store.js +2 -2
- package/dist/admin.js +11 -5
- package/dist/context.d.ts +26 -0
- package/dist/context.js +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/key-store.d.ts +4 -4
- package/dist/openapi.d.ts +8 -0
- package/dist/openapi.js +296 -0
- package/dist/routes/helpers.d.ts +22 -0
- package/dist/routes/helpers.js +32 -0
- package/dist/routes/persons.d.ts +13 -0
- package/dist/routes/persons.js +67 -0
- package/dist/routes/search.d.ts +9 -0
- package/dist/routes/search.js +40 -0
- package/dist/routes/sessions.d.ts +11 -0
- package/dist/routes/sessions.js +51 -0
- package/dist/schemas.d.ts +100 -0
- package/dist/schemas.js +108 -0
- package/dist/server.d.ts +7 -21
- package/dist/server.js +17 -156
- package/openapi.json +937 -0
- package/package.json +6 -4
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { eventBatchSchema, parseJsonBody, sessionInitSchema } from "../schemas";
|
|
3
|
+
import { clampLimit, resolvePersonMap } from "./helpers";
|
|
4
|
+
/**
|
|
5
|
+
* Session routes. Mount under `/v1`:
|
|
6
|
+
* POST /v1/sessions create a session
|
|
7
|
+
* POST /v1/sessions/:id/events append events
|
|
8
|
+
* GET /v1/sessions/:id fetch one session + its persons map
|
|
9
|
+
* GET /v1/sessions list recent sessions + persons map
|
|
10
|
+
*/
|
|
11
|
+
export function sessionsRoutes(cfg) {
|
|
12
|
+
const app = new Hono();
|
|
13
|
+
app.post("/sessions", async (c) => {
|
|
14
|
+
const body = await parseJsonBody(c, sessionInitSchema);
|
|
15
|
+
if (body instanceof Response)
|
|
16
|
+
return body;
|
|
17
|
+
const id = body.id ?? cfg.newSessionId();
|
|
18
|
+
const createdAt = new Date().toISOString();
|
|
19
|
+
await c.var.ctx.storage.createSession({ ...body, id, createdAt });
|
|
20
|
+
return c.json({ id });
|
|
21
|
+
});
|
|
22
|
+
app.post("/sessions/:id/events", async (c) => {
|
|
23
|
+
const id = c.req.param("id");
|
|
24
|
+
const body = await parseJsonBody(c, eventBatchSchema);
|
|
25
|
+
if (body instanceof Response)
|
|
26
|
+
return body;
|
|
27
|
+
try {
|
|
28
|
+
await c.var.ctx.storage.appendEvents(id, body.events);
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
return c.json({ error: "session_not_found", message: e.message }, 404);
|
|
32
|
+
}
|
|
33
|
+
return c.body(null, 204);
|
|
34
|
+
});
|
|
35
|
+
app.get("/sessions/:id", async (c) => {
|
|
36
|
+
const id = c.req.param("id");
|
|
37
|
+
const s = await c.var.ctx.storage.getSession(id);
|
|
38
|
+
if (!s)
|
|
39
|
+
return c.json({ error: "session_not_found" }, 404);
|
|
40
|
+
const persons = await resolvePersonMap(c.var.ctx.storage, [s]);
|
|
41
|
+
return c.json({ session: s, persons });
|
|
42
|
+
});
|
|
43
|
+
app.get("/sessions", async (c) => {
|
|
44
|
+
const limit = clampLimit(c, cfg.defaultListLimit, cfg.maxListLimit);
|
|
45
|
+
const channel = c.req.query("channel") || undefined;
|
|
46
|
+
const sessions = await c.var.ctx.storage.listSessions({ limit, channel });
|
|
47
|
+
const persons = await resolvePersonMap(c.var.ctx.storage, sessions);
|
|
48
|
+
return c.json({ sessions, persons });
|
|
49
|
+
});
|
|
50
|
+
return app;
|
|
51
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schemas for every request body the server accepts. These are the
|
|
3
|
+
* trust boundary: handlers parse with `parseJsonBody` and never cast raw
|
|
4
|
+
* JSON straight onto a wire type.
|
|
5
|
+
*
|
|
6
|
+
* The wire types themselves live in `@hexis-ai/engram-sdk` (dependency-free,
|
|
7
|
+
* reusable by language ports). Each schema is `satisfies z.ZodType<…>` so a
|
|
8
|
+
* drift between a schema and its wire type fails the build here.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import type { Context } from "hono";
|
|
12
|
+
export declare const sessionInitSchema: z.ZodObject<{
|
|
13
|
+
id: z.ZodOptional<z.ZodString>;
|
|
14
|
+
title: z.ZodOptional<z.ZodString>;
|
|
15
|
+
channel: z.ZodOptional<z.ZodString>;
|
|
16
|
+
participants: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
17
|
+
viewable_by: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
18
|
+
}, z.core.$strip>;
|
|
19
|
+
export declare const sessionEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
20
|
+
type: z.ZodLiteral<"step">;
|
|
21
|
+
seq: z.ZodNumber;
|
|
22
|
+
at: z.ZodString;
|
|
23
|
+
tool: z.ZodString;
|
|
24
|
+
resources: z.ZodArray<z.ZodString>;
|
|
25
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
26
|
+
type: z.ZodLiteral<"participant">;
|
|
27
|
+
seq: z.ZodNumber;
|
|
28
|
+
at: z.ZodString;
|
|
29
|
+
personId: z.ZodString;
|
|
30
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
31
|
+
type: z.ZodLiteral<"title">;
|
|
32
|
+
seq: z.ZodNumber;
|
|
33
|
+
at: z.ZodString;
|
|
34
|
+
title: z.ZodString;
|
|
35
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
36
|
+
type: z.ZodLiteral<"end">;
|
|
37
|
+
seq: z.ZodNumber;
|
|
38
|
+
at: z.ZodString;
|
|
39
|
+
}, z.core.$strip>], "type">;
|
|
40
|
+
export declare const eventBatchSchema: z.ZodObject<{
|
|
41
|
+
events: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
42
|
+
type: z.ZodLiteral<"step">;
|
|
43
|
+
seq: z.ZodNumber;
|
|
44
|
+
at: z.ZodString;
|
|
45
|
+
tool: z.ZodString;
|
|
46
|
+
resources: z.ZodArray<z.ZodString>;
|
|
47
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
48
|
+
type: z.ZodLiteral<"participant">;
|
|
49
|
+
seq: z.ZodNumber;
|
|
50
|
+
at: z.ZodString;
|
|
51
|
+
personId: z.ZodString;
|
|
52
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
53
|
+
type: z.ZodLiteral<"title">;
|
|
54
|
+
seq: z.ZodNumber;
|
|
55
|
+
at: z.ZodString;
|
|
56
|
+
title: z.ZodString;
|
|
57
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
58
|
+
type: z.ZodLiteral<"end">;
|
|
59
|
+
seq: z.ZodNumber;
|
|
60
|
+
at: z.ZodString;
|
|
61
|
+
}, z.core.$strip>], "type">>;
|
|
62
|
+
}, z.core.$strip>;
|
|
63
|
+
export declare const personCreateSchema: z.ZodObject<{
|
|
64
|
+
display_name: z.ZodOptional<z.ZodString>;
|
|
65
|
+
}, z.core.$strip>;
|
|
66
|
+
export declare const personUpdateSchema: z.ZodObject<{
|
|
67
|
+
display_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
68
|
+
}, z.core.$strip>;
|
|
69
|
+
export declare const searchRequestSchema: z.ZodObject<{
|
|
70
|
+
query: z.ZodUnion<readonly [z.ZodObject<{
|
|
71
|
+
sessionId: z.ZodString;
|
|
72
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
73
|
+
session: z.ZodObject<{
|
|
74
|
+
steps: z.ZodArray<z.ZodObject<{
|
|
75
|
+
tool: z.ZodString;
|
|
76
|
+
resources: z.ZodArray<z.ZodString>;
|
|
77
|
+
}, z.core.$strip>>;
|
|
78
|
+
participants: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
79
|
+
}, z.core.$strip>;
|
|
80
|
+
}, z.core.$strip>]>;
|
|
81
|
+
options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
82
|
+
}, z.core.$strip>;
|
|
83
|
+
export declare const createWorkspaceSchema: z.ZodObject<{
|
|
84
|
+
id: z.ZodOptional<z.ZodString>;
|
|
85
|
+
name: z.ZodOptional<z.ZodString>;
|
|
86
|
+
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
87
|
+
issueKey: z.ZodOptional<z.ZodBoolean>;
|
|
88
|
+
keyName: z.ZodOptional<z.ZodString>;
|
|
89
|
+
}, z.core.$strip>;
|
|
90
|
+
export declare const issueKeySchema: z.ZodObject<{
|
|
91
|
+
name: z.ZodOptional<z.ZodString>;
|
|
92
|
+
}, z.core.$strip>;
|
|
93
|
+
/**
|
|
94
|
+
* Read and validate a JSON request body. Returns the parsed value, or a
|
|
95
|
+
* `Response` (400) the caller should return as-is:
|
|
96
|
+
*
|
|
97
|
+
* const body = await parseJsonBody(c, sessionInitSchema);
|
|
98
|
+
* if (body instanceof Response) return body;
|
|
99
|
+
*/
|
|
100
|
+
export declare function parseJsonBody<T>(c: Context, schema: z.ZodType<T>): Promise<T | Response>;
|
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schemas for every request body the server accepts. These are the
|
|
3
|
+
* trust boundary: handlers parse with `parseJsonBody` and never cast raw
|
|
4
|
+
* JSON straight onto a wire type.
|
|
5
|
+
*
|
|
6
|
+
* The wire types themselves live in `@hexis-ai/engram-sdk` (dependency-free,
|
|
7
|
+
* reusable by language ports). Each schema is `satisfies z.ZodType<…>` so a
|
|
8
|
+
* drift between a schema and its wire type fails the build here.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
// --- Sessions --------------------------------------------------------
|
|
12
|
+
export const sessionInitSchema = z.object({
|
|
13
|
+
id: z.string().min(1).optional(),
|
|
14
|
+
title: z.string().optional(),
|
|
15
|
+
channel: z.string().optional(),
|
|
16
|
+
participants: z.array(z.string()).optional(),
|
|
17
|
+
viewable_by: z.array(z.string()).optional(),
|
|
18
|
+
});
|
|
19
|
+
const stepEventSchema = z.object({
|
|
20
|
+
type: z.literal("step"),
|
|
21
|
+
seq: z.number().int().nonnegative(),
|
|
22
|
+
at: z.string(),
|
|
23
|
+
tool: z.string(),
|
|
24
|
+
resources: z.array(z.string()),
|
|
25
|
+
});
|
|
26
|
+
const participantEventSchema = z.object({
|
|
27
|
+
type: z.literal("participant"),
|
|
28
|
+
seq: z.number().int().nonnegative(),
|
|
29
|
+
at: z.string(),
|
|
30
|
+
personId: z.string(),
|
|
31
|
+
});
|
|
32
|
+
const titleEventSchema = z.object({
|
|
33
|
+
type: z.literal("title"),
|
|
34
|
+
seq: z.number().int().nonnegative(),
|
|
35
|
+
at: z.string(),
|
|
36
|
+
title: z.string(),
|
|
37
|
+
});
|
|
38
|
+
const endEventSchema = z.object({
|
|
39
|
+
type: z.literal("end"),
|
|
40
|
+
seq: z.number().int().nonnegative(),
|
|
41
|
+
at: z.string(),
|
|
42
|
+
});
|
|
43
|
+
export const sessionEventSchema = z.discriminatedUnion("type", [
|
|
44
|
+
stepEventSchema,
|
|
45
|
+
participantEventSchema,
|
|
46
|
+
titleEventSchema,
|
|
47
|
+
endEventSchema,
|
|
48
|
+
]);
|
|
49
|
+
export const eventBatchSchema = z.object({
|
|
50
|
+
events: z.array(sessionEventSchema),
|
|
51
|
+
});
|
|
52
|
+
// --- Persons ---------------------------------------------------------
|
|
53
|
+
export const personCreateSchema = z.object({
|
|
54
|
+
display_name: z.string().optional(),
|
|
55
|
+
});
|
|
56
|
+
export const personUpdateSchema = z.object({
|
|
57
|
+
display_name: z.string().nullable().optional(),
|
|
58
|
+
});
|
|
59
|
+
// --- Search ----------------------------------------------------------
|
|
60
|
+
const searchQuerySchema = z.union([
|
|
61
|
+
z.object({ sessionId: z.string().min(1) }),
|
|
62
|
+
z.object({
|
|
63
|
+
session: z.object({
|
|
64
|
+
steps: z.array(z.object({ tool: z.string(), resources: z.array(z.string()) })),
|
|
65
|
+
participants: z.array(z.string()).optional(),
|
|
66
|
+
}),
|
|
67
|
+
}),
|
|
68
|
+
]);
|
|
69
|
+
export const searchRequestSchema = z.object({
|
|
70
|
+
query: searchQuerySchema,
|
|
71
|
+
// `options` is forwarded as-is to engram-core's pure `search()`, which
|
|
72
|
+
// tolerates partial/absent options. Only the query (which drives storage
|
|
73
|
+
// lookups) needs strict validation here.
|
|
74
|
+
options: z.record(z.string(), z.unknown()).optional(),
|
|
75
|
+
});
|
|
76
|
+
// --- Admin -----------------------------------------------------------
|
|
77
|
+
export const createWorkspaceSchema = z.object({
|
|
78
|
+
id: z.string().optional(),
|
|
79
|
+
name: z.string().optional(),
|
|
80
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
81
|
+
issueKey: z.boolean().optional(),
|
|
82
|
+
keyName: z.string().optional(),
|
|
83
|
+
});
|
|
84
|
+
export const issueKeySchema = z.object({
|
|
85
|
+
name: z.string().optional(),
|
|
86
|
+
});
|
|
87
|
+
// --- Helper ----------------------------------------------------------
|
|
88
|
+
/**
|
|
89
|
+
* Read and validate a JSON request body. Returns the parsed value, or a
|
|
90
|
+
* `Response` (400) the caller should return as-is:
|
|
91
|
+
*
|
|
92
|
+
* const body = await parseJsonBody(c, sessionInitSchema);
|
|
93
|
+
* if (body instanceof Response) return body;
|
|
94
|
+
*/
|
|
95
|
+
export async function parseJsonBody(c, schema) {
|
|
96
|
+
let raw;
|
|
97
|
+
try {
|
|
98
|
+
raw = await c.req.json();
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return c.json({ error: "invalid_json" }, 400);
|
|
102
|
+
}
|
|
103
|
+
const result = schema.safeParse(raw);
|
|
104
|
+
if (!result.success) {
|
|
105
|
+
return c.json({ error: "invalid_body", issues: result.error.issues }, 400);
|
|
106
|
+
}
|
|
107
|
+
return result.data;
|
|
108
|
+
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,20 +1,8 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import type {
|
|
2
|
+
import type { AuthResolver, Env } from "./context";
|
|
3
3
|
import { type AdminOptions } from "./admin";
|
|
4
|
-
/**
|
|
5
|
-
* Resolve an API key (raw `Authorization: Bearer <key>` token) into a
|
|
6
|
-
* workspace context. Throwing or returning null short-circuits to 401.
|
|
7
|
-
*/
|
|
8
|
-
export interface AuthResolver {
|
|
9
|
-
(apiKey: string): Promise<WorkspaceContext | null> | WorkspaceContext | null;
|
|
10
|
-
}
|
|
11
|
-
export interface WorkspaceContext {
|
|
12
|
-
workspaceId: string;
|
|
13
|
-
/** The storage adapter scoped to this workspace. */
|
|
14
|
-
storage: StorageAdapter;
|
|
15
|
-
}
|
|
16
4
|
export interface CreateServerOptions {
|
|
17
|
-
/** Resolves Bearer tokens into workspace contexts. */
|
|
5
|
+
/** Resolves Bearer / X-Api-Key tokens into workspace contexts. */
|
|
18
6
|
auth: AuthResolver;
|
|
19
7
|
/**
|
|
20
8
|
* Generates session ids. Defaults to `crypto.randomUUID()`.
|
|
@@ -32,11 +20,9 @@ export interface CreateServerOptions {
|
|
|
32
20
|
*/
|
|
33
21
|
admin?: AdminOptions;
|
|
34
22
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
23
|
+
/**
|
|
24
|
+
* Build the engram HTTP app. Wiring only: this sets up cross-cutting
|
|
25
|
+
* middleware (request id + access log), the workspace auth gate, and mounts
|
|
26
|
+
* the `/v1` route modules (`routes/*`) and the optional `/admin/v1` router.
|
|
27
|
+
*/
|
|
41
28
|
export declare function createServer(opts: CreateServerOptions): Hono<Env>;
|
|
42
|
-
export {};
|
package/dist/server.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import { buildIndex, search, } from "@hexis-ai/engram-core";
|
|
3
2
|
import { log, newRequestId } from "./logger";
|
|
4
3
|
import { createAdminRouter } from "./admin";
|
|
4
|
+
import { sessionsRoutes } from "./routes/sessions";
|
|
5
|
+
import { personsRoutes } from "./routes/persons";
|
|
6
|
+
import { searchRoutes } from "./routes/search";
|
|
7
|
+
/**
|
|
8
|
+
* Build the engram HTTP app. Wiring only: this sets up cross-cutting
|
|
9
|
+
* middleware (request id + access log), the workspace auth gate, and mounts
|
|
10
|
+
* the `/v1` route modules (`routes/*`) and the optional `/admin/v1` router.
|
|
11
|
+
*/
|
|
5
12
|
export function createServer(opts) {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
const cfg = {
|
|
14
|
+
newSessionId: opts.newSessionId ?? (() => crypto.randomUUID()),
|
|
15
|
+
defaultListLimit: opts.defaultListLimit ?? 100,
|
|
16
|
+
maxListLimit: opts.maxListLimit ?? 500,
|
|
17
|
+
};
|
|
9
18
|
const app = new Hono();
|
|
10
19
|
// Request id + access log middleware. Runs for every route.
|
|
11
20
|
app.use("*", async (c, next) => {
|
|
@@ -54,6 +63,7 @@ export function createServer(opts) {
|
|
|
54
63
|
if (opts.admin) {
|
|
55
64
|
app.route("/admin/v1", createAdminRouter(opts.admin));
|
|
56
65
|
}
|
|
66
|
+
// Workspace auth gate — every `/v1/*` route runs behind this.
|
|
57
67
|
app.use("/v1/*", async (c, next) => {
|
|
58
68
|
const apiKey = c.req.header("x-api-key") ??
|
|
59
69
|
c.req.header("authorization")?.match(/^Bearer\s+(.+)$/i)?.[1];
|
|
@@ -68,157 +78,8 @@ export function createServer(opts) {
|
|
|
68
78
|
// Identity probe — echoes the workspace the caller's key resolves to.
|
|
69
79
|
// Used by clients (e.g. engram-web) to label which tenant they're viewing.
|
|
70
80
|
app.get("/v1/me", (c) => c.json({ workspaceId: c.var.ctx.workspaceId }));
|
|
71
|
-
|
|
72
|
-
app.
|
|
73
|
-
|
|
74
|
-
if (body === null)
|
|
75
|
-
return c.json({ error: "invalid_json" }, 400);
|
|
76
|
-
const id = body.id ?? newId();
|
|
77
|
-
const createdAt = new Date().toISOString();
|
|
78
|
-
await c.var.ctx.storage.createSession({
|
|
79
|
-
...body,
|
|
80
|
-
id,
|
|
81
|
-
createdAt,
|
|
82
|
-
});
|
|
83
|
-
return c.json({ id });
|
|
84
|
-
});
|
|
85
|
-
app.post("/v1/sessions/:id/events", async (c) => {
|
|
86
|
-
const id = c.req.param("id");
|
|
87
|
-
const body = (await c.req.json().catch(() => null));
|
|
88
|
-
if (!body || !Array.isArray(body.events)) {
|
|
89
|
-
return c.json({ error: "events_array_required" }, 400);
|
|
90
|
-
}
|
|
91
|
-
try {
|
|
92
|
-
await c.var.ctx.storage.appendEvents(id, body.events);
|
|
93
|
-
}
|
|
94
|
-
catch (e) {
|
|
95
|
-
return c.json({ error: "session_not_found", message: e.message }, 404);
|
|
96
|
-
}
|
|
97
|
-
return c.body(null, 204);
|
|
98
|
-
});
|
|
99
|
-
app.get("/v1/sessions/:id", async (c) => {
|
|
100
|
-
const id = c.req.param("id");
|
|
101
|
-
const s = await c.var.ctx.storage.getSession(id);
|
|
102
|
-
if (!s)
|
|
103
|
-
return c.json({ error: "session_not_found" }, 404);
|
|
104
|
-
const persons = await resolvePersonMap(c.var.ctx.storage, [s]);
|
|
105
|
-
return c.json({ session: s, persons });
|
|
106
|
-
});
|
|
107
|
-
app.get("/v1/sessions", async (c) => {
|
|
108
|
-
const limit = clampLimit(c, defaultLimit, maxLimit);
|
|
109
|
-
const channel = c.req.query("channel") || undefined;
|
|
110
|
-
const sessions = await c.var.ctx.storage.listSessions({ limit, channel });
|
|
111
|
-
const persons = await resolvePersonMap(c.var.ctx.storage, sessions);
|
|
112
|
-
return c.json({ sessions, persons });
|
|
113
|
-
});
|
|
114
|
-
app.post("/v1/search", async (c) => {
|
|
115
|
-
const body = (await c.req.json().catch(() => null));
|
|
116
|
-
if (!body || !body.query)
|
|
117
|
-
return c.json({ error: "query_required" }, 400);
|
|
118
|
-
const corpus = await c.var.ctx.storage.listSessions({ limit: maxLimit });
|
|
119
|
-
let queryQuery;
|
|
120
|
-
if ("sessionId" in body.query) {
|
|
121
|
-
const q = await c.var.ctx.storage.getSession(body.query.sessionId);
|
|
122
|
-
if (!q)
|
|
123
|
-
return c.json({ error: "query_session_not_found" }, 404);
|
|
124
|
-
queryQuery = q;
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
queryQuery = body.query.session;
|
|
128
|
-
}
|
|
129
|
-
const index = buildIndex(corpus);
|
|
130
|
-
const opts = body.options ?? {};
|
|
131
|
-
const queryParticipants = opts.queryParticipants ?? queryQuery.participants ?? [];
|
|
132
|
-
const results = search(queryQuery.steps, index, {
|
|
133
|
-
...opts,
|
|
134
|
-
queryParticipants,
|
|
135
|
-
});
|
|
136
|
-
const persons = await resolvePersonMap(c.var.ctx.storage, results.map((r) => r.session));
|
|
137
|
-
return c.json({ results, persons });
|
|
138
|
-
});
|
|
139
|
-
// --- Persons ---------------------------------------------------------
|
|
140
|
-
app.post("/v1/persons", async (c) => {
|
|
141
|
-
const body = (await c.req.json().catch(() => null));
|
|
142
|
-
if (body === null)
|
|
143
|
-
return c.json({ error: "invalid_json" }, 400);
|
|
144
|
-
const p = await c.var.ctx.storage.createPerson(body);
|
|
145
|
-
return c.json(p, 201);
|
|
146
|
-
});
|
|
147
|
-
app.put("/v1/persons/:id", async (c) => {
|
|
148
|
-
const id = c.req.param("id");
|
|
149
|
-
const body = (await c.req.json().catch(() => null));
|
|
150
|
-
if (body === null)
|
|
151
|
-
return c.json({ error: "invalid_json" }, 400);
|
|
152
|
-
const p = await c.var.ctx.storage.upsertPerson(id, body);
|
|
153
|
-
return c.json(p);
|
|
154
|
-
});
|
|
155
|
-
app.patch("/v1/persons/:id", async (c) => {
|
|
156
|
-
const id = c.req.param("id");
|
|
157
|
-
const body = (await c.req.json().catch(() => null));
|
|
158
|
-
if (body === null)
|
|
159
|
-
return c.json({ error: "invalid_json" }, 400);
|
|
160
|
-
const p = await c.var.ctx.storage.updatePerson(id, body);
|
|
161
|
-
if (!p)
|
|
162
|
-
return c.json({ error: "person_not_found" }, 404);
|
|
163
|
-
return c.json(p);
|
|
164
|
-
});
|
|
165
|
-
app.get("/v1/persons/:id", async (c) => {
|
|
166
|
-
const id = c.req.param("id");
|
|
167
|
-
const p = await c.var.ctx.storage.getPerson(id);
|
|
168
|
-
if (!p)
|
|
169
|
-
return c.json({ error: "person_not_found" }, 404);
|
|
170
|
-
return c.json(p);
|
|
171
|
-
});
|
|
172
|
-
app.get("/v1/persons", async (c) => {
|
|
173
|
-
const limit = clampLimit(c, defaultLimit, maxLimit);
|
|
174
|
-
const q = c.req.query("q") || undefined;
|
|
175
|
-
const persons = await c.var.ctx.storage.listPersons({ limit, q });
|
|
176
|
-
return c.json({ persons });
|
|
177
|
-
});
|
|
178
|
-
app.get("/v1/persons/:id/sessions", async (c) => {
|
|
179
|
-
const id = c.req.param("id");
|
|
180
|
-
const limit = clampLimit(c, defaultLimit, maxLimit);
|
|
181
|
-
const channel = c.req.query("channel") || undefined;
|
|
182
|
-
const scopeRaw = c.req.query("scope");
|
|
183
|
-
const scope = scopeRaw === "viewable" ? "viewable" : "participant";
|
|
184
|
-
const sessions = await c.var.ctx.storage.sessionsForPerson(id, {
|
|
185
|
-
limit,
|
|
186
|
-
channel,
|
|
187
|
-
scope,
|
|
188
|
-
});
|
|
189
|
-
const persons = await resolvePersonMap(c.var.ctx.storage, sessions);
|
|
190
|
-
return c.json({ sessions, persons });
|
|
191
|
-
});
|
|
81
|
+
app.route("/v1", sessionsRoutes(cfg));
|
|
82
|
+
app.route("/v1", personsRoutes(cfg));
|
|
83
|
+
app.route("/v1", searchRoutes(cfg));
|
|
192
84
|
return app;
|
|
193
85
|
}
|
|
194
|
-
function clampLimit(c, def, max) {
|
|
195
|
-
const raw = c.req.query("limit");
|
|
196
|
-
if (!raw)
|
|
197
|
-
return def;
|
|
198
|
-
const n = parseInt(raw, 10);
|
|
199
|
-
if (isNaN(n) || n < 1)
|
|
200
|
-
return def;
|
|
201
|
-
return Math.min(n, max);
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Build a deduped person_id → PersonInfo map covering every person id
|
|
205
|
-
* referenced by `sessions` (participants ∪ viewable_by). Returns {} if
|
|
206
|
-
* there are no session rows. Persons that exist as ids in sessions but
|
|
207
|
-
* have no engram_persons row are simply absent from the map.
|
|
208
|
-
*/
|
|
209
|
-
async function resolvePersonMap(storage, sessions) {
|
|
210
|
-
const ids = new Set();
|
|
211
|
-
for (const s of sessions) {
|
|
212
|
-
for (const id of s.participants ?? [])
|
|
213
|
-
ids.add(id);
|
|
214
|
-
for (const id of s.viewable_by ?? [])
|
|
215
|
-
ids.add(id);
|
|
216
|
-
}
|
|
217
|
-
if (ids.size === 0)
|
|
218
|
-
return {};
|
|
219
|
-
const list = await storage.getPersons([...ids]);
|
|
220
|
-
const map = {};
|
|
221
|
-
for (const p of list)
|
|
222
|
-
map[p.id] = p;
|
|
223
|
-
return map;
|
|
224
|
-
}
|