feat(api): add type_name to events + /schema endpoint (#15)
This commit is contained in:
parent
eb7edd8141
commit
e57bbb2727
@ -307,7 +307,7 @@ export async function createEvent(
|
||||
.run()
|
||||
}
|
||||
|
||||
const event: Event = { id, type_hash: typeHash, payload, created_at: createdAt }
|
||||
const event: Event = { id, type_hash: typeHash, type_name: typeName, payload, created_at: createdAt }
|
||||
const { fired: reactionsFired, payloads: reactionResults } = await triggerReactionChain(db, event)
|
||||
|
||||
return { event, reactions_fired: reactionsFired, reaction_results: reactionResults }
|
||||
@ -319,7 +319,20 @@ export async function getEvent(db: D1Database, id: number): Promise<Event | null
|
||||
.bind(id)
|
||||
.first<{ id: number; type_hash: string; payload: string; created_at: number }>()
|
||||
if (!row) return null
|
||||
return { ...row, payload: JSON.parse(row.payload) }
|
||||
|
||||
// Resolve type_name from event_def_names
|
||||
const nameRow = await db
|
||||
.prepare('SELECT name FROM event_def_names WHERE current_hash = ?')
|
||||
.bind(row.type_hash)
|
||||
.first<{ name: string }>()
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
type_hash: row.type_hash,
|
||||
type_name: nameRow?.name || undefined,
|
||||
payload: JSON.parse(row.payload),
|
||||
created_at: row.created_at,
|
||||
}
|
||||
}
|
||||
|
||||
export async function findEventsByRef(
|
||||
@ -359,7 +372,25 @@ export async function findEventsByRef(
|
||||
const total = countResult?.count || 0
|
||||
|
||||
const rows = await dataQuery.all<{ id: number; type_hash: string; payload: string; created_at: number }>()
|
||||
const events = (rows.results || []).map((row) => ({ ...row, payload: JSON.parse(row.payload) }))
|
||||
|
||||
// Resolve type_names for all unique type_hashes
|
||||
const typeHashes = [...new Set((rows.results || []).map((r) => r.type_hash))]
|
||||
const typeNameMap = new Map<string, string>()
|
||||
for (const hash of typeHashes) {
|
||||
const nameRow = await db
|
||||
.prepare('SELECT name FROM event_def_names WHERE current_hash = ?')
|
||||
.bind(hash)
|
||||
.first<{ name: string }>()
|
||||
if (nameRow) typeNameMap.set(hash, nameRow.name)
|
||||
}
|
||||
|
||||
const events = (rows.results || []).map((row) => ({
|
||||
id: row.id,
|
||||
type_hash: row.type_hash,
|
||||
type_name: typeNameMap.get(row.type_hash),
|
||||
payload: JSON.parse(row.payload),
|
||||
created_at: row.created_at,
|
||||
}))
|
||||
|
||||
return { events, total }
|
||||
}
|
||||
@ -1287,6 +1318,30 @@ export function buildReactionPayload(
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Schema (self-describing endpoint)
|
||||
// ============================================
|
||||
|
||||
export async function getSchema(db: D1Database): Promise<{
|
||||
object_defs: Array<{ name: string }>
|
||||
event_defs: Array<{ name: string; hash: string; schema: any }>
|
||||
projection_defs: Array<{ name: string; params: any; value_schema: any }>
|
||||
}> {
|
||||
const objectDefs = await listObjectDefs(db)
|
||||
const eventDefs = await listEventDefs(db)
|
||||
const projectionDefs = await listProjectionDefs(db)
|
||||
|
||||
return {
|
||||
object_defs: objectDefs.map((d) => ({ name: d.name })),
|
||||
event_defs: eventDefs.map((d) => ({ name: d.name, hash: d.hash, schema: d.schema })),
|
||||
projection_defs: projectionDefs.map((d) => ({
|
||||
name: d.name,
|
||||
params: d.params,
|
||||
value_schema: d.value_schema,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// API Keys
|
||||
// ============================================
|
||||
|
||||
@ -444,6 +444,90 @@ describe('Auth', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================
|
||||
// Schema
|
||||
// ============================================
|
||||
|
||||
describe('Schema', () => {
|
||||
it('GET /schema returns empty arrays on empty database', async () => {
|
||||
const res = await app.fetch(req('GET', '/schema', undefined, ''), { DB: db, API_TOKEN: API_TOKEN })
|
||||
expect(res.status).toBe(200)
|
||||
const json = await res.json()
|
||||
expect(json.object_defs).toEqual([])
|
||||
expect(json.event_defs).toEqual([])
|
||||
expect(json.projection_defs).toEqual([])
|
||||
})
|
||||
|
||||
it('GET /schema returns full schema after definitions are created', async () => {
|
||||
// Create object defs
|
||||
await app.fetch(req('POST', '/object-defs', { name: 'agent' }), { DB: db, API_TOKEN: API_TOKEN })
|
||||
await app.fetch(req('POST', '/object-defs', { name: 'task' }), { DB: db, API_TOKEN: API_TOKEN })
|
||||
|
||||
// Create event def
|
||||
const eventSchema = {
|
||||
properties: {
|
||||
participant: { type: 'ref' as const, object_type: 'agent' },
|
||||
subject: { type: 'ref' as const, object_type: 'task' },
|
||||
},
|
||||
}
|
||||
await app.fetch(req('POST', '/event-defs', { name: 'task_assigned', schema: eventSchema }), {
|
||||
DB: db,
|
||||
API_TOKEN: API_TOKEN,
|
||||
})
|
||||
|
||||
// Create projection def
|
||||
const projDef = {
|
||||
name: 'current_assignee',
|
||||
sources: [
|
||||
{
|
||||
event_def: 'task_assigned',
|
||||
bindings: { subject: '$task_id' },
|
||||
expression: 'event.participant',
|
||||
},
|
||||
],
|
||||
params: { task_id: { type: 'ref' } },
|
||||
value_schema: { type: 'ref' },
|
||||
initial_value: '',
|
||||
}
|
||||
await app.fetch(req('POST', '/projection-defs', projDef), { DB: db, API_TOKEN: API_TOKEN })
|
||||
|
||||
// GET /schema (no auth needed)
|
||||
const res = await app.fetch(req('GET', '/schema', undefined, ''), { DB: db, API_TOKEN: API_TOKEN })
|
||||
expect(res.status).toBe(200)
|
||||
const json = await res.json()
|
||||
|
||||
// object_defs
|
||||
expect(json.object_defs).toHaveLength(2)
|
||||
expect(json.object_defs.map((d: any) => d.name).sort()).toEqual(['agent', 'task'])
|
||||
|
||||
// event_defs
|
||||
expect(json.event_defs).toHaveLength(1)
|
||||
expect(json.event_defs[0].name).toBe('task_assigned')
|
||||
expect(json.event_defs[0].hash).toBeDefined()
|
||||
expect(json.event_defs[0].schema).toEqual(eventSchema)
|
||||
|
||||
// projection_defs
|
||||
expect(json.projection_defs).toHaveLength(1)
|
||||
expect(json.projection_defs[0].name).toBe('current_assignee')
|
||||
expect(json.projection_defs[0].params).toEqual({ task_id: { type: 'ref' } })
|
||||
expect(json.projection_defs[0].value_schema).toEqual({ type: 'ref' })
|
||||
})
|
||||
|
||||
it('GET /schema does not require auth', async () => {
|
||||
// Request with no token at all
|
||||
const request = new Request('http://test/schema', {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
const res = await app.fetch(request, { DB: db, API_TOKEN: API_TOKEN })
|
||||
expect(res.status).toBe(200)
|
||||
const json = await res.json()
|
||||
expect(json.object_defs).toBeDefined()
|
||||
expect(json.event_defs).toBeDefined()
|
||||
expect(json.projection_defs).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================
|
||||
// Object Defs
|
||||
// ============================================
|
||||
@ -615,7 +699,7 @@ describe('Events', () => {
|
||||
await app.fetch(req('POST', '/event-defs', { name: 'task_assigned', schema }), { DB: db, API_TOKEN: API_TOKEN })
|
||||
})
|
||||
|
||||
it('POST /events creates event with resolved type_hash', async () => {
|
||||
it('POST /events creates event with resolved type_hash and type_name', async () => {
|
||||
const payload = { participant: agentId, subject: taskId }
|
||||
const res = await app.fetch(req('POST', '/events', { type: 'task_assigned', payload }), {
|
||||
DB: db,
|
||||
@ -625,6 +709,7 @@ describe('Events', () => {
|
||||
const json = await res.json()
|
||||
expect(typeof json.event.id).toBe('number')
|
||||
expect(json.event.type_hash).toBeDefined()
|
||||
expect(json.event.type_name).toBe('task_assigned')
|
||||
expect(json.event.payload).toEqual(payload)
|
||||
expect(tables.events).toHaveLength(1)
|
||||
expect(tables.event_refs).toHaveLength(2)
|
||||
@ -639,7 +724,7 @@ describe('Events', () => {
|
||||
expect(res.status).toBe(201)
|
||||
})
|
||||
|
||||
it('GET /events/:id returns event', async () => {
|
||||
it('GET /events/:id returns event with type_name', async () => {
|
||||
const created = await app.fetch(
|
||||
req('POST', '/events', { type: 'task_assigned', payload: { participant: agentId, subject: taskId } }),
|
||||
{ DB: db, API_TOKEN: API_TOKEN },
|
||||
@ -649,10 +734,11 @@ describe('Events', () => {
|
||||
expect(res.status).toBe(200)
|
||||
const json = await res.json()
|
||||
expect(json.id).toBe(event.id)
|
||||
expect(json.type_name).toBe('task_assigned')
|
||||
expect(json.payload).toEqual({ participant: agentId, subject: taskId })
|
||||
})
|
||||
|
||||
it('GET /events?ref=X returns events by ref', async () => {
|
||||
it('GET /events?ref=X returns events by ref with type_name', async () => {
|
||||
await app.fetch(
|
||||
req('POST', '/events', { type: 'task_assigned', payload: { participant: agentId, subject: taskId } }),
|
||||
{
|
||||
@ -665,6 +751,7 @@ describe('Events', () => {
|
||||
const json = await res.json()
|
||||
expect(json.events).toHaveLength(1)
|
||||
expect(json.total).toBe(1)
|
||||
expect(json.events[0].type_name).toBe('task_assigned')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -38,6 +38,7 @@ import {
|
||||
listApiKeys,
|
||||
deleteApiKey,
|
||||
validateApiKey,
|
||||
getSchema,
|
||||
} from './engine'
|
||||
import type {
|
||||
CreateObjectDefRequest,
|
||||
@ -123,9 +124,18 @@ app.get('/health', (c) => {
|
||||
return c.json({ status: 'ok', version: '2.4.0' })
|
||||
})
|
||||
|
||||
// Auth middleware for all routes except health, ui, and POST /events (which has its own dual auth)
|
||||
// ============================================
|
||||
// Schema (self-describing, no auth)
|
||||
// ============================================
|
||||
|
||||
app.get('/schema', async (c) => {
|
||||
const schema = await getSchema(c.env.DB)
|
||||
return c.json(schema)
|
||||
})
|
||||
|
||||
// Auth middleware for all routes except health, schema, ui, and POST /events (which has its own dual auth)
|
||||
app.use('*', async (c, next) => {
|
||||
if (c.req.path === '/health' || c.req.path.startsWith('/ui')) return next()
|
||||
if (c.req.path === '/health' || c.req.path === '/schema' || c.req.path.startsWith('/ui')) return next()
|
||||
if (c.req.method === 'POST' && c.req.path === '/events') return next()
|
||||
return bearerAuth(c.env.API_TOKEN)(c, next)
|
||||
})
|
||||
|
||||
@ -107,6 +107,7 @@ export interface Object {
|
||||
export interface Event {
|
||||
id: number
|
||||
type_hash: string
|
||||
type_name?: string
|
||||
payload: Record<string, any>
|
||||
created_at: number
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user