feat(cli): complete API coverage — logs, api-keys, schema, handler (#14)

This commit is contained in:
小橘 2026-04-13 02:25:24 +00:00
parent e57bbb2727
commit 50fef48ebe
11 changed files with 643 additions and 7 deletions

View File

@ -52,10 +52,38 @@ export interface Reaction {
projection_def_hash: string
params_hash: string
params: Record<string, unknown>
action: 'webhook' | 'emit_event'
action: 'webhook' | 'emit_event' | 'handler'
webhook_url?: string
emit_event_type?: string
emit_payload_template?: string
handler_code?: string
handler_timeout_ms?: number
created_at: number
}
export interface ApiKey {
id: number
name: string
prefix: string
allowed_events?: string[]
rate_limit?: number
created_at: number
}
export interface ReactionLog {
id: number
reaction_id: number
event_id: number
status: string
created_at: number
}
export interface RequestLog {
id: number
api_key_id: number
method: string
path: string
status: number
created_at: number
}
@ -244,10 +272,12 @@ export class OGraphClient {
projectionDef: string,
params: Record<string, unknown>,
options: {
action?: 'webhook' | 'emit_event'
action?: 'webhook' | 'emit_event' | 'handler'
webhook_url?: string
emit_event_type?: string
emit_payload_template?: string
handler_code?: string
handler_timeout_ms?: number
},
): Promise<Reaction> {
const action = options.action ?? 'webhook'
@ -260,6 +290,8 @@ export class OGraphClient {
webhook_url: options.webhook_url,
emit_event_type: options.emit_event_type,
emit_payload_template: options.emit_payload_template,
handler_code: options.handler_code,
handler_timeout_ms: options.handler_timeout_ms,
}),
})
}
@ -273,6 +305,79 @@ export class OGraphClient {
return this.request<{ ok: boolean }>(`/reactions/${id}`, { method: 'DELETE' })
}
// ─── Reaction Logs ─────────────────────────────────────────────────────────
async listReactionLogs(
limit?: number,
offset?: number,
reactionId?: number,
): Promise<{ reaction_logs: ReactionLog[]; total: number }> {
const params = new URLSearchParams()
if (limit !== undefined) params.set('limit', String(limit))
if (offset !== undefined) params.set('offset', String(offset))
if (reactionId !== undefined) params.set('reaction_id', String(reactionId))
const qs = params.toString()
return this.request<{ reaction_logs: ReactionLog[]; total: number }>(
`/reaction-logs${qs ? `?${qs}` : ''}`,
)
}
// ─── Request Logs ─────────────────────────────────────────────────────────────
async listRequestLogs(
limit?: number,
offset?: number,
apiKeyId?: number,
): Promise<{ request_logs: RequestLog[]; total: number }> {
const params = new URLSearchParams()
if (limit !== undefined) params.set('limit', String(limit))
if (offset !== undefined) params.set('offset', String(offset))
if (apiKeyId !== undefined) params.set('api_key_id', String(apiKeyId))
const qs = params.toString()
return this.request<{ request_logs: RequestLog[]; total: number }>(
`/request-logs${qs ? `?${qs}` : ''}`,
)
}
// ─── API Keys ─────────────────────────────────────────────────────────────────
async createApiKey(
name: string,
allowedEvents?: string[],
rateLimit?: number,
): Promise<{ api_key: ApiKey; plaintext_key: string }> {
const body: Record<string, unknown> = { name }
if (allowedEvents) body.allowed_events = allowedEvents
if (rateLimit !== undefined) body.rate_limit = rateLimit
return this.request<{ api_key: ApiKey; plaintext_key: string }>('/api-keys', {
method: 'POST',
body: JSON.stringify(body),
})
}
async listApiKeys(
limit?: number,
offset?: number,
): Promise<{ api_keys: ApiKey[]; total: number }> {
const params = new URLSearchParams()
if (limit !== undefined) params.set('limit', String(limit))
if (offset !== undefined) params.set('offset', String(offset))
const qs = params.toString()
return this.request<{ api_keys: ApiKey[]; total: number }>(
`/api-keys${qs ? `?${qs}` : ''}`,
)
}
async deleteApiKey(id: number): Promise<{ deleted: number }> {
return this.request<{ deleted: number }>(`/api-keys/${id}`, { method: 'DELETE' })
}
// ─── Schema ────────────────────────────────────────────────────────────────────
async getSchema(): Promise<{ object_defs: ObjectDef[]; event_defs: EventDef[]; projection_defs: ProjectionDef[] }> {
return this.request<{ object_defs: ObjectDef[]; event_defs: EventDef[]; projection_defs: ProjectionDef[] }>('/schema')
}
// ─── Health ────────────────────────────────────────────────────────────────────
async health(): Promise<HealthResponse> {

View File

@ -0,0 +1,103 @@
// api-keys commands
import { Command } from 'commander'
import { OGraphClient } from '../client.js'
const c = {
reset: '\x1b[0m',
green: '\x1b[32m',
cyan: '\x1b[36m',
yellow: '\x1b[33m',
red: '\x1b[31m',
bold: '\x1b[1m',
}
function fail(msg: string) {
console.error(`${c.red}${c.reset} ${msg}`)
}
export function createApiKeysCommand(): Command {
const cmd = new Command('api-keys')
cmd.description('Manage API keys')
// create
const create = new Command('create')
create.description('Create a new API key')
create.requiredOption('--name <name>', 'Key name')
create.option('--allowed-events <events>', 'Comma-separated allowed event types')
create.option('--rate-limit <n>', 'Rate limit (requests per minute)', parseInt)
create.option('--json', 'output raw JSON')
create.action(async (opts: { name: string; allowedEvents?: string; rateLimit?: number; json?: boolean }) => {
const client = new OGraphClient()
try {
await client.init()
const allowedEvents = opts.allowedEvents ? opts.allowedEvents.split(',').map((s) => s.trim()) : undefined
const result = await client.createApiKey(opts.name, allowedEvents, opts.rateLimit)
if (opts.json) {
console.log(JSON.stringify(result, null, 2))
return
}
console.log(`${c.green}${c.reset} Created API key: ${c.cyan}${result.api_key.name}${c.reset} (id: ${result.api_key.id})`)
console.log(` ${c.yellow}Key: ${result.plaintext_key}${c.reset}`)
console.log(` ${c.yellow}⚠ Save this key now — it cannot be shown again.${c.reset}`)
} catch (err) {
fail(String(err instanceof Error ? err.message : err))
process.exit(1)
}
})
// list
const list = new Command('list')
list.description('List API keys')
list.option('--limit <n>', 'Max results', parseInt)
list.option('--offset <n>', 'Offset', parseInt)
list.option('--json', 'output raw JSON')
list.action(async (opts: { limit?: number; offset?: number; json?: boolean }) => {
const client = new OGraphClient()
try {
await client.init()
const result = await client.listApiKeys(opts.limit, opts.offset)
if (opts.json) {
console.log(JSON.stringify(result, null, 2))
return
}
if (result.api_keys.length === 0) {
console.log('No API keys found.')
return
}
console.log(`${c.bold}API Keys${c.reset} (total: ${result.total})`)
for (const key of result.api_keys) {
const events = key.allowed_events?.join(', ') || 'all'
console.log(` ${c.cyan}${key.id}${c.reset} ${key.name} prefix:${key.prefix} events:[${events}]`)
}
} catch (err) {
fail(String(err instanceof Error ? err.message : err))
process.exit(1)
}
})
// delete
const del = new Command('delete')
del.description('Delete an API key')
del.argument('<id>', 'API key ID')
del.option('--json', 'output raw JSON')
del.action(async (id: string, opts: { json?: boolean }) => {
const client = new OGraphClient()
try {
await client.init()
const result = await client.deleteApiKey(parseInt(id, 10))
if (opts.json) {
console.log(JSON.stringify(result, null, 2))
return
}
console.log(`${c.green}${c.reset} Deleted API key: ${id}`)
} catch (err) {
fail(String(err instanceof Error ? err.message : err))
process.exit(1)
}
})
cmd.addCommand(create)
cmd.addCommand(list)
cmd.addCommand(del)
return cmd
}

View File

@ -76,5 +76,35 @@ export function createEventDefsCommand(): Command {
})
cmd.addCommand(create)
// list subcommand (also available as default action above)
const list = new Command('list')
list.description('List all event types')
list.option('--json', 'output raw JSON')
list.action(async (opts: { json?: boolean }) => {
const client = new OGraphClient()
try {
await client.init()
const defs = await client.listEventDefs()
if (opts.json) {
console.log(JSON.stringify(defs, null, 2))
return
}
if (defs.length === 0) {
console.log('No event types defined.')
return
}
console.log(`${c.bold}Event Types${c.reset}`)
for (const d of defs) {
const props = Object.keys(d.schema?.properties ?? {}).join(', ')
console.log(` ${c.cyan}${d.name}${c.reset} {${props}}`)
}
} catch (err) {
fail(String(err instanceof Error ? err.message : err))
process.exit(1)
}
})
cmd.addCommand(list)
return cmd
}

