@proteos/sdk 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +40 -0
- package/dist/chunk-7RGN4E22.cjs +1185 -0
- package/dist/chunk-7RGN4E22.cjs.map +1 -0
- package/dist/chunk-XJP5WCRZ.js +1125 -0
- package/dist/chunk-XJP5WCRZ.js.map +1 -0
- package/dist/index.cjs +2384 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +5225 -0
- package/dist/index.d.ts +5225 -0
- package/dist/index.js +2146 -0
- package/dist/index.js.map +1 -0
- package/dist/meta/index.cjs +204 -0
- package/dist/meta/index.cjs.map +1 -0
- package/dist/meta/index.d.cts +2 -0
- package/dist/meta/index.d.ts +2 -0
- package/dist/meta/index.js +3 -0
- package/dist/meta/index.js.map +1 -0
- package/dist/types-BNsjfU8N.d.cts +3299 -0
- package/dist/types-BNsjfU8N.d.ts +3299 -0
- package/package.json +86 -0
- package/src/agent/agents.ts +53 -0
- package/src/agent/index.ts +134 -0
- package/src/agent/mcp-servers.ts +102 -0
- package/src/agent/prompts.ts +80 -0
- package/src/agent/session-types.ts +397 -0
- package/src/agent/sessions.ts +197 -0
- package/src/agent/skills.ts +89 -0
- package/src/agent/tools.ts +53 -0
- package/src/agent/types.ts +362 -0
- package/src/auth/index.ts +111 -0
- package/src/auth/me.ts +46 -0
- package/src/auth/organizations.ts +128 -0
- package/src/auth/platform-entities.ts +78 -0
- package/src/auth/roles.ts +213 -0
- package/src/auth/types.ts +294 -0
- package/src/auth/users.ts +226 -0
- package/src/client.ts +441 -0
- package/src/connector/index.ts +120 -0
- package/src/connector/types.ts +150 -0
- package/src/conversation/index.ts +297 -0
- package/src/conversation/types.ts +590 -0
- package/src/conversation/voice.ts +123 -0
- package/src/data/index.ts +53 -0
- package/src/data/queries.ts +66 -0
- package/src/data/records.ts +122 -0
- package/src/data/types.ts +89 -0
- package/src/errors.ts +148 -0
- package/src/events/index.ts +172 -0
- package/src/events/types.ts +77 -0
- package/src/functions/actions.ts +95 -0
- package/src/functions/index.ts +32 -0
- package/src/functions/types.ts +71 -0
- package/src/http/index.ts +2 -0
- package/src/http/query-params.ts +106 -0
- package/src/index.ts +598 -0
- package/src/iterator.ts +183 -0
- package/src/knowledge/graph.ts +35 -0
- package/src/knowledge/index.ts +104 -0
- package/src/knowledge/labels.ts +70 -0
- package/src/knowledge/links.ts +65 -0
- package/src/knowledge/nodes.ts +198 -0
- package/src/knowledge/record-links.ts +66 -0
- package/src/knowledge/types.ts +569 -0
- package/src/meta/apps.ts +107 -0
- package/src/meta/components.ts +124 -0
- package/src/meta/currency/index.ts +202 -0
- package/src/meta/entities.ts +193 -0
- package/src/meta/filters.ts +76 -0
- package/src/meta/index.ts +227 -0
- package/src/meta/layout/common-props.ts +93 -0
- package/src/meta/layout/control-registry.json +70 -0
- package/src/meta/layout/control-registry.ts +92 -0
- package/src/meta/layout/elements.ts +203 -0
- package/src/meta/layout/index.ts +41 -0
- package/src/meta/layout/page-layout.ts +35 -0
- package/src/meta/layout/size-value.ts +27 -0
- package/src/meta/list-views.ts +109 -0
- package/src/meta/lists.ts +104 -0
- package/src/meta/menu-configurations.ts +128 -0
- package/src/meta/modules.ts +159 -0
- package/src/meta/pages.ts +106 -0
- package/src/meta/types.ts +1115 -0
- package/src/meta/variables.ts +98 -0
- package/src/storage/files.ts +183 -0
- package/src/storage/index.ts +33 -0
- package/src/storage/types.ts +70 -0
- package/src/types/common.ts +143 -0
- package/src/types/index.ts +28 -0
- package/src/types/options.ts +95 -0
- package/src/workflow/executions.ts +99 -0
- package/src/workflow/index.ts +109 -0
- package/src/workflow/node-types.ts +50 -0
- package/src/workflow/types.ts +658 -0
- package/src/workflow/workflows.ts +152 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ProteosClient } from '../client.js'
|
|
2
|
+
import { type QueryService, QueryServiceImpl } from './queries.js'
|
|
3
|
+
import { type RecordService, RecordServiceImpl } from './records.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Facade for the data-service (records + raw-SQL query API).
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* ```ts
|
|
10
|
+
* const client = new ProteosClient({ baseUrl, tokenProvider });
|
|
11
|
+
* const data = new DataClient(client);
|
|
12
|
+
* const page = await data.records.listPage('customer');
|
|
13
|
+
* const result = await data.queries.execute('SELECT id, stage FROM opportunity');
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export class DataClient {
|
|
17
|
+
readonly records: RecordService
|
|
18
|
+
readonly queries: QueryService
|
|
19
|
+
|
|
20
|
+
constructor(client: ProteosClient) {
|
|
21
|
+
this.records = new RecordServiceImpl(client)
|
|
22
|
+
this.queries = new QueryServiceImpl(client)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Re-export service interfaces
|
|
27
|
+
// Re-export types
|
|
28
|
+
export type {
|
|
29
|
+
QueryExecuteMeta,
|
|
30
|
+
QueryExecuteResponse,
|
|
31
|
+
QueryRow,
|
|
32
|
+
QueryService,
|
|
33
|
+
QueryValidateMeta,
|
|
34
|
+
QueryValidateResponse,
|
|
35
|
+
} from './queries.js'
|
|
36
|
+
export type { RecordService } from './records.js'
|
|
37
|
+
export type {
|
|
38
|
+
BatchTransactionError,
|
|
39
|
+
BatchTransactionStatus,
|
|
40
|
+
BatchUpsertRecordsResponse,
|
|
41
|
+
BatchUpsertTransaction,
|
|
42
|
+
BatchUpsertTransactionResult,
|
|
43
|
+
ListRecordsOptions,
|
|
44
|
+
RecordData,
|
|
45
|
+
} from './types.js'
|
|
46
|
+
|
|
47
|
+
// Re-export Zod schemas
|
|
48
|
+
export {
|
|
49
|
+
BatchTransactionErrorSchema,
|
|
50
|
+
BatchUpsertRecordsResponseSchema,
|
|
51
|
+
BatchUpsertTransactionResultSchema,
|
|
52
|
+
BatchUpsertTransactionSchema,
|
|
53
|
+
} from './types.js'
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ProteosClient } from '../client.js'
|
|
2
|
+
|
|
3
|
+
const QUERY_BASE_PATH = '/data/v1/query'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A single row from a query result. Keys match the result-set columns
|
|
7
|
+
* (camelCase, with `id`/`created_at`/`updated_at` reserved). The data-service
|
|
8
|
+
* hoists JSONB columns into nested objects, so values can be primitives,
|
|
9
|
+
* nested records, or arrays.
|
|
10
|
+
*/
|
|
11
|
+
export type QueryRow = Record<string, unknown>
|
|
12
|
+
|
|
13
|
+
export interface QueryExecuteMeta {
|
|
14
|
+
columns: string[]
|
|
15
|
+
items: number
|
|
16
|
+
limit_applied: number
|
|
17
|
+
/** Only present when `true`. */
|
|
18
|
+
was_default_limit_applied?: boolean
|
|
19
|
+
execution_time_ms: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface QueryExecuteResponse {
|
|
23
|
+
data: QueryRow[]
|
|
24
|
+
meta?: QueryExecuteMeta
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface QueryValidateMeta {
|
|
28
|
+
limit_applied: number
|
|
29
|
+
was_default_limit_applied?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface QueryValidateResponse {
|
|
33
|
+
valid: boolean
|
|
34
|
+
/** The SQL the server would execute (entity-rewritten, schema-qualified). */
|
|
35
|
+
rewritten_sql?: string
|
|
36
|
+
tables?: string[]
|
|
37
|
+
meta?: QueryValidateMeta
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Service for the raw-SQL query endpoint of the data-service.
|
|
42
|
+
*
|
|
43
|
+
* Accepts SELECT-only Postgres SQL with bare attribute references; the
|
|
44
|
+
* server rewrites bare attributes into JSONB accessors against the
|
|
45
|
+
* tenant's schema and runs per-table OpenFGA authorization checks.
|
|
46
|
+
*/
|
|
47
|
+
export interface QueryService {
|
|
48
|
+
/** Executes the SQL and returns the result rows + meta. */
|
|
49
|
+
execute(sql: string): Promise<QueryExecuteResponse>
|
|
50
|
+
/** Parses + authorizes the SQL without running it. */
|
|
51
|
+
validate(sql: string): Promise<QueryValidateResponse>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class QueryServiceImpl implements QueryService {
|
|
55
|
+
constructor(private readonly client: ProteosClient) {}
|
|
56
|
+
|
|
57
|
+
async execute(sql: string): Promise<QueryExecuteResponse> {
|
|
58
|
+
return this.client.request<QueryExecuteResponse>('POST', `${QUERY_BASE_PATH}/execute`, { sql })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async validate(sql: string): Promise<QueryValidateResponse> {
|
|
62
|
+
return this.client.request<QueryValidateResponse>('POST', `${QUERY_BASE_PATH}/validate`, {
|
|
63
|
+
sql,
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { ProteosClient } from '../client.js'
|
|
2
|
+
import { PageIterator } from '../iterator.js'
|
|
3
|
+
import type { ListResult } from '../types/common.js'
|
|
4
|
+
import type {
|
|
5
|
+
BatchUpsertRecordsResponse,
|
|
6
|
+
BatchUpsertTransaction,
|
|
7
|
+
ListRecordsOptions,
|
|
8
|
+
RecordData,
|
|
9
|
+
} from './types.js'
|
|
10
|
+
|
|
11
|
+
const RECORDS_BASE_PATH = '/data/v1/records'
|
|
12
|
+
const BATCH_RECORDS_BASE_PATH = '/data/v1/batch/records'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Service for managing records (per-entity data rows) via the data-service.
|
|
16
|
+
*
|
|
17
|
+
* Records are opaque JSON objects whose schema is defined per-entity in the
|
|
18
|
+
* metadata service. An entity must be registered (via metadata-service, which
|
|
19
|
+
* in turn provisions a Postgres table) before records for it can be created.
|
|
20
|
+
*/
|
|
21
|
+
export interface RecordService {
|
|
22
|
+
/**
|
|
23
|
+
* Lists records for an entity as an async iterator. Pages are 0-indexed
|
|
24
|
+
* (first page is 0); `PageIterator` starts there automatically.
|
|
25
|
+
*/
|
|
26
|
+
list(
|
|
27
|
+
entitySlug: string,
|
|
28
|
+
options?: ListRecordsOptions,
|
|
29
|
+
): PageIterator<RecordData, ListRecordsOptions>
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Fetches a single page of records.
|
|
33
|
+
*/
|
|
34
|
+
listPage(entitySlug: string, options?: ListRecordsOptions): Promise<ListResult<RecordData>>
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Gets a single record by id.
|
|
38
|
+
* @throws {ProteosError} If the record is not found (404).
|
|
39
|
+
*/
|
|
40
|
+
get(entitySlug: string, id: string): Promise<RecordData>
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Creates a new record for the given entity.
|
|
44
|
+
*/
|
|
45
|
+
create(entitySlug: string, data: RecordData): Promise<RecordData>
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Partially updates a record.
|
|
49
|
+
*/
|
|
50
|
+
update(entitySlug: string, id: string, data: Partial<RecordData>): Promise<RecordData>
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Deletes a record.
|
|
54
|
+
*/
|
|
55
|
+
delete(entitySlug: string, id: string): Promise<void>
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Batch-upserts records. Each transaction succeeds/fails independently;
|
|
59
|
+
* the response contains a per-transaction status so callers (e.g. import
|
|
60
|
+
* flows) can render per-row feedback.
|
|
61
|
+
*/
|
|
62
|
+
batchUpsert(
|
|
63
|
+
entitySlug: string,
|
|
64
|
+
transactions: BatchUpsertTransaction[],
|
|
65
|
+
): Promise<BatchUpsertRecordsResponse>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Implementation of RecordService.
|
|
70
|
+
*/
|
|
71
|
+
export class RecordServiceImpl implements RecordService {
|
|
72
|
+
constructor(private readonly client: ProteosClient) {}
|
|
73
|
+
|
|
74
|
+
list(
|
|
75
|
+
entitySlug: string,
|
|
76
|
+
options: ListRecordsOptions = {},
|
|
77
|
+
): PageIterator<RecordData, ListRecordsOptions> {
|
|
78
|
+
return new PageIterator((opts) => this.listPage(entitySlug, opts), options)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async listPage(
|
|
82
|
+
entitySlug: string,
|
|
83
|
+
options: ListRecordsOptions = {},
|
|
84
|
+
): Promise<ListResult<RecordData>> {
|
|
85
|
+
return this.client.requestWithQuery<ListResult<RecordData>>(
|
|
86
|
+
'GET',
|
|
87
|
+
`${RECORDS_BASE_PATH}/${entitySlug}`,
|
|
88
|
+
options,
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async get(entitySlug: string, id: string): Promise<RecordData> {
|
|
93
|
+
return this.client.request<RecordData>('GET', `${RECORDS_BASE_PATH}/${entitySlug}/${id}`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async create(entitySlug: string, data: RecordData): Promise<RecordData> {
|
|
97
|
+
return this.client.request<RecordData>('POST', `${RECORDS_BASE_PATH}/${entitySlug}`, data)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async update(entitySlug: string, id: string, data: Partial<RecordData>): Promise<RecordData> {
|
|
101
|
+
return this.client.request<RecordData>(
|
|
102
|
+
'PATCH',
|
|
103
|
+
`${RECORDS_BASE_PATH}/${entitySlug}/${id}`,
|
|
104
|
+
data,
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async delete(entitySlug: string, id: string): Promise<void> {
|
|
109
|
+
await this.client.request<void>('DELETE', `${RECORDS_BASE_PATH}/${entitySlug}/${id}`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async batchUpsert(
|
|
113
|
+
entitySlug: string,
|
|
114
|
+
transactions: BatchUpsertTransaction[],
|
|
115
|
+
): Promise<BatchUpsertRecordsResponse> {
|
|
116
|
+
return this.client.request<BatchUpsertRecordsResponse>(
|
|
117
|
+
'POST',
|
|
118
|
+
`${BATCH_RECORDS_BASE_PATH}/${entitySlug}/upsert`,
|
|
119
|
+
transactions,
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A record is an open-shape JSON object whose keys are the entity's attribute
|
|
5
|
+
* names plus system fields (`id`, `created_at`, `updated_at`, `created_by`,
|
|
6
|
+
* `updated_by`). Because attributes vary per entity, values are typed as `any`
|
|
7
|
+
* at the SDK layer — consumers narrow or cast as needed.
|
|
8
|
+
*/
|
|
9
|
+
export type RecordData = Record<string, any>
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Options for listing records.
|
|
13
|
+
*
|
|
14
|
+
* Sorting: use `sort: "field:asc"` for single-column; pipe-separated for
|
|
15
|
+
* multi-column (`"field1:asc|field2:desc"`). Defaults to `created_at:desc`.
|
|
16
|
+
*
|
|
17
|
+
* Arbitrary filter keys pass through as query params. Supported operators
|
|
18
|
+
* (via bracket syntax) include `[eq]`, `[ne]`, `[gt]`, `[gte]`, `[lt]`,
|
|
19
|
+
* `[lte]`, `[in]` (pipe-separated), `[not_in]`, `[contains]`, `[starts_with]`,
|
|
20
|
+
* `[ends_with]`, `[empty]`, `[not_empty]`. Missing operator defaults to `[eq]`.
|
|
21
|
+
*/
|
|
22
|
+
export interface ListRecordsOptions {
|
|
23
|
+
/** Page number (0-indexed). First page is 0. */
|
|
24
|
+
page?: number
|
|
25
|
+
/** Number of items per page (default: 20 per backend) */
|
|
26
|
+
page_size?: number
|
|
27
|
+
/**
|
|
28
|
+
* Sort spec. Format: `"field:direction"`, or pipe-separated for multi-column:
|
|
29
|
+
* `"field1:asc|field2:desc"`. Direction is `asc` | `desc`.
|
|
30
|
+
*/
|
|
31
|
+
sort?: string
|
|
32
|
+
/** Arbitrary attribute filters (flat query-string params). */
|
|
33
|
+
[key: string]: unknown
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Batch operations
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* One transaction within a batch operation.
|
|
42
|
+
* `transaction_id` is client-supplied so you can match results back to your
|
|
43
|
+
* source rows (used e.g. by import flows).
|
|
44
|
+
*/
|
|
45
|
+
export interface BatchUpsertTransaction {
|
|
46
|
+
transaction_id: string
|
|
47
|
+
data: Record<string, unknown>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type BatchTransactionStatus = 'success' | 'error'
|
|
51
|
+
|
|
52
|
+
export interface BatchTransactionError {
|
|
53
|
+
code: string
|
|
54
|
+
message: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface BatchUpsertTransactionResult {
|
|
58
|
+
transaction_id: string
|
|
59
|
+
status: BatchTransactionStatus
|
|
60
|
+
record?: RecordData
|
|
61
|
+
error?: BatchTransactionError
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface BatchUpsertRecordsResponse {
|
|
65
|
+
results: BatchUpsertTransactionResult[]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Zod schemas for runtime validation (optional, for consumers who want it)
|
|
69
|
+
|
|
70
|
+
export const BatchUpsertTransactionSchema = z.object({
|
|
71
|
+
transaction_id: z.string(),
|
|
72
|
+
data: z.record(z.unknown()),
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
export const BatchTransactionErrorSchema = z.object({
|
|
76
|
+
code: z.string(),
|
|
77
|
+
message: z.string(),
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
export const BatchUpsertTransactionResultSchema = z.object({
|
|
81
|
+
transaction_id: z.string(),
|
|
82
|
+
status: z.enum(['success', 'error']),
|
|
83
|
+
record: z.record(z.unknown()).optional(),
|
|
84
|
+
error: BatchTransactionErrorSchema.optional(),
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
export const BatchUpsertRecordsResponseSchema = z.object({
|
|
88
|
+
results: z.array(BatchUpsertTransactionResultSchema),
|
|
89
|
+
})
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common error codes returned by the Proteos API.
|
|
3
|
+
*/
|
|
4
|
+
export const ErrorCode = {
|
|
5
|
+
NOT_FOUND: 'not_found',
|
|
6
|
+
UNAUTHORIZED: 'unauthorized',
|
|
7
|
+
FORBIDDEN: 'forbidden',
|
|
8
|
+
BAD_REQUEST: 'bad_request',
|
|
9
|
+
CONFLICT: 'conflict',
|
|
10
|
+
INTERNAL_SERVER_ERROR: 'internal_server_error',
|
|
11
|
+
INVALID_PAYLOAD: 'invalid_payload',
|
|
12
|
+
INVALID_PERSONA: 'invalid_persona',
|
|
13
|
+
} as const
|
|
14
|
+
|
|
15
|
+
export type ErrorCodeType = (typeof ErrorCode)[keyof typeof ErrorCode]
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Error returned by the Proteos API.
|
|
19
|
+
* Contains HTTP status code, error code, and human-readable message.
|
|
20
|
+
*/
|
|
21
|
+
export class ProteosError extends Error {
|
|
22
|
+
/** HTTP status code of the response */
|
|
23
|
+
readonly httpStatus: number
|
|
24
|
+
/** API error code (e.g., 'not_found', 'unauthorized') */
|
|
25
|
+
readonly code: ErrorCodeType | string
|
|
26
|
+
|
|
27
|
+
constructor(message: string, httpStatus: number, code: ErrorCodeType | string) {
|
|
28
|
+
super(message)
|
|
29
|
+
this.name = 'ProteosError'
|
|
30
|
+
this.httpStatus = httpStatus
|
|
31
|
+
this.code = code
|
|
32
|
+
|
|
33
|
+
// V8-specific API for cleaner stack traces; not present in all runtimes
|
|
34
|
+
const v8Capture = (Error as unknown as { captureStackTrace?: (t: object, c: unknown) => void })
|
|
35
|
+
.captureStackTrace
|
|
36
|
+
if (v8Capture) v8Capture(this, this.constructor)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns a formatted string representation of the error.
|
|
41
|
+
*/
|
|
42
|
+
override toString(): string {
|
|
43
|
+
return `${this.code}: ${this.message} (HTTP ${this.httpStatus})`
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Type guard to check if an error is a ProteosError.
|
|
49
|
+
*/
|
|
50
|
+
export function isProteosError(error: unknown): error is ProteosError {
|
|
51
|
+
return error instanceof ProteosError
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Type guard to check if an error is a 404 Not Found error.
|
|
56
|
+
*/
|
|
57
|
+
export function isNotFound(error: unknown): error is ProteosError {
|
|
58
|
+
return isProteosError(error) && error.httpStatus === 404
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Type guard to check if an error is a 401 Unauthorized error.
|
|
63
|
+
*/
|
|
64
|
+
export function isUnauthorized(error: unknown): error is ProteosError {
|
|
65
|
+
return isProteosError(error) && error.httpStatus === 401
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Type guard to check if an error is a 403 Forbidden error.
|
|
70
|
+
*/
|
|
71
|
+
export function isForbidden(error: unknown): error is ProteosError {
|
|
72
|
+
return isProteosError(error) && error.httpStatus === 403
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Type guard to check if an error is a 400 Bad Request error.
|
|
77
|
+
*/
|
|
78
|
+
export function isBadRequest(error: unknown): error is ProteosError {
|
|
79
|
+
return isProteosError(error) && error.httpStatus === 400
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Type guard to check if an error is a 409 Conflict error.
|
|
84
|
+
*/
|
|
85
|
+
export function isConflict(error: unknown): error is ProteosError {
|
|
86
|
+
return isProteosError(error) && error.httpStatus === 409
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Maps HTTP status codes to default error codes.
|
|
91
|
+
*/
|
|
92
|
+
export function getDefaultErrorCode(httpStatus: number): ErrorCodeType | string {
|
|
93
|
+
switch (httpStatus) {
|
|
94
|
+
case 400:
|
|
95
|
+
return ErrorCode.BAD_REQUEST
|
|
96
|
+
case 401:
|
|
97
|
+
return ErrorCode.UNAUTHORIZED
|
|
98
|
+
case 403:
|
|
99
|
+
return ErrorCode.FORBIDDEN
|
|
100
|
+
case 404:
|
|
101
|
+
return ErrorCode.NOT_FOUND
|
|
102
|
+
case 409:
|
|
103
|
+
return ErrorCode.CONFLICT
|
|
104
|
+
default:
|
|
105
|
+
return ErrorCode.INTERNAL_SERVER_ERROR
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Response shape for API error responses.
|
|
111
|
+
*/
|
|
112
|
+
export interface ApiErrorResponse {
|
|
113
|
+
code?: string
|
|
114
|
+
message?: string
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Parses an error response from the API and creates a ProteosError.
|
|
119
|
+
*/
|
|
120
|
+
export async function parseErrorResponse(response: Response): Promise<ProteosError> {
|
|
121
|
+
const httpStatus = response.status
|
|
122
|
+
let code: string = getDefaultErrorCode(httpStatus)
|
|
123
|
+
let message = 'Unknown error'
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const body = await response.text()
|
|
127
|
+
|
|
128
|
+
// Try to parse as JSON
|
|
129
|
+
try {
|
|
130
|
+
const json = JSON.parse(body) as ApiErrorResponse
|
|
131
|
+
if (json.code) {
|
|
132
|
+
code = json.code
|
|
133
|
+
}
|
|
134
|
+
if (json.message) {
|
|
135
|
+
message = json.message
|
|
136
|
+
} else {
|
|
137
|
+
message = body || `HTTP ${httpStatus}`
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// Not JSON, use body as message
|
|
141
|
+
message = body.trim() || `HTTP ${httpStatus}`
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
message = `HTTP ${httpStatus}`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return new ProteosError(message, httpStatus, code)
|
|
148
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { ProteosClient } from '../client.js'
|
|
2
|
+
import type {
|
|
3
|
+
ListEventsOptions,
|
|
4
|
+
PlatformEvent,
|
|
5
|
+
PublishEventRequest,
|
|
6
|
+
RedriveResult,
|
|
7
|
+
TailOptions,
|
|
8
|
+
Topic,
|
|
9
|
+
} from './types.js'
|
|
10
|
+
|
|
11
|
+
const EVENTS_BASE_PATH = '/events/v1'
|
|
12
|
+
|
|
13
|
+
// A live tail is a long-poll; override the client's default request timeout so
|
|
14
|
+
// it isn't aborted mid-stream. ~24h is well under the setTimeout overflow bound
|
|
15
|
+
// and acts as a practical cap on a single tail session. Real teardown is the
|
|
16
|
+
// caller's AbortSignal.
|
|
17
|
+
const TAIL_TIMEOUT_MS = 24 * 60 * 60 * 1000
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Facade for event-service: a read/admin view over the Redis-Streams bus.
|
|
21
|
+
* Everything is topic-centric, so the surface lives on `topics`.
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* const events = new EventsClient(client)
|
|
25
|
+
* const topics = await events.topics.list()
|
|
26
|
+
* const recent = await events.topics.events('record.contact.events', { limit: 50 })
|
|
27
|
+
* for await (const msg of events.topics.tail('record.contact.events', { signal })) {
|
|
28
|
+
* // append msg
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export class EventsClient {
|
|
33
|
+
readonly topics: TopicService
|
|
34
|
+
|
|
35
|
+
constructor(client: ProteosClient) {
|
|
36
|
+
this.topics = new TopicServiceImpl(client)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Topic discovery, event reads + live tail, and admin actions. */
|
|
41
|
+
export interface TopicService {
|
|
42
|
+
/** List the caller-org's topics. */
|
|
43
|
+
list(): Promise<Topic[]>
|
|
44
|
+
/** Read the most-recent events on a topic (newest first). */
|
|
45
|
+
events(topic: string, options?: ListEventsOptions): Promise<PlatformEvent[]>
|
|
46
|
+
/**
|
|
47
|
+
* Live-tail new events on a topic until the AbortSignal fires. Yields each
|
|
48
|
+
* event as it arrives (chunked NDJSON under the hood).
|
|
49
|
+
*/
|
|
50
|
+
tail(topic: string, options?: TailOptions): AsyncGenerator<PlatformEvent, void, unknown>
|
|
51
|
+
/** Publish a test event onto a topic — delivered to real consumers. */
|
|
52
|
+
publish(topic: string, request: PublishEventRequest): Promise<PlatformEvent>
|
|
53
|
+
/** Move a dead-letter stream back to its source topic for re-processing. */
|
|
54
|
+
redrive(topic: string): Promise<RedriveResult>
|
|
55
|
+
/** Empty a topic's stream (keeps consumer groups). Destructive. */
|
|
56
|
+
purge(topic: string): Promise<void>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class TopicServiceImpl implements TopicService {
|
|
60
|
+
constructor(private readonly client: ProteosClient) {}
|
|
61
|
+
|
|
62
|
+
async list(): Promise<Topic[]> {
|
|
63
|
+
const response = await this.client.request<{ data: Topic[] }>(
|
|
64
|
+
'GET',
|
|
65
|
+
`${EVENTS_BASE_PATH}/topics`,
|
|
66
|
+
)
|
|
67
|
+
return response.data ?? []
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async events(topic: string, options: ListEventsOptions = {}): Promise<PlatformEvent[]> {
|
|
71
|
+
const response = await this.client.requestWithQuery<{ data: PlatformEvent[] }, ListEventsOptions>(
|
|
72
|
+
'GET',
|
|
73
|
+
`${EVENTS_BASE_PATH}/topics/${encodeURIComponent(topic)}/events`,
|
|
74
|
+
options,
|
|
75
|
+
)
|
|
76
|
+
return response.data ?? []
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async *tail(topic: string, options: TailOptions = {}): AsyncGenerator<PlatformEvent, void, unknown> {
|
|
80
|
+
const path = `${EVENTS_BASE_PATH}/topics/${encodeURIComponent(topic)}/events?follow=true`
|
|
81
|
+
let body: ReadableStream<Uint8Array> | null
|
|
82
|
+
try {
|
|
83
|
+
;({ body } = await this.client.requestRaw('GET', path, undefined, {
|
|
84
|
+
timeout: TAIL_TIMEOUT_MS,
|
|
85
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
86
|
+
headers: { Accept: 'application/x-ndjson' },
|
|
87
|
+
}))
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (isAbort(error)) return
|
|
90
|
+
throw error
|
|
91
|
+
}
|
|
92
|
+
if (!body) return
|
|
93
|
+
|
|
94
|
+
const reader = body.getReader()
|
|
95
|
+
const decoder = new TextDecoder()
|
|
96
|
+
let buffer = ''
|
|
97
|
+
try {
|
|
98
|
+
while (true) {
|
|
99
|
+
const { done, value } = await reader.read()
|
|
100
|
+
if (done) break
|
|
101
|
+
buffer += decoder.decode(value, { stream: true })
|
|
102
|
+
let newlineIndex = buffer.indexOf('\n')
|
|
103
|
+
while (newlineIndex !== -1) {
|
|
104
|
+
const line = buffer.slice(0, newlineIndex).trim()
|
|
105
|
+
buffer = buffer.slice(newlineIndex + 1)
|
|
106
|
+
const event = parseLine(line)
|
|
107
|
+
if (event) yield event
|
|
108
|
+
newlineIndex = buffer.indexOf('\n')
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const remainder = parseLine(buffer.trim())
|
|
112
|
+
if (remainder) yield remainder
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (!isAbort(error)) throw error
|
|
115
|
+
} finally {
|
|
116
|
+
reader.releaseLock()
|
|
117
|
+
void body.cancel().catch(() => {})
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async publish(topic: string, request: PublishEventRequest): Promise<PlatformEvent> {
|
|
122
|
+
return this.client.request<PlatformEvent>(
|
|
123
|
+
'POST',
|
|
124
|
+
`${EVENTS_BASE_PATH}/topics/${encodeURIComponent(topic)}/events`,
|
|
125
|
+
request,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async redrive(topic: string): Promise<RedriveResult> {
|
|
130
|
+
return this.client.request<RedriveResult>(
|
|
131
|
+
'POST',
|
|
132
|
+
`${EVENTS_BASE_PATH}/topics/${encodeURIComponent(topic)}/redrive`,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async purge(topic: string): Promise<void> {
|
|
137
|
+
await this.client.request<void>(
|
|
138
|
+
'POST',
|
|
139
|
+
`${EVENTS_BASE_PATH}/topics/${encodeURIComponent(topic)}/purge`,
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** True when an error is an AbortError (the tail's normal stop path). */
|
|
145
|
+
function isAbort(error: unknown): boolean {
|
|
146
|
+
return error instanceof DOMException && error.name === 'AbortError'
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Parses one NDJSON line into a PlatformEvent, returning null for a blank or
|
|
151
|
+
* unparseable line. A single bad frame should be skipped, not tear down the
|
|
152
|
+
* whole tail.
|
|
153
|
+
*/
|
|
154
|
+
function parseLine(line: string): PlatformEvent | null {
|
|
155
|
+
if (!line) return null
|
|
156
|
+
try {
|
|
157
|
+
return JSON.parse(line) as PlatformEvent
|
|
158
|
+
} catch {
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export type {
|
|
164
|
+
ConsumerGroup,
|
|
165
|
+
ListEventsOptions,
|
|
166
|
+
PlatformEvent,
|
|
167
|
+
PublishEventRequest,
|
|
168
|
+
RedriveResult,
|
|
169
|
+
TailOptions,
|
|
170
|
+
Topic,
|
|
171
|
+
TopicKind,
|
|
172
|
+
} from './types.js'
|