Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/types/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Includes:
- **Entities** — `Organization`, `User`, `Agent`, `Wallet`, `Policy`, `Budget`, `Transaction`, `Proposal`, `Approval`, `Notification`, `ApiKey`, `Webhook`, `Session`, `MemoryRecord`, and more.
- **Enums** — mirrored 1:1 with the `astroid-api` Prisma schema.
- **Response envelope** — `ApiResponse<T>` = `{ success, data, meta, requestId }`.
- **Pagination** — `PaginationParams`, `Paginated<T>`, `PaginationMeta`.
- **Pagination** — `PaginationParams`, `PaginatedResponse`, `Paginated<T>`, `PaginationMeta`, `CursorPaginationParams`, `CursorPaginated`.
- **Webhooks & events** — dot.case event names and a fully-typed `WebhookEventDataMap`.
- **AI-native** — `PaymentIntent`, `PaymentIntentResult`.

Expand Down
14 changes: 4 additions & 10 deletions packages/types/src/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,14 @@ export interface AnalyticsQueryParams extends PaginationParams {
}

/**
* Analytics list queries: the shared {@link AnalyticsQuery} filters plus
* standard pagination controls (`page`, `limit`, `sort`, `order`, `search`).
* Analytics list queries: the shared analytics filters plus standard
* pagination controls (`page`, `limit`, `order`, `cursor`).
*
* Applied to the tabular analytics endpoints (per-agent and per-budget rows) so
* clients can page through large historical result sets without pulling the
* full payload into memory.
*/
export interface AnalyticsListParams extends AnalyticsQuery, PaginationParams {}

/**
* Alias for a paginated analytics results payload. Keeps the narrow, row-level
* item type explicit at call sites (e.g. {@link AgentSpendingRow}).
*/
export type PaginatedResponse<TItem> = Paginated<TItem>;
export type AnalyticsListParams = AnalyticsQueryParams;