View File

@ -66,5 +66,34 @@ export function createObjectDefsCommand(): Command {
})
cmd.addCommand(create)
// list subcommand (also available as default action above)
const list = new Command('list')
list.description('List all object types')
list.option('--json', 'output raw JSON')
list.action(async (opts: { json?: boolean }) => {
const client = new OGraphClient()
try {
await client.init()
const defs = await client.listObjectDefs()
if (opts.json) {
console.log(JSON.stringify(defs, null, 2))
return
}
if (defs.length === 0) {
console.log('No object types defined.')
return
}
console.log(`${c.bold}Object Types${c.reset}`)
for (const d of defs) {
console.log(` ${c.cyan}${d.name}${c.reset}`)
}
} catch (err) {
fail(String(err instanceof Error ? err.message : err))
process.exit(1)
}
})
cmd.addCommand(list)
return cmd
}

View File

@ -140,5 +140,34 @@ export function createProjectionDefsCommand(): Command {
)
cmd.addCommand(create)
// list subcommand (also available as default action above)
const list = new Command('list')
list.description('List all projection definitions')
list.option('--json', 'output raw JSON')
list.action(async (opts: { json?: boolean }) => {
const client = new OGraphClient()
try {
await client.init()
const defs = await client.listProjectionDefs()
if (opts.json) {
console.log(JSON.stringify(defs, null, 2))
return
}
if (defs.length === 0) {
console.log('No projection definitions found.')
return
}
console.log(`${c.bold}Projection Definitions${c.reset}`)
for (const d of defs) {
console.log(` ${c.cyan}${d.name}${c.reset} sources: ${d.sources?.length ?? 0}`)
}
} catch (err) {
fail(String(err instanceof Error ? err.message : err))
process.exit(1)
}
})
cmd.addCommand(list)
return cmd
}

