feat(cli): complete API coverage — logs, api-keys, schema, handler (#14)
This commit is contained in:
parent
e57bbb2727
commit
50fef48ebe
@ -52,10 +52,38 @@ export interface Reaction {
|
|||||||
projection_def_hash: string
|
projection_def_hash: string
|
||||||
params_hash: string
|
params_hash: string
|
||||||
params: Record<string, unknown>
|
params: Record<string, unknown>
|
||||||
action: 'webhook' | 'emit_event'
|
action: 'webhook' | 'emit_event' | 'handler'
|
||||||
webhook_url?: string
|
webhook_url?: string
|
||||||
emit_event_type?: string
|
emit_event_type?: string
|
||||||
emit_payload_template?: 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
|
created_at: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,10 +272,12 @@ export class OGraphClient {
|
|||||||
projectionDef: string,
|
projectionDef: string,
|
||||||
params: Record<string, unknown>,
|
params: Record<string, unknown>,
|
||||||
options: {
|
options: {
|
||||||
action?: 'webhook' | 'emit_event'
|
action?: 'webhook' | 'emit_event' | 'handler'
|
||||||
webhook_url?: string
|
webhook_url?: string
|
||||||
emit_event_type?: string
|
emit_event_type?: string
|
||||||
emit_payload_template?: string
|
emit_payload_template?: string
|
||||||
|
handler_code?: string
|
||||||
|
handler_timeout_ms?: number
|
||||||
},
|
},
|
||||||
): Promise<Reaction> {
|
): Promise<Reaction> {
|
||||||
const action = options.action ?? 'webhook'
|
const action = options.action ?? 'webhook'
|
||||||
@ -260,6 +290,8 @@ export class OGraphClient {
|
|||||||
webhook_url: options.webhook_url,
|
webhook_url: options.webhook_url,
|
||||||
emit_event_type: options.emit_event_type,
|
emit_event_type: options.emit_event_type,
|
||||||
emit_payload_template: options.emit_payload_template,
|
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' })
|
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 ────────────────────────────────────────────────────────────────────
|
// ─── Health ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async health(): Promise<HealthResponse> {
|
async health(): Promise<HealthResponse> {
|
||||||
|
|||||||
103
packages/cli/src/commands/api-keys.ts
Normal file
103
packages/cli/src/commands/api-keys.ts
Normal 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
|
||||||
|
}
|
||||||
@ -76,5 +76,35 @@ export function createEventDefsCommand(): Command {
|
|||||||
})
|
})
|
||||||
|
|
||||||
cmd.addCommand(create)
|
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
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,5 +66,34 @@ export function createObjectDefsCommand(): Command {
|
|||||||
})
|
})
|
||||||
|
|
||||||
cmd.addCommand(create)
|
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
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@ -140,5 +140,34 @@ export function createProjectionDefsCommand(): Command {
|
|||||||
)
|
)
|
||||||
|
|
||||||
cmd.addCommand(create)
|
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
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
55
packages/cli/src/commands/reaction-logs.ts
Normal file
55
packages/cli/src/commands/reaction-logs.ts
Normal 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
|
||||||
|
}
|
||||||
@ -23,10 +23,12 @@ export function createReactionsCommand(): Command {
|
|||||||
create.description('Create a new reaction')
|
create.description('Create a new reaction')
|
||||||
create.requiredOption('--projection <name>', 'Projection def name')
|
create.requiredOption('--projection <name>', 'Projection def name')
|
||||||
create.option('--params <json>', 'Params JSON')
|
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('--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-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('--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.option('--json', 'output raw JSON')
|
||||||
create.action(
|
create.action(
|
||||||
async (opts: {
|
async (opts: {
|
||||||
@ -36,6 +38,8 @@ export function createReactionsCommand(): Command {
|
|||||||
webhook?: string
|
webhook?: string
|
||||||
emitType?: string
|
emitType?: string
|
||||||
emitTemplate?: string
|
emitTemplate?: string
|
||||||
|
handlerCode?: string
|
||||||
|
handlerTimeout?: number
|
||||||
json?: boolean
|
json?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
const client = new OGraphClient()
|
const client = new OGraphClient()
|
||||||
@ -52,9 +56,9 @@ export function createReactionsCommand(): Command {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = opts.action as 'webhook' | 'emit_event'
|
const action = opts.action as 'webhook' | 'emit_event' | 'handler'
|
||||||
if (action !== 'webhook' && action !== 'emit_event') {
|
if (action !== 'webhook' && action !== 'emit_event' && action !== 'handler') {
|
||||||
fail('--action must be "webhook" or "emit_event"')
|
fail('--action must be "webhook", "emit_event", or "handler"')
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -68,12 +72,19 @@ export function createReactionsCommand(): Command {
|
|||||||
process.exit(1)
|
process.exit(1)
|
||||||
return
|
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, {
|
const reaction = await client.createReaction(opts.projection, params, {
|
||||||
action,
|
action,
|
||||||
webhook_url: opts.webhook,
|
webhook_url: opts.webhook,
|
||||||
emit_event_type: opts.emitType,
|
emit_event_type: opts.emitType,
|
||||||
emit_payload_template: opts.emitTemplate,
|
emit_payload_template: opts.emitTemplate,
|
||||||
|
handler_code: opts.handlerCode,
|
||||||
|
handler_timeout_ms: opts.handlerTimeout,
|
||||||
})
|
})
|
||||||
if (opts.json) {
|
if (opts.json) {
|
||||||
console.log(JSON.stringify(reaction, null, 2))
|
console.log(JSON.stringify(reaction, null, 2))
|
||||||
@ -108,7 +119,12 @@ export function createReactionsCommand(): Command {
|
|||||||
}
|
}
|
||||||
console.log(`${c.bold}Reactions${c.reset}`)
|
console.log(`${c.bold}Reactions${c.reset}`)
|
||||||
for (const r of reactions) {
|
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}`)
|
console.log(` ${c.cyan}${r.id}${c.reset} [${r.action}] ${target}`)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
55
packages/cli/src/commands/request-logs.ts
Normal file
55
packages/cli/src/commands/request-logs.ts
Normal 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
|
||||||
|
}
|
||||||
57
packages/cli/src/commands/schema.ts
Normal file
57
packages/cli/src/commands/schema.ts
Normal 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
|
||||||
|
}
|
||||||
@ -10,6 +10,10 @@ import { createEventsCommand } from './commands/events.js'
|
|||||||
import { createProjectionDefsCommand } from './commands/projection-defs.js'
|
import { createProjectionDefsCommand } from './commands/projection-defs.js'
|
||||||
import { createProjectionsCommand } from './commands/projections.js'
|
import { createProjectionsCommand } from './commands/projections.js'
|
||||||
import { createReactionsCommand } from './commands/reactions.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 { createHealthCommand } from './commands/health.js'
|
||||||
import { createDeployCommand } from './commands/deploy.js'
|
import { createDeployCommand } from './commands/deploy.js'
|
||||||
|
|
||||||
@ -27,6 +31,10 @@ program.addCommand(createEventsCommand())
|
|||||||
program.addCommand(createProjectionDefsCommand())
|
program.addCommand(createProjectionDefsCommand())
|
||||||
program.addCommand(createProjectionsCommand())
|
program.addCommand(createProjectionsCommand())
|
||||||
program.addCommand(createReactionsCommand())
|
program.addCommand(createReactionsCommand())
|
||||||
|
program.addCommand(createReactionLogsCommand())
|
||||||
|
program.addCommand(createRequestLogsCommand())
|
||||||
|
program.addCommand(createApiKeysCommand())
|
||||||
|
program.addCommand(createSchemaCommand())
|
||||||
program.addCommand(createHealthCommand())
|
program.addCommand(createHealthCommand())
|
||||||
program.addCommand(createDeployCommand())
|
program.addCommand(createDeployCommand())
|
||||||
|
|
||||||
|
|||||||
@ -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', () => {
|
describe('listReactions', () => {
|
||||||
it('GETs /reactions', async () => {
|
it('GETs /reactions', async () => {
|
||||||
await initClient()
|
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 ──────────────────────────────────────────────────────────
|
// ─── error handling ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe('error handling', () => {
|
describe('error handling', () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user