/** A single (timestamp, value) point in a time series. */
export interface TimeSeriesPoint {
Expand All @@ -43,7 +37,7 @@ export interface VolumeSummary {
}

export interface AnalyticsMetricsResponse {
points: TimeSeriesMetricPoint[];
points: TimeSeriesPoint[];
summary: VolumeSummary;
}

Expand Down
7 changes: 6 additions & 1 deletion packages/types/src/budget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
*/

import type { DecimalString, IsoDateTime } from './entities.js';
import type { Budget, BudgetPeriod } from './entities.js';
import type { Budget } from './entities.js';
import type { BudgetPeriod } from './enums.js';
import type { PaginationParams } from './common.js';

/** A prospective spend draw to simulate against a budget. */
export interface BudgetSimulationInput {
Expand Down Expand Up @@ -37,6 +39,9 @@ export interface BudgetSimulationResult {
windowEnd: IsoDateTime;
}

/** Health of a budget's current allocation, bucketed by utilisation. */
export type BudgetAllocationState = 'healthy' | 'warning' | 'critical' | 'exhausted';

/** A utilization snapshot for a single budget. */
export interface BudgetUtilization {
budgetId: string;
Expand Down
80 changes: 22 additions & 58 deletions packages/types/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ export interface ApiError {
details?: Record<string, unknown>;
}

/** Standard pagination request parameters. */
export interface PaginationParams {
/** 1-based page number (offset pagination). */
page?: number;
/** Opaque cursor for keyset pagination. */
cursor?: string;
/** Maximum number of items to return per page. */
limit?: number;
/** Sort direction. */
order?: 'asc' | 'desc';
}

/** Full pagination metadata attached to a list response. */
export interface PaginationMeta {
/** The current 1-based page number. */
Expand Down Expand Up @@ -97,69 +109,21 @@ export interface CursorPaginated<T> {

/** Standard metadata returned with API responses. */
export interface ResponseMeta {
/** Opaque cursor for resuming keyset pagination. */
cursor?: string;
hasMore?: boolean;
/** Cursor to pass back for the next page, or `null` when the result set is exhausted. */
nextCursor?: string | null;
/** The current 1-based page number (offset pagination). */
page?: number;
/** The page size used for this response. */
limit?: number;
/** Total number of matching items across all pages. */
total?: number;
/** Whether more pages follow this one. */
hasMore?: boolean;
[key: string]: unknown;
}

/** Standard API error payload structure. */
export interface ApiError {
code: string;
message: string;
details?: Record<string, unknown>;
}

/**
* Machine-readable error codes returned by the Astroid API.
*
* Mirrors the backend error catalogue exactly so the SDK's error classes can
* branch on a stable value rather than a message string.
*/
export const ApiErrorCode = {
/** Missing or invalid credentials. */
AUTHENTICATION_ERROR: 'AUTHENTICATION_ERROR',
/** Request was not authenticated. */
UNAUTHORIZED: 'UNAUTHORIZED',
/** The supplied API key is invalid or revoked. */
INVALID_API_KEY: 'INVALID_API_KEY',
/** The access token has expired. */
TOKEN_EXPIRED: 'TOKEN_EXPIRED',
/** Authenticated but not permitted to perform this action. */
FORBIDDEN: 'FORBIDDEN',
/** The request failed schema or business validation. */
VALIDATION_ERROR: 'VALIDATION_ERROR',
/** The request was malformed. */
BAD_REQUEST: 'BAD_REQUEST',
/** The requested resource does not exist. */
NOT_FOUND: 'NOT_FOUND',
/** The request conflicts with the current resource state. */
CONFLICT: 'CONFLICT',
/** A transaction violates one or more spending policies. */
POLICY_VIOLATION: 'POLICY_VIOLATION',
/** The transaction's risk score exceeds the configured threshold. */
RISK_THRESHOLD_EXCEEDED: 'RISK_THRESHOLD_EXCEEDED',
/** The transaction would exceed an available budget. */
BUDGET_EXCEEDED: 'BUDGET_EXCEEDED',
/** The source account lacks sufficient funds. */
INSUFFICIENT_FUNDS: 'INSUFFICIENT_FUNDS',
/** The wallet is frozen and cannot transact. */
WALLET_FROZEN: 'WALLET_FROZEN',
/** The action requires human approval before it can execute. */
APPROVAL_REQUIRED: 'APPROVAL_REQUIRED',
/** Rate limit exceeded. */
RATE_LIMITED: 'RATE_LIMITED',
/** A network-level failure occurred before a response was received. */
NETWORK_ERROR: 'NETWORK_ERROR',
/** The request timed out. */
TIMEOUT: 'TIMEOUT',
/** An unexpected server error occurred. */
INTERNAL_ERROR: 'INTERNAL_ERROR',
/** The service is temporarily unavailable. */
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
} as const;
export type ApiErrorCode = (typeof ApiErrorCode)[keyof typeof ApiErrorCode];

/** A successful API response envelope. */
export interface ApiSuccessResponse<T> {
success: true;
Expand Down
125 changes: 125 additions & 0 deletions packages/types/src/pagination.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { describe, it, expect, expectTypeOf } from 'vitest';
import type {
CursorPaginated,
CursorPaginationParams,
Paginated,
PaginatedResponse,
PaginationMeta,
PaginationParams,
ResponseMeta,
} from './index.js';

interface SampleItem {
id: string;
name: string;
}

describe('@astroid/types — pagination', () => {
it('PaginationParams exposes only optional request parameters', () => {
expectTypeOf<PaginationParams>().toEqualTypeOf<{
page?: number;
cursor?: string;
limit?: number;
order?: 'asc' | 'desc';
}>();
});

it('PaginationParams accepts offset-, cursor- and empty-shaped params', () => {
const offset: PaginationParams = { page: 2, limit: 25 };
const cursor: PaginationParams = { cursor: 'cur_abc', limit: 25, order: 'desc' };
const empty: PaginationParams = {};
expect(offset.page).toBe(2);
expect(cursor.cursor).toBe('cur_abc');
expect(empty).toEqual({});
});

it('PaginatedResponse is generic and wraps an item array with optional metadata', () => {
expectTypeOf<PaginatedResponse<SampleItem>>().toEqualTypeOf<{
data: SampleItem[];
meta?: ResponseMeta;
}>();
});

it('PaginatedResponse compiles with cursor-based metadata', () => {
const page: PaginatedResponse<SampleItem> = {
data: [{ id: '1', name: 'one' }],
meta: { nextCursor: 'cur_next', limit: 25, total: 120, hasMore: true },
};
expect(page.data).toHaveLength(1);
expect(page.data[0]?.name).toBe('one');
expect(page.meta?.nextCursor).toBe('cur_next');
});

it('PaginatedResponse compiles with offset-based metadata', () => {
const page: PaginatedResponse<SampleItem> = {
data: [{ id: '2', name: 'two' }],
meta: { page: 1, limit: 25, total: 50 },
};
expect(page.meta?.page).toBe(1);
expect(page.meta?.total).toBe(50);
});

it('PaginatedResponse allows an unwrapped response', () => {
const page: PaginatedResponse<SampleItem> = { data: [] };
expectTypeOf(page.meta).toEqualTypeOf<ResponseMeta | undefined>();
});

it('PaginationMeta requires the full offset-pagination metadata set', () => {
expectTypeOf<PaginationMeta>().toEqualTypeOf<{
page: number;
limit: number;
total: number;
totalPages: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
}>();
});

it('Paginated is generic and requires full pagination metadata', () => {
expectTypeOf<Paginated<SampleItem>>().toEqualTypeOf<{
data: SampleItem[];
meta: PaginationMeta;
}>();
});

it('CursorPaginationParams exposes only cursor, limit and order', () => {
expectTypeOf<CursorPaginationParams>().toEqualTypeOf<{
cursor?: string;
limit?: number;
order?: 'asc' | 'desc';
}>();
});

it('CursorPaginated is generic and follows a keyset envelope', () => {
expectTypeOf<CursorPaginated<SampleItem>>().toEqualTypeOf<{
items: SampleItem[];
nextCursor: string | null;
hasMore: boolean;
}>();
const page: CursorPaginated<SampleItem> = { items: [], nextCursor: null, hasMore: false };
expect(page.nextCursor).toBeNull();
});

it('PaginatedResponse is assignable to a generic unwrapping helper', () => {
const unwrap = <T>(response: PaginatedResponse<T>): T[] => response.data;
const page: PaginatedResponse<SampleItem> = {
data: [{ id: '3', name: 'three' }],
meta: { total: 1 },
};
const expected: SampleItem[] = unwrap(page);
expectTypeOf(expected).toEqualTypeOf<SampleItem[]>();
});

it('ResponseMeta surfaces standard pagination fields', () => {
const meta: ResponseMeta = {
cursor: 'cur_before',
nextCursor: 'cur_after',
page: 3,
limit: 50,
total: 250,
hasMore: true,
};
expect(meta.nextCursor).toBe('cur_after');
expectTypeOf(meta.nextCursor).toEqualTypeOf<string | null | undefined>();
});
});
Loading