View File

@ -0,0 +1,55 @@
// reaction-logs commands
import { Command } from 'commander'
import { OGraphClient } from '../client.js'
const c = {
reset: '\x1b[0m',
green: '\x1b[32m',
cyan: '\x1b[36m',
yellow: '\x1b[33m',
red: '\x1b[31m',
bold: '\x1b[1m',
}
function fail(msg: string) {
console.error(`${c.red}${c.reset} ${msg}`)
}
export function createReactionLogsCommand(): Command {
const cmd = new Command('reaction-logs')
cmd.description('View reaction execution logs')
// list
const list = new Command('list')
list.description('List reaction logs')
list.option('--limit <n>', 'Max results', parseInt)
list.option('--offset <n>', 'Offset', parseInt)
list.option('--reaction-id <n>', 'Filter by reaction ID', parseInt)
list.option('--json', 'output raw JSON')
list.action(async (opts: { limit?: number; offset?: number; reactionId?: number; json?: boolean }) => {
const client = new OGraphClient()
try {
await client.init()
const result = await client.listReactionLogs(opts.limit, opts.offset, opts.reactionId)
if (opts.json) {
console.log(JSON.stringify(result, null, 2))
return
}
if (result.reaction_logs.length === 0) {
console.log('No reaction logs found.')
return
}
console.log(`${c.bold}Reaction Logs${c.reset} (total: ${result.total})`)
for (const log of result.reaction_logs) {
const statusColor = log.status === 'success' ? c.green : c.red
console.log(` ${c.cyan}${log.id}${c.reset} reaction:${log.reaction_id} event:${log.event_id} ${statusColor}${log.status}${c.reset}`)
}
} catch (err) {
fail(String(err instanceof Error ? err.message : err))
process.exit(1)
}
})
cmd.addCommand(list)
return cmd
}

View File

