@openmeter/sdk 1.0.0-alpha.4 → 1.0.0-beta.2
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/README.md +42 -46
- package/clients/client.ts +153 -0
- package/clients/event.ts +109 -0
- package/clients/meter.ts +84 -0
- package/dist/clients/client.d.ts +40 -0
- package/dist/clients/client.js +101 -0
- package/dist/clients/event.d.ts +65 -0
- package/dist/clients/event.js +38 -0
- package/dist/clients/meter.d.ts +52 -0
- package/dist/clients/meter.js +52 -0
- package/dist/index.d.ts +10 -22
- package/dist/index.js +7 -25
- package/dist/schemas/openapi.d.ts +414 -0
- package/dist/schemas/openapi.js +5 -0
- package/dist/test/agent.d.ts +2 -0
- package/dist/test/agent.js +105 -0
- package/dist/test/mocks.d.ts +11 -0
- package/dist/test/mocks.js +21 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/index.ts +13 -49
- package/package.json +53 -56
- package/schemas/openapi.ts +421 -0
- package/dist/generated/HttpService.d.ts +0 -14
- package/dist/generated/HttpService.js +0 -27
- package/dist/generated/core/ApiError.d.ts +0 -10
- package/dist/generated/core/ApiError.js +0 -17
- package/dist/generated/core/ApiRequestOptions.d.ts +0 -13
- package/dist/generated/core/ApiRequestOptions.js +0 -2
- package/dist/generated/core/ApiResult.d.ts +0 -7
- package/dist/generated/core/ApiResult.js +0 -2
- package/dist/generated/core/BaseHttpRequest.d.ts +0 -8
- package/dist/generated/core/BaseHttpRequest.js +0 -7
- package/dist/generated/core/CancelablePromise.d.ts +0 -20
- package/dist/generated/core/CancelablePromise.js +0 -93
- package/dist/generated/core/NodeHttpRequest.d.ts +0 -14
- package/dist/generated/core/NodeHttpRequest.js +0 -17
- package/dist/generated/core/OpenAPI.d.ts +0 -16
- package/dist/generated/core/OpenAPI.js +0 -12
- package/dist/generated/core/request.d.ts +0 -34
- package/dist/generated/core/request.js +0 -268
- package/dist/generated/index.d.ts +0 -29
- package/dist/generated/index.js +0 -22
- package/dist/generated/models/Event.d.ts +0 -49
- package/dist/generated/models/Event.js +0 -11
- package/dist/generated/models/IdOrSlug.d.ts +0 -1
- package/dist/generated/models/IdOrSlug.js +0 -2
- package/dist/generated/models/Meter.d.ts +0 -30
- package/dist/generated/models/Meter.js +0 -2
- package/dist/generated/models/MeterAggregation.d.ts +0 -10
- package/dist/generated/models/MeterAggregation.js +0 -12
- package/dist/generated/models/MeterValue.d.ts +0 -10
- package/dist/generated/models/MeterValue.js +0 -2
- package/dist/generated/models/Namespace.d.ts +0 -6
- package/dist/generated/models/Namespace.js +0 -2
- package/dist/generated/models/Problem.d.ts +0 -4
- package/dist/generated/models/Problem.js +0 -2
- package/dist/generated/models/WindowSize.d.ts +0 -5
- package/dist/generated/models/WindowSize.js +0 -7
- package/dist/generated/models/meterIdOrSlug.d.ts +0 -5
- package/dist/generated/models/meterIdOrSlug.js +0 -2
- package/dist/generated/models/namespaceParam.d.ts +0 -4
- package/dist/generated/models/namespaceParam.js +0 -2
- package/dist/generated/schemas/$Event.d.ts +0 -59
- package/dist/generated/schemas/$Event.js +0 -60
- package/dist/generated/schemas/$IdOrSlug.d.ts +0 -13
- package/dist/generated/schemas/$IdOrSlug.js +0 -14
- package/dist/generated/schemas/$Meter.d.ts +0 -44
- package/dist/generated/schemas/$Meter.js +0 -45
- package/dist/generated/schemas/$MeterAggregation.d.ts +0 -3
- package/dist/generated/schemas/$MeterAggregation.js +0 -4
- package/dist/generated/schemas/$MeterValue.d.ts +0 -26
- package/dist/generated/schemas/$MeterValue.js +0 -27
- package/dist/generated/schemas/$Namespace.d.ts +0 -9
- package/dist/generated/schemas/$Namespace.js +0 -10
- package/dist/generated/schemas/$Problem.d.ts +0 -6
- package/dist/generated/schemas/$Problem.js +0 -7
- package/dist/generated/schemas/$WindowSize.d.ts +0 -3
- package/dist/generated/schemas/$WindowSize.js +0 -4
- package/dist/generated/schemas/$meterIdOrSlug.d.ts +0 -4
- package/dist/generated/schemas/$meterIdOrSlug.js +0 -5
- package/dist/generated/schemas/$namespaceParam.d.ts +0 -5
- package/dist/generated/schemas/$namespaceParam.js +0 -6
- package/dist/generated/services/DefaultService.d.ts +0 -16
- package/dist/generated/services/DefaultService.js +0 -22
- package/dist/generated/services/EventsService.d.ts +0 -17
- package/dist/generated/services/EventsService.js +0 -29
- package/dist/generated/services/MetersService.d.ts +0 -75
- package/dist/generated/services/MetersService.js +0 -140
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
## Install
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
npm install --save @openmeter/sdk@
|
|
6
|
+
npm install --save @openmeter/sdk@beta
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
## Example
|
|
@@ -15,78 +15,74 @@ const openmeter = new OpenMeter({ baseUrl: 'http://localhost:8888' })
|
|
|
15
15
|
|
|
16
16
|
// Ingesting an event
|
|
17
17
|
const event: Event = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
specversion: '1.0',
|
|
19
|
+
id: 'id-1',
|
|
20
|
+
source: 'my-app',
|
|
21
|
+
type: 'my-type',
|
|
22
|
+
subject: 'my-awesome-user-id',
|
|
23
|
+
time: new Date(),
|
|
24
|
+
data: {
|
|
25
|
+
api_calls: 1,
|
|
26
|
+
},
|
|
27
27
|
}
|
|
28
|
-
await openmeter.events.
|
|
28
|
+
await openmeter.events.ingest(event)
|
|
29
29
|
|
|
30
30
|
// Fetching a meter
|
|
31
|
-
const meter = await openmeter.meters.
|
|
31
|
+
const meter = await openmeter.meters.get('m1')
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
## API
|
|
35
35
|
|
|
36
|
-
The OpenMeter SDK uses [openapi-typescript-codegen](https://www.npmjs.com/package/openapi-typescript-codegen) under the hood to generate the HTTP client.
|
|
37
|
-
|
|
38
36
|
### Events
|
|
39
37
|
|
|
40
|
-
####
|
|
38
|
+
#### ingest
|
|
41
39
|
|
|
42
40
|
```ts
|
|
43
41
|
import { type Event } from '@openmeter/sdk'
|
|
44
42
|
|
|
45
43
|
const event: Event = {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
44
|
+
specversion: '1.0',
|
|
45
|
+
id: 'id-1',
|
|
46
|
+
source: 'my-app',
|
|
47
|
+
type: 'my-type',
|
|
48
|
+
subject: 'my-awesome-user-id',
|
|
49
|
+
time: new Date(),
|
|
50
|
+
data: {
|
|
51
|
+
api_calls: 1,
|
|
52
|
+
},
|
|
55
53
|
}
|
|
56
|
-
await openmeter.events.
|
|
54
|
+
await openmeter.events.ingest(event)
|
|
57
55
|
```
|
|
58
56
|
|
|
59
57
|
### Meters
|
|
60
58
|
|
|
61
|
-
####
|
|
59
|
+
#### list
|
|
60
|
+
|
|
61
|
+
List meters.
|
|
62
62
|
|
|
63
63
|
```ts
|
|
64
|
-
const meters = await openmeter.meters.
|
|
64
|
+
const meters = await openmeter.meters.list()
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
####
|
|
67
|
+
#### get
|
|
68
|
+
|
|
69
|
+
Get one meter by slug.
|
|
68
70
|
|
|
69
71
|
```ts
|
|
70
|
-
const meter = await openmeter.meters.
|
|
72
|
+
const meter = await openmeter.meters.get('m1')
|
|
71
73
|
```
|
|
72
74
|
|
|
73
|
-
####
|
|
75
|
+
#### values
|
|
76
|
+
|
|
77
|
+
Get back meter values.
|
|
74
78
|
|
|
75
79
|
```ts
|
|
76
|
-
import {
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const values = await openmeter.meters.getMeterValues(
|
|
85
|
-
meterSlug,
|
|
86
|
-
namespace,
|
|
87
|
-
subject,
|
|
88
|
-
from,
|
|
89
|
-
to,
|
|
90
|
-
windowSize
|
|
91
|
-
)
|
|
80
|
+
import { WindowSize } from '@openmeter/sdk'
|
|
81
|
+
|
|
82
|
+
const values = await openmeter.meters.values('my-meter-slug', {
|
|
83
|
+
subject: 'user-1',
|
|
84
|
+
from: new Date('2021-01-01'),
|
|
85
|
+
to: new Date('2021-01-02'),
|
|
86
|
+
windowSize: WindowSize.HOUR
|
|
87
|
+
})
|
|
92
88
|
```
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { IncomingHttpHeaders } from 'http'
|
|
2
|
+
import { Dispatcher, request } from 'undici'
|
|
3
|
+
import { components } from '../schemas/openapi.js'
|
|
4
|
+
|
|
5
|
+
export type OpenMeterConfig = {
|
|
6
|
+
baseUrl: string
|
|
7
|
+
token?: string
|
|
8
|
+
username?: string
|
|
9
|
+
password?: string
|
|
10
|
+
headers?: IncomingHttpHeaders
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type RequestOptions = {
|
|
14
|
+
headers?: IncomingHttpHeaders
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type Problem = components['schemas']['Problem']
|
|
18
|
+
|
|
19
|
+
type UndiciRequestOptions = { dispatcher?: Dispatcher } & Omit<Dispatcher.RequestOptions, 'origin' | 'path' | 'method'> & Partial<Pick<Dispatcher.RequestOptions, 'method'>>
|
|
20
|
+
|
|
21
|
+
export class BaseClient {
|
|
22
|
+
protected config: OpenMeterConfig
|
|
23
|
+
|
|
24
|
+
constructor(config: OpenMeterConfig) {
|
|
25
|
+
this.config = config
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected async request<T>({
|
|
29
|
+
path,
|
|
30
|
+
method,
|
|
31
|
+
searchParams,
|
|
32
|
+
headers,
|
|
33
|
+
body,
|
|
34
|
+
options
|
|
35
|
+
}: {
|
|
36
|
+
path: string
|
|
37
|
+
method: Dispatcher.HttpMethod,
|
|
38
|
+
searchParams?: URLSearchParams,
|
|
39
|
+
headers?: IncomingHttpHeaders,
|
|
40
|
+
body?: string | Buffer | Uint8Array,
|
|
41
|
+
options?: RequestOptions
|
|
42
|
+
}): Promise<T> {
|
|
43
|
+
// Building URL
|
|
44
|
+
const url = this.getUrl(path, searchParams)
|
|
45
|
+
|
|
46
|
+
// Request options
|
|
47
|
+
const reqHeaders: IncomingHttpHeaders = {
|
|
48
|
+
Accept: 'application/json',
|
|
49
|
+
...headers,
|
|
50
|
+
...this.getAuthHeaders(),
|
|
51
|
+
...this.config.headers,
|
|
52
|
+
...options?.headers,
|
|
53
|
+
}
|
|
54
|
+
const reqOpts: UndiciRequestOptions = {
|
|
55
|
+
method,
|
|
56
|
+
headers: reqHeaders
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Optional body
|
|
60
|
+
if (body) {
|
|
61
|
+
if (!reqHeaders['Content-Type'] && !reqHeaders['content-type']) {
|
|
62
|
+
throw new Error('Content Type is required with body')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
reqOpts.body = body
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const resp = await request(url, reqOpts)
|
|
69
|
+
|
|
70
|
+
// Error handling
|
|
71
|
+
if (resp.statusCode > 399) {
|
|
72
|
+
if (resp.headers['content-type'] === 'application/problem+json') {
|
|
73
|
+
const problem = await resp.body.json() as Problem
|
|
74
|
+
throw new HttpError({
|
|
75
|
+
statusCode: resp.statusCode,
|
|
76
|
+
problem,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Requests can fail before API, in this case we only have a status code
|
|
81
|
+
throw new HttpError({
|
|
82
|
+
statusCode: resp.statusCode,
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Response parsing
|
|
87
|
+
if (resp.statusCode === 204) {
|
|
88
|
+
return undefined as unknown as T
|
|
89
|
+
}
|
|
90
|
+
if (resp.headers['content-type'] === 'application/json') {
|
|
91
|
+
return await resp.body.json() as T
|
|
92
|
+
}
|
|
93
|
+
if (!resp.headers['content-type']) {
|
|
94
|
+
throw new Error('Missing content type')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
throw new Error(`Unknown content type: ${resp.headers['content-type']}`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
protected getUrl(path: string, searchParams?: URLSearchParams) {
|
|
101
|
+
let qs = searchParams ? searchParams.toString() : ''
|
|
102
|
+
qs = qs.length > 0 ? `?${qs}` : ''
|
|
103
|
+
const url = new URL(`${path}${qs}`, this.config.baseUrl)
|
|
104
|
+
return url
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
protected getAuthHeaders(): IncomingHttpHeaders {
|
|
108
|
+
if (this.config.token) {
|
|
109
|
+
return {
|
|
110
|
+
authorization: `Bearer ${this.config.token} `,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (this.config.username && this.config.password) {
|
|
115
|
+
const encoded = Buffer.from(
|
|
116
|
+
`${this.config.username}:${this.config.password} `
|
|
117
|
+
).toString('base64')
|
|
118
|
+
return {
|
|
119
|
+
authorization: `Basic ${encoded} `,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
protected static toURLSearchParams(params: Record<string, string | number | Date | string[]>): URLSearchParams {
|
|
127
|
+
const searchParams = new URLSearchParams()
|
|
128
|
+
|
|
129
|
+
for (const [key, value] of Object.entries(params)) {
|
|
130
|
+
if (Array.isArray(value)) {
|
|
131
|
+
searchParams.append(key, value.join(','))
|
|
132
|
+
} else if (value instanceof Date) {
|
|
133
|
+
searchParams.append(key, value.toISOString())
|
|
134
|
+
} else {
|
|
135
|
+
searchParams.append(key, value.toString())
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return searchParams
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export class HttpError extends Error {
|
|
144
|
+
public statusCode: number
|
|
145
|
+
public problem?: Problem
|
|
146
|
+
|
|
147
|
+
constructor({ statusCode, problem }: { statusCode: number; problem?: Problem }) {
|
|
148
|
+
super(problem?.type || 'unexpected status code')
|
|
149
|
+
this.name = 'HttpError'
|
|
150
|
+
this.statusCode = statusCode
|
|
151
|
+
this.problem = problem
|
|
152
|
+
}
|
|
153
|
+
}
|
package/clients/event.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
import { components } from '../schemas/openapi.js'
|
|
3
|
+
import { RequestOptions, BaseClient, OpenMeterConfig } from './client.js'
|
|
4
|
+
|
|
5
|
+
// We export Event instead
|
|
6
|
+
type CloudEvents = components['schemas']['Event']
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Usage Event
|
|
10
|
+
*/
|
|
11
|
+
export type Event = {
|
|
12
|
+
/**
|
|
13
|
+
* @description The version of the CloudEvents specification which the event uses.
|
|
14
|
+
* @example 1.0
|
|
15
|
+
*/
|
|
16
|
+
specversion?: string
|
|
17
|
+
/**
|
|
18
|
+
* @description Unique identifier for the event, defaults to uuid v4.
|
|
19
|
+
* @example "5c10fade-1c9e-4d6c-8275-c52c36731d3c"
|
|
20
|
+
*/
|
|
21
|
+
id?: string
|
|
22
|
+
/**
|
|
23
|
+
* Format: uri-reference
|
|
24
|
+
* @description Identifies the context in which an event happened, defaults to: @openmeter/sdk
|
|
25
|
+
* @example services/service-0
|
|
26
|
+
*/
|
|
27
|
+
source?: string
|
|
28
|
+
/**
|
|
29
|
+
* @description Describes the type of event related to the originating occurrence.
|
|
30
|
+
* @example "api_request"
|
|
31
|
+
*/
|
|
32
|
+
type: string
|
|
33
|
+
/**
|
|
34
|
+
* @description Describes the subject of the event in the context of the event producer (identified by source).
|
|
35
|
+
* @example "customer_id"
|
|
36
|
+
*/
|
|
37
|
+
subject: string
|
|
38
|
+
/**
|
|
39
|
+
* Format: date-time
|
|
40
|
+
* @description Date of when the occurrence happened.
|
|
41
|
+
* @example new Date('2023-01-01T01:01:01.001Z')
|
|
42
|
+
*/
|
|
43
|
+
time?: Date
|
|
44
|
+
/**
|
|
45
|
+
* Format: uri
|
|
46
|
+
* @description Identifies the schema that data adheres to.
|
|
47
|
+
*/
|
|
48
|
+
dataschema?: string
|
|
49
|
+
/**
|
|
50
|
+
* @description Content type of the data value. Must adhere to RFC 2046 format.
|
|
51
|
+
* @example application/json
|
|
52
|
+
* @enum {string|null}
|
|
53
|
+
*/
|
|
54
|
+
datacontenttype?: 'application/json'
|
|
55
|
+
/**
|
|
56
|
+
* @description The event payload.
|
|
57
|
+
* @example {
|
|
58
|
+
* "duration_ms": "12",
|
|
59
|
+
* "path": "/hello"
|
|
60
|
+
* }
|
|
61
|
+
*/
|
|
62
|
+
data: Record<string, string | number | Record<string, string | number>>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class EventsClient extends BaseClient {
|
|
66
|
+
constructor(config: OpenMeterConfig) {
|
|
67
|
+
super(config)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Ingest usage event in a CloudEvents format
|
|
72
|
+
* @see https://cloudevents.io
|
|
73
|
+
*/
|
|
74
|
+
public async ingest(
|
|
75
|
+
usageEvent: Event,
|
|
76
|
+
options?: RequestOptions
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
if (usageEvent.datacontenttype && usageEvent.datacontenttype !== 'application/json') {
|
|
79
|
+
throw new TypeError(
|
|
80
|
+
`Unsupported datacontenttype: ${usageEvent.datacontenttype}`
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// We default where we can to lower the barrier to use CloudEvents
|
|
85
|
+
const body: CloudEvents = {
|
|
86
|
+
specversion: usageEvent.specversion ?? '1.0',
|
|
87
|
+
id: usageEvent.id ?? crypto.randomUUID(),
|
|
88
|
+
source: usageEvent.source ?? '@openmeter/sdk',
|
|
89
|
+
type: usageEvent.type,
|
|
90
|
+
subject: usageEvent.subject,
|
|
91
|
+
time: usageEvent.time?.toISOString(),
|
|
92
|
+
datacontenttype: usageEvent.datacontenttype,
|
|
93
|
+
dataschema: usageEvent.dataschema,
|
|
94
|
+
data: usageEvent.data
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Making Request
|
|
98
|
+
return await this.request({
|
|
99
|
+
path: '/api/v1/events',
|
|
100
|
+
method: 'POST',
|
|
101
|
+
body: JSON.stringify(body),
|
|
102
|
+
headers: {
|
|
103
|
+
'Content-Type': 'application/cloudevents+json',
|
|
104
|
+
},
|
|
105
|
+
options
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
package/clients/meter.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { paths, components } from '../schemas/openapi.js'
|
|
2
|
+
import { BaseClient, OpenMeterConfig, RequestOptions } from './client.js'
|
|
3
|
+
|
|
4
|
+
export enum WindowSize {
|
|
5
|
+
MINUTE = 'MINUTE',
|
|
6
|
+
HOUR = 'HOUR',
|
|
7
|
+
DAY = 'DAY'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export enum MeterAggregation {
|
|
11
|
+
SUM = 'SUM',
|
|
12
|
+
COUNT = 'COUNT',
|
|
13
|
+
AVG = 'AVG',
|
|
14
|
+
MIN = 'MIN',
|
|
15
|
+
MAX = 'MAX',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type MeterQueryParams = {
|
|
19
|
+
subject?: string
|
|
20
|
+
/**
|
|
21
|
+
* @description Start date.
|
|
22
|
+
* Must be aligned with the window size.
|
|
23
|
+
* Inclusive.
|
|
24
|
+
*/
|
|
25
|
+
from?: Date
|
|
26
|
+
/**
|
|
27
|
+
* @description End date.
|
|
28
|
+
* Must be aligned with the window size.
|
|
29
|
+
* Inclusive.
|
|
30
|
+
*/
|
|
31
|
+
to?: Date
|
|
32
|
+
/** @description If not specified, a single usage aggregate will be returned for the entirety of the specified period for each subject and group. */
|
|
33
|
+
windowSize?: WindowSizeType
|
|
34
|
+
/** @description If not specified a single aggregate will be returned for each subject and time window. */
|
|
35
|
+
groupBy?: string[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type MeterQueryResponse = paths['/api/v1/meters/{meterIdOrSlug}/values']['get']['responses']['200']['content']['application/json']
|
|
39
|
+
|
|
40
|
+
export type MeterValue = components['schemas']['MeterValue']
|
|
41
|
+
export type Meter = components['schemas']['Meter']
|
|
42
|
+
export type WindowSizeType = components['schemas']['WindowSize']
|
|
43
|
+
|
|
44
|
+
export class MetersClient extends BaseClient {
|
|
45
|
+
constructor(config: OpenMeterConfig) {
|
|
46
|
+
super(config)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get one meter by slug
|
|
51
|
+
*/
|
|
52
|
+
public async get(slug: string, options?: RequestOptions): Promise<Meter> {
|
|
53
|
+
return this.request<Meter>({
|
|
54
|
+
method: 'GET',
|
|
55
|
+
path: `/api/v1/meters/${slug}`,
|
|
56
|
+
options,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* List meters
|
|
62
|
+
*/
|
|
63
|
+
public async list(options?: RequestOptions): Promise<Meter[]> {
|
|
64
|
+
return this.request<Meter[]>({
|
|
65
|
+
method: 'GET',
|
|
66
|
+
path: `/api/v1/meters`,
|
|
67
|
+
options,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get aggregated values of a meter
|
|
73
|
+
*/
|
|
74
|
+
public async values(slug: string, params?: MeterQueryParams, options?: RequestOptions): Promise<MeterQueryResponse> {
|
|
75
|
+
const searchParams = params ? BaseClient.toURLSearchParams(params) : undefined
|
|
76
|
+
return this.request<MeterQueryResponse>({
|
|
77
|
+
method: 'GET',
|
|
78
|
+
path: `/api/v1/meters/${slug}/values`,
|
|
79
|
+
searchParams,
|
|
80
|
+
options,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
/// <reference types="node" />
|
|
4
|
+
import { IncomingHttpHeaders } from 'http';
|
|
5
|
+
import { Dispatcher } from 'undici';
|
|
6
|
+
import { components } from '../schemas/openapi.js';
|
|
7
|
+
export type OpenMeterConfig = {
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
token?: string;
|
|
10
|
+
username?: string;
|
|
11
|
+
password?: string;
|
|
12
|
+
headers?: IncomingHttpHeaders;
|
|
13
|
+
};
|
|
14
|
+
export type RequestOptions = {
|
|
15
|
+
headers?: IncomingHttpHeaders;
|
|
16
|
+
};
|
|
17
|
+
export type Problem = components['schemas']['Problem'];
|
|
18
|
+
export declare class BaseClient {
|
|
19
|
+
protected config: OpenMeterConfig;
|
|
20
|
+
constructor(config: OpenMeterConfig);
|
|
21
|
+
protected request<T>({ path, method, searchParams, headers, body, options }: {
|
|
22
|
+
path: string;
|
|
23
|
+
method: Dispatcher.HttpMethod;
|
|
24
|
+
searchParams?: URLSearchParams;
|
|
25
|
+
headers?: IncomingHttpHeaders;
|
|
26
|
+
body?: string | Buffer | Uint8Array;
|
|
27
|
+
options?: RequestOptions;
|
|
28
|
+
}): Promise<T>;
|
|
29
|
+
protected getUrl(path: string, searchParams?: URLSearchParams): import("url").URL;
|
|
30
|
+
protected getAuthHeaders(): IncomingHttpHeaders;
|
|
31
|
+
protected static toURLSearchParams(params: Record<string, string | number | Date | string[]>): URLSearchParams;
|
|
32
|
+
}
|
|
33
|
+
export declare class HttpError extends Error {
|
|
34
|
+
statusCode: number;
|
|
35
|
+
problem?: Problem;
|
|
36
|
+
constructor({ statusCode, problem }: {
|
|
37
|
+
statusCode: number;
|
|
38
|
+
problem?: Problem;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { request } from 'undici';
|
|
2
|
+
export class BaseClient {
|
|
3
|
+
config;
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.config = config;
|
|
6
|
+
}
|
|
7
|
+
async request({ path, method, searchParams, headers, body, options }) {
|
|
8
|
+
// Building URL
|
|
9
|
+
const url = this.getUrl(path, searchParams);
|
|
10
|
+
// Request options
|
|
11
|
+
const reqHeaders = {
|
|
12
|
+
Accept: 'application/json',
|
|
13
|
+
...headers,
|
|
14
|
+
...this.getAuthHeaders(),
|
|
15
|
+
...this.config.headers,
|
|
16
|
+
...options?.headers,
|
|
17
|
+
};
|
|
18
|
+
const reqOpts = {
|
|
19
|
+
method,
|
|
20
|
+
headers: reqHeaders
|
|
21
|
+
};
|
|
22
|
+
// Optional body
|
|
23
|
+
if (body) {
|
|
24
|
+
if (!reqHeaders['Content-Type'] && !reqHeaders['content-type']) {
|
|
25
|
+
throw new Error('Content Type is required with body');
|
|
26
|
+
}
|
|
27
|
+
reqOpts.body = body;
|
|
28
|
+
}
|
|
29
|
+
const resp = await request(url, reqOpts);
|
|
30
|
+
// Error handling
|
|
31
|
+
if (resp.statusCode > 399) {
|
|
32
|
+
if (resp.headers['content-type'] === 'application/problem+json') {
|
|
33
|
+
const problem = await resp.body.json();
|
|
34
|
+
throw new HttpError({
|
|
35
|
+
statusCode: resp.statusCode,
|
|
36
|
+
problem,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
// Requests can fail before API, in this case we only have a status code
|
|
40
|
+
throw new HttpError({
|
|
41
|
+
statusCode: resp.statusCode,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// Response parsing
|
|
45
|
+
if (resp.statusCode === 204) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
if (resp.headers['content-type'] === 'application/json') {
|
|
49
|
+
return await resp.body.json();
|
|
50
|
+
}
|
|
51
|
+
if (!resp.headers['content-type']) {
|
|
52
|
+
throw new Error('Missing content type');
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`Unknown content type: ${resp.headers['content-type']}`);
|
|
55
|
+
}
|
|
56
|
+
getUrl(path, searchParams) {
|
|
57
|
+
let qs = searchParams ? searchParams.toString() : '';
|
|
58
|
+
qs = qs.length > 0 ? `?${qs}` : '';
|
|
59
|
+
const url = new URL(`${path}${qs}`, this.config.baseUrl);
|
|
60
|
+
return url;
|
|
61
|
+
}
|
|
62
|
+
getAuthHeaders() {
|
|
63
|
+
if (this.config.token) {
|
|
64
|
+
return {
|
|
65
|
+
authorization: `Bearer ${this.config.token} `,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (this.config.username && this.config.password) {
|
|
69
|
+
const encoded = Buffer.from(`${this.config.username}:${this.config.password} `).toString('base64');
|
|
70
|
+
return {
|
|
71
|
+
authorization: `Basic ${encoded} `,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
static toURLSearchParams(params) {
|
|
77
|
+
const searchParams = new URLSearchParams();
|
|
78
|
+
for (const [key, value] of Object.entries(params)) {
|
|
79
|
+
if (Array.isArray(value)) {
|
|
80
|
+
searchParams.append(key, value.join(','));
|
|
81
|
+
}
|
|
82
|
+
else if (value instanceof Date) {
|
|
83
|
+
searchParams.append(key, value.toISOString());
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
searchParams.append(key, value.toString());
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return searchParams;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export class HttpError extends Error {
|
|
93
|
+
statusCode;
|
|
94
|
+
problem;
|
|
95
|
+
constructor({ statusCode, problem }) {
|
|
96
|
+
super(problem?.type || 'unexpected status code');
|
|
97
|
+
this.name = 'HttpError';
|
|
98
|
+
this.statusCode = statusCode;
|
|
99
|
+
this.problem = problem;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { RequestOptions, BaseClient, OpenMeterConfig } from './client.js';
|
|
2
|
+
/**
|
|
3
|
+
* Usage Event
|
|
4
|
+
*/
|
|
5
|
+
export type Event = {
|
|
6
|
+
/**
|
|
7
|
+
* @description The version of the CloudEvents specification which the event uses.
|
|
8
|
+
* @example 1.0
|
|
9
|
+
*/
|
|
10
|
+
specversion?: string;
|
|
11
|
+
/**
|
|
12
|
+
* @description Unique identifier for the event, defaults to uuid v4.
|
|
13
|
+
* @example "5c10fade-1c9e-4d6c-8275-c52c36731d3c"
|
|
14
|
+
*/
|
|
15
|
+
id?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Format: uri-reference
|
|
18
|
+
* @description Identifies the context in which an event happened, defaults to: @openmeter/sdk
|
|
19
|
+
* @example services/service-0
|
|
20
|
+
*/
|
|
21
|
+
source?: string;
|
|
22
|
+
/**
|
|
23
|
+
* @description Describes the type of event related to the originating occurrence.
|
|
24
|
+
* @example "api_request"
|
|
25
|
+
*/
|
|
26
|
+
type: string;
|
|
27
|
+
/**
|
|
28
|
+
* @description Describes the subject of the event in the context of the event producer (identified by source).
|
|
29
|
+
* @example "customer_id"
|
|
30
|
+
*/
|
|
31
|
+
subject: string;
|
|
32
|
+
/**
|
|
33
|
+
* Format: date-time
|
|
34
|
+
* @description Date of when the occurrence happened.
|
|
35
|
+
* @example new Date('2023-01-01T01:01:01.001Z')
|
|
36
|
+
*/
|
|
37
|
+
time?: Date;
|
|
38
|
+
/**
|
|
39
|
+
* Format: uri
|
|
40
|
+
* @description Identifies the schema that data adheres to.
|
|
41
|
+
*/
|
|
42
|
+
dataschema?: string;
|
|
43
|
+
/**
|
|
44
|
+
* @description Content type of the data value. Must adhere to RFC 2046 format.
|
|
45
|
+
* @example application/json
|
|
46
|
+
* @enum {string|null}
|
|
47
|
+
*/
|
|
48
|
+
datacontenttype?: 'application/json';
|
|
49
|
+
/**
|
|
50
|
+
* @description The event payload.
|
|
51
|
+
* @example {
|
|
52
|
+
* "duration_ms": "12",
|
|
53
|
+
* "path": "/hello"
|
|
54
|
+
* }
|
|
55
|
+
*/
|
|
56
|
+
data: Record<string, string | number | Record<string, string | number>>;
|
|
57
|
+
};
|
|
58
|
+
export declare class EventsClient extends BaseClient {
|
|
59
|
+
constructor(config: OpenMeterConfig);
|
|
60
|
+
/**
|
|
61
|
+
* Ingest usage event in a CloudEvents format
|
|
62
|
+
* @see https://cloudevents.io
|
|
63
|
+
*/
|
|
64
|
+
ingest(usageEvent: Event, options?: RequestOptions): Promise<void>;
|
|
65
|
+
}
|