diff --git a/packages/cli/src/client.ts b/packages/cli/src/client.ts index f4fcdf4..789a936 100644 --- a/packages/cli/src/client.ts +++ b/packages/cli/src/client.ts @@ -52,10 +52,38 @@ export interface Reaction { projection_def_hash: string params_hash: string params: Record - 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, 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 { 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 = { 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 { diff --git a/packages/cli/src/commands/api-keys.ts b/packages/cli/src/commands/api-keys.ts new file mode 100644 index 0000000..7b8e907 --- /dev/null +++ b/packages/cli/src/commands/api-keys.ts @@ -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 ', 'Key name') + create.option('--allowed-events ', 'Comma-separated allowed event types') + create.option('--rate-limit ', '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 ', 'Max results', parseInt) + list.option('--offset ', '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('', '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 +} diff --git a/packages/cli/src/commands/event-defs.ts b/packages/cli/src/commands/event-defs.ts index c85a640..ce70c58 100644 --- a/packages/cli/src/commands/event-defs.ts +++ b/packages/cli/src/commands/event-defs.ts @@ -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 } diff --git a/packages/cli/src/commands/object-defs.ts b/packages/cli/src/commands/object-defs.ts index 8bbd5e0..2b5c0d0 100644 --- a/packages/cli/src/commands/object-defs.ts +++ b/packages/cli/src/commands/object-defs.ts @@ -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 } diff --git a/packages/cli/src/commands/projection-defs.ts b/packages/cli/src/commands/projection-defs.ts index 2f4b4c3..0e89e8f 100644 --- a/packages/cli/src/commands/projection-defs.ts +++ b/packages/cli/src/commands/projection-defs.ts @@ -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 } diff --git a/packages/cli/src/commands/reaction-logs.ts b/packages/cli/src/commands/reaction-logs.ts new file mode 100644 index 0000000..dc18e5e --- /dev/null +++ b/packages/cli/src/commands/reaction-logs.ts @@ -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 ', 'Max results', parseInt) + list.option('--offset ', 'Offset', parseInt) + list.option('--reaction-id ', '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 +} diff --git a/packages/cli/src/commands/reactions.ts b/packages/cli/src/commands/reactions.ts index 0f4e0aa..48bd30d 100644 --- a/packages/cli/src/commands/reactions.ts +++ b/packages/cli/src/commands/reactions.ts @@ -23,10 +23,12 @@ export function createReactionsCommand(): Command { create.description('Create a new reaction') create.requiredOption('--projection ', 'Projection def name') create.option('--params ', 'Params JSON') - create.option('--action ', 'Action type: webhook (default) or emit_event', 'webhook') + create.option('--action ', 'Action type: webhook (default), emit_event, or handler', 'webhook') create.option('--webhook ', 'Webhook URL (required when --action webhook)') create.option('--emit-type ', 'Event type to emit (required when --action emit_event)') create.option('--emit-template ', 'JSONata template for emitted event payload') + create.option('--handler-code ', 'Handler code (required when --action handler)') + create.option('--handler-timeout ', '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 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) { diff --git a/packages/cli/src/commands/request-logs.ts b/packages/cli/src/commands/request-logs.ts new file mode 100644 index 0000000..3d19c15 --- /dev/null +++ b/packages/cli/src/commands/request-logs.ts @@ -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 ', 'Max results', parseInt) + list.option('--offset ', 'Offset', parseInt) + list.option('--api-key-id ', '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 +} diff --git a/packages/cli/src/commands/schema.ts b/packages/cli/src/commands/schema.ts new file mode 100644 index 0000000..a6e9dda --- /dev/null +++ b/packages/cli/src/commands/schema.ts @@ -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 +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index e56bd37..4e3438e 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -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()) diff --git a/packages/cli/test/client.test.ts b/packages/cli/test/client.test.ts index 5c16bcb..c0014fe 100644 --- a/packages/cli/test/client.test.ts +++ b/packages/cli/test/client.test.ts @@ -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', () => {