@ -23,10 +23,12 @@ export function createReactionsCommand(): Command {
create.description('Create a new reaction')
create.requiredOption('--projection <name>', 'Projection def name')
create.option('--params <json>', 'Params JSON')
create.option('--action <type>', 'Action type: webhook (default) or emit_event', 'webhook')
create.option('--action <type>', 'Action type: webhook (default), emit_event, or handler', 'webhook')
create.option('--webhook <url>', 'Webhook URL (required when --action webhook)')
create.option('--emit-type <event_type>', 'Event type to emit (required when --action emit_event)')
create.option('--emit-template <jsonata>', 'JSONata template for emitted event payload')
create.option('--handler-code <code>', 'Handler code (required when --action handler)')
create.option('--handler-timeout <ms>', 'Handler timeout in ms (default 5000)', parseInt)
create.option('--json', 'output raw JSON')
create.action(
async (opts: {
@ -36,6 +38,8 @@ export function createReactionsCommand(): Command {
webhook?: string
emitType?: string
emitTemplate?: string
handlerCode?: string
handlerTimeout?: number
json?: boolean
}) => {
const client = new OGraphClient()
@ -52,9 +56,9 @@ export function createReactionsCommand(): Command {
}
}
const action = opts.action as 'webhook' | 'emit_event'
if (action !== 'webhook' && action !== 'emit_event') {
fail('--action must be "webhook" or "emit_event"')
const action = opts.action as 'webhook' | 'emit_event' | 'handler'
if (action !== 'webhook' && action !== 'emit_event' && action !== 'handler') {
fail('--action must be "webhook", "emit_event", or "handler"')
process.exit(1)
return
}
@ -68,12 +72,19 @@ export function createReactionsCommand(): Command {
process.exit(1)
return
}
if (action === 'handler' && !opts.handlerCode) {
fail('--handler-code <code> is required when --action handler')
process.exit(1)
return
}
const reaction = await client.createReaction(opts.projection, params, {
action,
webhook_url: opts.webhook,
emit_event_type: opts.emitType,
emit_payload_template: opts.emitTemplate,
handler_code: opts.handlerCode,
handler_timeout_ms: opts.handlerTimeout,
})
if (opts.json) {
console.log(JSON.stringify(reaction, null, 2))
@ -108,7 +119,12 @@ export function createReactionsCommand(): Command {
}
console.log(`${c.bold}Reactions${c.reset}`)
for (const r of reactions) {
const target = r.action === 'emit_event' ? `emit:${r.emit_event_type}` : (r.webhook_url ?? '')
const target =
r.action === 'emit_event'
? `emit:${r.emit_event_type}`
: r.action === 'handler'
? 'handler'
: (r.webhook_url ?? '')
console.log(` ${c.cyan}${r.id}${c.reset} [${r.action}] ${target}`)
}
} catch (err) {

View File

@ -0,0 +1,55 @@
// request-logs commands
import { Command } from 'commander'
import { OGraphClient } from '../client.js'
const c = {
reset: '\x1b[0m',
green: '\x1b[32m',
cyan: '\x1b[36m',
yellow: '\x1b[33m',
red: '\x1b[31m',
bold: '\x1b[1m',
}
function fail(msg: string) {
console.error(`${c.red}${c.reset} ${msg}`)
}
export function createRequestLogsCommand(): Command {
const cmd = new Command('request-logs')
cmd.description('View API request logs')
// list
const list = new Command('list')
list.description('List request logs')
list.option('--limit <n>', 'Max results', parseInt)
list.option('--offset <n>', 'Offset', parseInt)
list.option('--api-key-id <n>', 'Filter by API key ID', parseInt)
list.option('--json', 'output raw JSON')
list.action(async (opts: { limit?: number; offset?: number; apiKeyId?: number; json?: boolean }) => {
const client = new OGraphClient()
try {
await client.init()
const result = await client.listRequestLogs(opts.limit, opts.offset, opts.apiKeyId)
if (opts.json) {
console.log(JSON.stringify(result, null, 2))
return
}
if (result.request_logs.length === 0) {
console.log('No request logs found.')
return
}
console.log(`${c.bold}Request Logs${c.reset} (total: ${result.total})`)
for (const log of result.request_logs) {
const statusColor = log.status < 400 ? c.green : c.red
console.log(` ${c.cyan}${log.id}${c.reset} key:${log.api_key_id} ${log.method} ${log.path} ${statusColor}${log.status}${c.reset}`)
}
} catch (err) {
fail(String(err instanceof Error ? err.message : err))
process.exit(1)
}
})
cmd.addCommand(list)
return cmd
}

View File

@ -0,0 +1,57 @@
// schema command
import { Command } from 'commander'
import { OGraphClient } from '../client.js'
const c = {
reset: '\x1b[0m',
green: '\x1b[32m',
cyan: '\x1b[36m',
yellow: '\x1b[33m',
red: '\x1b[31m',
bold: '\x1b[1m',
}
function fail(msg: string) {
console.error(`${c.red}${c.reset} ${msg}`)
}
export function createSchemaCommand(): Command {
const cmd = new Command('schema')
cmd.description('Show all definitions summary')
cmd.option('--json', 'output raw JSON')
cmd.action(async (opts: { json?: boolean }) => {
const client = new OGraphClient()
try {
await client.init()
const schema = await client.getSchema()
if (opts.json) {
console.log(JSON.stringify(schema, null, 2))
return
}
console.log(`${c.bold}Schema Overview${c.reset}\n`)
console.log(`${c.bold}Object Types${c.reset} (${schema.object_defs.length})`)
for (const d of schema.object_defs) {
console.log(` ${c.cyan}${d.name}${c.reset}`)
}
console.log()
console.log(`${c.bold}Event Types${c.reset} (${schema.event_defs.length})`)
for (const d of schema.event_defs) {
const props = Object.keys(d.schema?.properties ?? {}).join(', ')
console.log(` ${c.cyan}${d.name}${c.reset} {${props}}`)
}
console.log()
console.log(`${c.bold}Projection Definitions${c.reset} (${schema.projection_defs.length})`)
for (const d of schema.projection_defs) {
console.log(` ${c.cyan}${d.name}${c.reset} sources: ${d.sources?.length ?? 0}`)
}
} catch (err) {
fail(String(err instanceof Error ? err.message : err))
process.exit(1)
}
})
return cmd
}

View File

@ -10,6 +10,10 @@ import { createEventsCommand } from './commands/events.js'
import { createProjectionDefsCommand } from './commands/projection-defs.js'
import { createProjectionsCommand } from './commands/projections.js'
import { createReactionsCommand } from './commands/reactions.js'
import { createReactionLogsCommand } from './commands/reaction-logs.js'
import { createRequestLogsCommand } from './commands/request-logs.js'
import { createApiKeysCommand } from './commands/api-keys.js'
import { createSchemaCommand } from './commands/schema.js'
import { createHealthCommand } from './commands/health.js'
import { createDeployCommand } from './commands/deploy.js'
@ -27,6 +31,10 @@ program.addCommand(createEventsCommand())
program.addCommand(createProjectionDefsCommand())
program.addCommand(createProjectionsCommand())
program.addCommand(createReactionsCommand())
program.addCommand(createReactionLogsCommand())
program.addCommand(createRequestLogsCommand())
program.addCommand(createApiKeysCommand())
program.addCommand(createSchemaCommand())
program.addCommand(createHealthCommand())
program.addCommand(createDeployCommand())

View File

@ -318,6 +318,39 @@ describe('OGraphClient v2.4', () => {
})
})
describe('createReaction (handler)', () => {
it('POSTs to /reactions with action=handler and handler_code', async () => {
await initClient()
mockOk({
id: 3,
projection_def_hash: 'hashABC',
params_hash: 'paramHash',
params: {},
action: 'handler',
handler_code: 'export default (event) => console.log(event)',
handler_timeout_ms: 10000,
created_at: 1234,
})
const result = await client.createReaction(
'userCount',
{},
{
action: 'handler',
handler_code: 'export default (event) => console.log(event)',
handler_timeout_ms: 10000,
},
)
expect(result.id).toBe(3)
expect(result.action).toBe('handler')
expect(result.handler_code).toBe('export default (event) => console.log(event)')
expect(result.handler_timeout_ms).toBe(10000)
const body = JSON.parse(mockFetch.mock.calls[0][1].body as string)
expect(body.action).toBe('handler')
expect(body.handler_code).toBe('export default (event) => console.log(event)')
expect(body.handler_timeout_ms).toBe(10000)
})
})
describe('listReactions', () => {
it('GETs /reactions', async () => {
await initClient()
@ -353,6 +386,122 @@ describe('OGraphClient v2.4', () => {
})
})
// ─── reaction logs ────────────────────────────────────────────────────────
describe('listReactionLogs', () => {
it('GETs /reaction-logs without filters', async () => {
await initClient()
mockOk({ reaction_logs: [{ id: 1, reaction_id: 10, event_id: 20, status: 'success', created_at: 1234 }], total: 1 })
const result = await client.listReactionLogs()
expect(result.reaction_logs).toHaveLength(1)
expect(result.total).toBe(1)
expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/reaction-logs')
})
it('GETs /reaction-logs with reaction_id filter', async () => {
await initClient()
mockOk({ reaction_logs: [], total: 0 })
await client.listReactionLogs(10, 0, 5)
expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/reaction-logs?limit=10&offset=0&reaction_id=5')
})
})
// ─── request logs ────────────────────────────────────────────────────────────
describe('listRequestLogs', () => {
it('GETs /request-logs without filters', async () => {
await initClient()
mockOk({ request_logs: [{ id: 1, api_key_id: 3, method: 'POST', path: '/events', status: 200, created_at: 1234 }], total: 1 })
const result = await client.listRequestLogs()
expect(result.request_logs).toHaveLength(1)
expect(result.total).toBe(1)
expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/request-logs')
})
it('GETs /request-logs with api_key_id filter', async () => {
await initClient()
mockOk({ request_logs: [], total: 0 })
await client.listRequestLogs(20, 5, 7)
expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/request-logs?limit=20&offset=5&api_key_id=7')
})
})
// ─── api keys ────────────────────────────────────────────────────────────────
describe('createApiKey', () => {
it('POSTs to /api-keys with name', async () => {
await initClient()
mockOk({ api_key: { id: 1, name: 'test-key', prefix: 'og_abc', created_at: 1234 }, plaintext_key: 'og_abc_secret123' })
const result = await client.createApiKey('test-key')
expect(result.api_key.name).toBe('test-key')
expect(result.plaintext_key).toBe('og_abc_secret123')
const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit]
expect(url).toBe('https://api.example.com/api-keys')
expect(opts.method).toBe('POST')
const body = JSON.parse(opts.body as string)
expect(body.name).toBe('test-key')
})
it('POSTs to /api-keys with allowed_events and rate_limit', async () => {
await initClient()
mockOk({ api_key: { id: 2, name: 'limited', prefix: 'og_xyz', allowed_events: ['UserCreated'], rate_limit: 100, created_at: 1234 }, plaintext_key: 'og_xyz_key456' })
const result = await client.createApiKey('limited', ['UserCreated'], 100)
expect(result.api_key.allowed_events).toEqual(['UserCreated'])
const body = JSON.parse(mockFetch.mock.calls[0][1].body as string)
expect(body.allowed_events).toEqual(['UserCreated'])
expect(body.rate_limit).toBe(100)
})
})
describe('listApiKeys', () => {
it('GETs /api-keys', async () => {
await initClient()
mockOk({ api_keys: [{ id: 1, name: 'test-key', prefix: 'og_abc', created_at: 1234 }], total: 1 })
const result = await client.listApiKeys()
expect(result.api_keys).toHaveLength(1)
expect(result.total).toBe(1)
expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/api-keys')
})
it('GETs /api-keys with pagination', async () => {
await initClient()
mockOk({ api_keys: [], total: 0 })
await client.listApiKeys(5, 10)
expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/api-keys?limit=5&offset=10')
})
})
describe('deleteApiKey', () => {
it('DELETEs /api-keys/:id', async () => {
await initClient()
mockOk({ deleted: 1 })
const result = await client.deleteApiKey(3)
expect(result.deleted).toBe(1)
const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit]
expect(url).toBe('https://api.example.com/api-keys/3')
expect(opts.method).toBe('DELETE')
})
})
// ─── schema ──────────────────────────────────────────────────────────────────
describe('getSchema', () => {
it('GETs /schema and returns all definitions', async () => {
await initClient()
mockOk({
object_defs: [{ name: 'user' }],
event_defs: [{ name: 'UserCreated', schema: { properties: {} } }],
projection_defs: [{ name: 'userCount', sources: [] }],
})
const result = await client.getSchema()
expect(result.object_defs).toHaveLength(1)
expect(result.event_defs).toHaveLength(1)
expect(result.projection_defs).toHaveLength(1)
expect(result.object_defs[0].name).toBe('user')
expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/schema')
})
})
// ─── error handling ──────────────────────────────────────────────────────────
describe('error handling', () => {