Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .golangci.yaml +68 -0
- .goreleaser.yaml +45 -0
- .grype/config.yaml +5 -0
- .hfignore +40 -0
- .mcp.json +9 -0
- .nvmrc +1 -0
- .semgrepignore +9 -0
- .spectral.yaml +9 -0
- .syft.yaml +7 -0
- .vscode/default.code-workspace +12 -0
- .vscode/launch.json +102 -0
- .vscode/settings.json +32 -0
- AGENTS.md +323 -0
- Agent.md +148 -0
- CLAUDE.md +3 -0
- CODE_OF_CONDUCT.md +68 -0
- CONTRIBUTING.md +53 -0
- Dockerfile +98 -0
- LICENSE +202 -0
- Makefile +356 -0
- README.md +158 -5
- SECURITY.md +29 -0
- api/api.gen.go +0 -0
- api/api.go +2 -0
- api/client/go/README.md +65 -0
- api/client/go/client.gen.go +0 -0
- api/client/go/client.go +48 -0
- api/client/go/client_test.go +327 -0
- api/client/go/codegen.yaml +11 -0
- api/client/go/error.go +14 -0
- api/client/javascript/.gitignore +23 -0
- api/client/javascript/.npmignore +27 -0
- api/client/javascript/.npmrc +1 -0
- api/client/javascript/Makefile +42 -0
- api/client/javascript/README.md +267 -0
- api/client/javascript/biome.json +67 -0
- api/client/javascript/index.ts +1 -0
- api/client/javascript/orval.config.ts +38 -0
- api/client/javascript/package.json +102 -0
- api/client/javascript/patches/openapi-typescript.patch +0 -0
- api/client/javascript/pnpm-lock.yaml +0 -0
- api/client/javascript/pnpm-workspace.yaml +10 -0
- api/client/javascript/scripts/add-as-const.ts +16 -0
- api/client/javascript/scripts/generate.ts +51 -0
- api/client/javascript/src/client/addons.ts +122 -0
- api/client/javascript/src/client/apps.ts +335 -0
- api/client/javascript/src/client/billing.ts +525 -0
- api/client/javascript/src/client/common.ts +66 -0
- api/client/javascript/src/client/customers.ts +672 -0
- api/client/javascript/src/client/debug.ts +25 -0
.golangci.yaml
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# yaml-language-server: $schema=https://golangci-lint.run/jsonschema/golangci.jsonschema.json
|
| 2 |
+
version: "2"
|
| 3 |
+
linters:
|
| 4 |
+
default: none
|
| 5 |
+
enable:
|
| 6 |
+
- bodyclose
|
| 7 |
+
- errcheck
|
| 8 |
+
- govet
|
| 9 |
+
- ineffassign
|
| 10 |
+
- misspell
|
| 11 |
+
- nolintlint
|
| 12 |
+
- sloglint
|
| 13 |
+
- staticcheck
|
| 14 |
+
- unconvert
|
| 15 |
+
# FIXME: re-enable following linters
|
| 16 |
+
#- unparam
|
| 17 |
+
#- unused
|
| 18 |
+
- whitespace
|
| 19 |
+
#- wsl
|
| 20 |
+
settings:
|
| 21 |
+
misspell:
|
| 22 |
+
locale: US
|
| 23 |
+
nolintlint:
|
| 24 |
+
require-specific: true
|
| 25 |
+
revive:
|
| 26 |
+
confidence: 0
|
| 27 |
+
staticcheck:
|
| 28 |
+
checks:
|
| 29 |
+
- "all"
|
| 30 |
+
- "-QF1008"
|
| 31 |
+
- "-SA1019" # https://staticcheck.dev/docs/checks/#SA1019
|
| 32 |
+
# FIXME: re-enable the following checks
|
| 33 |
+
- "-ST1003"
|
| 34 |
+
- "-ST1016"
|
| 35 |
+
exclusions:
|
| 36 |
+
generated: lax
|
| 37 |
+
presets:
|
| 38 |
+
- comments
|
| 39 |
+
- common-false-positives
|
| 40 |
+
- legacy
|
| 41 |
+
- std-error-handling
|
| 42 |
+
paths:
|
| 43 |
+
- collector/benthos/internal
|
| 44 |
+
- third_party$
|
| 45 |
+
- builtin$
|
| 46 |
+
- examples$
|
| 47 |
+
formatters:
|
| 48 |
+
enable:
|
| 49 |
+
- gci
|
| 50 |
+
- gofmt
|
| 51 |
+
- gofumpt
|
| 52 |
+
- goimports
|
| 53 |
+
settings:
|
| 54 |
+
gci:
|
| 55 |
+
sections:
|
| 56 |
+
- standard
|
| 57 |
+
- default
|
| 58 |
+
- prefix(github.com/openmeterio/openmeter)
|
| 59 |
+
goimports:
|
| 60 |
+
local-prefixes:
|
| 61 |
+
- github.com/openmeterio/openmeter
|
| 62 |
+
exclusions:
|
| 63 |
+
generated: lax
|
| 64 |
+
paths:
|
| 65 |
+
- collector/benthos/internal
|
| 66 |
+
- third_party$
|
| 67 |
+
- builtin$
|
| 68 |
+
- examples$
|
.goreleaser.yaml
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This is an example .goreleaser.yml file with some sensible defaults.
|
| 2 |
+
# Make sure to check the documentation at https://goreleaser.com
|
| 3 |
+
project_name: openmeter
|
| 4 |
+
|
| 5 |
+
dist: build/dist
|
| 6 |
+
|
| 7 |
+
gomod:
|
| 8 |
+
proxy: true
|
| 9 |
+
|
| 10 |
+
before:
|
| 11 |
+
hooks:
|
| 12 |
+
- go mod tidy
|
| 13 |
+
builds:
|
| 14 |
+
- env:
|
| 15 |
+
- CGO_ENABLED=0
|
| 16 |
+
main: .
|
| 17 |
+
goos:
|
| 18 |
+
- linux
|
| 19 |
+
- windows
|
| 20 |
+
- darwin
|
| 21 |
+
|
| 22 |
+
archives:
|
| 23 |
+
- format: tar.gz
|
| 24 |
+
# this name template makes the OS and Arch compatible with the results of uname.
|
| 25 |
+
name_template: >-
|
| 26 |
+
{{ .ProjectName }}_
|
| 27 |
+
{{- title .Os }}_
|
| 28 |
+
{{- if eq .Arch "amd64" }}x86_64
|
| 29 |
+
{{- else if eq .Arch "386" }}i386
|
| 30 |
+
{{- else }}{{ .Arch }}{{ end }}
|
| 31 |
+
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
| 32 |
+
# use zip for windows archives
|
| 33 |
+
format_overrides:
|
| 34 |
+
- goos: windows
|
| 35 |
+
format: zip
|
| 36 |
+
checksum:
|
| 37 |
+
name_template: "checksums.txt"
|
| 38 |
+
snapshot:
|
| 39 |
+
name_template: "{{ incpatch .Version }}-next"
|
| 40 |
+
changelog:
|
| 41 |
+
sort: asc
|
| 42 |
+
filters:
|
| 43 |
+
exclude:
|
| 44 |
+
- "^docs:"
|
| 45 |
+
- "^test:"
|
.grype/config.yaml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
ignore:
|
| 3 |
+
# Nats server admin api authorization bypass: https://github.com/advisories/ghsa-fhg8-qxh5-7q3w
|
| 4 |
+
# This is not an issue, as we never start a nats server in our codebase
|
| 5 |
+
- vulnerability: GHSA-fhg8-qxh5-7q3w
|
.hfignore
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.git/
|
| 2 |
+
.devenv/
|
| 3 |
+
.direnv/
|
| 4 |
+
.env.local
|
| 5 |
+
.pre-commit-config.yaml
|
| 6 |
+
build/
|
| 7 |
+
config.yaml
|
| 8 |
+
tmp/
|
| 9 |
+
.tmp.*
|
| 10 |
+
.DS_Store
|
| 11 |
+
go.work
|
| 12 |
+
go.work.sum
|
| 13 |
+
__debug_bin*
|
| 14 |
+
openmeter.log
|
| 15 |
+
.gocache
|
| 16 |
+
.gomodcache
|
| 17 |
+
.claude/
|
| 18 |
+
CLAUDE.local.md
|
| 19 |
+
api/v3/templates/chi-middleware.tmpl
|
| 20 |
+
.codegraph/
|
| 21 |
+
node_modules/
|
| 22 |
+
dist/
|
| 23 |
+
.agents/
|
| 24 |
+
.vscode/
|
| 25 |
+
.github/
|
| 26 |
+
.spectral.yaml
|
| 27 |
+
.syft.yaml
|
| 28 |
+
.fossa.yaml
|
| 29 |
+
.editorconfig
|
| 30 |
+
.coderabbit.yaml
|
| 31 |
+
.golangci.yaml
|
| 32 |
+
.golangci-fast.yaml
|
| 33 |
+
.goreleaser.yaml
|
| 34 |
+
.mcp.json
|
| 35 |
+
.semgrepignore
|
| 36 |
+
atlas.hcl
|
| 37 |
+
flake.lock
|
| 38 |
+
flake.nix
|
| 39 |
+
justfile
|
| 40 |
+
Makefile
|
.mcp.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"mcpServers": {
|
| 3 |
+
"codegraph": {
|
| 4 |
+
"type": "stdio",
|
| 5 |
+
"command": "codegraph",
|
| 6 |
+
"args": ["serve", "--mcp"]
|
| 7 |
+
}
|
| 8 |
+
}
|
| 9 |
+
}
|
.nvmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
v26.4.0
|
.semgrepignore
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
api/client/python/src/openmeter/_serialization.py
|
| 2 |
+
**/*_test.go
|
| 3 |
+
|
| 4 |
+
# These are only included due to bugous //nosemgrep support in current version, please remove them once the semgrep has been upgraded
|
| 5 |
+
|
| 6 |
+
openmeter/billing/service/seq.go
|
| 7 |
+
collector/benthos/input/otel_log.go
|
| 8 |
+
openmeter/productcatalog/feature/connector.go
|
| 9 |
+
openmeter/watermill/driver/kafka/broker.go
|
.spectral.yaml
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
extends: [[spectral:oas, recommended]]
|
| 2 |
+
rules:
|
| 3 |
+
info-contact: off
|
| 4 |
+
|
| 5 |
+
# Disabled due to TypeSpec conversion
|
| 6 |
+
oas3-valid-schema-example: off
|
| 7 |
+
no-$ref-siblings: off
|
| 8 |
+
path-params: off
|
| 9 |
+
oas3-unused-component: off
|
.syft.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
select-catalogers:
|
| 2 |
+
- -go-module-binary-cataloger
|
| 3 |
+
- -github-action-workflow-usage-cataloger
|
| 4 |
+
- -github-actions-usage-cataloger
|
| 5 |
+
|
| 6 |
+
exclude:
|
| 7 |
+
- "./.devenv"
|
.vscode/default.code-workspace
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"folders": [
|
| 3 |
+
{
|
| 4 |
+
"name": "root",
|
| 5 |
+
"path": "../"
|
| 6 |
+
},
|
| 7 |
+
{
|
| 8 |
+
"name": "typespec",
|
| 9 |
+
"path": "../api/spec"
|
| 10 |
+
}
|
| 11 |
+
]
|
| 12 |
+
}
|
.vscode/launch.json
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
// Use IntelliSense to learn about possible attributes.
|
| 3 |
+
// Hover to view descriptions of existing attributes.
|
| 4 |
+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
| 5 |
+
"version": "0.2.0",
|
| 6 |
+
"configurations": [
|
| 7 |
+
{
|
| 8 |
+
"name": "Launch Server",
|
| 9 |
+
"type": "go",
|
| 10 |
+
"request": "launch",
|
| 11 |
+
"mode": "auto",
|
| 12 |
+
"program": "${workspaceFolder}/cmd/server",
|
| 13 |
+
"args": [
|
| 14 |
+
"--config",
|
| 15 |
+
"${workspaceFolder}/config.yaml",
|
| 16 |
+
"--stripe-disable-webhook-registration",
|
| 17 |
+
]
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"name": "Launch Sink-Worker",
|
| 21 |
+
"type": "go",
|
| 22 |
+
"request": "launch",
|
| 23 |
+
"mode": "auto",
|
| 24 |
+
"program": "${workspaceFolder}/cmd/sink-worker",
|
| 25 |
+
"args": [
|
| 26 |
+
"--config",
|
| 27 |
+
"${workspaceFolder}/config.yaml",
|
| 28 |
+
// Let's prevent port collision with server
|
| 29 |
+
"--telemetry-address",
|
| 30 |
+
":10001",
|
| 31 |
+
]
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"name": "Launch Balance Worker",
|
| 35 |
+
"type": "go",
|
| 36 |
+
"request": "launch",
|
| 37 |
+
"mode": "auto",
|
| 38 |
+
"program": "${workspaceFolder}/cmd/balance-worker",
|
| 39 |
+
"args": [
|
| 40 |
+
"--config",
|
| 41 |
+
"${workspaceFolder}/config.yaml",
|
| 42 |
+
"--telemetry-address",
|
| 43 |
+
":10002"
|
| 44 |
+
]
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"name": "Launch Notification Service",
|
| 48 |
+
"type": "go",
|
| 49 |
+
"request": "launch",
|
| 50 |
+
"mode": "auto",
|
| 51 |
+
"program": "${workspaceFolder}/cmd/notification-service",
|
| 52 |
+
"args": [
|
| 53 |
+
"--config",
|
| 54 |
+
"${workspaceFolder}/config.yaml",
|
| 55 |
+
"--telemetry-address",
|
| 56 |
+
":10003"
|
| 57 |
+
]
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"name": "Launch recalculate entitlement snapshots",
|
| 61 |
+
"type": "go",
|
| 62 |
+
"request": "launch",
|
| 63 |
+
"mode": "auto",
|
| 64 |
+
"program": "${workspaceFolder}/cmd/jobs",
|
| 65 |
+
"args": [
|
| 66 |
+
"--config",
|
| 67 |
+
"${workspaceFolder}/config.yaml",
|
| 68 |
+
"entitlement",
|
| 69 |
+
"recalculate-balance-snapshots"
|
| 70 |
+
]
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"name": "Launch billing-worker Service",
|
| 74 |
+
"type": "go",
|
| 75 |
+
"request": "launch",
|
| 76 |
+
"mode": "auto",
|
| 77 |
+
"program": "${workspaceFolder}/cmd/billing-worker",
|
| 78 |
+
"args": [
|
| 79 |
+
"--config",
|
| 80 |
+
"${workspaceFolder}/config.yaml",
|
| 81 |
+
"--telemetry-address",
|
| 82 |
+
":10004",
|
| 83 |
+
// force foreground billing advancement strategy, or billing-worker will not start (this way the config can be set to queued to validate that in local dev mode)
|
| 84 |
+
"--billing-advancement-strategy=foreground",
|
| 85 |
+
]
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"name": "Launch periodic jobs",
|
| 89 |
+
"type": "go",
|
| 90 |
+
"request": "launch",
|
| 91 |
+
"mode": "auto",
|
| 92 |
+
"program": "${workspaceFolder}/cmd/jobs",
|
| 93 |
+
"args": [
|
| 94 |
+
"--config",
|
| 95 |
+
"${workspaceFolder}/config.yaml",
|
| 96 |
+
"billing",
|
| 97 |
+
"subscriptionsync",
|
| 98 |
+
"list"
|
| 99 |
+
]
|
| 100 |
+
},
|
| 101 |
+
]
|
| 102 |
+
}
|
.vscode/settings.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"[helm]": {
|
| 3 |
+
"editor.formatOnSave": false
|
| 4 |
+
},
|
| 5 |
+
"go.testEnvVars": {
|
| 6 |
+
"TZ": "UTC",
|
| 7 |
+
"POSTGRES_HOST": "127.0.0.1",
|
| 8 |
+
"OPENMETER_ADDRESS": "http://127.0.0.1:38888",
|
| 9 |
+
// Useful when the schema is evolving during development (currently only supported
|
| 10 |
+
// by billing)
|
| 11 |
+
// "TEST_DISABLE_ATLAS": "true"
|
| 12 |
+
},
|
| 13 |
+
"gopls": {
|
| 14 |
+
"formatting.gofumpt": true
|
| 15 |
+
},
|
| 16 |
+
"go.lintTool": "golangci-lint",
|
| 17 |
+
"go.lintFlags": [
|
| 18 |
+
"--fast",
|
| 19 |
+
"--fix",
|
| 20 |
+
"-c",
|
| 21 |
+
".golangci.yaml"
|
| 22 |
+
],
|
| 23 |
+
"files.exclude": {
|
| 24 |
+
"**/node_modules": false
|
| 25 |
+
},
|
| 26 |
+
"typespec.tsp-server.path": "${workspaceFolder}/api/spec/node_modules/@typespec/compiler",
|
| 27 |
+
// dynamic forces confluent-kafka-go to build against local librdkafka
|
| 28 |
+
// wireinject is kept for gopls so it can analyse wire.go injection files
|
| 29 |
+
"go.buildTags": "wireinject,dynamic",
|
| 30 |
+
// Tests must NOT include wireinject — it causes wire stubs to run instead of wire_gen.go
|
| 31 |
+
"go.testTags": "dynamic"
|
| 32 |
+
}
|
AGENTS.md
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenMeter
|
| 2 |
+
|
| 3 |
+
OpenMeter is a usage metering and billing platform for AI and DevTool companies, built in Go.
|
| 4 |
+
|
| 5 |
+
## Quick Reference
|
| 6 |
+
|
| 7 |
+
Use the `Makefile` for all common tasks. A `justfile` also exists but is seldom used.
|
| 8 |
+
OpenMeter is a metering and billing platform with usage based pricing and access control.
|
| 9 |
+
|
| 10 |
+
## Tips for working with the codebase
|
| 11 |
+
|
| 12 |
+
If during your work anything confuses you or something isn't trivial for you, please augment AGENTS.md with your findings so next time it will be easier for you. AGENTS.md files are for you to edit and update as you go so you can interact with the codebase the most effectively.
|
| 13 |
+
|
| 14 |
+
Development commands are run via `Makefile`, it contains all commonly used commands during development. A `justfile` is also present but seldom used. Use the Makefile commands for common tasks like running tests, generating code, linting, etc.
|
| 15 |
+
The committed `.nvmrc` is the GitHub Actions source of truth for Node-based jobs on GitHub-hosted runners. Keep it aligned with the Nix `.#ci` shell's `node -v`; `flake.nix` refreshes it in `enterShell`, and CI validates the file against the Nix shell before running builds.
|
| 16 |
+
|
| 17 |
+
## AGENTS.md maintenance
|
| 18 |
+
|
| 19 |
+
- Treat this file as long-lived project guidance for all agents and contributors.
|
| 20 |
+
- Treat AGENTS.md as the repo-local source of truth. Do not bypass coding style, workflow, testing, or documentation guidance in this file. If a requested change appears to conflict with AGENTS.md, ask the human developer/reviewer to confirm the exception before proceeding, and make the exception explicit in the handoff.
|
| 21 |
+
- Prefer durable wording over time-based wording (avoid labels like "recent", "latest", "today").
|
| 22 |
+
- Keep entries actionable and specific (what to do, where, and why), not conversational history.
|
| 23 |
+
- Capture universal truths and cross-cutting coding conventions here when they become repeated practice or reviewer expectation. Do not leave them only in chat or pull request comments.
|
| 24 |
+
- Capture subsystem-specific guidance in the closest applicable nested `AGENTS.md` when the guidance should always apply to a subtree, such as `api/spec/AGENTS.md` for TypeSpec and SDK guidance. Use skills for reusable workflows or domain procedures that agents opt into for a task. Skills must stay usable by both Claude and Codex: write them as plain repo guidance, keep `.agents/skills` as the source of truth, and avoid assistant-specific assumptions unless a workflow truly requires them.
|
| 25 |
+
- When adding new guidance, fold it into the most relevant section and remove/merge stale or duplicate notes.
|
| 26 |
+
|
| 27 |
+
## Testing
|
| 28 |
+
|
| 29 |
+
| Task | Command |
|
| 30 |
+
|------|---------|
|
| 31 |
+
| Start dependencies | `make up` |
|
| 32 |
+
| Stop dependencies | `make down` |
|
| 33 |
+
| Run API server (hot reload) | `make server` |
|
| 34 |
+
| Run all tests | `make test` (root module only; excludes `e2e/`, its own module) |
|
| 35 |
+
| Run e2e tests | `make etoe` |
|
| 36 |
+
| Generate all code | `make generate-all` |
|
| 37 |
+
| Generate Go code only | `make generate` (runs `go generate ./...`) |
|
| 38 |
+
| Generate API + SDKs | `make gen-api` |
|
| 39 |
+
| Lint all | `make lint` |
|
| 40 |
+
| Lint Go only | `make lint-go` |
|
| 41 |
+
| Format code | `make fmt` |
|
| 42 |
+
| Tidy modules | `make mod` (root + `collector` + `e2e`) |
|
| 43 |
+
| Build all binaries | `make build` |
|
| 44 |
+
|
| 45 |
+
## Architecture
|
| 46 |
+
|
| 47 |
+
**Entry points:** `cmd/server`, `cmd/billing-worker`, `cmd/balance-worker`, `cmd/sink-worker`, `cmd/notification-service`, `cmd/jobs`
|
| 48 |
+
|
| 49 |
+
Core business logic is in `openmeter/`, shared utilities in `pkg/`, API layer in `api/`.
|
| 50 |
+
|
| 51 |
+
**Stack:** Go + PostgreSQL (Ent ORM) + Kafka + ClickHouse. API defined in TypeSpec, generated to OpenAPI.
|
| 52 |
+
|
| 53 |
+
Domain packages under `openmeter/` follow a layered service/adapter pattern. See the `/service` skill for full details.
|
| 54 |
+
|
| 55 |
+
`cmd/server/main.go` now migrates the database before creating the default namespace. Register namespace handlers before `initNamespace(...)` if they must provision the default namespace during startup.
|
| 56 |
+
|
| 57 |
+
**Module layout:** the repo is three separate Go modules. The root module (`github.com/openmeterio/openmeter`) holds all production code (`cmd/`, `openmeter/`, `pkg/`, etc.). `api/v3/client` is the standalone, publishable v3 Go SDK module. `e2e/` is a third, never-published, test-only module that imports both — it pins itself to the working tree of each via `replace github.com/openmeterio/openmeter => ../` and `replace .../api/v3/client => ../api/v3/client`, so e2e always tests local code regardless of what's tagged. The root module must never `require` the SDK module: a `require` on an untagged nested module resolves to an unresolvable `v0.0.0` for anyone outside this repo (the `replace` directive that makes it resolve locally is invisible downstream), so any code that needs the SDK — today, only `e2e/` — has to live in its own module rather than the root one. Because of this, root `go build ./...` / `go test ./...` / `go vet ./...` no longer see `e2e/` at all; use `make etoe` (runs it against a live server) or `go test -C e2e ./...` / `go vet -C e2e ./...` (compiles it standalone, no server needed) instead. `make lint-go` and `make mod` already cover all three modules. For editor/gopls support across all three, run `go work init . ./api/v3/client ./e2e` locally — `go.work`/`go.work.sum` are gitignored and must never be committed.
|
| 58 |
+
|
| 59 |
+
### Project Layout
|
| 60 |
+
|
| 61 |
+
```
|
| 62 |
+
cmd/ # Service entrypoints
|
| 63 |
+
openmeter/ # Core business logic (billing, customer, entitlement, meter, etc.)
|
| 64 |
+
openmeter/ent/schema/ # Ent entity definitions (source of truth for DB schema)
|
| 65 |
+
openmeter/ent/db/ # Generated ent code (DO NOT EDIT)
|
| 66 |
+
api/ # API specs, generated code, SDKs
|
| 67 |
+
api/spec/ # TypeSpec API definitions (source of truth for API)
|
| 68 |
+
pkg/ # Shared utility packages
|
| 69 |
+
tools/migrate/ # Migration tooling and SQL migration files
|
| 70 |
+
e2e/ # End-to-end tests
|
| 71 |
+
deploy/ # Helm charts
|
| 72 |
+
docs/ # Documentation and ADRs
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
## Code Generation
|
| 76 |
+
|
| 77 |
+
All generated files have `// Code generated by X, DO NOT EDIT.` headers — never edit them manually:
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
| Generated artifact | Source | Regenerate with |
|
| 81 |
+
|---|---|---|
|
| 82 |
+
| `api/openapi.yaml`, `api/openapi.cloud.yaml` | TypeSpec in `api/spec/` | `make gen-api` |
|
| 83 |
+
| `api/client/javascript/`, `api/client/go/` | OpenAPI spec | `make gen-api` |
|
| 84 |
+
| `api/v3/client/` (v3 Go SDK, standalone module) | TypeSpec in `api/spec/` via `@openmeter/typespec-go` | `make gen-api` |
|
| 85 |
+
| `api/api.gen.go`, `api/v3/api.gen.go` | OpenAPI spec via oapi-codegen | `make gen-api` |
|
| 86 |
+
| `api/client/go/client.gen.go` | OpenAPI spec | `make gen-api` |
|
| 87 |
+
| `**/ent/db/` | Ent schema in `openmeter/ent/schema/` | `make generate` |
|
| 88 |
+
| `**/wire_gen.go` | Wire providers in `**/wire.go` | `make generate` |
|
| 89 |
+
| `**/convert.gen.go` | Goverter converter interfaces (`**/convert.go`) | `make generate` |
|
| 90 |
+
| `billing/derived.gen.go` | Goderive annotations | `make generate` |
|
| 91 |
+
| `tools/migrate/migrations/` | Ent schema diff | `atlas migrate --env local diff <name>` |
|
| 92 |
+
|
| 93 |
+
**Workflow for changing the API:**
|
| 94 |
+
|
| 95 |
+
1. Edit TypeSpec files in `api/spec/`
|
| 96 |
+
2. Run `make gen-api` to regenerate OpenAPI spec and SDKs
|
| 97 |
+
3. Run `make generate` to regenerate Go server/client code
|
| 98 |
+
|
| 99 |
+
The TypeSpec JS client emitted from `api/spec/packages/aip` now lands in `api/spec/packages/aip-client-javascript/`. The emitter regenerates `src/`, `README.md`, and five conformance test files (`tests/client.spec.ts`, `tests/meters.spec.ts`, `tests/errors.spec.ts`, `tests/nesting.spec.ts`, `tests/internal.spec.ts`); every regenerated file carries a `Code generated by @openmeter/typespec-typescript. DO NOT EDIT.` header — treat files without that header as hand-written. `package.json` is **stable, hand-maintained, and committed** (the emitter's `writeOutput` only writes the paths it lists, so the manifest survives regeneration). The test suite is vitest + `@fetch-mock/vitest`; keep hand-written tests and helpers in `tests/` (never `src/`), and put test-runner dependencies/scripts in the `api/spec/package.json` workspace root rather than the client package manifest. Static publish metadata (`name`, `license`, `homepage`, `repository`) lives directly in the client `package.json`; only the per-release `version` is injected at publish time. Operations marked `x-internal` or `x-private` in the TypeSpec source are emitted but quarantined under `client.internal.<group>.<method>` (`src/sdk/internal.ts`, an `Internal` aggregate with `Internal<Group>` facade classes; internal ops share their group's `funcs/` and `models/operations/` modules, a group whose ops are all internal gets no public facade, and the `Internal` class is deliberately not re-exported from the package root — the name belongs to the Internal Server Error model). Removing an operation from the spec leaves stale generated files behind — the emitter never deletes outputs, so `git rm` them (and `rm` the gitignored `*.assert.ts` companions, which `git rm` cannot clean up on collaborators' checkouts).
|
| 100 |
+
|
| 101 |
+
The emitted `api/spec/packages/aip-client-javascript/src/sdk/sdk.ts` exposes aggregated sub-client getters on the `OpenMeter` class (e.g. `events`, `meters`, `customers`, `entitlements`, `subscriptions`, `billing`, `features`, `plans`, `addons`, `planAddons`, `tax`, `defaults`). Access operations through those getters, e.g. `sdk.meters.list()`, `sdk.customers.create(...)`, `sdk.plans.create(...)`. These grouped-client methods throw `HTTPError` on failure; the same operations are also available as tree-shakeable standalone functions under `src/funcs/` that return a `Result` instead of throwing.
|
| 102 |
+
|
| 103 |
+
**Workflow for changing Go types/DI:**
|
| 104 |
+
|
| 105 |
+
1. Edit the source files (ent schema, wire.go, converter interfaces)
|
| 106 |
+
2. Run `make generate` (or `go generate ./...`)
|
| 107 |
+
|
| 108 |
+
## Database Migrations
|
| 109 |
+
|
| 110 |
+
Uses [ent](https://entgo.io) for schema definition and [Atlas](https://atlasgo.io/) for migration generation. Migrations are in `tools/migrate/migrations/` using golang-migrate format.
|
| 111 |
+
|
| 112 |
+
**Schema files:** `openmeter/ent/schema/*.go`
|
| 113 |
+
|
| 114 |
+
**Workflow for schema changes:**
|
| 115 |
+
|
| 116 |
+
1. Edit the ent schema in `openmeter/ent/schema/`
|
| 117 |
+
2. Run `make generate` to regenerate ent code in `openmeter/ent/db/`
|
| 118 |
+
3. Generate migration: `atlas migrate --env local diff <migration-name>`
|
| 119 |
+
- This creates timestamped `.up.sql` / `.down.sql` files in `tools/migrate/migrations/`
|
| 120 |
+
- Also updates `tools/migrate/migrations/atlas.sum`
|
| 121 |
+
4. Versioned migrations run automatically on startup when `postgres.autoMigrate` is set to `migration`; runtime Ent migration is not supported
|
| 122 |
+
|
| 123 |
+
**Ent view caveat:** in this repo's current Ent/Atlas setup, schemas declared with `ent.View` can generate query code under `openmeter/ent/db/`, but they do not appear in `openmeter/ent/db/migrate/schema.go` or the generated `migrate.Tables` list. If `atlas migrate --env local diff ...` reports no changes for a new view, verify whether the view exists in generated migration metadata before debugging Atlas; view DDL may need an explicit SQL migration until generator support is added.
|
| 124 |
+
|
| 125 |
+
**Atlas config:** `atlas.hcl` — schema source is `ent://openmeter/ent/schema`, migrations dir is `file://tools/migrate/migrations`.
|
| 126 |
+
|
| 127 |
+
**Local Postgres:** `postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable`
|
| 128 |
+
|
| 129 |
+
Runtime Ent schema migration is not supported. `postgres.autoMigrate: ent` must fail validation; use `migration`, `migration-job`, or `false`. `openmeter-jobs migrate adopt-ent` contains a frozen compatibility bridge for databases created by Ent before migration baseline `20260709134422`; it stops at that baseline, after which `openmeter-jobs migrate` performs the normal target-version upgrade. Keep the frozen descriptors and reconciliation scripts under `tools/migrate/legacyent` independent from current Ent generation so later schema changes cannot alter the adoption baseline. Ent and Atlas can derive different names for equivalent indexes and constraints, so reconciliation must preserve the canonical Atlas names and `TestLegacyEntAdoptionSchemaParity` must continue comparing the adopted database with a from-scratch Atlas database.
|
| 130 |
+
|
| 131 |
+
## Testing
|
| 132 |
+
|
| 133 |
+
Tests require PostgreSQL running locally. Start it with `docker compose up -d postgres`.
|
| 134 |
+
|
| 135 |
+
Keep domain test helpers under `openmeter/.../testutils` independent from `app/common`. Build test dependencies from the underlying package constructors (repos, adapters, services, `lockr`) instead of importing the application wiring layer, or unrelated wiring additions can create test-only import cycles.
|
| 136 |
+
|
| 137 |
+
For usage-based billing lifecycle tests, prefer driving behavior through `charges.Service.Create`, `AdvanceCharges`, and `ApplyPatches` rather than calling lower-level charge adapters directly. To model late-arriving or newly visible usage, use `MockStreamingConnector` events with explicit `StoredAt` values (or `SetSimpleEvents`) so the test exercises the real stored-at cutoff logic in finalization.
|
| 138 |
+
|
| 139 |
+
For OpenMeter Go tests that touch the database, explicitly set `POSTGRES_HOST=127.0.0.1`. Without it, many suites will skip during setup even if PostgreSQL is running and the repo environment is otherwise loaded correctly.
|
| 140 |
+
|
| 141 |
+
Use the repo's Nix CI dev shell when `go`, `gofmt`, or other toolchain binaries are missing from the ambient shell. The CI and local-compatible invocation pattern is:
|
| 142 |
+
|
| 143 |
+
```bash
|
| 144 |
+
nix develop --impure .#ci -c <command>
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
Always invoke `nix develop` with the repo root as the working directory (use absolute paths in `<command>` instead of `cd`-ing first). The devenv `enterShell` writes CWD-relative state: run from a subdirectory it drops `.devenv/`, `.nvmrc`, and `.pre-commit-config.yaml` there (which then fail `prettier --check` in lint) and reinstalls the git pre-commit hook with that subdirectory's config path baked in, breaking later commits until a root-CWD `nix develop` run repairs it.
|
| 148 |
+
|
| 149 |
+
Codex's default shell may not auto-load `.envrc`, so `direnv`-managed tools like `go` can be missing even when the repo is configured correctly. In that case, run commands through `nix develop --impure .#ci -c ...` explicitly instead of assuming the ambient shell reflects the flake environment. `direnv exec . <command>` is also a valid one-off fallback when `direnv` is installed and the repo has already been allowed.
|
| 150 |
+
|
| 151 |
+
When invoking commands through Codex tools, prefer direct command execution. Do not wrap commands in `sh -lc`, `bash -lc`, or other helper shells when the command can be run directly. For environment variables, prefer `env KEY=value <command>` or `KEY=value <command>` over shell-wrapped forms. This keeps failures attributable to the actual toolchain/runtime being tested.
|
| 152 |
+
|
| 153 |
+
In tests, prefer `t.Context()` when a `testing.T` or `testing.TB` is available instead of introducing `context.Background()`. This keeps cancellation and test-scoped lifecycle tied to the test harness.
|
| 154 |
+
|
| 155 |
+
Prefer one consistent test harness style over mixed ad hoc structures. Use production-backed paths, such as rating-backed or service-backed fixtures, when the real path can express the scenario; keep hand-assembled fixtures for cases that cannot be produced realistically. If a behavior is a suite-wide rule, hardcode it into the shared harness instead of exposing it as per-test knobs.
|
| 156 |
+
|
| 157 |
+
Avoid redundant test helpers and duplicate setup paths. Prefer parameterizing one helper over maintaining near-identical helpers, use literal helper names that state exactly what they do, and inline single-use helpers that only wrap setup, conversion, or assertions even when the test becomes longer. Add a test helper only when it is used by at least two tests in the same package or when the helper name captures non-obvious domain semantics that would otherwise be easy to miss. Clean up dead test helpers immediately after refactors.
|
| 158 |
+
|
| 159 |
+
For service and lifecycle subtests, start each subtest body with concise intent comments when the scenario is non-trivial:
|
| 160 |
+
|
| 161 |
+
```go
|
| 162 |
+
// given:
|
| 163 |
+
// - ...
|
| 164 |
+
// when:
|
| 165 |
+
// - ...
|
| 166 |
+
// then:
|
| 167 |
+
// - ...
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
When using `clock.FreezeTime(...)` in tests, immediately pair it with `defer clock.UnFreeze()` in the same scope so later assertions or subtests do not inherit frozen time accidentally.
|
| 171 |
+
|
| 172 |
+
When asserting `alpacadecimal.Decimal` equality in tests, prefer `require.Equal(t, expectedFloat64, actual.InexactFloat64())` over boolean assertions like `require.True(t, expected.Equal(actual))` when precision requirements allow it. Prefer simple `float64(5)`-style literals over verbose decimal construction for expected values. Inline one-off expected balance structs at the assertion site; name expected balances only when reused or when the name carries useful phase semantics across subtests.
|
| 173 |
+
|
| 174 |
+
After each meaningful test-related change, run focused `go vet` and focused `go test` for the touched package.
|
| 175 |
+
|
| 176 |
+
Examples:
|
| 177 |
+
|
| 178 |
+
```bash
|
| 179 |
+
nix develop --impure .#ci -c gofmt -w openmeter/ledger/historical/entry.go
|
| 180 |
+
nix develop --impure .#ci -c make lint-go
|
| 181 |
+
nix develop --impure .#ci -c env POSTGRES_HOST=127.0.0.1 go test -tags=dynamic ./openmeter/ledger/historical/...
|
| 182 |
+
```
|
| 183 |
+
|
| 184 |
+
| Command | Description |
|
| 185 |
+
|---------|-------------|
|
| 186 |
+
| `make test` | Run all tests (parallel: `-p 128 -parallel 16`) |
|
| 187 |
+
| `make test-nocache` | Run tests bypassing cache |
|
| 188 |
+
| `make test-all` | Run tests including Svix/Redis dependencies |
|
| 189 |
+
| `make test-go-sdk` | Build, vet, and test the v3 Go SDK module (`api/v3/client`) |
|
| 190 |
+
| `make etoe` | Run e2e tests (requires docker compose dependencies) |
|
| 191 |
+
|
| 192 |
+
**Running a single package directly:**
|
| 193 |
+
|
| 194 |
+
```bash
|
| 195 |
+
POSTGRES_HOST=127.0.0.1 go test -tags=dynamic -v ./openmeter/billing/...
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
Key flags: `-tags=dynamic` (required for confluent-kafka-go), `-p 128 -parallel 16` (used by Make). Set `POSTGRES_HOST=127.0.0.1` or tests requiring Postgres will be skipped. `e2e/` is its own module and its import graph never reaches confluent-kafka-go, so `-tags=dynamic` is not needed there — `go test -C e2e ./...` (or `TZ=UTC OPENMETER_ADDRESS=... go test -C e2e ./...`) is enough.
|
| 199 |
+
|
| 200 |
+
See the `/test` skill for testing patterns, TestEnv setup, and examples.
|
| 201 |
+
|
| 202 |
+
## Building
|
| 203 |
+
|
| 204 |
+
```bash
|
| 205 |
+
make build # All binaries → build/
|
| 206 |
+
make build-server # Just the server
|
| 207 |
+
```
|
| 208 |
+
|
| 209 |
+
All builds use `GO_BUILD_FLAGS=-tags=dynamic`.
|
| 210 |
+
|
| 211 |
+
## Configuration
|
| 212 |
+
|
| 213 |
+
- Copy `config.example.yaml` to `config.yaml` (done automatically by Make targets)
|
| 214 |
+
- Load the repository environment with `direnv`, or run commands with `direnv exec . <command>`, so project-specific environment variables and tool configuration are applied consistently
|
| 215 |
+
- Key settings: `postgres.url`, `postgres.autoMigrate`, `billing`, `notification`, meter definitions
|
| 216 |
+
- `credits.enabled` needs explicit guarding at multiple layers: ledger-backed customer credit handlers in `api/v3/server`, customer ledger hooks, and namespace/default-account provisioning are wired separately and must each stay disabled when credits are off.
|
| 217 |
+
- When `credits.enabled` is `false`, `app/common` wires ledger account services/resolvers to noop implementations. Any ledger account backfill that must write real `ledger_accounts` / `ledger_customer_accounts` rows needs to construct concrete ledger account + resolver adapters directly instead of relying on the default DI outputs.
|
| 218 |
+
- Make targets for running services will warn if `config.yaml` is outdated vs `config.example.yaml`
|
| 219 |
+
|
| 220 |
+
## Coding Conventions
|
| 221 |
+
|
| 222 |
+
See the `/service` skill for service/adapter patterns, constructors, input types, errors, transactions, hooks, logging, multi-tenancy, and DI wiring. See the `/api` skill for HTTP handler patterns and ValidationIssue. See the `/ent` skill for Ent ORM patterns and Postgres type gotchas. See the `/ledger` skill for ledger package architecture, wiring, and testing. See the `/subscription` skill for subscription domain model, sync algorithm, patch system, workflow layer, and addon sub-system. See the `/notification` skill for notification event pipeline, Kafka consumers, Svix webhook delivery, reconciliation loop, and payload versioning.
|
| 223 |
+
|
| 224 |
+
For TypeSpec-specific coding constraints, update `api/spec/AGENTS.md` instead of adding them here.
|
| 225 |
+
|
| 226 |
+
### Documentation Constraints
|
| 227 |
+
|
| 228 |
+
- When adding comments or docstrings, document intent and domain constraints that are only available from human author context, not facts a reader can infer by reading the codebase. Avoid comments that merely translate obvious conditions, such as saying that a branch runs when `servicePeriod > 0`. A good comment should be understandable without the author's chat context and should explain why the code deliberately includes or excludes a case. For fallback or guard comments, name the concrete input shape or lifecycle state, the invariant being protected, the chosen behavior, and what would go wrong if the guard or fallback were removed. Avoid vague phrases like "can still arrive" or "as needed".
|
| 229 |
+
- Add a docstring to domain helpers when the name compresses important business semantics that are easy to misread at call sites. Explain the observable business contract and why excluded cases are excluded, not the implementation mechanics.
|
| 230 |
+
- When refactoring or reverting code, preserve existing explanatory comments by default. Remove or rewrite a comment only when the code change makes it false, stale, or misleading.
|
| 231 |
+
|
| 232 |
+
### Go Style Constraints
|
| 233 |
+
|
| 234 |
+
- For Go string enum constants, name values as `<Type><Value>` so the constant carries its enum type at the use site, for example `InvoiceStatusDraft` instead of `Draft`.
|
| 235 |
+
- Do not extract helper functions only to hide a couple of simple operations or short guard checks. If the helper would only wrap 2-4 lines and its name does not add meaningful domain or business intent, keep the code inline even when there is some duplication. Readers can inspect the function body to see what the code does; prefer function names that explain the domain reason for the call over names that merely restate the implementation steps. When you encounter a leftover pass-through wrapper that only calls another function without adding behavior, remove it and call the underlying function directly, even if it is outside the immediate change area.
|
| 236 |
+
- Do not hide non-trivial branching or domain translation inside local inline functions. If a closure performs type switching, validation, persistence mapping, or meaningful domain conversion, make it a named helper near the code that uses it so it is discoverable, testable, and grep-friendly. Reserve inline closures for tiny callbacks where the surrounding API requires a function literal and the logic is obvious at the call site.
|
| 237 |
+
- For `Validate() error` methods, prefer collecting all validation issues into `var errs []error` and returning `models.NewNillableGenericValidationError(errors.Join(errs...))` instead of returning on the first invalid field. Preserve field context with wrapped errors like `fmt.Errorf("field: %w", err)` and use plain `errors.New(...)` for simple local checks.
|
| 238 |
+
- Do not introduce `context.Background()` or `context.TODO()` to sidestep missing context propagation in application code. Either propagate the caller's context through the full call path, or remove the unused `context.Context` parameter from the API if the operation is purely local and does not need cancellation, deadlines, or request-scoped values.
|
| 239 |
+
- Never use `panic` in non-test code paths. If a new failure mode is possible, change the function signature to return an error and propagate it explicitly.
|
| 240 |
+
- In production constructors and initialization, do not use `slog.Default()` as a fallback dependency. Require a `*slog.Logger` in config/provider inputs and inject it explicitly.
|
| 241 |
+
- Prefer standard library `slices` and `maps` helpers for common collection operations, and use `github.com/samber/lo` when it makes pointer literals or collection transformations clearer than local wrappers or hand-written loops. See the `/samber-lo` skill for common OpenMeter use cases and caveats. Do not add local wrappers such as `ptr`, `loPtr`, `must`, or `loMust` when standard helpers or `lo` already cover the need.
|
| 242 |
+
- Use repo helper packages when they capture a common pattern better than ad hoc closures. For example, use `pkg/slicesx` for existing slice helpers (but prefer `samber/lo` and `slices` system packages if they fit), and use `pkg/syncx.OnceValues` for lazy context-aware database lookups that may be needed by multiple callbacks but should execute at most once.
|
| 243 |
+
- Keep helper functions honest and narrow. If a production helper is only called once and is just a short guard or a few straightforward lines, inline it unless the name carries meaningful domain semantics. Do not add helpers for trivial single-use struct literals, do not hide aggregate mutation inside construction helpers, and return the domain value a helper actually builds rather than a broader wrapper needed by one caller.
|
| 244 |
+
- For files and functions that convert between domain, API, and DB representations, use the `/go-types-conversion` skill. In prose, prefer `map` / `mapped` terminology for domain representation translation and avoid `project` / `projected` for that meaning; function names must still follow the skill's `FromAPI...`, `ToAPI...`, `FromDB...`, and `ToDB...` conventions.
|
| 245 |
+
|
| 246 |
+
### Generation And Dependency Constraints
|
| 247 |
+
|
| 248 |
+
- When `make generate` or `atlas migrate --env local diff ...` adds incidental `go.sum` entries, such as `tablewriter`, drop those `go.sum` changes unless the task explicitly requires a dependency change.
|
| 249 |
+
|
| 250 |
+
## Key Dependencies
|
| 251 |
+
|
| 252 |
+
| Category | Libraries |
|
| 253 |
+
|----------|-----------|
|
| 254 |
+
| DB | PostgreSQL (Ent ORM, Atlas migrations, pgx driver) |
|
| 255 |
+
| Analytics | ClickHouse |
|
| 256 |
+
| Events | Kafka (confluent-kafka-go) + Watermill |
|
| 257 |
+
| HTTP | Chi router + oapi-codegen |
|
| 258 |
+
| Invoicing | GOBL (invoice format) |
|
| 259 |
+
| Webhooks | Svix |
|
| 260 |
+
| Observability | OpenTelemetry |
|
| 261 |
+
| Config | Viper + Cobra |
|
| 262 |
+
| Utilities | samber/lo |
|
| 263 |
+
|
| 264 |
+
## CodeGraph
|
| 265 |
+
|
| 266 |
+
CodeGraph builds a semantic knowledge graph of the codebase (~1,800 Go files, ~36k symbols) for faster, smarter code exploration. The index lives in `.codegraph/codegraph.db` (gitignored). Generated files (`ent/db/`, `*_gen.go`, `wire_gen.go`, `*.gen.go`) are excluded.
|
| 267 |
+
|
| 268 |
+
### If `.codegraph/` exists
|
| 269 |
+
|
| 270 |
+
**Default to CodeGraph, not Grep/Glob/find.** CodeGraph understands symbols, call relationships, and file structure — those tools return string matches. On a ~1,800-file Go codebase the symbol-aware answer is almost always what you wanted. Fall back to Grep/Glob **only** when CodeGraph returns no results or the query is inherently textual (string literals, comments, log messages, SQL, YAML keys).
|
| 271 |
+
|
| 272 |
+
**Never call `codegraph_explore` or `codegraph_context` in the main session.** These tools return large source code blocks that fill up main-session context fast. Instead, spawn an Explore agent for any exploration question (e.g., "how does billing sync work?", "where is entitlement reset implemented?").
|
| 273 |
+
|
| 274 |
+
When spawning Explore agents, include this instruction in the prompt:
|
| 275 |
+
|
| 276 |
+
> This project has CodeGraph initialized (.codegraph/ exists). Use `codegraph_explore` as your PRIMARY exploration tool — it returns full source code sections from all relevant files in one call.
|
| 277 |
+
>
|
| 278 |
+
> **Rules:**
|
| 279 |
+
> 1. Follow the explore call budget in the `codegraph_explore` tool description — it scales automatically based on project size.
|
| 280 |
+
> 2. Do NOT re-read files that `codegraph_explore` already returned source code for. The source sections are complete and authoritative.
|
| 281 |
+
> 3. Only fall back to Grep/Glob/Read for files listed under "Additional relevant files" if you need more detail, or if CodeGraph returned no results.
|
| 282 |
+
|
| 283 |
+
**The main session should use these lightweight tools directly** for targeted lookups before making edits:
|
| 284 |
+
|
| 285 |
+
| Tool | Use for | Example |
|
| 286 |
+
|------|---------|---------|
|
| 287 |
+
| `codegraph_search` | Find symbols by name | `query: "BillingService"` |
|
| 288 |
+
| `codegraph_callers` | Who calls this function? | Before renaming or changing a signature |
|
| 289 |
+
| `codegraph_callees` | What does this function call? | Understanding a function's dependencies |
|
| 290 |
+
| `codegraph_impact` | Blast radius of a change | Before refactoring a shared type |
|
| 291 |
+
| `codegraph_node` | Single symbol details | Quick check on a struct or interface |
|
| 292 |
+
| `codegraph_files` | Project file tree | Faster than Glob for directory overviews |
|
| 293 |
+
| `codegraph_status` | Index health check | Verify the index is up to date |
|
| 294 |
+
|
| 295 |
+
### Choosing CodeGraph vs Grep/Glob
|
| 296 |
+
|
| 297 |
+
| Task | Prefer | Reason |
|
| 298 |
+
|------|--------|--------|
|
| 299 |
+
| "Where is `BillingService` defined?" | `codegraph_search` | Symbol lookup with location + signature |
|
| 300 |
+
| "Who calls `ListCustomers`?" | `codegraph_callers` | Call-graph edges, not text matches |
|
| 301 |
+
| "What does `Reconcile` call?" | `codegraph_callees` | Dependencies of a function |
|
| 302 |
+
| "Blast radius of changing this type?" | `codegraph_impact` | Transitive reverse-deps |
|
| 303 |
+
| "Show the `Filter` interface fields" | `codegraph_node` | Single-symbol detail without reading whole file |
|
| 304 |
+
| "List files under `api/v3/filters/`" | `codegraph_files` | Indexed tree; no disk walk |
|
| 305 |
+
| Find a string literal / log message / SQL fragment | `Grep` | Not a symbol |
|
| 306 |
+
| Find files by glob pattern (`**/*.tsp`) | `Glob` | CodeGraph indexes Go; non-Go globs go through Glob |
|
| 307 |
+
| Navigate a specific known path | `Read` | Direct reads are always fine |
|
| 308 |
+
| Running `find` on the shell | Don't | Use `codegraph_files` or `Glob` |
|
| 309 |
+
| Running `grep`/`rg` on the shell | Don't | Use `Grep` (or `codegraph_search` for symbols) |
|
| 310 |
+
|
| 311 |
+
Rule of thumb: **if the target is a Go identifier, start with CodeGraph. If it's a string, start with Grep.** Never shell out to `grep`, `rg`, or `find` — the dedicated tools (`Grep`, `Glob`, `codegraph_*`) give better output and permission handling.
|
| 312 |
+
|
| 313 |
+
### Keeping the index fresh
|
| 314 |
+
|
| 315 |
+
At the start of work, refresh CodeGraph before exploring Go code. If `.codegraph/` exists, run `codegraph sync`; if it does not exist, run `codegraph init -i` without asking first. Run `codegraph index` for a full rebuild if the index seems stale or after branch switches.
|
| 316 |
+
|
| 317 |
+
### If `.codegraph/` does NOT exist
|
| 318 |
+
|
| 319 |
+
Initialize it with `codegraph init -i` before doing code exploration. It indexes the Go codebase quickly and keeps symbol-aware lookup available.
|
| 320 |
+
|
| 321 |
+
## Skills
|
| 322 |
+
|
| 323 |
+
Skills are created inside [.agents/skills](.agents/skills/) by default and then symlinked to [.claude/skills](.claude/skills). Make sure you always treat `.agents/skills` as the source of truth. Keep skill guidance compatible with both Claude and Codex; avoid instructions that assume only one agent runtime unless the skill is explicitly about that runtime.
|
Agent.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Space Deployment Agent Guide
|
| 2 |
+
|
| 3 |
+
This file outlines the deployment configuration, API exposure, and deployment workflow for OpenMeter on Hugging Face Spaces.
|
| 4 |
+
|
| 5 |
+
## 1. Deployment Configuration
|
| 6 |
+
|
| 7 |
+
### Target Space
|
| 8 |
+
- **Profile:** `Leon4gr45`
|
| 9 |
+
- **Space:** `openmeter`
|
| 10 |
+
- **Full Identifier:** `Leon4gr45/openmeter`
|
| 11 |
+
- **Frontend Port:** `7860` (mandatory for all Hugging Face Spaces)
|
| 12 |
+
|
| 13 |
+
### Deployment Method
|
| 14 |
+
Choose the correct SDK based on the app type based on the codebase language:
|
| 15 |
+
- **Docker SDK** is used for OpenMeter (as a Go application).
|
| 16 |
+
|
| 17 |
+
### HF Token
|
| 18 |
+
- The environment variable `HF_TOKEN` must be set to your Hugging Face Space token at execution time (never hardcode the token).
|
| 19 |
+
- All monitoring, upload, and log-streaming commands rely on this token.
|
| 20 |
+
|
| 21 |
+
### Required Files
|
| 22 |
+
- `Dockerfile` (packaged to run with the mock server enabled for space readiness)
|
| 23 |
+
- `README.md` with Hugging Face YAML frontmatter specifying:
|
| 24 |
+
```yaml
|
| 25 |
+
---
|
| 26 |
+
title: Openmeter
|
| 27 |
+
sdk: docker
|
| 28 |
+
app_port: 7860
|
| 29 |
+
---
|
| 30 |
+
```
|
| 31 |
+
- `.hfignore` to exclude unnecessary files.
|
| 32 |
+
- `Agent.md` (this file, committed before deployment).
|
| 33 |
+
|
| 34 |
+
---
|
| 35 |
+
|
| 36 |
+
## 2. API Exposure and Documentation
|
| 37 |
+
|
| 38 |
+
### Mandatory Endpoints
|
| 39 |
+
Every deployment **must** expose:
|
| 40 |
+
|
| 41 |
+
- **`/health`**
|
| 42 |
+
- Returns HTTP 200 `OK` (plain text or JSON) when the app is ready.
|
| 43 |
+
- Required for Hugging Face to transition the Space from *starting* → *running*.
|
| 44 |
+
|
| 45 |
+
- **`/api-docs`**
|
| 46 |
+
- Documents **all** available API endpoints.
|
| 47 |
+
- Must be reachable at:
|
| 48 |
+
`https://Leon4gr45-openmeter.hf.space/api-docs`
|
| 49 |
+
|
| 50 |
+
---
|
| 51 |
+
|
| 52 |
+
### Functional Endpoints
|
| 53 |
+
|
| 54 |
+
The mock server exposed in this space implements the following critical OpenMeter functional endpoints (all of which are documented in `/api-docs` via `/api/swagger.json`):
|
| 55 |
+
|
| 56 |
+
#### 1. Ingest Events
|
| 57 |
+
- **Method:** POST
|
| 58 |
+
- **Path:** `/api/v1/events`
|
| 59 |
+
- **Purpose:** Ingest event data in CloudEvents format.
|
| 60 |
+
- **Request Example:**
|
| 61 |
+
```json
|
| 62 |
+
{
|
| 63 |
+
"specversion": "1.0",
|
| 64 |
+
"type": "request",
|
| 65 |
+
"id": "00001",
|
| 66 |
+
"time": "2026-07-07T00:00:00.001Z",
|
| 67 |
+
"source": "my-service",
|
| 68 |
+
"subject": "customer-1",
|
| 69 |
+
"data": { "method": "GET", "route": "/api/hello" }
|
| 70 |
+
}
|
| 71 |
+
```
|
| 72 |
+
- **Response Example:**
|
| 73 |
+
```json
|
| 74 |
+
{
|
| 75 |
+
"status": "accepted"
|
| 76 |
+
}
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
#### 2. List Meters
|
| 80 |
+
- **Method:** GET
|
| 81 |
+
- **Path:** `/api/v1/meters`
|
| 82 |
+
- **Purpose:** List all configured usage meters.
|
| 83 |
+
- **Request:** (None)
|
| 84 |
+
- **Response Example:**
|
| 85 |
+
```json
|
| 86 |
+
[
|
| 87 |
+
{
|
| 88 |
+
"id": "api_requests_total",
|
| 89 |
+
"slug": "api_requests_total",
|
| 90 |
+
"description": "API Requests",
|
| 91 |
+
"eventType": "request",
|
| 92 |
+
"aggregation": "COUNT"
|
| 93 |
+
},
|
| 94 |
+
{
|
| 95 |
+
"id": "tokens_total",
|
| 96 |
+
"slug": "tokens_total",
|
| 97 |
+
"description": "AI Token Usage",
|
| 98 |
+
"eventType": "prompt",
|
| 99 |
+
"aggregation": "SUM"
|
| 100 |
+
}
|
| 101 |
+
]
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
#### 3. Query Meter Usage
|
| 105 |
+
- **Method:** GET
|
| 106 |
+
- **Path:** `/api/v1/meters/{meterIdOrSlug}/query`
|
| 107 |
+
- **Purpose:** Query usage data for a specific meter.
|
| 108 |
+
- **Request Parameters:** `windowSize=HOUR`
|
| 109 |
+
- **Response Example:**
|
| 110 |
+
```json
|
| 111 |
+
{
|
| 112 |
+
"data": [
|
| 113 |
+
{
|
| 114 |
+
"value": 150,
|
| 115 |
+
"windowStart": "2026-07-07T00:00:00Z",
|
| 116 |
+
"windowEnd": "2026-07-07T01:00:00Z"
|
| 117 |
+
}
|
| 118 |
+
]
|
| 119 |
+
}
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
---
|
| 123 |
+
|
| 124 |
+
## 3. Deployment Workflow
|
| 125 |
+
|
| 126 |
+
Precondition: Check that the space is empty of files and delete any which are still in there and not belonging to the project to be uploaded.
|
| 127 |
+
|
| 128 |
+
### Standard Deployment Command
|
| 129 |
+
After any code change, run:
|
| 130 |
+
|
| 131 |
+
```bash
|
| 132 |
+
HF_TOKEN=<HF_TOKEN_VALUE> hf upload Leon4gr45/openmeter . . --repo-type=space
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
### Scan build and run logs
|
| 136 |
+
Get build logs (SSE):
|
| 137 |
+
```bash
|
| 138 |
+
curl -N -H "Authorization: Bearer <HF_TOKEN_VALUE>" "https://huggingface.co/api/spaces/Leon4gr45/openmeter/logs/build"
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
Get run logs (SSE) once the build logs succeed:
|
| 142 |
+
```bash
|
| 143 |
+
curl -N -H "Authorization: Bearer <HF_TOKEN_VALUE>" "https://huggingface.co/api/spaces/Leon4gr45/openmeter/logs/run"
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
Cycle monitoring and deployment:
|
| 147 |
+
- Wait up to 300 seconds to see if the deployment has been successful.
|
| 148 |
+
- If not, check logs for errors, fix the issues in the codebase, redeploy, and monitor again in a cycle until the space is running and reacts successfully to API endpoints.
|
CLAUDE.md
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenMeter Development Guide
|
| 2 |
+
|
| 3 |
+
See consolidated agents instructions in @AGENTS.md
|
CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Contributor Covenant Code of Conduct
|
| 2 |
+
|
| 3 |
+
## Our Pledge
|
| 4 |
+
|
| 5 |
+
In the interest of fostering an open and welcoming environment,
|
| 6 |
+
we as contributors and maintainers pledge to make participation in our project
|
| 7 |
+
and our community a harassment-free experience for everyone, regardless of age,
|
| 8 |
+
body size, disability, ethnicity, sex characteristics, gender identity and expression,
|
| 9 |
+
level of experience, education, socio-economic status, nationality, personal appearance,
|
| 10 |
+
race, religion, or sexual identity and orientation.
|
| 11 |
+
|
| 12 |
+
## Our Standards
|
| 13 |
+
|
| 14 |
+
Examples of behavior that contributes to creating a positive environment include:
|
| 15 |
+
|
| 16 |
+
- Using welcoming and inclusive language
|
| 17 |
+
- Being respectful of differing viewpoints and experiences
|
| 18 |
+
- Gracefully accepting constructive criticism
|
| 19 |
+
- Focusing on what is best for the community
|
| 20 |
+
- Showing empathy towards other community members
|
| 21 |
+
|
| 22 |
+
Examples of unacceptable behavior by participants include:
|
| 23 |
+
|
| 24 |
+
- The use of sexualized language or imagery and unwelcome sexual attention or advances
|
| 25 |
+
- Trolling, insulting/derogatory comments, and personal or political attacks
|
| 26 |
+
- Public or private harassment
|
| 27 |
+
- Publishing others' private information, such as a physical or electronic address, without explicit permission
|
| 28 |
+
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
| 29 |
+
|
| 30 |
+
## Our Responsibilities
|
| 31 |
+
|
| 32 |
+
Project maintainers are responsible for clarifying the standards of acceptable behavior
|
| 33 |
+
and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
| 34 |
+
|
| 35 |
+
Project maintainers have the right and responsibility to remove, edit,
|
| 36 |
+
or reject comments, commits, code, wiki edits, issues,
|
| 37 |
+
and other contributions that are not aligned to this Code of Conduct,
|
| 38 |
+
or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate,
|
| 39 |
+
threatening, offensive, or harmful.
|
| 40 |
+
|
| 41 |
+
## Scope
|
| 42 |
+
|
| 43 |
+
This Code of Conduct applies within all project spaces,
|
| 44 |
+
and it also applies when an individual is representing the project or its community in public spaces.
|
| 45 |
+
Examples of representing a project or community include using an official project e-mail address,
|
| 46 |
+
posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
| 47 |
+
Representation of a project may be further defined and clarified by project maintainers.
|
| 48 |
+
|
| 49 |
+
## Enforcement
|
| 50 |
+
|
| 51 |
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [conduct@openmeter.io][conduct-email].
|
| 52 |
+
All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances.
|
| 53 |
+
The project team is obligated to maintain confidentiality with regard to the reporter of an incident.
|
| 54 |
+
Further details of specific enforcement policies may be posted separately.
|
| 55 |
+
|
| 56 |
+
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary
|
| 57 |
+
or permanent repercussions as determined by other members of the project's leadership.
|
| 58 |
+
|
| 59 |
+
[conduct-email]: mailto:conduct@openmeter.io
|
| 60 |
+
|
| 61 |
+
## Attribution
|
| 62 |
+
|
| 63 |
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
| 64 |
+
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
| 65 |
+
|
| 66 |
+
[homepage]: https://www.contributor-covenant.org
|
| 67 |
+
|
| 68 |
+
For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq
|
CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Contributing
|
| 2 |
+
|
| 3 |
+
Thanks for your interest in contributing to OpenMeter!
|
| 4 |
+
|
| 5 |
+
Here are a few general guidelines on contributing and reporting bugs that we ask you to review.
|
| 6 |
+
Following these guidelines helps to communicate that you respect the time of the contributors managing and developing this open source project.
|
| 7 |
+
In return, they should reciprocate that respect in addressing your issue, assessing changes, and helping you finalize your pull requests.
|
| 8 |
+
In that spirit of mutual respect, we endeavor to review incoming issues and pull requests within 10 days,
|
| 9 |
+
and will close any lingering issues or pull requests after 60 days of inactivity.
|
| 10 |
+
|
| 11 |
+
Please note that all of your interactions in the project are subject to our [Code of Conduct](/CODE_OF_CONDUCT.md).
|
| 12 |
+
This includes creation of issues or pull requests, commenting on issues or pull requests,
|
| 13 |
+
and extends to all interactions in any real-time space e.g., Slack, Discord, etc.
|
| 14 |
+
|
| 15 |
+
## Reporting issues
|
| 16 |
+
|
| 17 |
+
Before reporting a new issue, please ensure that the issue was not already reported or fixed by searching through our issue tracker.
|
| 18 |
+
|
| 19 |
+
When creating a new issue, please be sure to include a **title and clear description**, as much relevant information as possible, and, if possible, a test case.
|
| 20 |
+
|
| 21 |
+
**If you discover a security bug, please do not report it through GitHub issues. Instead, please see security procedures in [SECURITY.md](/SECURITY.md).**
|
| 22 |
+
|
| 23 |
+
## Sending pull requests
|
| 24 |
+
|
| 25 |
+
Before sending a new pull request, take a look at existing pull requests and issues to see if the proposed change or fix has been discussed in the past,
|
| 26 |
+
or if the change was already implemented but not yet released.
|
| 27 |
+
|
| 28 |
+
We expect new pull requests to include tests for any affected behavior, and, as we follow semantic versioning,
|
| 29 |
+
we may reserve breaking changes until the next major version release.
|
| 30 |
+
|
| 31 |
+
### Ensuring All Requested Reviewers Approve
|
| 32 |
+
|
| 33 |
+
By default, pull requests can often be merged once the minimum number of required approvals (e.g., from CODEOWNERS or branch protection rules) is met. However, sometimes you might explicitly request reviews from specific individuals because their input is crucial for that particular PR.
|
| 34 |
+
|
| 35 |
+
To ensure that *all* individuals you've specifically requested using the GitHub "Reviewers" UI must approve before merging, follow these steps:
|
| 36 |
+
|
| 37 |
+
1. **Request Reviews:** Use the standard GitHub interface on the pull request page to request reviews from the necessary individuals.
|
| 38 |
+
2. **Add Label:** Add the label `require-all-reviewers` to the pull request.
|
| 39 |
+
|
| 40 |
+
When this label is present, an automated check named "Review Gatekeeper" will run. This check will only pass if **every single user** listed under the "Reviewers" section has submitted an **approving** review. This check is required for merging, preventing merges until all explicitly requested reviewers are satisfied.
|
| 41 |
+
|
| 42 |
+
If the label is removed, the "Review Gatekeeper" check will be skipped.
|
| 43 |
+
|
| 44 |
+
## Other ways to contribute
|
| 45 |
+
|
| 46 |
+
We welcome anyone that wants to contribute to triage and reply to open issues to help troubleshoot and fix existing bugs.
|
| 47 |
+
Here is what you can do:
|
| 48 |
+
|
| 49 |
+
- Help ensure that existing issues follows the recommendations from the _[Reporting Issues](#reporting-issues)_ section,
|
| 50 |
+
providing feedback to the issue's author on what might be missing.
|
| 51 |
+
- Review and update the existing content of our [documentation](https://openmeter.io) with up-to-date instructions and code samples.
|
| 52 |
+
- Review existing pull requests, and testing patches against real existing applications.
|
| 53 |
+
- Write a test, or add a missing test case to an existing test.
|
Dockerfile
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx
|
| 2 |
+
|
| 3 |
+
FROM --platform=$BUILDPLATFORM golang:1.26.4-alpine3.23@sha256:18b460dd17542c2ba43299a633cf6ebfc1115101509531471d7cfce1019af083 AS builder
|
| 4 |
+
|
| 5 |
+
COPY --link --from=xx / /
|
| 6 |
+
|
| 7 |
+
RUN xx-apk add --update --no-cache ca-certificates make git curl clang lld
|
| 8 |
+
|
| 9 |
+
ARG TARGETPLATFORM
|
| 10 |
+
|
| 11 |
+
RUN xx-apk --update --no-cache add musl-dev gcc
|
| 12 |
+
|
| 13 |
+
WORKDIR /src
|
| 14 |
+
|
| 15 |
+
ARG GOPROXY
|
| 16 |
+
|
| 17 |
+
ENV CGO_ENABLED=1
|
| 18 |
+
|
| 19 |
+
ENV GOCACHE=/go/cache
|
| 20 |
+
ENV GOMODCACHE=/go/pkg/mod
|
| 21 |
+
|
| 22 |
+
COPY --link go.mod go.sum ./
|
| 23 |
+
|
| 24 |
+
RUN --mount=type=cache,target=/go/pkg/mod \
|
| 25 |
+
--mount=type=cache,target=/go/cache \
|
| 26 |
+
xx-go mod download -x
|
| 27 |
+
|
| 28 |
+
ARG VERSION
|
| 29 |
+
|
| 30 |
+
COPY --link . .
|
| 31 |
+
|
| 32 |
+
RUN chmod +x entrypoint.sh
|
| 33 |
+
|
| 34 |
+
# See https://github.com/confluentinc/confluent-kafka-go#librdkafka
|
| 35 |
+
# See https://github.com/confluentinc/confluent-kafka-go#static-builds-on-linux
|
| 36 |
+
# Build server binary (default)
|
| 37 |
+
RUN --mount=type=cache,target=/go/pkg/mod \
|
| 38 |
+
--mount=type=cache,target=/go/cache \
|
| 39 |
+
xx-go build -ldflags "-linkmode external -extldflags \"-static\" -X main.version=${VERSION}" -tags musl -o /usr/local/bin/openmeter ./cmd/server
|
| 40 |
+
|
| 41 |
+
RUN xx-verify /usr/local/bin/openmeter
|
| 42 |
+
|
| 43 |
+
# Build sink-worker binary
|
| 44 |
+
RUN --mount=type=cache,target=/go/pkg/mod \
|
| 45 |
+
--mount=type=cache,target=/go/cache \
|
| 46 |
+
xx-go build -ldflags "-linkmode external -extldflags \"-static\" -X main.version=${VERSION}" -tags musl -o /usr/local/bin/openmeter-sink-worker ./cmd/sink-worker
|
| 47 |
+
|
| 48 |
+
RUN xx-verify /usr/local/bin/openmeter-sink-worker
|
| 49 |
+
|
| 50 |
+
# Build balance-worker binary
|
| 51 |
+
RUN --mount=type=cache,target=/go/pkg/mod \
|
| 52 |
+
--mount=type=cache,target=/go/cache \
|
| 53 |
+
xx-go build -ldflags "-linkmode external -extldflags \"-static\" -X main.version=${VERSION}" -tags musl -o /usr/local/bin/openmeter-balance-worker ./cmd/balance-worker
|
| 54 |
+
|
| 55 |
+
RUN xx-verify /usr/local/bin/openmeter-balance-worker
|
| 56 |
+
|
| 57 |
+
# Build notification-service binary
|
| 58 |
+
RUN --mount=type=cache,target=/go/pkg/mod \
|
| 59 |
+
--mount=type=cache,target=/go/cache \
|
| 60 |
+
xx-go build -ldflags "-linkmode external -extldflags \"-static\" -X main.version=${VERSION}" -tags musl -o /usr/local/bin/openmeter-notification-service ./cmd/notification-service
|
| 61 |
+
|
| 62 |
+
RUN xx-verify /usr/local/bin/openmeter-notification-service
|
| 63 |
+
|
| 64 |
+
# Build billing-worker binary
|
| 65 |
+
RUN --mount=type=cache,target=/go/pkg/mod \
|
| 66 |
+
--mount=type=cache,target=/go/cache \
|
| 67 |
+
xx-go build -ldflags "-linkmode external -extldflags \"-static\" -X main.version=${VERSION}" -tags musl -o /usr/local/bin/openmeter-billing-worker ./cmd/billing-worker
|
| 68 |
+
|
| 69 |
+
RUN xx-verify /usr/local/bin/openmeter-billing-worker
|
| 70 |
+
|
| 71 |
+
# Build periodic jobs binary
|
| 72 |
+
RUN --mount=type=cache,target=/go/pkg/mod \
|
| 73 |
+
--mount=type=cache,target=/go/cache \
|
| 74 |
+
xx-go build -ldflags "-linkmode external -extldflags \"-static\" -X main.version=${VERSION}" -tags musl -o /usr/local/bin/openmeter-jobs ./cmd/jobs
|
| 75 |
+
|
| 76 |
+
RUN xx-verify /usr/local/bin/openmeter-jobs
|
| 77 |
+
|
| 78 |
+
FROM alpine:3.24.1@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b
|
| 79 |
+
|
| 80 |
+
ENV MOCK_SERVER=true
|
| 81 |
+
ENV PORT=7860
|
| 82 |
+
|
| 83 |
+
RUN apk add --update --no-cache ca-certificates tzdata bash
|
| 84 |
+
|
| 85 |
+
SHELL ["/bin/bash", "-c"]
|
| 86 |
+
|
| 87 |
+
COPY --link --from=builder /usr/local/bin/openmeter /usr/local/bin/
|
| 88 |
+
COPY --link --from=builder /usr/local/bin/openmeter-sink-worker /usr/local/bin/
|
| 89 |
+
COPY --link --from=builder /usr/local/bin/openmeter-balance-worker /usr/local/bin/
|
| 90 |
+
COPY --link --from=builder /usr/local/bin/openmeter-notification-service /usr/local/bin/
|
| 91 |
+
COPY --link --from=builder /usr/local/bin/openmeter-billing-worker /usr/local/bin/
|
| 92 |
+
COPY --link --from=builder /usr/local/bin/openmeter-jobs /usr/local/bin/
|
| 93 |
+
COPY --link --from=builder /src/go.* /usr/local/src/openmeter/
|
| 94 |
+
COPY --link --from=builder /src/entrypoint.sh /entrypoint.sh
|
| 95 |
+
|
| 96 |
+
ENTRYPOINT ["/entrypoint.sh"]
|
| 97 |
+
|
| 98 |
+
CMD openmeter
|
LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
Apache License
|
| 3 |
+
Version 2.0, January 2004
|
| 4 |
+
http://www.apache.org/licenses/
|
| 5 |
+
|
| 6 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 7 |
+
|
| 8 |
+
1. Definitions.
|
| 9 |
+
|
| 10 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 11 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 12 |
+
|
| 13 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 14 |
+
the copyright owner that is granting the License.
|
| 15 |
+
|
| 16 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 17 |
+
other entities that control, are controlled by, or are under common
|
| 18 |
+
control with that entity. For the purposes of this definition,
|
| 19 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 20 |
+
direction or management of such entity, whether by contract or
|
| 21 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 22 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 23 |
+
|
| 24 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 25 |
+
exercising permissions granted by this License.
|
| 26 |
+
|
| 27 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 28 |
+
including but not limited to software source code, documentation
|
| 29 |
+
source, and configuration files.
|
| 30 |
+
|
| 31 |
+
"Object" form shall mean any form resulting from mechanical
|
| 32 |
+
transformation or translation of a Source form, including but
|
| 33 |
+
not limited to compiled object code, generated documentation,
|
| 34 |
+
and conversions to other media types.
|
| 35 |
+
|
| 36 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 37 |
+
Object form, made available under the License, as indicated by a
|
| 38 |
+
copyright notice that is included in or attached to the work
|
| 39 |
+
(an example is provided in the Appendix below).
|
| 40 |
+
|
| 41 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 42 |
+
form, that is based on (or derived from) the Work and for which the
|
| 43 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 44 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 45 |
+
of this License, Derivative Works shall not include works that remain
|
| 46 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 47 |
+
the Work and Derivative Works thereof.
|
| 48 |
+
|
| 49 |
+
"Contribution" shall mean any work of authorship, including
|
| 50 |
+
the original version of the Work and any modifications or additions
|
| 51 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 52 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 53 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 54 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 55 |
+
means any form of electronic, verbal, or written communication sent
|
| 56 |
+
to the Licensor or its representatives, including but not limited to
|
| 57 |
+
communication on electronic mailing lists, source code control systems,
|
| 58 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 59 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 60 |
+
excluding communication that is conspicuously marked or otherwise
|
| 61 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 62 |
+
|
| 63 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 64 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 65 |
+
subsequently incorporated within the Work.
|
| 66 |
+
|
| 67 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 68 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 69 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 70 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 71 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 72 |
+
Work and such Derivative Works in Source or Object form.
|
| 73 |
+
|
| 74 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 75 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 76 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 77 |
+
(except as stated in this section) patent license to make, have made,
|
| 78 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 79 |
+
where such license applies only to those patent claims licensable
|
| 80 |
+
by such Contributor that are necessarily infringed by their
|
| 81 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 82 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 83 |
+
institute patent litigation against any entity (including a
|
| 84 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 85 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 86 |
+
or contributory patent infringement, then any patent licenses
|
| 87 |
+
granted to You under this License for that Work shall terminate
|
| 88 |
+
as of the date such litigation is filed.
|
| 89 |
+
|
| 90 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 91 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 92 |
+
modifications, and in Source or Object form, provided that You
|
| 93 |
+
meet the following conditions:
|
| 94 |
+
|
| 95 |
+
(a) You must give any other recipients of the Work or
|
| 96 |
+
Derivative Works a copy of this License; and
|
| 97 |
+
|
| 98 |
+
(b) You must cause any modified files to carry prominent notices
|
| 99 |
+
stating that You changed the files; and
|
| 100 |
+
|
| 101 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 102 |
+
that You distribute, all copyright, patent, trademark, and
|
| 103 |
+
attribution notices from the Source form of the Work,
|
| 104 |
+
excluding those notices that do not pertain to any part of
|
| 105 |
+
the Derivative Works; and
|
| 106 |
+
|
| 107 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 108 |
+
distribution, then any Derivative Works that You distribute must
|
| 109 |
+
include a readable copy of the attribution notices contained
|
| 110 |
+
within such NOTICE file, excluding those notices that do not
|
| 111 |
+
pertain to any part of the Derivative Works, in at least one
|
| 112 |
+
of the following places: within a NOTICE text file distributed
|
| 113 |
+
as part of the Derivative Works; within the Source form or
|
| 114 |
+
documentation, if provided along with the Derivative Works; or,
|
| 115 |
+
within a display generated by the Derivative Works, if and
|
| 116 |
+
wherever such third-party notices normally appear. The contents
|
| 117 |
+
of the NOTICE file are for informational purposes only and
|
| 118 |
+
do not modify the License. You may add Your own attribution
|
| 119 |
+
notices within Derivative Works that You distribute, alongside
|
| 120 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 121 |
+
that such additional attribution notices cannot be construed
|
| 122 |
+
as modifying the License.
|
| 123 |
+
|
| 124 |
+
You may add Your own copyright statement to Your modifications and
|
| 125 |
+
may provide additional or different license terms and conditions
|
| 126 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 127 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 128 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 129 |
+
the conditions stated in this License.
|
| 130 |
+
|
| 131 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 132 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 133 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 134 |
+
this License, without any additional terms or conditions.
|
| 135 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 136 |
+
the terms of any separate license agreement you may have executed
|
| 137 |
+
with Licensor regarding such Contributions.
|
| 138 |
+
|
| 139 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 140 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 141 |
+
except as required for reasonable and customary use in describing the
|
| 142 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 143 |
+
|
| 144 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 145 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 146 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 147 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 148 |
+
implied, including, without limitation, any warranties or conditions
|
| 149 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 150 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 151 |
+
appropriateness of using or redistributing the Work and assume any
|
| 152 |
+
risks associated with Your exercise of permissions under this License.
|
| 153 |
+
|
| 154 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 155 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 156 |
+
unless required by applicable law (such as deliberate and grossly
|
| 157 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 158 |
+
liable to You for damages, including any direct, indirect, special,
|
| 159 |
+
incidental, or consequential damages of any character arising as a
|
| 160 |
+
result of this License or out of the use or inability to use the
|
| 161 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 162 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 163 |
+
other commercial damages or losses), even if such Contributor
|
| 164 |
+
has been advised of the possibility of such damages.
|
| 165 |
+
|
| 166 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 167 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 168 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 169 |
+
or other liability obligations and/or rights consistent with this
|
| 170 |
+
License. However, in accepting such obligations, You may act only
|
| 171 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 172 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 173 |
+
defend, and hold each Contributor harmless for any liability
|
| 174 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 175 |
+
of your accepting any such warranty or additional liability.
|
| 176 |
+
|
| 177 |
+
END OF TERMS AND CONDITIONS
|
| 178 |
+
|
| 179 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 180 |
+
|
| 181 |
+
To apply the Apache License to your work, attach the following
|
| 182 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 183 |
+
replaced with your own identifying information. (Don't include
|
| 184 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 185 |
+
comment syntax for the file format. We also recommend that a
|
| 186 |
+
file or class name and description of purpose be included on the
|
| 187 |
+
same "printed page" as the copyright notice for easier
|
| 188 |
+
identification within third-party archives.
|
| 189 |
+
|
| 190 |
+
Copyright [yyyy] [name of copyright owner]
|
| 191 |
+
|
| 192 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 193 |
+
you may not use this file except in compliance with the License.
|
| 194 |
+
You may obtain a copy of the License at
|
| 195 |
+
|
| 196 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 197 |
+
|
| 198 |
+
Unless required by applicable law or agreed to in writing, software
|
| 199 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 200 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 201 |
+
See the License for the specific language governing permissions and
|
| 202 |
+
limitations under the License.
|
Makefile
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# A Self-Documenting Makefile: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
|
| 2 |
+
|
| 3 |
+
ROOT_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
| 4 |
+
|
| 5 |
+
# Docker-local svix secret, only used for testing
|
| 6 |
+
SVIX_JWT_SECRET = DUMMY_JWT_SECRET
|
| 7 |
+
|
| 8 |
+
# dynamic forces confluent-kafka-go to build against local librdkafka
|
| 9 |
+
GO_BUILD_FLAGS = -tags=dynamic
|
| 10 |
+
GO_TEST_PACKAGE_PARALLELISM ?= 128
|
| 11 |
+
GO_TEST_FLAGS = -p ${GO_TEST_PACKAGE_PARALLELISM} -parallel 16 ${GO_BUILD_FLAGS}
|
| 12 |
+
GOTESTSUM_FLAGS ?= --format pkgname-and-test-fails --hide-summary=skipped
|
| 13 |
+
GO_LINT_PATH ?= ./...
|
| 14 |
+
|
| 15 |
+
.PHONY: up
|
| 16 |
+
up: ## Start the dependencies via docker compose. `export COMPOSE_PROFILES=dev,redis,...`
|
| 17 |
+
$(call print-target)
|
| 18 |
+
docker compose up -d
|
| 19 |
+
|
| 20 |
+
.PHONY: down
|
| 21 |
+
down: ## Stop the dependencies via docker compose
|
| 22 |
+
$(call print-target)
|
| 23 |
+
docker compose down --remove-orphans --volumes
|
| 24 |
+
|
| 25 |
+
.PHONY: patch-oapi-templates
|
| 26 |
+
patch-oapi-templates: ## Patch oapi-codegen chi-middleware template with custom filter parsing
|
| 27 |
+
$(call print-target)
|
| 28 |
+
@go mod download github.com/oapi-codegen/oapi-codegen/v2
|
| 29 |
+
@OAPI_MOD_DIR=$$(go list -m -f '{{.Dir}}' github.com/oapi-codegen/oapi-codegen/v2) && \
|
| 30 |
+
if [ -z "$$OAPI_MOD_DIR" ]; then echo "error: could not locate oapi-codegen/v2 module dir"; exit 1; fi && \
|
| 31 |
+
cp "$$OAPI_MOD_DIR/pkg/codegen/templates/chi/chi-middleware.tmpl" api/v3/templates/chi-middleware.tmpl && \
|
| 32 |
+
chmod u+w api/v3/templates/chi-middleware.tmpl && \
|
| 33 |
+
patch -p1 -d api/v3/templates < api/v3/templates/chi-middleware.tmpl.patch
|
| 34 |
+
|
| 35 |
+
.PHONY: update-openapi
|
| 36 |
+
update-openapi: patch-oapi-templates ## Update OpenAPI spec
|
| 37 |
+
$(call print-target)
|
| 38 |
+
$(MAKE) -C api/spec generate
|
| 39 |
+
go generate ./api/...
|
| 40 |
+
|
| 41 |
+
.PHONY: generate-javascript-sdk
|
| 42 |
+
generate-javascript-sdk: ## Generate JavaScript SDK
|
| 43 |
+
$(call print-target)
|
| 44 |
+
$(MAKE) -C api/client/javascript generate
|
| 45 |
+
|
| 46 |
+
.PHONY: gen-api
|
| 47 |
+
gen-api: update-openapi generate-javascript-sdk ## Generate API and SDKs
|
| 48 |
+
$(call print-target)
|
| 49 |
+
|
| 50 |
+
.PHONY: generate-all
|
| 51 |
+
generate-all: update-openapi generate-javascript-sdk ## Execute all code generators
|
| 52 |
+
$(call print-target)
|
| 53 |
+
go generate ./...
|
| 54 |
+
|
| 55 |
+
.PHONY: migrate-check
|
| 56 |
+
migrate-check: migrate-check-schema migrate-check-diff migrate-check-lint migrate-check-validate ## Validate migrations
|
| 57 |
+
|
| 58 |
+
.PHONY: migrate-check-schema
|
| 59 |
+
migrate-check-schema: ## Ensure ent schema is in sync with generated code
|
| 60 |
+
$(call print-target)
|
| 61 |
+
go generate -x ./openmeter/ent/...
|
| 62 |
+
@if ! git diff --quiet -- openmeter/ent || [ -n "$$(git ls-files --others --exclude-standard -- openmeter/ent)" ]; then \
|
| 63 |
+
git --no-pager diff -- openmeter/ent; \
|
| 64 |
+
git ls-files --others --exclude-standard -- openmeter/ent; \
|
| 65 |
+
echo "!!! schema is not in sync with generated code — run 'go generate ./openmeter/ent/...' and commit the changes !!!"; \
|
| 66 |
+
exit 1; \
|
| 67 |
+
fi
|
| 68 |
+
|
| 69 |
+
.PHONY: migrate-check-diff
|
| 70 |
+
migrate-check-diff: ## Ensure migrations are in sync with schema (runs atlas migrate diff against a clean target)
|
| 71 |
+
$(call print-target)
|
| 72 |
+
atlas migrate --env local diff migrate-check >/dev/null
|
| 73 |
+
@if ! git diff --quiet -- tools/migrate/migrations || [ -n "$$(git ls-files --others --exclude-standard -- tools/migrate/migrations)" ]; then \
|
| 74 |
+
git --no-pager diff -- tools/migrate/migrations; \
|
| 75 |
+
git ls-files --others --exclude-standard -- tools/migrate/migrations; \
|
| 76 |
+
echo "!!! migrations are not in sync with schema — run 'atlas migrate --env local diff <name>' and commit the generated files !!!"; \
|
| 77 |
+
exit 1; \
|
| 78 |
+
fi
|
| 79 |
+
|
| 80 |
+
.PHONY: migrate-check-lint
|
| 81 |
+
migrate-check-lint: ## Lint the last 10 migrations
|
| 82 |
+
$(call print-target)
|
| 83 |
+
atlas migrate --env local lint --latest 10
|
| 84 |
+
|
| 85 |
+
.PHONY: migrate-check-validate
|
| 86 |
+
migrate-check-validate: ## Validate migration checksums
|
| 87 |
+
$(call print-target)
|
| 88 |
+
atlas migrate --env local validate
|
| 89 |
+
|
| 90 |
+
.PHONY: generate-sqlc-testdata
|
| 91 |
+
generate-sqlc-testdata: ## Generate SQLC testdata for a specific version (make generate-sqlc-testdata VERSION=20240826120919)
|
| 92 |
+
$(call print-target)
|
| 93 |
+
@if [ -z "$(VERSION)" ]; then echo "Usage: make generate-sqlc-testdata VERSION=<migration_version>"; exit 1; fi
|
| 94 |
+
VERSION=$(VERSION) ./tools/migrate/generate-sqlc-testdata.sh
|
| 95 |
+
|
| 96 |
+
.PHONY: generate
|
| 97 |
+
generate: patch-oapi-templates ## Generate code
|
| 98 |
+
$(call print-target)
|
| 99 |
+
go generate ./...
|
| 100 |
+
|
| 101 |
+
.PHONY: generate-view-sql
|
| 102 |
+
generate-view-sql: ## Generate SQL for ent.View schemas
|
| 103 |
+
$(call print-target)
|
| 104 |
+
go run ./tools/migrate/cmd/viewgen
|
| 105 |
+
|
| 106 |
+
.PHONY: build-dir
|
| 107 |
+
build-dir:
|
| 108 |
+
@mkdir -p build
|
| 109 |
+
|
| 110 |
+
.PHONY: build
|
| 111 |
+
build: build-server build-sink-worker build-benthos-collector build-balance-worker build-billing-worker build-notification-service build-jobs ## Build all binaries
|
| 112 |
+
|
| 113 |
+
COLLECTOR_DIR := $(ROOT_DIR)/collector
|
| 114 |
+
COLLECTOR_RELEASE_OUTPUT_DIR := $(ROOT_DIR)/build/release/benthos-collector_$(GOOS)_$(GOARCH)
|
| 115 |
+
|
| 116 |
+
collector-release-output-dir:
|
| 117 |
+
$(if $(GOOS),,$(error GOOS is not set))
|
| 118 |
+
$(if $(GOARCH),,$(error GOARCH is not set))
|
| 119 |
+
@mkdir -p $(COLLECTOR_RELEASE_OUTPUT_DIR)
|
| 120 |
+
|
| 121 |
+
# Cross-compile the benthos-collector binary for release archives.
|
| 122 |
+
# Usage: make build-benthos-collector-release GOOS=linux GOARCH=amd64 VERSION=v1.2.3
|
| 123 |
+
# Produces build/release/benthos-collector_<GOOS>_<GOARCH>/benthos (+ README.md, LICENSE)
|
| 124 |
+
.PHONY: build-benthos-collector-release
|
| 125 |
+
build-benthos-collector-release: | collector-release-output-dir ## Cross-compile benthos-collector for release (set GOOS/GOARCH/VERSION)
|
| 126 |
+
$(call print-target)
|
| 127 |
+
$(if $(GOOS),,$(error GOOS is not set))
|
| 128 |
+
$(if $(GOARCH),,$(error GOARCH is not set))
|
| 129 |
+
@rm -rf "$(COLLECTOR_RELEASE_OUTPUT_DIR)"/* && \
|
| 130 |
+
CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) \
|
| 131 |
+
go build -C $(COLLECTOR_DIR) -trimpath \
|
| 132 |
+
-ldflags "-s -w -X main.version=$(or $(VERSION),unknown)" \
|
| 133 |
+
-o "$(COLLECTOR_RELEASE_OUTPUT_DIR)/benthos" ./cmd && \
|
| 134 |
+
cp README.md LICENSE "$(COLLECTOR_RELEASE_OUTPUT_DIR)/"
|
| 135 |
+
|
| 136 |
+
COLLECTOR_RELEASE_NAME := benthos-collector_$(GOOS)_$(GOARCH)
|
| 137 |
+
|
| 138 |
+
# Produces build/release/benthos-collector_<GOOS>_<GOARCH>.tar.gz from the directory above.
|
| 139 |
+
.PHONY: archive-benthos-collector-release
|
| 140 |
+
archive-benthos-collector-release: ## Archive the cross-compiled benthos-collector (set GOOS/GOARCH)
|
| 141 |
+
$(call print-target)
|
| 142 |
+
$(if $(GOOS),,$(error GOOS is not set))
|
| 143 |
+
$(if $(GOARCH),,$(error GOARCH is not set))
|
| 144 |
+
@tar -C build/release -czf "build/release/$(COLLECTOR_RELEASE_NAME).tar.gz" "$(COLLECTOR_RELEASE_NAME)"
|
| 145 |
+
|
| 146 |
+
.PHONY: build-server
|
| 147 |
+
build-server: | build-dir ## Build server binary
|
| 148 |
+
$(call print-target)
|
| 149 |
+
go build -o build/server ${GO_BUILD_FLAGS} ./cmd/server
|
| 150 |
+
|
| 151 |
+
.PHONY: build-sink-worker
|
| 152 |
+
build-sink-worker: | build-dir ## Build sink-worker binary
|
| 153 |
+
$(call print-target)
|
| 154 |
+
go build -o build/sink-worker ${GO_BUILD_FLAGS} ./cmd/sink-worker
|
| 155 |
+
|
| 156 |
+
.PHONY: build-benthos-collector
|
| 157 |
+
build-benthos-collector: | build-dir ## Build benthos collector binary
|
| 158 |
+
$(call print-target)
|
| 159 |
+
go build -C $(COLLECTOR_DIR) -o ../build/benthos-collector ${GO_BUILD_FLAGS} ./cmd
|
| 160 |
+
|
| 161 |
+
.PHONY: build-balance-worker
|
| 162 |
+
build-balance-worker: | build-dir ## Build balance-worker binary
|
| 163 |
+
$(call print-target)
|
| 164 |
+
go build -o build/balance-worker ${GO_BUILD_FLAGS} ./cmd/balance-worker
|
| 165 |
+
|
| 166 |
+
.PHONY: build-billing-worker
|
| 167 |
+
build-billing-worker: | build-dir ## Build billing-worker binary
|
| 168 |
+
$(call print-target)
|
| 169 |
+
go build -o build/billing-worker ${GO_BUILD_FLAGS} ./cmd/billing-worker
|
| 170 |
+
|
| 171 |
+
.PHONY: build-notification-service
|
| 172 |
+
build-notification-service: | build-dir ## Build notification-service binary
|
| 173 |
+
$(call print-target)
|
| 174 |
+
go build -o build/notification-service ${GO_BUILD_FLAGS} ./cmd/notification-service
|
| 175 |
+
|
| 176 |
+
.PHONY: build-jobs
|
| 177 |
+
build-jobs: | build-dir ## Build jobs binary
|
| 178 |
+
$(call print-target)
|
| 179 |
+
go build -o build/jobs ${GO_BUILD_FLAGS} ./cmd/jobs
|
| 180 |
+
|
| 181 |
+
config.yaml:
|
| 182 |
+
cp config.example.yaml config.yaml
|
| 183 |
+
|
| 184 |
+
.PHONY: server
|
| 185 |
+
server: ## Run sink-worker
|
| 186 |
+
@ if [ config.yaml -ot config.example.yaml ]; then diff -u config.yaml config.example.yaml || (echo "!!! The configuration example changed. Please update your config.yaml file accordingly (or at least touch it). !!!" && false); fi
|
| 187 |
+
$(call print-target)
|
| 188 |
+
air -c ./cmd/server/.air.toml
|
| 189 |
+
|
| 190 |
+
.PHONY: sink-worker
|
| 191 |
+
sink-worker: ## Run sink-worker
|
| 192 |
+
@ if [ config.yaml -ot config.example.yaml ]; then diff -u config.yaml config.example.yaml || (echo "!!! The configuration example changed. Please update your config.yaml file accordingly (or at least touch it). !!!" && false); fi
|
| 193 |
+
$(call print-target)
|
| 194 |
+
air -c ./cmd/sink-worker/.air.toml
|
| 195 |
+
|
| 196 |
+
.PHONY: balance-worker
|
| 197 |
+
balance-worker: ## Run balance-worker
|
| 198 |
+
@ if [ config.yaml -ot config.example.yaml ]; then diff -u config.yaml config.example.yaml || (echo "!!! The configuration example changed. Please update your config.yaml file accordingly (or at least touch it). !!!" && false); fi
|
| 199 |
+
$(call print-target)
|
| 200 |
+
air -c ./cmd/balance-worker/.air.toml
|
| 201 |
+
|
| 202 |
+
.PHONY: billing-worker
|
| 203 |
+
billing-worker: ## Run billing-worker
|
| 204 |
+
@ if [ config.yaml -ot config.example.yaml ]; then diff -u config.yaml config.example.yaml || (echo "!!! The configuration example changed. Please update your config.yaml file accordingly (or at least touch it). !!!" && false); fi
|
| 205 |
+
$(call print-target)
|
| 206 |
+
air -c ./cmd/billing-worker/.air.toml
|
| 207 |
+
|
| 208 |
+
.PHONY: notification-service
|
| 209 |
+
notification-service: ## Run notification-service
|
| 210 |
+
@ if [ config.yaml -ot config.example.yaml ]; then diff -u config.yaml config.example.yaml || (echo "!!! The configuration example changed. Please update your config.yaml file accordingly (or at least touch it). !!!" && false); fi
|
| 211 |
+
$(call print-target)
|
| 212 |
+
air -c ./cmd/notification-service/.air.toml
|
| 213 |
+
|
| 214 |
+
.PHONY: llm-cost-sync
|
| 215 |
+
llm-cost-sync: ## Sync LLM cost prices from external sources
|
| 216 |
+
$(call print-target)
|
| 217 |
+
go run ./cmd/jobs llm-cost sync
|
| 218 |
+
|
| 219 |
+
.PHONY: etoe
|
| 220 |
+
etoe: ## Run e2e tests
|
| 221 |
+
$(call print-target)
|
| 222 |
+
$(MAKE) -C e2e test-local
|
| 223 |
+
|
| 224 |
+
.PHONY: etoe-slow
|
| 225 |
+
etoe-slow: ## Run e2e tests with slow tests enabled
|
| 226 |
+
$(call print-target)
|
| 227 |
+
export RUN_SLOW_TESTS=1
|
| 228 |
+
$(MAKE) -C e2e test-local
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
.PHONY: test
|
| 232 |
+
test: ## Run tests
|
| 233 |
+
$(call print-target)
|
| 234 |
+
PGPASSWORD=postgres psql -h 127.0.0.1 -U postgres postgres -c "SELECT version();" || (echo "!!! Postgres is not running. Please start it with 'docker compose up -d postgres' !!!" && false)
|
| 235 |
+
POSTGRES_HOST=127.0.0.1 gotestsum $(GOTESTSUM_FLAGS) -- $(GO_TEST_FLAGS) ./...
|
| 236 |
+
|
| 237 |
+
.PHONY: test-nocache
|
| 238 |
+
test-nocache: ## Run tests without cache
|
| 239 |
+
$(call print-target)
|
| 240 |
+
PGPASSWORD=postgres psql -h 127.0.0.1 -U postgres postgres -c "SELECT version();" || (echo "!!! Postgres is not running. Please start it with 'docker compose up -d postgres' !!!" && false)
|
| 241 |
+
POSTGRES_HOST=127.0.0.1 gotestsum $(GOTESTSUM_FLAGS) -- $(GO_TEST_FLAGS) -count=1 ./...
|
| 242 |
+
|
| 243 |
+
.PHONY: test-all
|
| 244 |
+
test-all: ## Run tests with svix dependencies, bypassing the test cache
|
| 245 |
+
$(call print-target)
|
| 246 |
+
docker compose up -d postgres svix redis
|
| 247 |
+
./tools/wait-for-compose.sh postgres svix redis
|
| 248 |
+
SVIX_HOST="localhost" SVIX_JWT_SECRET="$(SVIX_JWT_SECRET)" gotestsum $(GOTESTSUM_FLAGS) -- $(GO_TEST_FLAGS) -count=1 ./...
|
| 249 |
+
|
| 250 |
+
.PHONY: test-go-sdk
|
| 251 |
+
test-go-sdk: ## Build, vet, and test the v3 Go SDK module (api/v3/client)
|
| 252 |
+
$(call print-target)
|
| 253 |
+
cd api/v3/client && go build ./... && go vet ./... && gotestsum $(GOTESTSUM_FLAGS) -- ./...
|
| 254 |
+
|
| 255 |
+
.PHONY: lint
|
| 256 |
+
lint: lint-go lint-api-spec lint-openapi lint-helm ## Run linters
|
| 257 |
+
$(call print-target)
|
| 258 |
+
|
| 259 |
+
.PHONY: lint-api-spec
|
| 260 |
+
lint-api-spec: ## Lint OpenAPI spec
|
| 261 |
+
$(call print-target)
|
| 262 |
+
$(MAKE) -C api/spec lint
|
| 263 |
+
|
| 264 |
+
.PHONY: test-api-spec
|
| 265 |
+
test-api-spec: ## Run AIP TypeScript SDK and emitter tests
|
| 266 |
+
$(call print-target)
|
| 267 |
+
$(MAKE) -C api/spec test
|
| 268 |
+
|
| 269 |
+
.PHONY: lint-openapi
|
| 270 |
+
lint-openapi: ## Lint OpenAPI spec
|
| 271 |
+
$(call print-target)
|
| 272 |
+
spectral lint api/openapi.yaml api/openapi.cloud.yaml api/v3/openapi.yaml
|
| 273 |
+
|
| 274 |
+
.PHONY: lint-helm
|
| 275 |
+
lint-helm: ## Lint Helm charts
|
| 276 |
+
$(call print-target)
|
| 277 |
+
helm lint deploy/charts/openmeter
|
| 278 |
+
helm lint deploy/charts/benthos-collector
|
| 279 |
+
|
| 280 |
+
# Package a helm chart for release.
|
| 281 |
+
# Usage: make package-helm-chart CHART=openmeter VERSION=v1.2.3
|
| 282 |
+
# Produces build/helm/<CHART>-<version-without-v>.tgz
|
| 283 |
+
.PHONY: package-helm-chart
|
| 284 |
+
package-helm-chart: ## Package a helm chart for release (set CHART and VERSION)
|
| 285 |
+
$(call print-target)
|
| 286 |
+
@if [ -z "$(CHART)" ] || [ -z "$(VERSION)" ]; then echo "ERROR: CHART and VERSION are required"; exit 1; fi
|
| 287 |
+
@chart_dir="deploy/charts/$(CHART)" && \
|
| 288 |
+
version_no_v="$(VERSION:v%=%)" && \
|
| 289 |
+
mkdir -p build/helm && \
|
| 290 |
+
helm-docs --log-level info -s file -c "$$chart_dir" \
|
| 291 |
+
-t "deploy/charts/template.md" -t "$$chart_dir/README.tmpl.md" && \
|
| 292 |
+
helm dependency update "$$chart_dir" && \
|
| 293 |
+
helm package "$$chart_dir" \
|
| 294 |
+
--version "$$version_no_v" \
|
| 295 |
+
--app-version "$(VERSION)" \
|
| 296 |
+
--destination build/helm
|
| 297 |
+
|
| 298 |
+
.PHONY: lint-go
|
| 299 |
+
lint-go: ## Lint Go code
|
| 300 |
+
$(call print-target)
|
| 301 |
+
golangci-lint run -v $(GO_LINT_PATH)
|
| 302 |
+
cd api/v3/client && golangci-lint run -v ./...
|
| 303 |
+
go vet -C e2e ./...
|
| 304 |
+
cd e2e && golangci-lint run -v ./...
|
| 305 |
+
|
| 306 |
+
.PHONY: lint-go-fast
|
| 307 |
+
lint-go-fast: ## Lint Go bug-finding checks (set GO_LINT_PATH=./openmeter/ledger/...)
|
| 308 |
+
$(call print-target)
|
| 309 |
+
golangci-lint run -v --config .golangci-fast.yaml $(GO_LINT_PATH)
|
| 310 |
+
|
| 311 |
+
.PHONY: lint-go-style
|
| 312 |
+
lint-go-style: ## Lint Go formatting and import order
|
| 313 |
+
$(call print-target)
|
| 314 |
+
golangci-lint fmt -v -d $(GO_LINT_PATH)
|
| 315 |
+
|
| 316 |
+
.PHONY: lint-go-head
|
| 317 |
+
lint-go-head: ## Lint Go code since last commit
|
| 318 |
+
$(call print-target)
|
| 319 |
+
golangci-lint run --new-from-rev=HEAD~1
|
| 320 |
+
|
| 321 |
+
.PHONY: ci
|
| 322 |
+
ci: ## Run CI checks
|
| 323 |
+
$(call print-target)
|
| 324 |
+
$(MAKE) generate-all
|
| 325 |
+
$(MAKE) -j 10 lint test etoe
|
| 326 |
+
|
| 327 |
+
.PHONY: fmt
|
| 328 |
+
fmt: ## Format code
|
| 329 |
+
$(call print-target)
|
| 330 |
+
golangci-lint run --fix
|
| 331 |
+
|
| 332 |
+
.PHONY: mod
|
| 333 |
+
mod: ## go mod tidy
|
| 334 |
+
$(call print-target)
|
| 335 |
+
go mod tidy
|
| 336 |
+
go mod tidy -C collector
|
| 337 |
+
go mod tidy -C api/v3/client
|
| 338 |
+
go mod tidy -C e2e
|
| 339 |
+
|
| 340 |
+
.PHONY: seed
|
| 341 |
+
seed: ## Seed OpenMeter with test data
|
| 342 |
+
$(call print-target)
|
| 343 |
+
benthos -c etc/seed/seed.yaml
|
| 344 |
+
|
| 345 |
+
.PHONY: help
|
| 346 |
+
.DEFAULT_GOAL := help
|
| 347 |
+
help:
|
| 348 |
+
@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
| 349 |
+
|
| 350 |
+
# Variable outputting/exporting rules
|
| 351 |
+
var-%: ; @echo $($*)
|
| 352 |
+
varexport-%: ; @echo $*=$($*)
|
| 353 |
+
|
| 354 |
+
define print-target
|
| 355 |
+
@printf "Executing target: \033[36m$@\033[0m\n"
|
| 356 |
+
endef
|
README.md
CHANGED
|
@@ -1,10 +1,163 @@
|
|
| 1 |
---
|
| 2 |
title: Openmeter
|
| 3 |
-
emoji: 🏃
|
| 4 |
-
colorFrom: blue
|
| 5 |
-
colorTo: red
|
| 6 |
sdk: docker
|
| 7 |
-
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Openmeter
|
|
|
|
|
|
|
|
|
|
| 3 |
sdk: docker
|
| 4 |
+
app_port: 7860
|
| 5 |
---
|
| 6 |
|
| 7 |
+
<div align="center">
|
| 8 |
+
|
| 9 |
+

|
| 10 |
+
|
| 11 |
+
# OpenMeter
|
| 12 |
+
|
| 13 |
+
The open-source metering and billing platform
|
| 14 |
+
for AI, agentic and DevTool monetization.
|
| 15 |
+
|
| 16 |
+
[Docs](https://openmeter.io/docs) |
|
| 17 |
+
[Hosted](https://cloud.konghq.com/register?utm_campaign=metering_and_billing) |
|
| 18 |
+
[Blog](https://openmeter.io/blog) |
|
| 19 |
+
[Contributing](CONTRIBUTING.md)
|
| 20 |
+
|
| 21 |
+
[](https://github.com/openmeterio/openmeter/releases/latest)
|
| 22 |
+
[](https://github.com/openmeterio/openmeter/actions/workflows/ci.yaml)
|
| 23 |
+
[](https://goreportcard.com/report/github.com/openmeterio/openmeter)
|
| 24 |
+

|
| 25 |
+
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
---
|
| 29 |
+
|
| 30 |
+
OpenMeter is a real-time metering and billing engine that
|
| 31 |
+
helps you track usage, enforce limits, manage subscriptions,
|
| 32 |
+
and automate invoicing — all in one platform. Ingest events
|
| 33 |
+
via a simple API, define meters with flexible aggregations,
|
| 34 |
+
and connect usage data to billing, entitlements, and
|
| 35 |
+
customer-facing dashboards.
|
| 36 |
+
|
| 37 |
+
## Features
|
| 38 |
+
|
| 39 |
+
- **Usage Metering** — Ingest events in
|
| 40 |
+
[CloudEvents](https://cloudevents.io) format, define meters
|
| 41 |
+
with flexible aggregations (SUM, COUNT, AVG, MIN, MAX),
|
| 42 |
+
and query usage in real time.
|
| 43 |
+
- **Usage-Based Billing** — Generate invoices from metered
|
| 44 |
+
usage. Supports tiered, graduated, and flat-fee pricing
|
| 45 |
+
with automated invoice lifecycle management.
|
| 46 |
+
- **Usage Limits and Entitlements** — Enforce usage quotas
|
| 47 |
+
per feature with real-time balance tracking, boolean
|
| 48 |
+
feature flags, and grace periods.
|
| 49 |
+
- **Product Catalog** — Define plans, add-ons, features, and
|
| 50 |
+
rate cards. Manage subscriptions with mid-cycle changes,
|
| 51 |
+
prorating, and alignment.
|
| 52 |
+
- **Prepaid Credits** — Support paid or promotional credit grants
|
| 53 |
+
with priority-based burn-down and expiration.
|
| 54 |
+
- **Customer Portal** — Token-based self-service dashboards
|
| 55 |
+
so your customers can see their own usage.
|
| 56 |
+
- **Notifications** — Webhook-based alerts with configurable
|
| 57 |
+
rules and channels for usage thresholds and billing events.
|
| 58 |
+
- **LLM Cost Tracking** — First-class support for metering
|
| 59 |
+
AI token usage and computing model-specific costs.
|
| 60 |
+
|
| 61 |
+
## Getting Started
|
| 62 |
+
|
| 63 |
+
### Cloud
|
| 64 |
+
|
| 65 |
+
The fastest way to start.
|
| 66 |
+
[Start for free](https://cloud.konghq.com/register?utm_campaign=metering_and_billing)
|
| 67 |
+
and begin metering and billing in minutes —
|
| 68 |
+
no infrastructure to manage.
|
| 69 |
+
|
| 70 |
+
### Self-Hosted
|
| 71 |
+
|
| 72 |
+
Run OpenMeter locally with Docker Compose:
|
| 73 |
+
|
| 74 |
+
```sh
|
| 75 |
+
git clone git@github.com:openmeterio/openmeter.git
|
| 76 |
+
cd openmeter/quickstart
|
| 77 |
+
docker compose up -d
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
Then ingest your first event:
|
| 81 |
+
|
| 82 |
+
```sh
|
| 83 |
+
curl -X POST http://localhost:48888/api/v1/events \
|
| 84 |
+
-H 'Content-Type: application/cloudevents+json' \
|
| 85 |
+
--data-raw '{
|
| 86 |
+
"specversion": "1.0",
|
| 87 |
+
"type": "request",
|
| 88 |
+
"id": "00001",
|
| 89 |
+
"time": "2026-07-07T00:00:00.001Z",
|
| 90 |
+
"source": "my-service",
|
| 91 |
+
"subject": "customer-1",
|
| 92 |
+
"data": { "method": "GET", "route": "/api/hello" }
|
| 93 |
+
}'
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
Query your usage:
|
| 97 |
+
|
| 98 |
+
```sh
|
| 99 |
+
curl 'http://localhost:48888/api/v1/meters/api_requests_total/query?windowSize=HOUR' | jq
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
See the full [quickstart guide](/quickstart) for more details.
|
| 103 |
+
|
| 104 |
+
### Deploy to Production
|
| 105 |
+
|
| 106 |
+
Deploy to Kubernetes using our
|
| 107 |
+
[Helm chart](https://openmeter.io/docs/deploy/kubernetes).
|
| 108 |
+
|
| 109 |
+
## SDKs
|
| 110 |
+
|
| 111 |
+
| Language | Package | Source |
|
| 112 |
+
|----------------------|--------------------------------------------------------------------------------|----------------------------------------------------|
|
| 113 |
+
| Go | [openmeter](https://pkg.go.dev/github.com/openmeterio/openmeter/api/client/go) | [api/client/go](/api/client/go) |
|
| 114 |
+
| JavaScript / Node.js | [@openmeter/sdk](https://www.npmjs.com/package/@openmeter/sdk) | [api/client/javascript](/api/client/javascript) |
|
| 115 |
+
| Python | [openmeter](https://pypi.org/project/openmeter) | [api/client/python](/api/client/python) |
|
| 116 |
+
|
| 117 |
+
Don't see your language? Use the
|
| 118 |
+
[OpenAPI spec](https://github.com/openmeterio/openmeter/blob/main/api/openapi.yaml)
|
| 119 |
+
directly or
|
| 120 |
+
[request an SDK](https://github.com/openmeterio/openmeter/issues/new?assignees=&labels=area%2Fapi%2Ckind%2Ffeature&projects=&template=feature_request.yaml).
|
| 121 |
+
|
| 122 |
+
## Architecture
|
| 123 |
+
|
| 124 |
+
OpenMeter is built in Go with a stack optimized for
|
| 125 |
+
high-volume event ingestion and real-time aggregation:
|
| 126 |
+
|
| 127 |
+
| Component | Role |
|
| 128 |
+
|--------------------------|----------------------------------------------------------|
|
| 129 |
+
| **PostgreSQL** (Ent ORM) | Billing, subscriptions, entitlements, product catalog |
|
| 130 |
+
| **ClickHouse** | Real-time usage aggregation and analytics |
|
| 131 |
+
| **Kafka** | Event streaming and ingestion pipeline |
|
| 132 |
+
| **TypeSpec** | API-first design — OpenAPI spec and SDKs from TypeSpec |
|
| 133 |
+
|
| 134 |
+
## Community
|
| 135 |
+
|
| 136 |
+
We'd love to have you involved:
|
| 137 |
+
|
| 138 |
+
- **[Contributing](CONTRIBUTING.md)** — Start here if you
|
| 139 |
+
want to contribute code.
|
| 140 |
+
- **[Code of Conduct](CODE_OF_CONDUCT.md)** — Our community
|
| 141 |
+
guidelines.
|
| 142 |
+
- **[Blog](https://openmeter.io/blog)** — Product updates
|
| 143 |
+
and engineering deep dives.
|
| 144 |
+
|
| 145 |
+
## Development
|
| 146 |
+
|
| 147 |
+
Prerequisites: [Nix](https://nixos.org/download.html) and
|
| 148 |
+
[direnv](https://direnv.net/docs/installation.html) are
|
| 149 |
+
recommended. See [CONTRIBUTING.md](CONTRIBUTING.md) for
|
| 150 |
+
detailed setup instructions.
|
| 151 |
+
|
| 152 |
+
```sh
|
| 153 |
+
make up # Start dependencies (Postgres, Kafka, ClickHouse)
|
| 154 |
+
make server # Run the API server with hot reload
|
| 155 |
+
make test # Run tests
|
| 156 |
+
make lint # Run linters
|
| 157 |
+
```
|
| 158 |
+
|
| 159 |
+
## License
|
| 160 |
+
|
| 161 |
+
Licensed under [Apache 2.0](LICENSE).
|
| 162 |
+
|
| 163 |
+
[](https://app.fossa.com/projects/custom%2B38090%2Fgithub.com%2Fopenmeterio%2Fopenmeter?ref=badge_large)
|
SECURITY.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Security Policies and Procedures
|
| 2 |
+
|
| 3 |
+
This document outlines security procedures and general policies for OpenMeter.
|
| 4 |
+
|
| 5 |
+
- [Reporting a vulnerability](#reporting-a-vulnerability)
|
| 6 |
+
- [Disclosure policy](#disclosure-policy)
|
| 7 |
+
|
| 8 |
+
## Reporting a vulnerability
|
| 9 |
+
|
| 10 |
+
The OpenMeter team and community take all security issues seriously. Thank you for improving the security of our projects.
|
| 11 |
+
We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions.
|
| 12 |
+
|
| 13 |
+
**Report security issues using GitHub's [vulnerability reporting feature](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability).**
|
| 14 |
+
|
| 15 |
+
_Alternatively, you can send an email to `security@openmeter.io`._
|
| 16 |
+
|
| 17 |
+
Somebody from the OpenMeter team will acknowledge your report within 48 hours,
|
| 18 |
+
and will follow up with a more detailed response after that indicating the next steps in handling your report.
|
| 19 |
+
After the initial reply to your report, the team will endeavor to keep you informed of the progress towards a fix and full announcement,
|
| 20 |
+
and may ask for additional information or guidance.
|
| 21 |
+
|
| 22 |
+
## Disclosure policy
|
| 23 |
+
|
| 24 |
+
When the team receives a vulnerability report, they will assign it to a primary handler.
|
| 25 |
+
This person will coordinate the fix and release process, involving the following steps:
|
| 26 |
+
|
| 27 |
+
- Confirm the problem and determine the affected versions.
|
| 28 |
+
- Audit code to find any potential similar problems.
|
| 29 |
+
- Prepare fixes for all releases still under maintenance. These fixes will be released as quickly as possible.
|
api/api.gen.go
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
api/api.go
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//go:generate go tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=codegen.yaml ./openapi.yaml
|
| 2 |
+
package api
|
api/client/go/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenMeter Go SDK
|
| 2 |
+
|
| 3 |
+
## Install
|
| 4 |
+
|
| 5 |
+
```sh
|
| 6 |
+
go get github.com/openmeterio/openmeter/api/client/go@v1.0.0-beta.53
|
| 7 |
+
```
|
| 8 |
+
|
| 9 |
+
## Usage
|
| 10 |
+
|
| 11 |
+
Initialize client.
|
| 12 |
+
|
| 13 |
+
```go
|
| 14 |
+
import (
|
| 15 |
+
cloudevents "github.com/cloudevents/sdk-go/v2/event"
|
| 16 |
+
om "github.com/openmeterio/openmeter/api/client/go"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
func main() {
|
| 20 |
+
// Initialize OpenMeter client
|
| 21 |
+
om, err := openmeter.NewClientWithResponses("http://localhost:8888")
|
| 22 |
+
if err != nil {
|
| 23 |
+
panic(err.Error())
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// Use OpenMeter client
|
| 27 |
+
// ...
|
| 28 |
+
}
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
### Ingest Event
|
| 32 |
+
|
| 33 |
+
Report usage to OpenMeter.
|
| 34 |
+
|
| 35 |
+
```go
|
| 36 |
+
e := cloudevents.New()
|
| 37 |
+
e.SetID("00001")
|
| 38 |
+
e.SetSource("my-app")
|
| 39 |
+
e.SetType("tokens")
|
| 40 |
+
e.SetSubject("user-id")
|
| 41 |
+
e.SetTime(time.Now())
|
| 42 |
+
e.SetData("application/json", map[string]string{
|
| 43 |
+
"tokens": "15",
|
| 44 |
+
"model": "gpt-4",
|
| 45 |
+
})
|
| 46 |
+
|
| 47 |
+
_, err := client.IngestEventWithResponse(ctx, e)
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
### Query Meter
|
| 51 |
+
|
| 52 |
+
Retreive usage from OpenMeter.
|
| 53 |
+
|
| 54 |
+
```go
|
| 55 |
+
slug := "token-usage"
|
| 56 |
+
subject := []string{"user-id"}
|
| 57 |
+
from, _ := time.Parse(time.RFC3339, "2023-01-01T00:00:00Z")
|
| 58 |
+
to, _ := time.Parse(time.RFC3339, "2023-01-02T00:00:00Z")
|
| 59 |
+
resp, _ := client.QueryMeterWithResponse(ctx, slug, &om.QueryMeterParams{
|
| 60 |
+
Subject: &subject,
|
| 61 |
+
From: &from,
|
| 62 |
+
To: &to,
|
| 63 |
+
})
|
| 64 |
+
// resp.JSON200.Data
|
| 65 |
+
```
|
api/client/go/client.gen.go
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
api/client/go/client.go
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//go:generate go tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=codegen.yaml ../../openapi.cloud.yaml
|
| 2 |
+
package openmeter
|
| 3 |
+
|
| 4 |
+
import (
|
| 5 |
+
"context"
|
| 6 |
+
"fmt"
|
| 7 |
+
"net/http"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
func NewAuthClientWithResponses(server string, apiSecret string, opts ...ClientOption) (*ClientWithResponses, error) {
|
| 11 |
+
o := []ClientOption{WithRequestEditorFn(func(ctx context.Context, req *http.Request) error {
|
| 12 |
+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiSecret))
|
| 13 |
+
return nil
|
| 14 |
+
})}
|
| 15 |
+
o = append(opts, o...)
|
| 16 |
+
|
| 17 |
+
return NewClientWithResponses(server, o...)
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
func NewAuthClient(server string, apiSecret string, opts ...ClientOption) (*Client, error) {
|
| 21 |
+
o := []ClientOption{WithRequestEditorFn(func(ctx context.Context, req *http.Request) error {
|
| 22 |
+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiSecret))
|
| 23 |
+
return nil
|
| 24 |
+
})}
|
| 25 |
+
o = append(opts, o...)
|
| 26 |
+
|
| 27 |
+
return NewClient(server, o...)
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
// IngestEvents is a wrapper around generated client's IngestEventsWithApplicationCloudeventsPlusJSONBody
|
| 31 |
+
func (c *Client) IngestEvent(ctx context.Context, event Event, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
| 32 |
+
return c.IngestEventsWithApplicationCloudeventsPlusJSONBody(ctx, event, reqEditors...)
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// IngestEvents is a wrapper around generated client's IngestEventsWithApplicationCloudeventsBatchPlusJSONBody
|
| 36 |
+
func (c *Client) IngestEventBatch(ctx context.Context, events []Event, reqEditors ...RequestEditorFn) (*http.Response, error) {
|
| 37 |
+
return c.IngestEventsWithApplicationCloudeventsBatchPlusJSONBody(ctx, events, reqEditors...)
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// IngestEventsWithResponse is a wrapper around generated client's IngestEventsWithApplicationCloudeventsPlusJSONBodyWithResponse
|
| 41 |
+
func (c *ClientWithResponses) IngestEventWithResponse(ctx context.Context, event Event, reqEditors ...RequestEditorFn) (*IngestEventsResponse, error) {
|
| 42 |
+
return c.IngestEventsWithApplicationCloudeventsPlusJSONBodyWithResponse(ctx, event, reqEditors...)
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// IngestEventsWithResponse is a wrapper around generated client's IngestEventsWithApplicationCloudeventsBatchPlusJSONBodyWithResponse
|
| 46 |
+
func (c *ClientWithResponses) IngestEventBatchWithResponse(ctx context.Context, events []Event, reqEditors ...RequestEditorFn) (*IngestEventsResponse, error) {
|
| 47 |
+
return c.IngestEventsWithApplicationCloudeventsBatchPlusJSONBodyWithResponse(ctx, events, reqEditors...)
|
| 48 |
+
}
|
api/client/go/client_test.go
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package openmeter
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"context"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"net/http"
|
| 7 |
+
"net/http/httptest"
|
| 8 |
+
"testing"
|
| 9 |
+
"time"
|
| 10 |
+
|
| 11 |
+
cloudevents "github.com/cloudevents/sdk-go/v2/event"
|
| 12 |
+
"github.com/samber/lo"
|
| 13 |
+
"github.com/stretchr/testify/assert"
|
| 14 |
+
|
| 15 |
+
"github.com/openmeterio/openmeter/openmeter/meter"
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
func TestIngest(t *testing.T) {
|
| 19 |
+
ctx := context.Background()
|
| 20 |
+
|
| 21 |
+
// Create a mock server to test the client
|
| 22 |
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 23 |
+
assert.Equal(t, http.MethodPost, r.Method)
|
| 24 |
+
assert.Equal(t, "/api/v1/events", r.URL.Path)
|
| 25 |
+
assert.Equal(t, "application/cloudevents+json", r.Header.Get("Content-Type"))
|
| 26 |
+
|
| 27 |
+
w.WriteHeader(http.StatusOK)
|
| 28 |
+
}))
|
| 29 |
+
defer server.Close()
|
| 30 |
+
|
| 31 |
+
// Create a client with the mock server
|
| 32 |
+
om, err := NewClientWithResponses(server.URL)
|
| 33 |
+
assert.NoError(t, err)
|
| 34 |
+
|
| 35 |
+
resp, err := om.IngestEventWithResponse(ctx, mockEvent())
|
| 36 |
+
assert.NoError(t, err)
|
| 37 |
+
|
| 38 |
+
assert.Equal(t, http.StatusOK, resp.StatusCode())
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
func TestIngestBatch(t *testing.T) {
|
| 42 |
+
ctx := context.Background()
|
| 43 |
+
|
| 44 |
+
// Create a mock server to test the client
|
| 45 |
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 46 |
+
assert.Equal(t, http.MethodPost, r.Method)
|
| 47 |
+
assert.Equal(t, "/api/v1/events", r.URL.Path)
|
| 48 |
+
assert.Equal(t, "application/cloudevents-batch+json", r.Header.Get("Content-Type"))
|
| 49 |
+
|
| 50 |
+
w.WriteHeader(http.StatusOK)
|
| 51 |
+
}))
|
| 52 |
+
defer server.Close()
|
| 53 |
+
|
| 54 |
+
// Create a client with the mock server
|
| 55 |
+
om, err := NewClientWithResponses(server.URL)
|
| 56 |
+
assert.NoError(t, err)
|
| 57 |
+
|
| 58 |
+
resp, err := om.IngestEventBatchWithResponse(ctx, []cloudevents.Event{mockEvent()})
|
| 59 |
+
assert.NoError(t, err)
|
| 60 |
+
|
| 61 |
+
assert.Equal(t, http.StatusOK, resp.StatusCode())
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
func TetsListEvents(t *testing.T) {
|
| 65 |
+
ctx := context.Background()
|
| 66 |
+
|
| 67 |
+
event := mockEvent()
|
| 68 |
+
|
| 69 |
+
// Create a mock server to test the client
|
| 70 |
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 71 |
+
assert.Equal(t, http.MethodGet, r.Method)
|
| 72 |
+
assert.Equal(t, "/api/v1/events", r.URL.Path)
|
| 73 |
+
|
| 74 |
+
w.Header().Set("Content-Type", "application/json")
|
| 75 |
+
w.WriteHeader(http.StatusOK)
|
| 76 |
+
|
| 77 |
+
err := json.NewEncoder(w).Encode([]Event{event})
|
| 78 |
+
assert.NoError(t, err)
|
| 79 |
+
}))
|
| 80 |
+
defer server.Close()
|
| 81 |
+
|
| 82 |
+
// Create a client with the mock server
|
| 83 |
+
om, err := NewClientWithResponses(server.URL)
|
| 84 |
+
assert.NoError(t, err)
|
| 85 |
+
|
| 86 |
+
resp, err := om.ListEventsWithResponse(ctx, &ListEventsParams{})
|
| 87 |
+
assert.NoError(t, err)
|
| 88 |
+
|
| 89 |
+
assert.Equal(t, http.StatusOK, resp.StatusCode())
|
| 90 |
+
assert.Equal(t, lo.ToPtr([]Event{event}), resp.JSON200)
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
func TestAuth(t *testing.T) {
|
| 94 |
+
ctx := context.Background()
|
| 95 |
+
|
| 96 |
+
// Create a mock server to test the client
|
| 97 |
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 98 |
+
assert.Equal(t, http.MethodPost, r.Method)
|
| 99 |
+
assert.Equal(t, "/api/v1/events", r.URL.Path)
|
| 100 |
+
assert.Equal(t, "application/cloudevents+json", r.Header.Get("Content-Type"))
|
| 101 |
+
assert.Equal(t, "Bearer test-api-token", r.Header.Get("Authorization"))
|
| 102 |
+
|
| 103 |
+
w.WriteHeader(http.StatusOK)
|
| 104 |
+
}))
|
| 105 |
+
defer server.Close()
|
| 106 |
+
|
| 107 |
+
// Create a client with the mock server
|
| 108 |
+
om, err := NewAuthClientWithResponses(server.URL, "test-api-token")
|
| 109 |
+
assert.NoError(t, err)
|
| 110 |
+
|
| 111 |
+
resp, err := om.IngestEventWithResponse(ctx, mockEvent())
|
| 112 |
+
assert.NoError(t, err)
|
| 113 |
+
|
| 114 |
+
assert.Equal(t, http.StatusOK, resp.StatusCode())
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
func TestGetMeter(t *testing.T) {
|
| 118 |
+
ctx := context.Background()
|
| 119 |
+
|
| 120 |
+
meter := Meter{
|
| 121 |
+
Slug: "meter-1",
|
| 122 |
+
Description: lo.ToPtr("Test Meter"),
|
| 123 |
+
Aggregation: MeterAggregation(meter.MeterAggregationSum),
|
| 124 |
+
ValueProperty: lo.ToPtr("$.tokens"),
|
| 125 |
+
GroupBy: lo.ToPtr(map[string]string{"model": "$.model", "type": "$.type"}),
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
// Create a mock server to test the client
|
| 129 |
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 130 |
+
assert.Equal(t, http.MethodGet, r.Method)
|
| 131 |
+
assert.Equal(t, "/api/v1/meters/meter-1", r.URL.Path)
|
| 132 |
+
|
| 133 |
+
w.Header().Set("Content-Type", "application/json")
|
| 134 |
+
w.WriteHeader(http.StatusOK)
|
| 135 |
+
|
| 136 |
+
err := json.NewEncoder(w).Encode(meter)
|
| 137 |
+
assert.NoError(t, err)
|
| 138 |
+
}))
|
| 139 |
+
defer server.Close()
|
| 140 |
+
|
| 141 |
+
// Create a client with the mock server
|
| 142 |
+
om, err := NewClientWithResponses(server.URL)
|
| 143 |
+
assert.NoError(t, err)
|
| 144 |
+
|
| 145 |
+
resp, err := om.GetMeterWithResponse(ctx, "meter-1")
|
| 146 |
+
assert.NoError(t, err)
|
| 147 |
+
|
| 148 |
+
assert.Equal(t, http.StatusOK, resp.StatusCode())
|
| 149 |
+
assert.Equal(t, &meter, resp.JSON200)
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
func TestListMeters(t *testing.T) {
|
| 153 |
+
ctx := context.Background()
|
| 154 |
+
|
| 155 |
+
meters := []Meter{
|
| 156 |
+
{
|
| 157 |
+
Slug: "meter-1",
|
| 158 |
+
Description: lo.ToPtr("Test Meter"),
|
| 159 |
+
Aggregation: MeterAggregation(meter.MeterAggregationSum),
|
| 160 |
+
ValueProperty: lo.ToPtr("$.tokens"),
|
| 161 |
+
GroupBy: lo.ToPtr(map[string]string{"model": "$.model", "type": "$.type"}),
|
| 162 |
+
},
|
| 163 |
+
{
|
| 164 |
+
Slug: "meter-2",
|
| 165 |
+
Description: lo.ToPtr("Test Meter 2"),
|
| 166 |
+
Aggregation: MeterAggregation(meter.MeterAggregationSum),
|
| 167 |
+
ValueProperty: lo.ToPtr("$.tokens"),
|
| 168 |
+
GroupBy: lo.ToPtr(map[string]string{"model": "$.model", "type": "$.type"}),
|
| 169 |
+
},
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
// Create a mock server to test the client
|
| 173 |
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 174 |
+
assert.Equal(t, http.MethodGet, r.Method)
|
| 175 |
+
assert.Equal(t, "/api/v1/meters", r.URL.Path)
|
| 176 |
+
|
| 177 |
+
w.Header().Set("Content-Type", "application/json")
|
| 178 |
+
w.WriteHeader(http.StatusOK)
|
| 179 |
+
|
| 180 |
+
err := json.NewEncoder(w).Encode(meters)
|
| 181 |
+
assert.NoError(t, err)
|
| 182 |
+
}))
|
| 183 |
+
defer server.Close()
|
| 184 |
+
|
| 185 |
+
// Create a client with the mock server
|
| 186 |
+
om, err := NewClientWithResponses(server.URL)
|
| 187 |
+
assert.NoError(t, err)
|
| 188 |
+
|
| 189 |
+
resp, err := om.ListMetersWithResponse(ctx, &ListMetersParams{})
|
| 190 |
+
assert.NoError(t, err)
|
| 191 |
+
|
| 192 |
+
assert.Equal(t, http.StatusOK, resp.StatusCode())
|
| 193 |
+
assert.Equal(t, &meters, resp.JSON200)
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
func TestMeterQuery(t *testing.T) {
|
| 197 |
+
ctx := context.Background()
|
| 198 |
+
|
| 199 |
+
result := MeterQueryResult{
|
| 200 |
+
Data: []MeterQueryRow{
|
| 201 |
+
{
|
| 202 |
+
Subject: lo.ToPtr("customer-1"),
|
| 203 |
+
WindowStart: time.Now().UTC(),
|
| 204 |
+
WindowEnd: time.Now().UTC(),
|
| 205 |
+
Value: 123,
|
| 206 |
+
},
|
| 207 |
+
},
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
// Create a mock server to test the client
|
| 211 |
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 212 |
+
assert.Equal(t, http.MethodGet, r.Method)
|
| 213 |
+
assert.Equal(t, "/api/v1/meters/meter-1/query", r.URL.Path)
|
| 214 |
+
|
| 215 |
+
w.Header().Set("Content-Type", "application/json")
|
| 216 |
+
w.WriteHeader(http.StatusOK)
|
| 217 |
+
|
| 218 |
+
err := json.NewEncoder(w).Encode(result)
|
| 219 |
+
assert.NoError(t, err)
|
| 220 |
+
}))
|
| 221 |
+
defer server.Close()
|
| 222 |
+
|
| 223 |
+
// Create a client with the mock server
|
| 224 |
+
om, err := NewClientWithResponses(server.URL)
|
| 225 |
+
assert.NoError(t, err)
|
| 226 |
+
|
| 227 |
+
subjectFilter := []string{"customer-1"}
|
| 228 |
+
|
| 229 |
+
resp, err := om.QueryMeterWithResponse(ctx, "meter-1", &QueryMeterParams{
|
| 230 |
+
Subject: &subjectFilter,
|
| 231 |
+
})
|
| 232 |
+
assert.NoError(t, err)
|
| 233 |
+
|
| 234 |
+
assert.Equal(t, http.StatusOK, resp.StatusCode())
|
| 235 |
+
assert.Equal(t, &result, resp.JSON200)
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
func TestListSubjects(t *testing.T) {
|
| 239 |
+
ctx := context.Background()
|
| 240 |
+
|
| 241 |
+
subjects := []Subject{
|
| 242 |
+
{
|
| 243 |
+
Key: "customer-1",
|
| 244 |
+
DisplayName: lo.ToPtr("Customer 1"),
|
| 245 |
+
},
|
| 246 |
+
{
|
| 247 |
+
Key: "customer-2",
|
| 248 |
+
DisplayName: lo.ToPtr("Customer 2"),
|
| 249 |
+
},
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
// Create a mock server to test the client
|
| 253 |
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 254 |
+
assert.Equal(t, http.MethodGet, r.Method)
|
| 255 |
+
assert.Equal(t, "/api/v1/subjects", r.URL.Path)
|
| 256 |
+
|
| 257 |
+
w.Header().Set("Content-Type", "application/json")
|
| 258 |
+
w.WriteHeader(http.StatusOK)
|
| 259 |
+
|
| 260 |
+
err := json.NewEncoder(w).Encode(subjects)
|
| 261 |
+
assert.NoError(t, err)
|
| 262 |
+
}))
|
| 263 |
+
defer server.Close()
|
| 264 |
+
|
| 265 |
+
// Create a client with the mock server
|
| 266 |
+
om, err := NewClientWithResponses(server.URL)
|
| 267 |
+
assert.NoError(t, err)
|
| 268 |
+
|
| 269 |
+
resp, err := om.ListSubjectsWithResponse(ctx)
|
| 270 |
+
assert.NoError(t, err)
|
| 271 |
+
|
| 272 |
+
assert.Equal(t, http.StatusOK, resp.StatusCode())
|
| 273 |
+
assert.Equal(t, &subjects, resp.JSON200)
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
func TestUpsertSubject(t *testing.T) {
|
| 277 |
+
ctx := context.Background()
|
| 278 |
+
|
| 279 |
+
subject := Subject{
|
| 280 |
+
Key: "customer-1",
|
| 281 |
+
DisplayName: lo.ToPtr("Customer 1"),
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
// Create a mock server to test the client
|
| 285 |
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 286 |
+
assert.Equal(t, http.MethodPost, r.Method)
|
| 287 |
+
assert.Equal(t, "/api/v1/subjects", r.URL.Path)
|
| 288 |
+
|
| 289 |
+
w.Header().Set("Content-Type", "application/json")
|
| 290 |
+
w.WriteHeader(http.StatusOK)
|
| 291 |
+
|
| 292 |
+
err := json.NewEncoder(w).Encode([]Subject{subject})
|
| 293 |
+
assert.NoError(t, err)
|
| 294 |
+
}))
|
| 295 |
+
defer server.Close()
|
| 296 |
+
|
| 297 |
+
// Create a client with the mock server
|
| 298 |
+
om, err := NewClientWithResponses(server.URL)
|
| 299 |
+
assert.NoError(t, err)
|
| 300 |
+
|
| 301 |
+
resp, err := om.UpsertSubjectWithResponse(ctx, []SubjectUpsert{
|
| 302 |
+
{
|
| 303 |
+
Key: "customer-1",
|
| 304 |
+
DisplayName: lo.ToPtr("Customer 1"),
|
| 305 |
+
},
|
| 306 |
+
})
|
| 307 |
+
assert.NoError(t, err)
|
| 308 |
+
|
| 309 |
+
assert.Equal(t, http.StatusOK, resp.StatusCode())
|
| 310 |
+
assert.Equal(t, lo.ToPtr([]Subject{subject}), resp.JSON200)
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
// mockEvent creates a mock CloudEvent for testing
|
| 314 |
+
func mockEvent() cloudevents.Event {
|
| 315 |
+
e := cloudevents.New()
|
| 316 |
+
eventTime, _ := time.Parse(time.RFC3339, "2024-11-05T22:35:52.457Z")
|
| 317 |
+
e.SetTime(eventTime)
|
| 318 |
+
e.SetID("ec2672e8-458d-4c5e-8a3c-f3235dd38ba5")
|
| 319 |
+
e.SetSource("my-app")
|
| 320 |
+
e.SetType("usage-reports")
|
| 321 |
+
e.SetSubject("customer-1")
|
| 322 |
+
_ = e.SetData("application/json", map[string]string{
|
| 323 |
+
"reports": "123",
|
| 324 |
+
"type": "type",
|
| 325 |
+
})
|
| 326 |
+
return e
|
| 327 |
+
}
|
api/client/go/codegen.yaml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.json
|
| 2 |
+
package: openmeter
|
| 3 |
+
generate:
|
| 4 |
+
client: true
|
| 5 |
+
models: true
|
| 6 |
+
embedded-spec: true
|
| 7 |
+
compatibility:
|
| 8 |
+
# See: https://github.com/oapi-codegen/oapi-codegen/issues/778
|
| 9 |
+
disable-required-readonly-as-pointer: true
|
| 10 |
+
always-prefix-enum-values: true
|
| 11 |
+
output: ./client.gen.go
|
api/client/go/error.go
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package openmeter
|
| 2 |
+
|
| 3 |
+
// ErrResponse renderer type for handling all sorts of errors.
|
| 4 |
+
// In the best case scenario, the excellent github.com/pkg/errors package
|
| 5 |
+
// helps reveal information on the error, setting it on Err, and in the Render()
|
| 6 |
+
// method, using it to set the application-specific error code in AppCode.
|
| 7 |
+
type ErrResponse struct {
|
| 8 |
+
Err error `json:"-"` // low-level runtime error
|
| 9 |
+
|
| 10 |
+
StatusCode int `json:"statusCode"` // http response status code
|
| 11 |
+
StatusText string `json:"status"` // user-level status message
|
| 12 |
+
AppCode int64 `json:"code,omitempty"` // application-specific error code
|
| 13 |
+
Message string `json:"message,omitempty"` // application-level error message, for debugging
|
| 14 |
+
}
|
api/client/javascript/.gitignore
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# dot-files (.env, .git, ...)
|
| 2 |
+
|
| 3 |
+
# logs
|
| 4 |
+
logs
|
| 5 |
+
*.log
|
| 6 |
+
npm-debug.log*
|
| 7 |
+
|
| 8 |
+
# dependency directory
|
| 9 |
+
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
| 10 |
+
node_modules
|
| 11 |
+
|
| 12 |
+
# editor
|
| 13 |
+
.vscode/*
|
| 14 |
+
!.vscode/settings.json
|
| 15 |
+
!.vscode/tasks.json
|
| 16 |
+
!.vscode/launch.json
|
| 17 |
+
!.vscode/extensions.json
|
| 18 |
+
!.vscode/*.code-snippets
|
| 19 |
+
|
| 20 |
+
# build
|
| 21 |
+
.duel-cache/
|
| 22 |
+
tsconfig.tsbuildinfo
|
| 23 |
+
dist/
|
api/client/javascript/.npmignore
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# dot-files (.env, .git, .npmrc, ...)
|
| 2 |
+
.*
|
| 3 |
+
|
| 4 |
+
# logs
|
| 5 |
+
logs
|
| 6 |
+
*.log
|
| 7 |
+
npm-debug.log*
|
| 8 |
+
|
| 9 |
+
# dependency directory
|
| 10 |
+
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
| 11 |
+
node_modules
|
| 12 |
+
|
| 13 |
+
# editor
|
| 14 |
+
.vscode/*
|
| 15 |
+
!.vscode/settings.json
|
| 16 |
+
!.vscode/tasks.json
|
| 17 |
+
!.vscode/launch.json
|
| 18 |
+
!.vscode/extensions.json
|
| 19 |
+
!.vscode/*.code-snippets
|
| 20 |
+
|
| 21 |
+
# source
|
| 22 |
+
index.ts
|
| 23 |
+
src/
|
| 24 |
+
scripts/
|
| 25 |
+
vitest.config.ts
|
| 26 |
+
eslint.config.js
|
| 27 |
+
tsconfig*.json
|
api/client/javascript/.npmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
save-exact=true
|
api/client/javascript/Makefile
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# A Self-Documenting Makefile: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
|
| 2 |
+
|
| 3 |
+
.PHONY: generate
|
| 4 |
+
generate: ## Generate JavaScript SDK
|
| 5 |
+
$(call print-target)
|
| 6 |
+
pnpm --frozen-lockfile install
|
| 7 |
+
pnpm run generate
|
| 8 |
+
pnpm build
|
| 9 |
+
pnpm test
|
| 10 |
+
|
| 11 |
+
.PHONY: publish-javascript-sdk
|
| 12 |
+
publish-javascript-sdk: ## Publish JavaScript SDK
|
| 13 |
+
$(call print-target)
|
| 14 |
+
@if [ -z "$$JS_SDK_RELEASE_VERSION" ]; then \
|
| 15 |
+
echo "ERROR: JS_SDK_RELEASE_VERSION is required"; \
|
| 16 |
+
echo "Usage: JS_SDK_RELEASE_VERSION=1.2.3 make publish-javascript-sdk [JS_SDK_RELEASE_TAG=beta]"; \
|
| 17 |
+
exit 1; \
|
| 18 |
+
fi
|
| 19 |
+
|
| 20 |
+
@if [ -z "$$JS_SDK_RELEASE_TAG" ]; then \
|
| 21 |
+
echo "ERROR: JS_SDK_RELEASE_TAG is required"; \
|
| 22 |
+
echo "Usage: JS_SDK_RELEASE_VERSION=1.2.3 make publish-javascript-sdk [JS_SDK_RELEASE_TAG=beta]"; \
|
| 23 |
+
exit 1; \
|
| 24 |
+
fi
|
| 25 |
+
|
| 26 |
+
pnpm --frozen-lockfile install
|
| 27 |
+
pnpm version "$${JS_SDK_RELEASE_VERSION}" --no-git-tag-version
|
| 28 |
+
CACHE_BUSTER="$$(date --rfc-3339=seconds)" pnpm publish --no-git-checks --tag "$${JS_SDK_RELEASE_TAG}"
|
| 29 |
+
@echo "✅ Published $${JS_SDK_RELEASE_TAG} JavaScript SDK version $${JS_SDK_RELEASE_VERSION} with tag $${JS_SDK_RELEASE_TAG}"
|
| 30 |
+
|
| 31 |
+
.PHONY: help
|
| 32 |
+
.DEFAULT_GOAL := help
|
| 33 |
+
help:
|
| 34 |
+
@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
| 35 |
+
|
| 36 |
+
# Variable outputting/exporting rules
|
| 37 |
+
var-%: ; @echo $($*)
|
| 38 |
+
varexport-%: ; @echo $*=$($*)
|
| 39 |
+
|
| 40 |
+
define print-target
|
| 41 |
+
@printf "Executing target: \033[36m$@\033[0m\n"
|
| 42 |
+
endef
|
api/client/javascript/README.md
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenMeter JavaScript SDK
|
| 2 |
+
|
| 3 |
+
## Install
|
| 4 |
+
|
| 5 |
+
```sh
|
| 6 |
+
npm install --save @openmeter/sdk
|
| 7 |
+
```
|
| 8 |
+
|
| 9 |
+
## Configuration for accessing the OpenMeter API
|
| 10 |
+
|
| 11 |
+
To use the OpenMeter SDK on your backend, you need to configure `baseUrl` and `apiKey` for OpenMeter Cloud:
|
| 12 |
+
|
| 13 |
+
```ts
|
| 14 |
+
import { OpenMeter } from '@openmeter/sdk'
|
| 15 |
+
|
| 16 |
+
const openmeter = new OpenMeter({
|
| 17 |
+
baseUrl: 'https://openmeter.cloud',
|
| 18 |
+
apiKey: 'om_...',
|
| 19 |
+
})
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
## Configuration for accessing the OpenMeter Portal API
|
| 23 |
+
|
| 24 |
+
To use the OpenMeter Portal SDK on your frontend, you need to configure it use a portal token in your configuration:
|
| 25 |
+
|
| 26 |
+
```ts
|
| 27 |
+
import { OpenMeter } from '@openmeter/sdk/portal'
|
| 28 |
+
|
| 29 |
+
const openmeter = new OpenMeter({
|
| 30 |
+
baseUrl: 'https://openmeter.cloud',
|
| 31 |
+
portalToken: 'om_portal_...',
|
| 32 |
+
})
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
## Configuration for accessing the OpenMeter React SDK
|
| 36 |
+
|
| 37 |
+
To use the OpenMeter React SDK for the portal API, you need to configure a Portal Client and a React Context:
|
| 38 |
+
|
| 39 |
+
```ts
|
| 40 |
+
import {
|
| 41 |
+
OpenMeter,
|
| 42 |
+
OpenMeterProvider,
|
| 43 |
+
useOpenMeter,
|
| 44 |
+
} from '@openmeter/sdk/react'
|
| 45 |
+
|
| 46 |
+
function App() {
|
| 47 |
+
// get portal token from your backend
|
| 48 |
+
const openmeter = new OpenMeter({
|
| 49 |
+
baseUrl: 'https://openmeter.cloud',
|
| 50 |
+
portalToken,
|
| 51 |
+
})
|
| 52 |
+
|
| 53 |
+
return (
|
| 54 |
+
<OpenMeterProvider value={openmeter}>
|
| 55 |
+
<UsageComponent />
|
| 56 |
+
{/* ... */}
|
| 57 |
+
</OpenMeterProvider>
|
| 58 |
+
)
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
function UsageComponent() {
|
| 62 |
+
// get openmeter client from context
|
| 63 |
+
const openmeter = useOpenMeter()
|
| 64 |
+
|
| 65 |
+
// ...
|
| 66 |
+
}
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
## Ingest usage events
|
| 70 |
+
|
| 71 |
+
```ts
|
| 72 |
+
// Ingest a single AI token usage event
|
| 73 |
+
await openmeter.events.ingest({
|
| 74 |
+
type: 'ai-tokens',
|
| 75 |
+
subject: 'customer-acme-corp',
|
| 76 |
+
id: 'evt_01234567', // optional: auto-generated if not provided
|
| 77 |
+
source: 'llm-api-gateway', // optional: defaults to '@openmeter/sdk'
|
| 78 |
+
time: new Date(), // optional: defaults to current time
|
| 79 |
+
data: {
|
| 80 |
+
model: 'gpt-4',
|
| 81 |
+
type: 'input',
|
| 82 |
+
tokens: 1250,
|
| 83 |
+
},
|
| 84 |
+
})
|
| 85 |
+
|
| 86 |
+
// Ingest multiple events in a batch for better performance
|
| 87 |
+
await openmeter.events.ingest([
|
| 88 |
+
{
|
| 89 |
+
type: 'ai-tokens',
|
| 90 |
+
subject: 'customer-acme-corp',
|
| 91 |
+
data: { model: 'gpt-4', type: 'input', tokens: 850 },
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
type: 'ai-tokens',
|
| 95 |
+
subject: 'customer-acme-corp',
|
| 96 |
+
data: { model: 'gpt-4', type: 'output', tokens: 850 },
|
| 97 |
+
},
|
| 98 |
+
])
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
## Client API Reference
|
| 102 |
+
|
| 103 |
+
The OpenMeter SDK provides a comprehensive client interface organized into logical groups. Below is a complete reference of all available methods.
|
| 104 |
+
|
| 105 |
+
### Overview
|
| 106 |
+
|
| 107 |
+
| Namespace | Resource | Operation | Method | Description |
|
| 108 |
+
|-----------|----------|-----------|--------|-------------|
|
| 109 |
+
| **[Events](#events)** | | | | Track usage by ingesting events |
|
| 110 |
+
| | Events | Create | [`openmeter.events.ingest(events)`](./src/client/events.ts#L19) | Ingest a single event or batch of events |
|
| 111 |
+
| | Events | Read | [`openmeter.events.list(params?)`](./src/client/events.ts#L41) | List ingested events with advanced filtering |
|
| 112 |
+
| | Events | Read | [`openmeter.events.listV2(params?)`](./src/client/events.ts#L60) | List ingested events with advanced filtering (V2) |
|
| 113 |
+
| **[Meters](#meters)** | | | | Track and aggregate usage data from events |
|
| 114 |
+
| | Meters | Create | [`openmeter.meters.create(meter)`](./src/client/meters.ts#L19) | Create a new meter |
|
| 115 |
+
| | Meters | Read | [`openmeter.meters.get(idOrSlug)`](./src/client/meters.ts#L34) | Get a meter by ID or slug |
|
| 116 |
+
| | Meters | Read | [`openmeter.meters.list()`](./src/client/meters.ts#L55) | List all meters |
|
| 117 |
+
| | Meters | Read | [`openmeter.meters.query(idOrSlug, query?)`](./src/client/meters.ts#L70) | Query usage data |
|
| 118 |
+
| | Meters | Update | [`openmeter.meters.update(idOrSlug, meter)`](./src/client/meters.ts#L100) | Update a meter by ID or slug |
|
| 119 |
+
| | Meters | Delete | [`openmeter.meters.delete(idOrSlug)`](./src/client/meters.ts#L124) | Delete a meter by ID or slug |
|
| 120 |
+
| **[Subjects](#subjects)** | | | | Manage entities that consume resources |
|
| 121 |
+
| | Subjects | Create | [`openmeter.subjects.upsert(subjects)`](./src/client/subjects.ts#L21) | Create or update one or multiple subjects |
|
| 122 |
+
| | Subjects | Read | [`openmeter.subjects.get(idOrKey)`](./src/client/subjects.ts#L39) | Get a subject by ID or key |
|
| 123 |
+
| | Subjects | Read | [`openmeter.subjects.list()`](./src/client/subjects.ts#L60) | List all subjects |
|
| 124 |
+
| | Subjects | Delete | [`openmeter.subjects.delete(idOrKey)`](./src/client/subjects.ts#L74) | Delete a subject by ID or key |
|
| 125 |
+
| **[Customers](#customers)** | | | | Manage customer subscription lifecycles and plan assignments |
|
| 126 |
+
| | Customers | Create | [`openmeter.customers.create(customer)`](./src/client/customers.ts#L37) | Create a new customer |
|
| 127 |
+
| | Customers | Read | [`openmeter.customers.get(customerIdOrKey)`](./src/client/customers.ts#L52) | Get a customer by ID or key |
|
| 128 |
+
| | Customers | Read | [`openmeter.customers.list(query?)`](./src/client/customers.ts#L123) | List all customers |
|
| 129 |
+
| | Customers | Read | [`openmeter.customers.getAccess(customerIdOrKey)`](./src/client/customers.ts#L143) | Get customer access information |
|
| 130 |
+
| | Customers | Read | [`openmeter.customers.listSubscriptions(customerIdOrKey, query?)`](./src/client/customers.ts#L169) | List customer subscriptions |
|
| 131 |
+
| | Customers | Update | [`openmeter.customers.update(customerIdOrKey, customer)`](./src/client/customers.ts#L75) | Update a customer |
|
| 132 |
+
| | Customers | Delete | [`openmeter.customers.delete(customerIdOrKey)`](./src/client/customers.ts#L99) | Delete a customer |
|
| 133 |
+
| | Apps | Update | [`openmeter.customers.apps.upsert(customerIdOrKey, appData)`](./src/client/customers.ts#L200) | Upsert app data |
|
| 134 |
+
| | Apps | Read | [`openmeter.customers.apps.list(customerIdOrKey)`](./src/client/customers.ts#L228) | List app data |
|
| 135 |
+
| | Apps | Delete | [`openmeter.customers.apps.delete(customerIdOrKey, appId)`](./src/client/customers.ts#L254) | Delete app data |
|
| 136 |
+
| | Stripe | Update | [`openmeter.customers.stripe.upsert(customerIdOrKey, appDataBase)`](./src/client/customers.ts#L285) | Upsert Stripe app data |
|
| 137 |
+
| | Stripe | Read | [`openmeter.customers.stripe.get(customerIdOrKey)`](./src/client/customers.ts#L313) | Get Stripe app data |
|
| 138 |
+
| | Stripe | Create | [`openmeter.customers.stripe.createPortalSession(customerIdOrKey, params)`](./src/client/customers.ts#L337) | Create a Stripe customer portal session |
|
| 139 |
+
| | Entitlements V1 | Read | [`openmeter.customers.entitlementsV1.value(customerIdOrKey, featureKey)`](./src/client/customers.ts#L372) | Get entitlement value (V1 API) |
|
| 140 |
+
| | Entitlements | Read | [`openmeter.customers.entitlements.list(customerIdOrKey)`](./src/client/customers.ts#L401) | List entitlements |
|
| 141 |
+
| | Entitlements | Create | [`openmeter.customers.entitlements.create(customerIdOrKey, entitlement)`](./src/client/customers.ts#L428) | Create an entitlement |
|
| 142 |
+
| | Entitlements | Read | [`openmeter.customers.entitlements.get(customerIdOrKey, featureKeyOrId)`](./src/client/customers.ts#L454) | Get an entitlement |
|
| 143 |
+
| | Entitlements | Delete | [`openmeter.customers.entitlements.delete(customerIdOrKey, entitlementId)`](./src/client/customers.ts#L479) | Delete an entitlement |
|
| 144 |
+
| | Entitlements | Update | [`openmeter.customers.entitlements.override(customerIdOrKey, featureKeyOrId, entitlement)`](./src/client/customers.ts#L505) | Override an entitlement |
|
| 145 |
+
| | Entitlements | Read | [`openmeter.customers.entitlements.value(customerIdOrKey, featureKeyOrId, query?)`](./src/client/customers.ts#L588) | Get entitlement value |
|
| 146 |
+
| | Entitlements | Read | [`openmeter.customers.entitlements.history(customerIdOrKey, featureKeyOrId, query?)`](./src/client/customers.ts#L617) | Get entitlement history |
|
| 147 |
+
| | Entitlements | Update | [`openmeter.customers.entitlements.resetUsage(customerIdOrKey, entitlementId, body?)`](./src/client/customers.ts#L653) | Reset usage |
|
| 148 |
+
| | Entitlements | Read | [`openmeter.customers.entitlements.listGrants(customerIdOrKey, featureKeyOrId, query?)`](./src/client/customers.ts#L532) | List grants |
|
| 149 |
+
| | Entitlements | Create | [`openmeter.customers.entitlements.createGrant(customerIdOrKey, featureKeyOrId, grant)`](./src/client/customers.ts#L561) | Create a grant |
|
| 150 |
+
| **[Features](#features)** | | | | Define application capabilities and services |
|
| 151 |
+
| | Features | Create | [`openmeter.features.create(feature)`](./src/client/features.ts#L24) | Create a new feature |
|
| 152 |
+
| | Features | Read | [`openmeter.features.get(featureIdOrKey)`](./src/client/features.ts#L39) | Get a feature by ID |
|
| 153 |
+
| | Features | Read | [`openmeter.features.list(params?)`](./src/client/features.ts#L61) | List all features |
|
| 154 |
+
| | Features | Delete | [`openmeter.features.delete(featureIdOrKey)`](./src/client/features.ts#L84) | Delete a feature by ID |
|
| 155 |
+
| **[Entitlements (V1)](#entitlements-v1)** | | | | Subject-based usage limits and access controls |
|
| 156 |
+
| | Entitlements | Create | [`openmeter.entitlementsV1.create(subjectIdOrKey, entitlement)`](./src/client/entitlements.ts#L40) | Create an entitlement for a subject |
|
| 157 |
+
| | Entitlements | Read | [`openmeter.entitlementsV1.get(entitlementId)`](./src/client/entitlements.ts#L68) | Get an entitlement by ID |
|
| 158 |
+
| | Entitlements | Read | [`openmeter.entitlementsV1.list(query?)`](./src/client/entitlements.ts#L91) | List all entitlements |
|
| 159 |
+
| | Entitlements | Read | [`openmeter.entitlementsV1.value(subjectIdOrKey, featureIdOrKey, query?)`](./src/client/entitlements.ts#L147) | Get the value of an entitlement |
|
| 160 |
+
| | Entitlements | Read | [`openmeter.entitlementsV1.history(subjectIdOrKey, entitlementIdOrFeatureKey, query?)`](./src/client/entitlements.ts#L180) | Get the history of an entitlement |
|
| 161 |
+
| | Entitlements | Update | [`openmeter.entitlementsV1.override(subjectIdOrKey, entitlementIdOrFeatureKey, override)`](./src/client/entitlements.ts#L213) | Override an entitlement |
|
| 162 |
+
| | Entitlements | Update | [`openmeter.entitlementsV1.reset(subjectIdOrKey, entitlementIdOrFeatureKey, reset?)`](./src/client/entitlements.ts#L247) | Reset entitlement usage |
|
| 163 |
+
| | Entitlements | Delete | [`openmeter.entitlementsV1.delete(subjectIdOrKey, entitlementId)`](./src/client/entitlements.ts#L116) | Delete an entitlement |
|
| 164 |
+
| | Grants | Create | [`openmeter.entitlementsV1.grants.create(subjectIdOrKey, entitlementIdOrFeatureKey, grant)`](./src/client/entitlements.ts#L283) | Create a grant for an entitlement |
|
| 165 |
+
| | Grants | Read | [`openmeter.entitlementsV1.grants.list(subjectIdOrKey, entitlementIdOrFeatureKey, query?)`](./src/client/entitlements.ts#L314) | List grants for an entitlement |
|
| 166 |
+
| | Grants | Read | [`openmeter.entitlementsV1.grants.listAll(query?)`](./src/client/entitlements.ts#L345) | List all grants |
|
| 167 |
+
| | Grants | Delete | [`openmeter.entitlementsV1.grants.void(entitlementId, grantId)`](./src/client/entitlements.ts#L369) | Void a grant |
|
| 168 |
+
| **[Entitlements](#entitlements)** | | | | Customer-based entitlements and access controls |
|
| 169 |
+
| | Entitlements | Read | [`openmeter.entitlements.list(query?)`](./src/client/entitlements.ts#L404) | List all entitlements (admin purposes) |
|
| 170 |
+
| | Entitlements | Read | [`openmeter.entitlements.get(entitlementId)`](./src/client/entitlements.ts#L425) | Get an entitlement by ID |
|
| 171 |
+
| | Grants | Read | [`openmeter.entitlements.grants.list(query?)`](./src/client/entitlements.ts#L453) | List all grants (admin purposes) |
|
| 172 |
+
| | Grants | Delete | [`openmeter.entitlements.grants.void(grantId)`](./src/client/entitlements.ts#L478) | Void a grant |
|
| 173 |
+
| **[Plans](#plans)** | | | | Manage subscription plans and pricing|
|
| 174 |
+
| | Plans | Create | [`openmeter.plans.create(plan)`](./src/client/plans.ts#L28) | Create a new plan|
|
| 175 |
+
| | Plans | Read | [`openmeter.plans.get(planId)`](./src/client/plans.ts#L44) | Get a plan by ID|
|
| 176 |
+
| | Plans | Read | [`openmeter.plans.list(query?)`](./src/client/plans.ts#L66) | List all plans|
|
| 177 |
+
| | Plans | Update | [`openmeter.plans.update(planId, plan)`](./src/client/plans.ts#L85) | Update a plan|
|
| 178 |
+
| | Plans | Delete | [`openmeter.plans.delete(planId)`](./src/client/plans.ts#L105) | Delete a plan by ID|
|
| 179 |
+
| | Plans | Other | [`openmeter.plans.archive(planId)`](./src/client/plans.ts#L123) | Archive a plan|
|
| 180 |
+
| | Plans | Other | [`openmeter.plans.publish(planId)`](./src/client/plans.ts#L141) | Publish a plan|
|
| 181 |
+
| | Addons | Read | [`openmeter.plans.addons.list(planId)`](./src/client/plans.ts#L168) | List addons|
|
| 182 |
+
| | Addons | Create | [`openmeter.plans.addons.create(planId, addon)`](./src/client/plans.ts#L191) | Create an addon|
|
| 183 |
+
| | Addons | Read | [`openmeter.plans.addons.get(planId, planAddonId)`](./src/client/plans.ts#L212) | Get an addon by ID|
|
| 184 |
+
| | Addons | Update | [`openmeter.plans.addons.update(planId, planAddonId, addon)`](./src/client/plans.ts#L238) | Update an addon|
|
| 185 |
+
| | Addons | Delete | [`openmeter.plans.addons.delete(planId, planAddonId)`](./src/client/plans.ts#L263) | Delete an addon by ID|
|
| 186 |
+
| **[Addons](#addons)** | | | | Manage standalone addons available across plans|
|
| 187 |
+
| | Addons | Create | [`openmeter.addons.create(addon)`](./src/client/addons.ts#L15) | Create a new addon|
|
| 188 |
+
| | Addons | Read | [`openmeter.addons.get(addonId)`](./src/client/addons.ts#L48) | Get an addon by ID|
|
| 189 |
+
| | Addons | Read | [`openmeter.addons.list(query?)`](./src/client/addons.ts#L30) | List all addons|
|
| 190 |
+
| | Addons | Update | [`openmeter.addons.update(addonId, addon)`](./src/client/addons.ts#L64) | Update an addon|
|
| 191 |
+
| | Addons | Delete | [`openmeter.addons.delete(addonId)`](./src/client/addons.ts#L84) | Delete an addon by ID|
|
| 192 |
+
| | Addons | Other | [`openmeter.addons.publish(addonId)`](./src/client/addons.ts#L99) | Publish an addon|
|
| 193 |
+
| | Addons | Other | [`openmeter.addons.archive(addonId)`](./src/client/addons.ts#L114) | Archive an addon|
|
| 194 |
+
| **[Subscriptions](#subscriptions)** | | | | Manage customer subscriptions|
|
| 195 |
+
| | Subscriptions | Create | [`openmeter.subscriptions.create(body)`](./src/client/subscriptions.ts#L24) | Create a new subscription|
|
| 196 |
+
| | Subscriptions | Read | [`openmeter.subscriptions.get(subscriptionId)`](./src/client/subscriptions.ts#L39) | Get a subscription by ID|
|
| 197 |
+
| | Subscriptions | Update | [`openmeter.subscriptions.edit(subscriptionId, body)`](./src/client/subscriptions.ts#L61) | Edit a subscription|
|
| 198 |
+
| | Subscriptions | Delete | [`openmeter.subscriptions.delete(subscriptionId)`](./src/client/subscriptions.ts#L180) | Delete a subscription (only scheduled)|
|
| 199 |
+
| | Subscriptions | Other | [`openmeter.subscriptions.cancel(subscriptionId, body?)`](./src/client/subscriptions.ts#L85) | Cancel a subscription|
|
| 200 |
+
| | Subscriptions | Other | [`openmeter.subscriptions.change(subscriptionId, body)`](./src/client/subscriptions.ts#L110) | Change a subscription (upgrade/downgrade)|
|
| 201 |
+
| | Subscriptions | Other | [`openmeter.subscriptions.migrate(subscriptionId, body)`](./src/client/subscriptions.ts#L135) | Migrate to a new plan version|
|
| 202 |
+
| | Subscriptions | Other | [`openmeter.subscriptions.unscheduleCancelation(subscriptionId)`](./src/client/subscriptions.ts#L158) | Unschedule a subscription cancelation|
|
| 203 |
+
| **[Subscription Addons](#subscription-addons)** | | | | Manage addons attached to specific subscriptions|
|
| 204 |
+
| | Subscription Addons | Create | [`openmeter.subscriptionAddons.create(subscriptionId, body)`](./src/client/subscription-addons.ts#L16) | Create a new subscription addon|
|
| 205 |
+
| | Subscription Addons | Read | [`openmeter.subscriptionAddons.get(subscriptionId, subscriptionAddonId)`](./src/client/subscription-addons.ts#L58) | Get a subscription addon by ID|
|
| 206 |
+
| | Subscription Addons | Read | [`openmeter.subscriptionAddons.list(subscriptionId)`](./src/client/subscription-addons.ts#L39) | List all addons of a subscription|
|
| 207 |
+
| | Subscription Addons | Update | [`openmeter.subscriptionAddons.update(subscriptionId, subscriptionAddonId, body)`](./src/client/subscription-addons.ts#L82) | Update a subscription addon|
|
| 208 |
+
| **[Billing](#billing)** | | | | Comprehensive billing management (profiles, invoices, overrides)|
|
| 209 |
+
| | Profiles | Create | [`openmeter.billing.profiles.create(profile)`](./src/client/billing.ts#L42) | Create a billing profile|
|
| 210 |
+
| | Profiles | Read | [`openmeter.billing.profiles.get(id)`](./src/client/billing.ts#L60) | Get a billing profile by ID|
|
| 211 |
+
| | Profiles | Read | [`openmeter.billing.profiles.list(query?)`](./src/client/billing.ts#L80) | List billing profiles|
|
| 212 |
+
| | Profiles | Update | [`openmeter.billing.profiles.update(id, profile)`](./src/client/billing.ts#L101) | Update a billing profile|
|
| 213 |
+
| | Profiles | Delete | [`openmeter.billing.profiles.delete(id)`](./src/client/billing.ts#L123) | Delete a billing profile|
|
| 214 |
+
| | Invoices | Read | [`openmeter.billing.invoices.list(query?)`](./src/client/billing.ts#L150) | List invoices|
|
| 215 |
+
| | Invoices | Read | [`openmeter.billing.invoices.get(id, query?)`](./src/client/billing.ts#L170) | Get an invoice by ID|
|
| 216 |
+
| | Invoices | Update | [`openmeter.billing.invoices.update(id, invoice)`](./src/client/billing.ts#L192) | Update an invoice (draft or earlier)|
|
| 217 |
+
| | Invoices | Delete | [`openmeter.billing.invoices.delete(id)`](./src/client/billing.ts#L213) | Delete an invoice (draft or earlier)|
|
| 218 |
+
| | Invoices | Other | [`openmeter.billing.invoices.advance(id)`](./src/client/billing.ts#L235) | Advance invoice to next status|
|
| 219 |
+
| | Invoices | Other | [`openmeter.billing.invoices.approve(id)`](./src/client/billing.ts#L257) | Approve an invoice (sends to customer)|
|
| 220 |
+
| | Invoices | Other | [`openmeter.billing.invoices.retry(id, body?)`](./src/client/billing.ts#L278) | Retry advancing after failure|
|
| 221 |
+
| | Invoices | Other | [`openmeter.billing.invoices.void(id)`](./src/client/billing.ts#L302) | Void an invoice|
|
| 222 |
+
| | Invoices | Other | [`openmeter.billing.invoices.recalculateTax(id)`](./src/client/billing.ts#L325) | Recalculate invoice tax amounts|
|
| 223 |
+
| | Invoices | Other | [`openmeter.billing.invoices.simulate(customerId, query?)`](./src/client/billing.ts#L346) | Simulate an invoice for a customer|
|
| 224 |
+
| | Invoices | Create | [`openmeter.billing.invoices.createLineItems(customerId, body)`](./src/client/billing.ts#L377) | Create pending line items|
|
| 225 |
+
| | Invoices | Create | [`openmeter.billing.invoices.invoicePendingLines(customerId)`](./src/client/billing.ts#L401) | Invoice pending lines|
|
| 226 |
+
| | Customers | Create | [`openmeter.billing.customers.createOverride(customerId, body)`](./src/client/billing.ts#L427) | Create or update a customer override|
|
| 227 |
+
| | Customers | Read | [`openmeter.billing.customers.getOverride(customerId, id)`](./src/client/billing.ts#L450) | Get a customer override|
|
| 228 |
+
| | Customers | Read | [`openmeter.billing.customers.listOverrides(customerId)`](./src/client/billing.ts#L471) | List customer overrides|
|
| 229 |
+
| | Customers | Delete | [`openmeter.billing.customers.deleteOverride(customerId, id)`](./src/client/billing.ts#L489) | Delete a customer override|
|
| 230 |
+
| **[Apps](#apps)** | | | | Manage integrations and app marketplace|
|
| 231 |
+
| | Apps | Read | [`openmeter.apps.list(query?)`](./src/client/apps.ts#L32) | List installed apps|
|
| 232 |
+
| | Apps | Read | [`openmeter.apps.get(id)`](./src/client/apps.ts#L50) | Get an app by ID|
|
| 233 |
+
| | Apps | Update | [`openmeter.apps.update(id, body)`](./src/client/apps.ts#L69) | Update an app|
|
| 234 |
+
| | Apps | Delete | [`openmeter.apps.uninstall(id)`](./src/client/apps.ts#L89) | Uninstall an app|
|
| 235 |
+
| | Marketplace | Read | [`openmeter.apps.marketplace.list(query?)`](./src/client/apps.ts#L115) | List available marketplace apps|
|
| 236 |
+
| | Marketplace | Read | [`openmeter.apps.marketplace.get(id)`](./src/client/apps.ts#L133) | Get marketplace listing details|
|
| 237 |
+
| | Marketplace | Read | [`openmeter.apps.marketplace.getOauth2InstallUrl(id, redirectUrl)`](./src/client/apps.ts#L151) | Get OAuth2 install URL|
|
| 238 |
+
| | Marketplace | Other | [`openmeter.apps.marketplace.authorizeOauth2(id, body)`](./src/client/apps.ts#L172) | Authorize OAuth2 code|
|
| 239 |
+
| | Marketplace | Create | [`openmeter.apps.marketplace.installWithAPIKey(id, body)`](./src/client/apps.ts#L193) | Install app with API key|
|
| 240 |
+
| | Stripe | Create | [`openmeter.apps.stripe.createCheckoutSession(body)`](./src/client/apps.ts#L223) | Create a Stripe checkout session|
|
| 241 |
+
| | Stripe | Update | [`openmeter.apps.stripe.updateApiKey(body)`](./src/client/apps.ts#L243) | Update Stripe API key|
|
| 242 |
+
| | Custom Invoicing | Other | [`openmeter.apps.customInvoicing.draftSynchronized(body)`](./src/client/apps.ts#L271) | Submit draft synchronization results|
|
| 243 |
+
| | Custom Invoicing | Other | [`openmeter.apps.customInvoicing.issuingSynchronized(body)`](./src/client/apps.ts#L295) | Submit issuing synchronization results|
|
| 244 |
+
| | Custom Invoicing | Update | [`openmeter.apps.customInvoicing.updatePaymentStatus(invoiceId, body)`](./src/client/apps.ts#L319) | Update payment status|
|
| 245 |
+
| **[Notifications](#notifications)** | | | | Set up automated notifications for usage thresholds|
|
| 246 |
+
| | Channels | Create | [`openmeter.notifications.channels.create(channel)`](./src/client/notifications.ts#L40) | Create a notification channel|
|
| 247 |
+
| | Channels | Read | [`openmeter.notifications.channels.get(channelId)`](./src/client/notifications.ts#L58) | Get a notification channel by ID|
|
| 248 |
+
| | Channels | Update | [`openmeter.notifications.channels.update(channelId, channel)`](./src/client/notifications.ts#L84) | Update a notification channel|
|
| 249 |
+
| | Channels | Read | [`openmeter.notifications.channels.list(query?)`](./src/client/notifications.ts#L111) | List notification channels|
|
| 250 |
+
| | Channels | Delete | [`openmeter.notifications.channels.delete(channelId)`](./src/client/notifications.ts#L131) | Delete a notification channel|
|
| 251 |
+
| | Rules | Create | [`openmeter.notifications.rules.create(rule)`](./src/client/notifications.ts#L164) | Create a notification rule|
|
| 252 |
+
| | Rules | Read | [`openmeter.notifications.rules.get(ruleId)`](./src/client/notifications.ts#L182) | Get a notification rule by ID|
|
| 253 |
+
| | Rules | Update | [`openmeter.notifications.rules.update(ruleId, rule)`](./src/client/notifications.ts#L205) | Update a notification rule|
|
| 254 |
+
| | Rules | Read | [`openmeter.notifications.rules.list(query?)`](./src/client/notifications.ts#L229) | List notification rules|
|
| 255 |
+
| | Rules | Delete | [`openmeter.notifications.rules.delete(ruleId)`](./src/client/notifications.ts#L249) | Delete a notification rule|
|
| 256 |
+
| | Events | Read | [`openmeter.notifications.events.get(eventId)`](./src/client/notifications.ts#L282) | Get a notification event by ID|
|
| 257 |
+
| | Events | Read | [`openmeter.notifications.events.list(query?)`](./src/client/notifications.ts#L307) | List notification events|
|
| 258 |
+
| **[Portal](#portal)** | | | | Manage consumer portal tokens for customer-facing interfaces|
|
| 259 |
+
| | Portal | Create | [`openmeter.portal.create(body)`](./src/client/portal.ts#L19) | Create a consumer portal token|
|
| 260 |
+
| | Portal | Read | [`openmeter.portal.list(query?)`](./src/client/portal.ts#L34) | List consumer portal tokens|
|
| 261 |
+
| | Portal | Other | [`openmeter.portal.invalidate(query?)`](./src/client/portal.ts#L52) | Invalidate consumer portal tokens|
|
| 262 |
+
| **[Info](#info)** | | | | Utility endpoints for system information|
|
| 263 |
+
| | Info | Read | [`openmeter.info.listCurrencies()`](./src/client/info.ts#L18) | List all supported currencies|
|
| 264 |
+
| | Info | Read | [`openmeter.info.getProgress(id)`](./src/client/info.ts#L32) | Get progress of a long-running operation|
|
| 265 |
+
| **[Debug](#debug)** | | | | Debug utilities for monitoring and troubleshooting|
|
| 266 |
+
| | Debug | Read | [`openmeter.debug.getMetrics()`](./src/client/debug.ts#L18) | Get event ingestion metrics (OpenMetrics format)|
|
| 267 |
+
|
api/client/javascript/biome.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
| 3 |
+
"assist": {
|
| 4 |
+
"actions": {
|
| 5 |
+
"source": {
|
| 6 |
+
"organizeImports": "on",
|
| 7 |
+
"useSortedKeys": "on"
|
| 8 |
+
}
|
| 9 |
+
}
|
| 10 |
+
},
|
| 11 |
+
"files": {
|
| 12 |
+
"ignoreUnknown": false,
|
| 13 |
+
"includes": ["**"],
|
| 14 |
+
"maxSize": 20000000
|
| 15 |
+
},
|
| 16 |
+
"formatter": {
|
| 17 |
+
"enabled": true,
|
| 18 |
+
"indentStyle": "space",
|
| 19 |
+
"useEditorconfig": true
|
| 20 |
+
},
|
| 21 |
+
"javascript": {
|
| 22 |
+
"formatter": {
|
| 23 |
+
"arrowParentheses": "always",
|
| 24 |
+
"attributePosition": "auto",
|
| 25 |
+
"lineWidth": 80,
|
| 26 |
+
"quoteStyle": "single",
|
| 27 |
+
"semicolons": "asNeeded",
|
| 28 |
+
"trailingCommas": "all"
|
| 29 |
+
}
|
| 30 |
+
},
|
| 31 |
+
"linter": {
|
| 32 |
+
"enabled": true,
|
| 33 |
+
"rules": {
|
| 34 |
+
"recommended": true,
|
| 35 |
+
"style": {
|
| 36 |
+
"noInferrableTypes": "error",
|
| 37 |
+
"noParameterAssign": "error",
|
| 38 |
+
"noUnusedTemplateLiteral": "error",
|
| 39 |
+
"noUselessElse": "error",
|
| 40 |
+
"useAsConstAssertion": "error",
|
| 41 |
+
"useConst": "error",
|
| 42 |
+
"useDefaultParameterLast": "error",
|
| 43 |
+
"useEnumInitializers": "error",
|
| 44 |
+
"useNumberNamespace": "error",
|
| 45 |
+
"useSelfClosingElements": "error",
|
| 46 |
+
"useSingleVarDeclarator": "error"
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
},
|
| 50 |
+
"overrides": [
|
| 51 |
+
{
|
| 52 |
+
"assist": {
|
| 53 |
+
"actions": {
|
| 54 |
+
"source": {
|
| 55 |
+
"useSortedKeys": "off"
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
},
|
| 59 |
+
"includes": ["package.json"]
|
| 60 |
+
}
|
| 61 |
+
],
|
| 62 |
+
"vcs": {
|
| 63 |
+
"clientKind": "git",
|
| 64 |
+
"enabled": true,
|
| 65 |
+
"useIgnoreFile": true
|
| 66 |
+
}
|
| 67 |
+
}
|
api/client/javascript/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
export * from './src/client/index.js'
|
api/client/javascript/orval.config.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'orval'
|
| 2 |
+
|
| 3 |
+
export default defineConfig({
|
| 4 |
+
openmeter: {
|
| 5 |
+
input: {
|
| 6 |
+
target: '../../openapi.cloud.yaml',
|
| 7 |
+
},
|
| 8 |
+
output: {
|
| 9 |
+
formatter: 'biome',
|
| 10 |
+
clean: true,
|
| 11 |
+
client: 'zod',
|
| 12 |
+
mode: 'single',
|
| 13 |
+
namingConvention: 'PascalCase',
|
| 14 |
+
override: {
|
| 15 |
+
useDates: true,
|
| 16 |
+
zod: {
|
| 17 |
+
coerce: {
|
| 18 |
+
body: true,
|
| 19 |
+
header: false,
|
| 20 |
+
param: true,
|
| 21 |
+
query: true,
|
| 22 |
+
response: false,
|
| 23 |
+
},
|
| 24 |
+
generate: {
|
| 25 |
+
body: true,
|
| 26 |
+
header: false,
|
| 27 |
+
param: true,
|
| 28 |
+
query: true,
|
| 29 |
+
response: false,
|
| 30 |
+
},
|
| 31 |
+
},
|
| 32 |
+
},
|
| 33 |
+
propertySortOrder: 'Alphabetical',
|
| 34 |
+
target: './src/zod/index.ts',
|
| 35 |
+
tsconfig: './tsconfig.json',
|
| 36 |
+
},
|
| 37 |
+
},
|
| 38 |
+
})
|
api/client/javascript/package.json
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@openmeter/sdk",
|
| 3 |
+
"version": "0.0.0",
|
| 4 |
+
"description": "Client for OpenMeter: Real-Time and Scalable Usage Metering",
|
| 5 |
+
"license": "Apache 2.0",
|
| 6 |
+
"homepage": "https://openmeter.io",
|
| 7 |
+
"repository": {
|
| 8 |
+
"type": "git",
|
| 9 |
+
"url": "https://github.com/openmeterio/openmeter.git",
|
| 10 |
+
"directory": "api/client/javascript"
|
| 11 |
+
},
|
| 12 |
+
"type": "module",
|
| 13 |
+
"files": [
|
| 14 |
+
"dist"
|
| 15 |
+
],
|
| 16 |
+
"main": "./dist/index.js",
|
| 17 |
+
"types": "./dist/index.d.ts",
|
| 18 |
+
"exports": {
|
| 19 |
+
".": {
|
| 20 |
+
"import": {
|
| 21 |
+
"types": "./dist/index.d.ts",
|
| 22 |
+
"default": "./dist/index.js"
|
| 23 |
+
},
|
| 24 |
+
"require": {
|
| 25 |
+
"types": "./dist/cjs/index.d.cts",
|
| 26 |
+
"default": "./dist/cjs/index.cjs"
|
| 27 |
+
},
|
| 28 |
+
"default": "./dist/index.js"
|
| 29 |
+
},
|
| 30 |
+
"./portal": {
|
| 31 |
+
"import": {
|
| 32 |
+
"types": "./dist/src/portal/index.d.ts",
|
| 33 |
+
"default": "./dist/src/portal/index.js"
|
| 34 |
+
},
|
| 35 |
+
"require": {
|
| 36 |
+
"types": "./dist/cjs/src/portal/index.d.cts",
|
| 37 |
+
"default": "./dist/cjs/src/portal/index.cjs"
|
| 38 |
+
},
|
| 39 |
+
"default": "./dist/src/portal/index.js"
|
| 40 |
+
},
|
| 41 |
+
"./react": {
|
| 42 |
+
"import": {
|
| 43 |
+
"types": "./dist/src/react/context.d.ts",
|
| 44 |
+
"default": "./dist/src/react/context.js"
|
| 45 |
+
},
|
| 46 |
+
"default": "./dist/src/react/context.js"
|
| 47 |
+
},
|
| 48 |
+
"./zod": {
|
| 49 |
+
"import": {
|
| 50 |
+
"types": "./dist/src/zod/index.d.ts",
|
| 51 |
+
"default": "./dist/src/zod/index.js"
|
| 52 |
+
},
|
| 53 |
+
"require": {
|
| 54 |
+
"types": "./dist/cjs/src/zod/index.d.cts",
|
| 55 |
+
"default": "./dist/cjs/src/zod/index.cjs"
|
| 56 |
+
},
|
| 57 |
+
"default": "./dist/src/zod/index.js"
|
| 58 |
+
}
|
| 59 |
+
},
|
| 60 |
+
"scripts": {
|
| 61 |
+
"build": "duel",
|
| 62 |
+
"format": "biome format --write .",
|
| 63 |
+
"generate": "pnpm run generate:client && pnpm run generate:zod",
|
| 64 |
+
"generate:client": "tsx scripts/generate.ts && biome format --write ./src/client/schemas.ts",
|
| 65 |
+
"generate:zod": "orval && tsx scripts/add-as-const.ts && biome lint --write ./src/zod/index.ts && biome format --write ./src/zod/index.ts",
|
| 66 |
+
"lint": "tsc --noEmit && biome lint .",
|
| 67 |
+
"prepublishOnly": "pnpm run generate && pnpm run build && pnpm run lint && pnpm run test",
|
| 68 |
+
"pretest": "pnpm run build",
|
| 69 |
+
"test": "vitest --run",
|
| 70 |
+
"test:watch": "vitest --watch"
|
| 71 |
+
},
|
| 72 |
+
"engines": {
|
| 73 |
+
"node": ">=22.0.0"
|
| 74 |
+
},
|
| 75 |
+
"dependencies": {
|
| 76 |
+
"openapi-fetch": "0.17.0",
|
| 77 |
+
"openapi-typescript-helpers": "0.1.0"
|
| 78 |
+
},
|
| 79 |
+
"devDependencies": {
|
| 80 |
+
"@biomejs/biome": "2.4.16",
|
| 81 |
+
"@fetch-mock/vitest": "0.2.18",
|
| 82 |
+
"@knighted/duel": "4.1.0",
|
| 83 |
+
"@types/node": "25.9.2",
|
| 84 |
+
"@types/node-fetch": "2.6.13",
|
| 85 |
+
"@types/react": "19.2.17",
|
| 86 |
+
"fetch-mock": "12.6.0",
|
| 87 |
+
"openapi-typescript": "7.13.0",
|
| 88 |
+
"orval": "8.15.0",
|
| 89 |
+
"prettier": "3.8.3",
|
| 90 |
+
"react": "19.2.7",
|
| 91 |
+
"rollup": "4.61.1",
|
| 92 |
+
"tslib": "2.8.1",
|
| 93 |
+
"tsx": "4.22.4",
|
| 94 |
+
"typescript": "5.9.3",
|
| 95 |
+
"vitest": "4.1.8",
|
| 96 |
+
"zod": "4.4.3"
|
| 97 |
+
},
|
| 98 |
+
"packageManager": "pnpm@11.1.2+sha512.415a1cc25974731e75455c1468371be74c5aa5fb7621b50d4056d222451609f11412f23fd602e6169f1e060466641f798597e1be961a10688836a67b16569499",
|
| 99 |
+
"peerDependencies": {
|
| 100 |
+
"react": ">=18.0.0"
|
| 101 |
+
}
|
| 102 |
+
}
|
api/client/javascript/patches/openapi-typescript.patch
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
api/client/javascript/pnpm-lock.yaml
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
api/client/javascript/pnpm-workspace.yaml
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
blockExoticSubdeps: true
|
| 2 |
+
minimumReleaseAge: 4320
|
| 3 |
+
trustPolicy: no-downgrade
|
| 4 |
+
patchedDependencies:
|
| 5 |
+
openapi-typescript: patches/openapi-typescript.patch
|
| 6 |
+
overrides:
|
| 7 |
+
'vite@>=7.0.0 <=7.3.1': '>=7.3.2'
|
| 8 |
+
'vite@>=7.1.0 <=7.3.1': '>=7.3.2'
|
| 9 |
+
allowBuilds:
|
| 10 |
+
esbuild: true
|
api/client/javascript/scripts/add-as-const.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { readFileSync, writeFileSync } from 'node:fs'
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Post-generation workaround for orval's zod output: object-literal defaults
|
| 5 |
+
* are emitted without `as const`, so property values widen to `string` and
|
| 6 |
+
* fail Zod's `.default()` signature when the schema expects a literal type.
|
| 7 |
+
* Remove this script once orval emits `as const` for object-literal defaults.
|
| 8 |
+
* See: https://github.com/orval-labs/orval/issues/3244
|
| 9 |
+
*/
|
| 10 |
+
const file = new URL('../src/zod/index.ts', import.meta.url)
|
| 11 |
+
const src = readFileSync(file, 'utf8')
|
| 12 |
+
const fixed = src.replace(
|
| 13 |
+
/(^export const \w+Default =\s*\{[^{}]*\})/gm,
|
| 14 |
+
'$1 as const',
|
| 15 |
+
)
|
| 16 |
+
writeFileSync(file, fixed)
|
api/client/javascript/scripts/generate.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fs from 'node:fs'
|
| 2 |
+
import openapiTS, { astToString } from 'openapi-typescript'
|
| 3 |
+
import { factory, SyntaxKind } from 'typescript'
|
| 4 |
+
|
| 5 |
+
const DATE = factory.createTypeReferenceNode(factory.createIdentifier('Date')) // `Date`
|
| 6 |
+
const NULL = factory.createLiteralTypeNode(factory.createNull()) // `null`
|
| 7 |
+
const STRING = factory.createKeywordTypeNode(SyntaxKind.StringKeyword) // `string`
|
| 8 |
+
|
| 9 |
+
const schema = new URL('../../../openapi.cloud.yaml', import.meta.url)
|
| 10 |
+
|
| 11 |
+
const ast = await openapiTS(schema, {
|
| 12 |
+
defaultNonNullable: false,
|
| 13 |
+
rootTypes: true,
|
| 14 |
+
rootTypesNoSchemaPrefix: true,
|
| 15 |
+
transform(schemaObject, metadata) {
|
| 16 |
+
if (metadata.path === '#/components/schemas/Event') {
|
| 17 |
+
if (
|
| 18 |
+
schemaObject.type === 'string' &&
|
| 19 |
+
!schemaObject.nullable &&
|
| 20 |
+
!['customer-id', 'com.example.someevent'].includes(schemaObject.example)
|
| 21 |
+
) {
|
| 22 |
+
return {
|
| 23 |
+
questionToken: true,
|
| 24 |
+
schema: STRING,
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
if (schemaObject.format === 'date-time') {
|
| 29 |
+
const allowString =
|
| 30 |
+
(metadata.schema &&
|
| 31 |
+
'in' in metadata.schema &&
|
| 32 |
+
metadata.schema.in === 'query') ||
|
| 33 |
+
metadata.path?.includes('/parameters/query')
|
| 34 |
+
|
| 35 |
+
// allow string in query parameters
|
| 36 |
+
if (allowString) {
|
| 37 |
+
return schemaObject.nullable
|
| 38 |
+
? factory.createUnionTypeNode([DATE, NULL, STRING])
|
| 39 |
+
: factory.createUnionTypeNode([DATE, STRING])
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
return schemaObject.nullable
|
| 43 |
+
? factory.createUnionTypeNode([DATE, NULL])
|
| 44 |
+
: DATE
|
| 45 |
+
}
|
| 46 |
+
},
|
| 47 |
+
})
|
| 48 |
+
|
| 49 |
+
const contents = astToString(ast)
|
| 50 |
+
|
| 51 |
+
fs.writeFileSync('./src/client/schemas.ts', contents)
|
api/client/javascript/src/client/addons.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Client } from 'openapi-fetch'
|
| 2 |
+
import type { RequestOptions } from './common.js'
|
| 3 |
+
import type { AddonCreate, operations, paths } from './schemas.js'
|
| 4 |
+
import { transformResponse } from './utils.js'
|
| 5 |
+
|
| 6 |
+
export class Addons {
|
| 7 |
+
constructor(private client: Client<paths, `${string}/${string}`>) {}
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* Create a addon
|
| 11 |
+
* @param addon - The addon to create
|
| 12 |
+
* @param options - Optional request options
|
| 13 |
+
* @returns The created addon
|
| 14 |
+
*/
|
| 15 |
+
public async create(addon: AddonCreate, options?: RequestOptions) {
|
| 16 |
+
const resp = await this.client.POST('/api/v1/addons', {
|
| 17 |
+
body: addon,
|
| 18 |
+
...options,
|
| 19 |
+
})
|
| 20 |
+
|
| 21 |
+
return transformResponse(resp)
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/**
|
| 25 |
+
* List addons
|
| 26 |
+
* @param params - Optional parameters for listing addons
|
| 27 |
+
* @param options - Optional request options
|
| 28 |
+
* @returns A list of addons
|
| 29 |
+
*/
|
| 30 |
+
public async list(
|
| 31 |
+
params?: operations['listAddons']['parameters']['query'],
|
| 32 |
+
options?: RequestOptions,
|
| 33 |
+
) {
|
| 34 |
+
const resp = await this.client.GET('/api/v1/addons', {
|
| 35 |
+
params: { query: params },
|
| 36 |
+
...options,
|
| 37 |
+
})
|
| 38 |
+
|
| 39 |
+
return transformResponse(resp)
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/**
|
| 43 |
+
* Get an addon by ID
|
| 44 |
+
* @param addonId - The ID of the addon to retrieve
|
| 45 |
+
* @param options - Optional request options
|
| 46 |
+
* @returns The addon
|
| 47 |
+
*/
|
| 48 |
+
public async get(addonId: string, options?: RequestOptions) {
|
| 49 |
+
const resp = await this.client.GET('/api/v1/addons/{addonId}', {
|
| 50 |
+
params: { path: { addonId } },
|
| 51 |
+
...options,
|
| 52 |
+
})
|
| 53 |
+
|
| 54 |
+
return transformResponse(resp)
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
/**
|
| 58 |
+
* Update an addon
|
| 59 |
+
* @param addonId - The ID of the addon to update
|
| 60 |
+
* @param addon - The addon data to update
|
| 61 |
+
* @param options - Optional request options
|
| 62 |
+
* @returns The updated addon
|
| 63 |
+
*/
|
| 64 |
+
public async update(
|
| 65 |
+
addonId: string,
|
| 66 |
+
addon: operations['updateAddon']['requestBody']['content']['application/json'],
|
| 67 |
+
options?: RequestOptions,
|
| 68 |
+
) {
|
| 69 |
+
const resp = await this.client.PUT('/api/v1/addons/{addonId}', {
|
| 70 |
+
body: addon,
|
| 71 |
+
params: { path: { addonId } },
|
| 72 |
+
...options,
|
| 73 |
+
})
|
| 74 |
+
|
| 75 |
+
return transformResponse(resp)
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/**
|
| 79 |
+
* Delete an addon by ID
|
| 80 |
+
* @param addonId - The ID of the addon to delete
|
| 81 |
+
* @param options - Optional request options
|
| 82 |
+
* @returns void or standard error response structure
|
| 83 |
+
*/
|
| 84 |
+
public async delete(addonId: string, options?: RequestOptions) {
|
| 85 |
+
const resp = await this.client.DELETE('/api/v1/addons/{addonId}', {
|
| 86 |
+
params: { path: { addonId } },
|
| 87 |
+
...options,
|
| 88 |
+
})
|
| 89 |
+
|
| 90 |
+
return transformResponse(resp)
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
/**
|
| 94 |
+
* Publish an addon
|
| 95 |
+
* @param addonId - The ID of the addon to publish
|
| 96 |
+
* @param options - Optional request options
|
| 97 |
+
* @returns The published addon
|
| 98 |
+
*/
|
| 99 |
+
public async publish(addonId: string, options?: RequestOptions) {
|
| 100 |
+
const resp = await this.client.POST('/api/v1/addons/{addonId}/publish', {
|
| 101 |
+
params: { path: { addonId } },
|
| 102 |
+
...options,
|
| 103 |
+
})
|
| 104 |
+
|
| 105 |
+
return transformResponse(resp)
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
/**
|
| 109 |
+
* Archive an addon
|
| 110 |
+
* @param addonId - The ID of the addon to archive
|
| 111 |
+
* @param options - Optional request options
|
| 112 |
+
* @returns The archived addon
|
| 113 |
+
*/
|
| 114 |
+
public async archive(addonId: string, options?: RequestOptions) {
|
| 115 |
+
const resp = await this.client.POST('/api/v1/addons/{addonId}/archive', {
|
| 116 |
+
params: { path: { addonId } },
|
| 117 |
+
...options,
|
| 118 |
+
})
|
| 119 |
+
|
| 120 |
+
return transformResponse(resp)
|
| 121 |
+
}
|
| 122 |
+
}
|
api/client/javascript/src/client/apps.ts
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Client } from 'openapi-fetch'
|
| 2 |
+
import type { RequestOptions } from './common.js'
|
| 3 |
+
import type {
|
| 4 |
+
AppReplaceUpdate,
|
| 5 |
+
CreateStripeCheckoutSessionRequest,
|
| 6 |
+
operations,
|
| 7 |
+
paths,
|
| 8 |
+
} from './schemas.js'
|
| 9 |
+
import { transformResponse } from './utils.js'
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Apps
|
| 13 |
+
* Manage integrations for extending OpenMeter's functionality.
|
| 14 |
+
*/
|
| 15 |
+
export class Apps {
|
| 16 |
+
public marketplace: AppMarketplace
|
| 17 |
+
public stripe: AppStripe
|
| 18 |
+
public customInvoicing: AppCustomInvoicing
|
| 19 |
+
|
| 20 |
+
constructor(private client: Client<paths, `${string}/${string}`>) {
|
| 21 |
+
this.marketplace = new AppMarketplace(client)
|
| 22 |
+
this.stripe = new AppStripe(client)
|
| 23 |
+
this.customInvoicing = new AppCustomInvoicing(client)
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
* List apps
|
| 28 |
+
* @param query - The query parameters
|
| 29 |
+
* @param signal - An optional abort signal
|
| 30 |
+
* @returns The apps
|
| 31 |
+
*/
|
| 32 |
+
public async list(
|
| 33 |
+
query?: operations['listApps']['parameters']['query'],
|
| 34 |
+
options?: RequestOptions,
|
| 35 |
+
) {
|
| 36 |
+
const resp = await this.client.GET('/api/v1/apps', {
|
| 37 |
+
params: { query },
|
| 38 |
+
...options,
|
| 39 |
+
})
|
| 40 |
+
|
| 41 |
+
return transformResponse(resp)
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/**
|
| 45 |
+
* Get an app
|
| 46 |
+
* @param id - The ID of the app
|
| 47 |
+
* @param signal - An optional abort signal
|
| 48 |
+
* @returns The app
|
| 49 |
+
*/
|
| 50 |
+
public async get(
|
| 51 |
+
id: operations['getApp']['parameters']['path']['id'],
|
| 52 |
+
options?: RequestOptions,
|
| 53 |
+
) {
|
| 54 |
+
const resp = await this.client.GET('/api/v1/apps/{id}', {
|
| 55 |
+
params: { path: { id } },
|
| 56 |
+
...options,
|
| 57 |
+
})
|
| 58 |
+
|
| 59 |
+
return transformResponse(resp)
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/**
|
| 63 |
+
* Update an app
|
| 64 |
+
* @param id - The ID of the app
|
| 65 |
+
* @param body - The body of the request
|
| 66 |
+
* @param signal - An optional abort signal
|
| 67 |
+
* @returns The app
|
| 68 |
+
*/
|
| 69 |
+
public async update(
|
| 70 |
+
id: operations['updateApp']['parameters']['path']['id'],
|
| 71 |
+
body: AppReplaceUpdate,
|
| 72 |
+
options?: RequestOptions,
|
| 73 |
+
) {
|
| 74 |
+
const resp = await this.client.PUT('/api/v1/apps/{id}', {
|
| 75 |
+
body,
|
| 76 |
+
params: { path: { id } },
|
| 77 |
+
...options,
|
| 78 |
+
})
|
| 79 |
+
|
| 80 |
+
return transformResponse(resp)
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
/**
|
| 84 |
+
* Uninstall an app
|
| 85 |
+
* @param id - The ID of the app
|
| 86 |
+
* @param signal - An optional abort signal
|
| 87 |
+
* @returns The app
|
| 88 |
+
*/
|
| 89 |
+
public async uninstall(
|
| 90 |
+
id: operations['uninstallApp']['parameters']['path']['id'],
|
| 91 |
+
options?: RequestOptions,
|
| 92 |
+
) {
|
| 93 |
+
const resp = await this.client.DELETE('/api/v1/apps/{id}', {
|
| 94 |
+
params: { path: { id } },
|
| 95 |
+
...options,
|
| 96 |
+
})
|
| 97 |
+
|
| 98 |
+
return transformResponse(resp)
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
/**
|
| 103 |
+
* App Marketplace
|
| 104 |
+
* Available apps from the OpenMeter Marketplace.
|
| 105 |
+
*/
|
| 106 |
+
export class AppMarketplace {
|
| 107 |
+
constructor(private client: Client<paths, `${string}/${string}`>) {}
|
| 108 |
+
|
| 109 |
+
/**
|
| 110 |
+
* List available apps
|
| 111 |
+
* @param query - The query parameters
|
| 112 |
+
* @param signal - An optional abort signal
|
| 113 |
+
* @returns The apps
|
| 114 |
+
*/
|
| 115 |
+
public async list(
|
| 116 |
+
query?: operations['listMarketplaceListings']['parameters']['query'],
|
| 117 |
+
options?: RequestOptions,
|
| 118 |
+
) {
|
| 119 |
+
const resp = await this.client.GET('/api/v1/marketplace/listings', {
|
| 120 |
+
params: { query },
|
| 121 |
+
...options,
|
| 122 |
+
})
|
| 123 |
+
|
| 124 |
+
return transformResponse(resp)
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
/**
|
| 128 |
+
* Get details for a listing
|
| 129 |
+
* @param type - The type of the listing
|
| 130 |
+
* @param signal - An optional abort signal
|
| 131 |
+
* @returns The listing
|
| 132 |
+
*/
|
| 133 |
+
public async get(
|
| 134 |
+
type: operations['getMarketplaceListing']['parameters']['path']['type'],
|
| 135 |
+
options?: RequestOptions,
|
| 136 |
+
) {
|
| 137 |
+
const resp = await this.client.GET('/api/v1/marketplace/listings/{type}', {
|
| 138 |
+
params: { path: { type } },
|
| 139 |
+
...options,
|
| 140 |
+
})
|
| 141 |
+
|
| 142 |
+
return transformResponse(resp)
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
/**
|
| 146 |
+
* Install an app via OAuth. Returns a URL to start the OAuth 2.0 flow.
|
| 147 |
+
* @param type - The type of the listing
|
| 148 |
+
* @param signal - An optional abort signal
|
| 149 |
+
* @returns The OAuth2 install URL
|
| 150 |
+
*/
|
| 151 |
+
public async getOauth2InstallUrl(
|
| 152 |
+
type: operations['marketplaceOAuth2InstallGetURL']['parameters']['path']['type'],
|
| 153 |
+
options?: RequestOptions,
|
| 154 |
+
) {
|
| 155 |
+
const resp = await this.client.GET(
|
| 156 |
+
'/api/v1/marketplace/listings/{type}/install/oauth2',
|
| 157 |
+
{
|
| 158 |
+
params: { path: { type } },
|
| 159 |
+
...options,
|
| 160 |
+
},
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
return transformResponse(resp)
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
/**
|
| 167 |
+
* Authorize OAuth2 code. Verifies the OAuth code and exchanges it for a token and refresh token
|
| 168 |
+
* @param type - The type of the listing
|
| 169 |
+
* @param signal - An optional abort signal
|
| 170 |
+
* @returns The authorization URL
|
| 171 |
+
*/
|
| 172 |
+
public async authorizeOauth2(
|
| 173 |
+
type: operations['marketplaceOAuth2InstallAuthorize']['parameters']['path']['type'],
|
| 174 |
+
options?: RequestOptions,
|
| 175 |
+
) {
|
| 176 |
+
const resp = await this.client.GET(
|
| 177 |
+
'/api/v1/marketplace/listings/{type}/install/oauth2/authorize',
|
| 178 |
+
{
|
| 179 |
+
params: { path: { type } },
|
| 180 |
+
...options,
|
| 181 |
+
},
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
return transformResponse(resp)
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
/**
|
| 188 |
+
* Install an app via API key.
|
| 189 |
+
* @param type - The type of the listing
|
| 190 |
+
* @param signal - An optional abort signal
|
| 191 |
+
* @returns The installation
|
| 192 |
+
*/
|
| 193 |
+
public async installWithAPIKey(
|
| 194 |
+
type: operations['marketplaceAppAPIKeyInstall']['parameters']['path']['type'],
|
| 195 |
+
body: operations['marketplaceAppAPIKeyInstall']['requestBody']['content']['application/json'],
|
| 196 |
+
options?: RequestOptions,
|
| 197 |
+
) {
|
| 198 |
+
const resp = await this.client.POST(
|
| 199 |
+
'/api/v1/marketplace/listings/{type}/install/apikey',
|
| 200 |
+
{
|
| 201 |
+
body,
|
| 202 |
+
params: { path: { type } },
|
| 203 |
+
...options,
|
| 204 |
+
},
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
return transformResponse(resp)
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
/**
|
| 212 |
+
* Stripe App
|
| 213 |
+
*/
|
| 214 |
+
export class AppStripe {
|
| 215 |
+
constructor(private client: Client<paths, `${string}/${string}`>) {}
|
| 216 |
+
|
| 217 |
+
/**
|
| 218 |
+
* Create a checkout session
|
| 219 |
+
* @param body - The body of the request
|
| 220 |
+
* @param signal - An optional abort signal
|
| 221 |
+
* @returns The checkout session
|
| 222 |
+
*/
|
| 223 |
+
public async createCheckoutSession(
|
| 224 |
+
body: CreateStripeCheckoutSessionRequest,
|
| 225 |
+
options?: RequestOptions,
|
| 226 |
+
) {
|
| 227 |
+
const resp = await this.client.POST('/api/v1/stripe/checkout/sessions', {
|
| 228 |
+
body,
|
| 229 |
+
...options,
|
| 230 |
+
})
|
| 231 |
+
|
| 232 |
+
return transformResponse(resp)
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
/**
|
| 236 |
+
* Update Stripe API key
|
| 237 |
+
* @param id - The ID of the app
|
| 238 |
+
* @param body - The API key data
|
| 239 |
+
* @param options - The request options
|
| 240 |
+
* @returns The updated API key
|
| 241 |
+
* @deprecated
|
| 242 |
+
*/
|
| 243 |
+
public async updateApiKey(
|
| 244 |
+
id: string,
|
| 245 |
+
body: operations['updateStripeAPIKey']['requestBody']['content']['application/json'],
|
| 246 |
+
options?: RequestOptions,
|
| 247 |
+
) {
|
| 248 |
+
const resp = await this.client.PUT('/api/v1/apps/{id}/stripe/api-key', {
|
| 249 |
+
body,
|
| 250 |
+
params: { path: { id } },
|
| 251 |
+
...options,
|
| 252 |
+
})
|
| 253 |
+
|
| 254 |
+
return transformResponse(resp)
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
/**
|
| 259 |
+
* Custom Invoicing App
|
| 260 |
+
*/
|
| 261 |
+
export class AppCustomInvoicing {
|
| 262 |
+
constructor(private client: Client<paths, `${string}/${string}`>) {}
|
| 263 |
+
|
| 264 |
+
/**
|
| 265 |
+
* Submit draft synchronization results
|
| 266 |
+
* @param invoiceId - The ID of the invoice
|
| 267 |
+
* @param body - The body of the request
|
| 268 |
+
* @param options - The request options
|
| 269 |
+
* @returns The synchronization result
|
| 270 |
+
*/
|
| 271 |
+
public async draftSynchronized(
|
| 272 |
+
invoiceId: string,
|
| 273 |
+
body: operations['appCustomInvoicingDraftSynchronized']['requestBody']['content']['application/json'],
|
| 274 |
+
options?: RequestOptions,
|
| 275 |
+
) {
|
| 276 |
+
const resp = await this.client.POST(
|
| 277 |
+
'/api/v1/apps/custom-invoicing/{invoiceId}/draft/synchronized',
|
| 278 |
+
{
|
| 279 |
+
body,
|
| 280 |
+
params: { path: { invoiceId } },
|
| 281 |
+
...options,
|
| 282 |
+
},
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
return transformResponse(resp)
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
/**
|
| 289 |
+
* Submit issuing synchronization results
|
| 290 |
+
* @param invoiceId - The ID of the invoice
|
| 291 |
+
* @param body - The body of the request
|
| 292 |
+
* @param options - The request options
|
| 293 |
+
* @returns The synchronization result
|
| 294 |
+
*/
|
| 295 |
+
public async issuingSynchronized(
|
| 296 |
+
invoiceId: string,
|
| 297 |
+
body: operations['appCustomInvoicingIssuingSynchronized']['requestBody']['content']['application/json'],
|
| 298 |
+
options?: RequestOptions,
|
| 299 |
+
) {
|
| 300 |
+
const resp = await this.client.POST(
|
| 301 |
+
'/api/v1/apps/custom-invoicing/{invoiceId}/issuing/synchronized',
|
| 302 |
+
{
|
| 303 |
+
body,
|
| 304 |
+
params: { path: { invoiceId } },
|
| 305 |
+
...options,
|
| 306 |
+
},
|
| 307 |
+
)
|
| 308 |
+
|
| 309 |
+
return transformResponse(resp)
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
/**
|
| 313 |
+
* Update payment status
|
| 314 |
+
* @param invoiceId - The ID of the invoice
|
| 315 |
+
* @param body - The body of the request
|
| 316 |
+
* @param options - The request options
|
| 317 |
+
* @returns The payment status update result
|
| 318 |
+
*/
|
| 319 |
+
public async updatePaymentStatus(
|
| 320 |
+
invoiceId: string,
|
| 321 |
+
body: operations['appCustomInvoicingUpdatePaymentStatus']['requestBody']['content']['application/json'],
|
| 322 |
+
options?: RequestOptions,
|
| 323 |
+
) {
|
| 324 |
+
const resp = await this.client.POST(
|
| 325 |
+
'/api/v1/apps/custom-invoicing/{invoiceId}/payment/status',
|
| 326 |
+
{
|
| 327 |
+
body,
|
| 328 |
+
params: { path: { invoiceId } },
|
| 329 |
+
...options,
|
| 330 |
+
},
|
| 331 |
+
)
|
| 332 |
+
|
| 333 |
+
return transformResponse(resp)
|
| 334 |
+
}
|
| 335 |
+
}
|
api/client/javascript/src/client/billing.ts
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Client } from 'openapi-fetch'
|
| 2 |
+
import type { RequestOptions } from './common.js'
|
| 3 |
+
import type {
|
| 4 |
+
BillingProfileCreate,
|
| 5 |
+
BillingProfileCustomerOverrideCreate,
|
| 6 |
+
BillingProfileReplaceUpdateWithWorkflow,
|
| 7 |
+
InvoicePendingLineCreateInput,
|
| 8 |
+
InvoiceReplaceUpdate,
|
| 9 |
+
InvoiceSimulationInput,
|
| 10 |
+
operations,
|
| 11 |
+
paths,
|
| 12 |
+
VoidInvoiceActionInput,
|
| 13 |
+
} from './schemas.js'
|
| 14 |
+
import { transformResponse } from './utils.js'
|
| 15 |
+
/**
|
| 16 |
+
* Billing
|
| 17 |
+
*/
|
| 18 |
+
export class Billing {
|
| 19 |
+
public profiles: BillingProfiles
|
| 20 |
+
public invoices: BillingInvoices
|
| 21 |
+
public customers: BillingCustomers
|
| 22 |
+
|
| 23 |
+
constructor(private client: Client<paths, `${string}/${string}`>) {
|
| 24 |
+
this.profiles = new BillingProfiles(this.client)
|
| 25 |
+
this.invoices = new BillingInvoices(this.client)
|
| 26 |
+
this.customers = new BillingCustomers(this.client)
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/**
|
| 31 |
+
* Billing Profiles
|
| 32 |
+
*/
|
| 33 |
+
export class BillingProfiles {
|
| 34 |
+
constructor(private client: Client<paths, `${string}/${string}`>) {}
|
| 35 |
+
|
| 36 |
+
/**
|
| 37 |
+
* Create a billing profile
|
| 38 |
+
* @param billingProfile - The billing profile to create
|
| 39 |
+
* @param signal - An optional abort signal
|
| 40 |
+
* @returns The created billing profile
|
| 41 |
+
*/
|
| 42 |
+
public async create(
|
| 43 |
+
billingProfile: BillingProfileCreate,
|
| 44 |
+
options?: RequestOptions,
|
| 45 |
+
) {
|
| 46 |
+
const resp = await this.client.POST('/api/v1/billing/profiles', {
|
| 47 |
+
body: billingProfile,
|
| 48 |
+
...options,
|
| 49 |
+
})
|
| 50 |
+
|
| 51 |
+
return transformResponse(resp)
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/**
|
| 55 |
+
* Get a billing profile by ID
|
| 56 |
+
* @param id - The ID of the billing profile to get
|
| 57 |
+
* @param signal - An optional abort signal
|
| 58 |
+
* @returns The billing profile
|
| 59 |
+
*/
|
| 60 |
+
public async get(
|
| 61 |
+
id: operations['getBillingProfile']['parameters']['path']['id'],
|
| 62 |
+
options?: RequestOptions,
|
| 63 |
+
) {
|
| 64 |
+
const resp = await this.client.GET('/api/v1/billing/profiles/{id}', {
|
| 65 |
+
params: {
|
| 66 |
+
path: { id },
|
| 67 |
+
},
|
| 68 |
+
...options,
|
| 69 |
+
})
|
| 70 |
+
|
| 71 |
+
return transformResponse(resp)
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/**
|
| 75 |
+
* List billing profiles
|
| 76 |
+
* @param query - The query parameters
|
| 77 |
+
* @param signal - An optional abort signal
|
| 78 |
+
* @returns The billing profiles
|
| 79 |
+
*/
|
| 80 |
+
public async list(
|
| 81 |
+
query?: operations['listBillingProfiles']['parameters']['query'],
|
| 82 |
+
options?: RequestOptions,
|
| 83 |
+
) {
|
| 84 |
+
const resp = await this.client.GET('/api/v1/billing/profiles', {
|
| 85 |
+
params: {
|
| 86 |
+
query,
|
| 87 |
+
},
|
| 88 |
+
...options,
|
| 89 |
+
})
|
| 90 |
+
|
| 91 |
+
return transformResponse(resp)
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/**
|
| 95 |
+
* Update a billing profile
|
| 96 |
+
* @param id - The ID of the billing profile to update
|
| 97 |
+
* @param billingProfile - The billing profile to update
|
| 98 |
+
* @param signal - An optional abort signal
|
| 99 |
+
* @returns The updated billing profile
|
| 100 |
+
*/
|
| 101 |
+
public async update(
|
| 102 |
+
id: operations['updateBillingProfile']['parameters']['path']['id'],
|
| 103 |
+
billingProfile: BillingProfileReplaceUpdateWithWorkflow,
|
| 104 |
+
options?: RequestOptions,
|
| 105 |
+
) {
|
| 106 |
+
const resp = await this.client.PUT('/api/v1/billing/profiles/{id}', {
|
| 107 |
+
body: billingProfile,
|
| 108 |
+
params: {
|
| 109 |
+
path: { id },
|
| 110 |
+
},
|
| 111 |
+
...options,
|
| 112 |
+
})
|
| 113 |
+
|
| 114 |
+
return transformResponse(resp)
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
/**
|
| 118 |
+
* Delete a billing profile
|
| 119 |
+
* @param id - The ID of the billing profile to delete
|
| 120 |
+
* @param options - The request options
|
| 121 |
+
* @returns The deleted billing profile
|
| 122 |
+
*/
|
| 123 |
+
public async delete(
|
| 124 |
+
id: operations['deleteBillingProfile']['parameters']['path']['id'],
|
| 125 |
+
options?: RequestOptions,
|
| 126 |
+
) {
|
| 127 |
+
const resp = await this.client.DELETE('/api/v1/billing/profiles/{id}', {
|
| 128 |
+
params: {
|
| 129 |
+
path: { id },
|
| 130 |
+
},
|
| 131 |
+
...options,
|
| 132 |
+
})
|
| 133 |
+
|
| 134 |
+
return transformResponse(resp)
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
/**
|
| 139 |
+
* Billing Invoices
|
| 140 |
+
*/
|
| 141 |
+
export class BillingInvoices {
|
| 142 |
+
constructor(private client: Client<paths, `${string}/${string}`>) {}
|
| 143 |
+
|
| 144 |
+
/**
|
| 145 |
+
* List invoices
|
| 146 |
+
* @param query - The query parameters
|
| 147 |
+
* @param signal - An optional abort signal
|
| 148 |
+
* @returns The invoices
|
| 149 |
+
*/
|
| 150 |
+
public async list(
|
| 151 |
+
query?: operations['listInvoices']['parameters']['query'],
|
| 152 |
+
options?: RequestOptions,
|
| 153 |
+
) {
|
| 154 |
+
const resp = await this.client.GET('/api/v1/billing/invoices', {
|
| 155 |
+
params: {
|
| 156 |
+
query,
|
| 157 |
+
},
|
| 158 |
+
...options,
|
| 159 |
+
})
|
| 160 |
+
|
| 161 |
+
return transformResponse(resp)
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
/**
|
| 165 |
+
* Get an invoice by ID
|
| 166 |
+
* @param id - The ID of the invoice to get
|
| 167 |
+
* @param signal - An optional abort signal
|
| 168 |
+
* @returns The invoice
|
| 169 |
+
*/
|
| 170 |
+
public async get(
|
| 171 |
+
id: operations['getInvoice']['parameters']['path']['invoiceId'],
|
| 172 |
+
options?: RequestOptions,
|
| 173 |
+
) {
|
| 174 |
+
const resp = await this.client.GET('/api/v1/billing/invoices/{invoiceId}', {
|
| 175 |
+
params: {
|
| 176 |
+
path: { invoiceId: id },
|
| 177 |
+
},
|
| 178 |
+
...options,
|
| 179 |
+
})
|
| 180 |
+
|
| 181 |
+
return transformResponse(resp)
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
/**
|
| 185 |
+
* Update an invoice
|
| 186 |
+
* @description Only invoices in draft or earlier status can be updated.
|
| 187 |
+
* @param id - The ID of the invoice to update
|
| 188 |
+
* @param invoice - The invoice to update
|
| 189 |
+
* @param signal - An optional abort signal
|
| 190 |
+
* @returns The updated invoice
|
| 191 |
+
*/
|
| 192 |
+
public async update(
|
| 193 |
+
id: operations['updateInvoice']['parameters']['path']['invoiceId'],
|
| 194 |
+
invoice: InvoiceReplaceUpdate,
|
| 195 |
+
options?: RequestOptions,
|
| 196 |
+
) {
|
| 197 |
+
const resp = await this.client.PUT('/api/v1/billing/invoices/{invoiceId}', {
|
| 198 |
+
body: invoice,
|
| 199 |
+
params: { path: { invoiceId: id } },
|
| 200 |
+
...options,
|
| 201 |
+
})
|
| 202 |
+
|
| 203 |
+
return transformResponse(resp)
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
/**
|
| 207 |
+
* Delete an invoice
|
| 208 |
+
* @description Only invoices that are in the draft (or earlier) status can be deleted.
|
| 209 |
+
* @param id - The ID of the invoice to delete
|
| 210 |
+
* @param options - The request options
|
| 211 |
+
* @returns The deleted invoice
|
| 212 |
+
*/
|
| 213 |
+
public async delete(
|
| 214 |
+
id: operations['deleteInvoice']['parameters']['path']['invoiceId'],
|
| 215 |
+
options?: RequestOptions,
|
| 216 |
+
) {
|
| 217 |
+
const resp = await this.client.DELETE(
|
| 218 |
+
'/api/v1/billing/invoices/{invoiceId}',
|
| 219 |
+
{
|
| 220 |
+
params: { path: { invoiceId: id } },
|
| 221 |
+
...options,
|
| 222 |
+
},
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
return transformResponse(resp)
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
/**
|
| 229 |
+
* Advance the invoice to the next status
|
| 230 |
+
* @description The call doesn't "approve the invoice", it only advances the invoice to the next status if the transition would be automatic. The action can be called when the invoice's statusDetails' actions field contain the "advance" action.
|
| 231 |
+
* @param id - The ID of the invoice to advance
|
| 232 |
+
* @param signal - An optional abort signal
|
| 233 |
+
* @returns The advanced invoice
|
| 234 |
+
*/
|
| 235 |
+
public async advance(
|
| 236 |
+
id: operations['advanceInvoiceAction']['parameters']['path']['invoiceId'],
|
| 237 |
+
options?: RequestOptions,
|
| 238 |
+
) {
|
| 239 |
+
const resp = await this.client.POST(
|
| 240 |
+
'/api/v1/billing/invoices/{invoiceId}/advance',
|
| 241 |
+
{
|
| 242 |
+
params: { path: { invoiceId: id } },
|
| 243 |
+
...options,
|
| 244 |
+
},
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
return transformResponse(resp)
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
/**
|
| 251 |
+
* Approve an invoice
|
| 252 |
+
* @description This call instantly sends the invoice to the customer using the configured billing profile app.
|
| 253 |
+
* @param id - The ID of the invoice to approve
|
| 254 |
+
* @param signal - An optional abort signal
|
| 255 |
+
* @returns The approved invoice
|
| 256 |
+
*/
|
| 257 |
+
public async approve(
|
| 258 |
+
id: operations['approveInvoiceAction']['parameters']['path']['invoiceId'],
|
| 259 |
+
options?: RequestOptions,
|
| 260 |
+
) {
|
| 261 |
+
const resp = await this.client.POST(
|
| 262 |
+
'/api/v1/billing/invoices/{invoiceId}/approve',
|
| 263 |
+
{
|
| 264 |
+
params: { path: { invoiceId: id } },
|
| 265 |
+
...options,
|
| 266 |
+
},
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
return transformResponse(resp)
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
/**
|
| 273 |
+
* Retry advancing the invoice after a failed attempt.
|
| 274 |
+
* @param id - The ID of the invoice to retry
|
| 275 |
+
* @param signal - An optional abort signal
|
| 276 |
+
* @returns The retried invoice
|
| 277 |
+
*/
|
| 278 |
+
public async retry(
|
| 279 |
+
id: operations['retryInvoiceAction']['parameters']['path']['invoiceId'],
|
| 280 |
+
options?: RequestOptions,
|
| 281 |
+
) {
|
| 282 |
+
const resp = await this.client.POST(
|
| 283 |
+
'/api/v1/billing/invoices/{invoiceId}/retry',
|
| 284 |
+
{
|
| 285 |
+
params: { path: { invoiceId: id } },
|
| 286 |
+
...options,
|
| 287 |
+
},
|
| 288 |
+
)
|
| 289 |
+
|
| 290 |
+
return transformResponse(resp)
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
/**
|
| 294 |
+
* Void an invoice
|
| 295 |
+
* @description Void an invoice
|
| 296 |
+
*
|
| 297 |
+
* Only invoices that have been alread issued can be voided.
|
| 298 |
+
* @param id - The ID of the invoice to void
|
| 299 |
+
* @param signal - An optional abort signal
|
| 300 |
+
* @returns The voided invoice
|
| 301 |
+
*/
|
| 302 |
+
public async void(
|
| 303 |
+
id: operations['voidInvoiceAction']['parameters']['path']['invoiceId'],
|
| 304 |
+
body: VoidInvoiceActionInput,
|
| 305 |
+
options?: RequestOptions,
|
| 306 |
+
) {
|
| 307 |
+
const resp = await this.client.POST(
|
| 308 |
+
'/api/v1/billing/invoices/{invoiceId}/void',
|
| 309 |
+
{
|
| 310 |
+
body,
|
| 311 |
+
params: { path: { invoiceId: id } },
|
| 312 |
+
...options,
|
| 313 |
+
},
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
return transformResponse(resp)
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
/**
|
| 320 |
+
* Recalculate an invoice's tax amounts
|
| 321 |
+
* @param id - The ID of the invoice to recalculate
|
| 322 |
+
* @param signal - An optional abort signal
|
| 323 |
+
* @returns The recalculated invoice
|
| 324 |
+
*/
|
| 325 |
+
public async recalculateTax(
|
| 326 |
+
id: operations['recalculateInvoiceTaxAction']['parameters']['path']['invoiceId'],
|
| 327 |
+
options?: RequestOptions,
|
| 328 |
+
) {
|
| 329 |
+
const resp = await this.client.POST(
|
| 330 |
+
'/api/v1/billing/invoices/{invoiceId}/taxes/recalculate',
|
| 331 |
+
{
|
| 332 |
+
params: { path: { invoiceId: id } },
|
| 333 |
+
...options,
|
| 334 |
+
},
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
return transformResponse(resp)
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
/**
|
| 341 |
+
* Snapshot invoice line item quantities
|
| 342 |
+
* @description Snapshot the quantities of the invoice line items. This is useful for invoices that have usage-based line items.
|
| 343 |
+
* @param id - The ID of the invoice to snapshot
|
| 344 |
+
* @param signal - An optional abort signal
|
| 345 |
+
* @returns The invoice with snapshotted quantities
|
| 346 |
+
*/
|
| 347 |
+
public async snapshotQuantities(
|
| 348 |
+
id: operations['snapshotQuantitiesInvoiceAction']['parameters']['path']['invoiceId'],
|
| 349 |
+
options?: RequestOptions,
|
| 350 |
+
) {
|
| 351 |
+
const resp = await this.client.POST(
|
| 352 |
+
'/api/v1/billing/invoices/{invoiceId}/snapshot-quantities',
|
| 353 |
+
{
|
| 354 |
+
params: { path: { invoiceId: id } },
|
| 355 |
+
...options,
|
| 356 |
+
},
|
| 357 |
+
)
|
| 358 |
+
|
| 359 |
+
return transformResponse(resp)
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
/**
|
| 363 |
+
* Simulate an invoice for a customer
|
| 364 |
+
* @param id - The ID of the customer to simulate the invoice for
|
| 365 |
+
* @param signal - An optional abort signal
|
| 366 |
+
* @returns The simulated invoice
|
| 367 |
+
*/
|
| 368 |
+
public async simulate(
|
| 369 |
+
id: operations['simulateInvoice']['parameters']['path']['customerId'],
|
| 370 |
+
body: InvoiceSimulationInput,
|
| 371 |
+
options?: RequestOptions,
|
| 372 |
+
) {
|
| 373 |
+
const resp = await this.client.POST(
|
| 374 |
+
'/api/v1/billing/customers/{customerId}/invoices/simulate',
|
| 375 |
+
{
|
| 376 |
+
body,
|
| 377 |
+
params: { path: { customerId: id } },
|
| 378 |
+
...options,
|
| 379 |
+
},
|
| 380 |
+
)
|
| 381 |
+
|
| 382 |
+
return transformResponse(resp)
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
/**
|
| 386 |
+
* Create pending line items
|
| 387 |
+
* @description Create new pending line items (charges).
|
| 388 |
+
* This call is used to create a new pending line item for the customer if required a new
|
| 389 |
+
* gathering invoice will be created.
|
| 390 |
+
*
|
| 391 |
+
* A new invoice will be created if:
|
| 392 |
+
* - there is no invoice in gathering state
|
| 393 |
+
* - the currency of the line item doesn't match the currency of any invoices in gathering state
|
| 394 |
+
* @param customerId - The ID of the customer to create the line items for
|
| 395 |
+
* @param body - The line items to create
|
| 396 |
+
* @param signal - An optional abort signal
|
| 397 |
+
* @returns The created line items
|
| 398 |
+
*/
|
| 399 |
+
public async createLineItems(
|
| 400 |
+
customerId: operations['createPendingInvoiceLine']['parameters']['path']['customerId'],
|
| 401 |
+
body: InvoicePendingLineCreateInput,
|
| 402 |
+
options?: RequestOptions,
|
| 403 |
+
) {
|
| 404 |
+
const resp = await this.client.POST(
|
| 405 |
+
'/api/v1/billing/customers/{customerId}/invoices/pending-lines',
|
| 406 |
+
{
|
| 407 |
+
body,
|
| 408 |
+
params: { path: { customerId } },
|
| 409 |
+
...options,
|
| 410 |
+
},
|
| 411 |
+
)
|
| 412 |
+
|
| 413 |
+
return transformResponse(resp)
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
/**
|
| 417 |
+
* Invoice a customer based on the pending line items
|
| 418 |
+
* @description Create a new invoice from the pending line items. This should only be called if for some reason we need to invoice a customer outside of the normal billing cycle.
|
| 419 |
+
* @param body - The invoice data
|
| 420 |
+
* @param options - The request options
|
| 421 |
+
* @returns The created invoices
|
| 422 |
+
*/
|
| 423 |
+
public async invoicePendingLines(
|
| 424 |
+
body: operations['invoicePendingLinesAction']['requestBody']['content']['application/json'],
|
| 425 |
+
options?: RequestOptions,
|
| 426 |
+
) {
|
| 427 |
+
const resp = await this.client.POST('/api/v1/billing/invoices/invoice', {
|
| 428 |
+
body,
|
| 429 |
+
...options,
|
| 430 |
+
})
|
| 431 |
+
|
| 432 |
+
return transformResponse(resp)
|
| 433 |
+
}
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
/**
|
| 437 |
+
* Billing Customer Invoices and Overrides
|
| 438 |
+
*/
|
| 439 |
+
export class BillingCustomers {
|
| 440 |
+
constructor(private client: Client<paths, `${string}/${string}`>) {}
|
| 441 |
+
|
| 442 |
+
/**
|
| 443 |
+
* Create or update a customer override
|
| 444 |
+
* @param id - The ID of the customer to create the override for
|
| 445 |
+
* @param body - The customer override to create
|
| 446 |
+
* @param signal - An optional abort signal
|
| 447 |
+
* @returns The created customer override
|
| 448 |
+
*/
|
| 449 |
+
public async createOverride(
|
| 450 |
+
id: operations['upsertBillingProfileCustomerOverride']['parameters']['path']['customerId'],
|
| 451 |
+
body: BillingProfileCustomerOverrideCreate,
|
| 452 |
+
options?: RequestOptions,
|
| 453 |
+
) {
|
| 454 |
+
const resp = await this.client.PUT(
|
| 455 |
+
'/api/v1/billing/customers/{customerId}',
|
| 456 |
+
{
|
| 457 |
+
body,
|
| 458 |
+
params: { path: { customerId: id } },
|
| 459 |
+
...options,
|
| 460 |
+
},
|
| 461 |
+
)
|
| 462 |
+
|
| 463 |
+
return transformResponse(resp)
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
/**
|
| 467 |
+
* Get a customer override
|
| 468 |
+
* @param id - The ID of the customer to get the override for
|
| 469 |
+
* @param signal - An optional abort signal
|
| 470 |
+
* @returns The customer override
|
| 471 |
+
*/
|
| 472 |
+
public async getOverride(
|
| 473 |
+
id: operations['getBillingProfileCustomerOverride']['parameters']['path']['customerId'],
|
| 474 |
+
options?: RequestOptions,
|
| 475 |
+
) {
|
| 476 |
+
const resp = await this.client.GET(
|
| 477 |
+
'/api/v1/billing/customers/{customerId}',
|
| 478 |
+
{
|
| 479 |
+
params: { path: { customerId: id } },
|
| 480 |
+
...options,
|
| 481 |
+
},
|
| 482 |
+
)
|
| 483 |
+
|
| 484 |
+
return transformResponse(resp)
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
/**
|
| 488 |
+
* List customer overrides
|
| 489 |
+
* @param query - The query parameters
|
| 490 |
+
* @param signal - An optional abort signal
|
| 491 |
+
* @returns The customer overrides
|
| 492 |
+
*/
|
| 493 |
+
public async listOverrides(
|
| 494 |
+
query?: operations['listBillingProfileCustomerOverrides']['parameters']['query'],
|
| 495 |
+
options?: RequestOptions,
|
| 496 |
+
) {
|
| 497 |
+
const resp = await this.client.GET('/api/v1/billing/customers', {
|
| 498 |
+
params: { query },
|
| 499 |
+
...options,
|
| 500 |
+
})
|
| 501 |
+
|
| 502 |
+
return transformResponse(resp)
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
/**
|
| 506 |
+
* Delete a customer override
|
| 507 |
+
* @param id - The ID of the customer to delete the override for
|
| 508 |
+
* @param signal - An optional abort signal
|
| 509 |
+
* @returns The deleted customer override
|
| 510 |
+
*/
|
| 511 |
+
public async deleteOverride(
|
| 512 |
+
id: operations['deleteBillingProfileCustomerOverride']['parameters']['path']['customerId'],
|
| 513 |
+
options?: RequestOptions,
|
| 514 |
+
) {
|
| 515 |
+
const resp = await this.client.DELETE(
|
| 516 |
+
'/api/v1/billing/customers/{customerId}',
|
| 517 |
+
{
|
| 518 |
+
params: { path: { customerId: id } },
|
| 519 |
+
...options,
|
| 520 |
+
},
|
| 521 |
+
)
|
| 522 |
+
|
| 523 |
+
return transformResponse(resp)
|
| 524 |
+
}
|
| 525 |
+
}
|
api/client/javascript/src/client/common.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { UnexpectedProblemResponse } from './schemas.js'
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Request options
|
| 5 |
+
*/
|
| 6 |
+
export type RequestOptions = Pick<RequestInit, 'signal'>
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* An error that occurred during an HTTP request
|
| 10 |
+
*/
|
| 11 |
+
export class HTTPError extends Error {
|
| 12 |
+
public name = 'HTTPError'
|
| 13 |
+
public client = 'OpenMeter'
|
| 14 |
+
|
| 15 |
+
constructor(
|
| 16 |
+
public message: string,
|
| 17 |
+
public type: string,
|
| 18 |
+
public title: string,
|
| 19 |
+
public status: number,
|
| 20 |
+
public url: string,
|
| 21 |
+
protected __raw?: Record<string, unknown>,
|
| 22 |
+
) {
|
| 23 |
+
super(message)
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
static fromResponse(resp: {
|
| 27 |
+
response: Response
|
| 28 |
+
error?: UnexpectedProblemResponse
|
| 29 |
+
}): HTTPError {
|
| 30 |
+
if (
|
| 31 |
+
resp.response.headers.get('Content-Type') ===
|
| 32 |
+
'application/problem+json' &&
|
| 33 |
+
resp.error
|
| 34 |
+
) {
|
| 35 |
+
return new HTTPError(
|
| 36 |
+
`Request failed (${resp.response.url}) [${resp.response.status}]: ${resp.error.detail}`,
|
| 37 |
+
resp.error.type,
|
| 38 |
+
resp.error.title,
|
| 39 |
+
resp.error.status ?? resp.response.status,
|
| 40 |
+
resp.response.url,
|
| 41 |
+
resp.error,
|
| 42 |
+
)
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
return new HTTPError(
|
| 46 |
+
`Request failed (${resp.response.url}) [${resp.response.status}]: ${resp.response.statusText}`,
|
| 47 |
+
resp.response.statusText,
|
| 48 |
+
resp.response.statusText,
|
| 49 |
+
resp.response.status,
|
| 50 |
+
resp.response.url,
|
| 51 |
+
)
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
getField(key: string) {
|
| 55 |
+
return this.__raw?.[key]
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
/**
|
| 60 |
+
* Check if an error is an HTTPError
|
| 61 |
+
* @param error - The error to check
|
| 62 |
+
* @returns Whether the error is an HTTPError
|
| 63 |
+
*/
|
| 64 |
+
export function isHTTPError(error: unknown): error is HTTPError {
|
| 65 |
+
return error instanceof HTTPError
|
| 66 |
+
}
|
api/client/javascript/src/client/customers.ts
ADDED
|
@@ -0,0 +1,672 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Client } from 'openapi-fetch'
|
| 2 |
+
import type { RequestOptions } from './common.js'
|
| 3 |
+
import type {
|
| 4 |
+
CreateStripeCustomerPortalSessionParams,
|
| 5 |
+
CustomerAppData,
|
| 6 |
+
CustomerCreate,
|
| 7 |
+
CustomerReplaceUpdate,
|
| 8 |
+
operations,
|
| 9 |
+
paths,
|
| 10 |
+
StripeCustomerAppDataBase,
|
| 11 |
+
} from './schemas.js'
|
| 12 |
+
import { transformResponse } from './utils.js'
|
| 13 |
+
|
| 14 |
+
/**
|
| 15 |
+
* Customers
|
| 16 |
+
* Manage customer subscription lifecycles and plan assignments.
|
| 17 |
+
*/
|
| 18 |
+
export class Customers {
|
| 19 |
+
public apps: CustomerApps
|
| 20 |
+
public entitlementsV1: CustomerEntitlements
|
| 21 |
+
public entitlements: CustomerEntitlementsV2
|
| 22 |
+
public stripe: CustomerStripe
|
| 23 |
+
|
| 24 |
+
constructor(private client: Client<paths, `${string}/${string}`>) {
|
| 25 |
+
this.apps = new CustomerApps(client)
|
| 26 |
+
this.entitlementsV1 = new CustomerEntitlements(client)
|
| 27 |
+
this.entitlements = new CustomerEntitlementsV2(client)
|
| 28 |
+
this.stripe = new CustomerStripe(client)
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
/**
|
| 32 |
+
* Create a customer
|
| 33 |
+
* @param customer - The customer to create
|
| 34 |
+
* @param signal - An optional abort signal
|
| 35 |
+
* @returns The created customer
|
| 36 |
+
*/
|
| 37 |
+
public async create(customer: CustomerCreate, options?: RequestOptions) {
|
| 38 |
+
const resp = await this.client.POST('/api/v1/customers', {
|
| 39 |
+
body: customer,
|
| 40 |
+
...options,
|
| 41 |
+
})
|
| 42 |
+
|
| 43 |
+
return transformResponse(resp)
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/**
|
| 47 |
+
* Get a customer by ID
|
| 48 |
+
* @param customerIdOrKey - The ID or Key of the customer
|
| 49 |
+
* @param signal - An optional abort signal
|
| 50 |
+
* @returns The customer
|
| 51 |
+
*/
|
| 52 |
+
public async get(
|
| 53 |
+
customerIdOrKey: operations['getCustomer']['parameters']['path']['customerIdOrKey'],
|
| 54 |
+
options?: RequestOptions,
|
| 55 |
+
) {
|
| 56 |
+
const resp = await this.client.GET('/api/v1/customers/{customerIdOrKey}', {
|
| 57 |
+
params: {
|
| 58 |
+
path: {
|
| 59 |
+
customerIdOrKey,
|
| 60 |
+
},
|
| 61 |
+
},
|
| 62 |
+
...options,
|
| 63 |
+
})
|
| 64 |
+
|
| 65 |
+
return transformResponse(resp)
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
/**
|
| 69 |
+
* Update a customer
|
| 70 |
+
* @param customerIdOrKey - The ID or Key of the customer
|
| 71 |
+
* @param customer - The customer to update
|
| 72 |
+
* @param signal - An optional abort signal
|
| 73 |
+
* @returns The updated customer
|
| 74 |
+
*/
|
| 75 |
+
public async update(
|
| 76 |
+
customerIdOrKey: operations['updateCustomer']['parameters']['path']['customerIdOrKey'],
|
| 77 |
+
customer: CustomerReplaceUpdate,
|
| 78 |
+
options?: RequestOptions,
|
| 79 |
+
) {
|
| 80 |
+
const resp = await this.client.PUT('/api/v1/customers/{customerIdOrKey}', {
|
| 81 |
+
body: customer,
|
| 82 |
+
params: {
|
| 83 |
+
path: {
|
| 84 |
+
customerIdOrKey,
|
| 85 |
+
},
|
| 86 |
+
},
|
| 87 |
+
...options,
|
| 88 |
+
})
|
| 89 |
+
|
| 90 |
+
return transformResponse(resp)
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
/**
|
| 94 |
+
* Delete a customer
|
| 95 |
+
* @param customerIdOrKey - The ID or Key of the customer
|
| 96 |
+
* @param signal - An optional abort signal
|
| 97 |
+
* @returns The deleted customer
|
| 98 |
+
*/
|
| 99 |
+
public async delete(
|
| 100 |
+
customerIdOrKey: operations['deleteCustomer']['parameters']['path']['customerIdOrKey'],
|
| 101 |
+
options?: RequestOptions,
|
| 102 |
+
) {
|
| 103 |
+
const resp = await this.client.DELETE(
|
| 104 |
+
'/api/v1/customers/{customerIdOrKey}',
|
| 105 |
+
{
|
| 106 |
+
params: {
|
| 107 |
+
path: {
|
| 108 |
+
customerIdOrKey,
|
| 109 |
+
},
|
| 110 |
+
},
|
| 111 |
+
...options,
|
| 112 |
+
},
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
return transformResponse(resp)
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/**
|
| 119 |
+
* List customers
|
| 120 |
+
* @param signal - An optional abort signal
|
| 121 |
+
* @returns The list of customers
|
| 122 |
+
*/
|
| 123 |
+
public async list(
|
| 124 |
+
query?: operations['listCustomers']['parameters']['query'],
|
| 125 |
+
options?: RequestOptions,
|
| 126 |
+
) {
|
| 127 |
+
const resp = await this.client.GET('/api/v1/customers', {
|
| 128 |
+
params: {
|
| 129 |
+
query,
|
| 130 |
+
},
|
| 131 |
+
...options,
|
| 132 |
+
})
|
| 133 |
+
|
| 134 |
+
return transformResponse(resp)
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
/**
|
| 138 |
+
* Get customer access
|
| 139 |
+
* @param customerIdOrKey - The ID or Key of the customer
|
| 140 |
+
* @param options - Optional request options
|
| 141 |
+
* @returns The customer access information
|
| 142 |
+
*/
|
| 143 |
+
public async getAccess(
|
| 144 |
+
customerIdOrKey: operations['getCustomerAccess']['parameters']['path']['customerIdOrKey'],
|
| 145 |
+
options?: RequestOptions,
|
| 146 |
+
) {
|
| 147 |
+
const resp = await this.client.GET(
|
| 148 |
+
'/api/v1/customers/{customerIdOrKey}/access',
|
| 149 |
+
{
|
| 150 |
+
params: {
|
| 151 |
+
path: {
|
| 152 |
+
customerIdOrKey,
|
| 153 |
+
},
|
| 154 |
+
},
|
| 155 |
+
...options,
|
| 156 |
+
},
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
return transformResponse(resp)
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
/**
|
| 163 |
+
* List customer subscriptions
|
| 164 |
+
* @param customerIdOrKey - The ID or key of the customer
|
| 165 |
+
* @param query - The query parameters
|
| 166 |
+
* @param signal - An optional abort signal
|
| 167 |
+
* @returns The list of customer subscriptions
|
| 168 |
+
*/
|
| 169 |
+
public async listSubscriptions(
|
| 170 |
+
customerIdOrKey: operations['listCustomerSubscriptions']['parameters']['path']['customerIdOrKey'],
|
| 171 |
+
query?: operations['listCustomerSubscriptions']['parameters']['query'],
|
| 172 |
+
options?: RequestOptions,
|
| 173 |
+
) {
|
| 174 |
+
const resp = await this.client.GET(
|
| 175 |
+
'/api/v1/customers/{customerIdOrKey}/subscriptions',
|
| 176 |
+
{
|
| 177 |
+
params: { path: { customerIdOrKey }, query },
|
| 178 |
+
...options,
|
| 179 |
+
},
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
return transformResponse(resp)
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
/**
|
| 187 |
+
* Customer Apps
|
| 188 |
+
* Manage customer apps.
|
| 189 |
+
*/
|
| 190 |
+
export class CustomerApps {
|
| 191 |
+
constructor(private client: Client<paths, `${string}/${string}`>) {}
|
| 192 |
+
|
| 193 |
+
/**
|
| 194 |
+
* Upsert customer app data
|
| 195 |
+
* @param customerIdOrKey - The ID or Key of the customer
|
| 196 |
+
* @param appData - The app data to upsert
|
| 197 |
+
* @param signal - An optional abort signal
|
| 198 |
+
* @returns The upserted app data
|
| 199 |
+
*/
|
| 200 |
+
public async upsert(
|
| 201 |
+
customerIdOrKey: operations['upsertCustomerAppData']['parameters']['path']['customerIdOrKey'],
|
| 202 |
+
appData: CustomerAppData[],
|
| 203 |
+
options?: RequestOptions,
|
| 204 |
+
) {
|
| 205 |
+
const resp = await this.client.PUT(
|
| 206 |
+
'/api/v1/customers/{customerIdOrKey}/apps',
|
| 207 |
+
{
|
| 208 |
+
body: appData,
|
| 209 |
+
params: {
|
| 210 |
+
path: {
|
| 211 |
+
customerIdOrKey,
|
| 212 |
+
},
|
| 213 |
+
},
|
| 214 |
+
...options,
|
| 215 |
+
},
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
return transformResponse(resp)
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
/**
|
| 222 |
+
* List customer app data
|
| 223 |
+
* @param customerIdOrKey - The ID or key of the customer
|
| 224 |
+
* @param query - The query parameters
|
| 225 |
+
* @param signal - An optional abort signal
|
| 226 |
+
* @returns The list of customer app data
|
| 227 |
+
*/
|
| 228 |
+
public async list(
|
| 229 |
+
customerIdOrKey: operations['listCustomerAppData']['parameters']['path']['customerIdOrKey'],
|
| 230 |
+
query?: operations['listCustomerAppData']['parameters']['query'],
|
| 231 |
+
options?: RequestOptions,
|
| 232 |
+
) {
|
| 233 |
+
const resp = await this.client.GET(
|
| 234 |
+
'/api/v1/customers/{customerIdOrKey}/apps',
|
| 235 |
+
{
|
| 236 |
+
params: {
|
| 237 |
+
path: { customerIdOrKey },
|
| 238 |
+
query,
|
| 239 |
+
},
|
| 240 |
+
...options,
|
| 241 |
+
},
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
return transformResponse(resp)
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
/**
|
| 248 |
+
* Delete customer app data
|
| 249 |
+
* @param customerIdOrKey - The ID or key of the customer
|
| 250 |
+
* @param appId - The ID of the app
|
| 251 |
+
* @param signal - An optional abort signal
|
| 252 |
+
* @returns The deleted customer app data
|
| 253 |
+
*/
|
| 254 |
+
public async delete(
|
| 255 |
+
customerIdOrKey: operations['deleteCustomerAppData']['parameters']['path']['customerIdOrKey'],
|
| 256 |
+
appId: operations['deleteCustomerAppData']['parameters']['path']['appId'],
|
| 257 |
+
options?: RequestOptions,
|
| 258 |
+
) {
|
| 259 |
+
const resp = await this.client.DELETE(
|
| 260 |
+
'/api/v1/customers/{customerIdOrKey}/apps/{appId}',
|
| 261 |
+
{
|
| 262 |
+
params: { path: { appId, customerIdOrKey } },
|
| 263 |
+
...options,
|
| 264 |
+
},
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
return transformResponse(resp)
|
| 268 |
+
}
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
/**
|
| 272 |
+
* Customer Stripe
|
| 273 |
+
* Manage customer Stripe data.
|
| 274 |
+
*/
|
| 275 |
+
export class CustomerStripe {
|
| 276 |
+
constructor(private client: Client<paths, `${string}/${string}`>) {}
|
| 277 |
+
|
| 278 |
+
/**
|
| 279 |
+
* Upsert customer stripe app data
|
| 280 |
+
* @param customerIdOrKey - The ID or Key of the customer
|
| 281 |
+
* @param appData - The app data to upsert
|
| 282 |
+
* @param signal - An optional abort signal
|
| 283 |
+
* @returns The upserted customer stripe app data
|
| 284 |
+
*/
|
| 285 |
+
public async upsert(
|
| 286 |
+
customerIdOrKey: operations['upsertCustomerStripeAppData']['parameters']['path']['customerIdOrKey'],
|
| 287 |
+
stripeAppData: StripeCustomerAppDataBase,
|
| 288 |
+
options?: RequestOptions,
|
| 289 |
+
) {
|
| 290 |
+
const resp = await this.client.PUT(
|
| 291 |
+
'/api/v1/customers/{customerIdOrKey}/stripe',
|
| 292 |
+
{
|
| 293 |
+
body: stripeAppData,
|
| 294 |
+
params: {
|
| 295 |
+
path: {
|
| 296 |
+
customerIdOrKey,
|
| 297 |
+
},
|
| 298 |
+
},
|
| 299 |
+
...options,
|
| 300 |
+
},
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
return transformResponse(resp)
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
/**
|
| 307 |
+
* Get customer stripe app data
|
| 308 |
+
* @param customerIdOrKey - The ID or key of the customer
|
| 309 |
+
* @param query - The query parameters
|
| 310 |
+
* @param signal - An optional abort signal
|
| 311 |
+
* @returns The customer stripe app data
|
| 312 |
+
*/
|
| 313 |
+
public async get(
|
| 314 |
+
customerIdOrKey: operations['getCustomerStripeAppData']['parameters']['path']['customerIdOrKey'],
|
| 315 |
+
options?: RequestOptions,
|
| 316 |
+
) {
|
| 317 |
+
const resp = await this.client.GET(
|
| 318 |
+
'/api/v1/customers/{customerIdOrKey}/stripe',
|
| 319 |
+
{
|
| 320 |
+
params: {
|
| 321 |
+
path: { customerIdOrKey },
|
| 322 |
+
},
|
| 323 |
+
...options,
|
| 324 |
+
},
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
return transformResponse(resp)
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
/**
|
| 331 |
+
* Create a Stripe customer portal session
|
| 332 |
+
* @param customerIdOrKey - The ID or Key of the customer
|
| 333 |
+
* @param params - The parameters for creating a Stripe customer portal session
|
| 334 |
+
* @param signal - An optional abort signal
|
| 335 |
+
* @returns The Stripe customer portal session
|
| 336 |
+
*/
|
| 337 |
+
public async createPortalSession(
|
| 338 |
+
customerIdOrKey: operations['createCustomerStripePortalSession']['parameters']['path']['customerIdOrKey'],
|
| 339 |
+
params: CreateStripeCustomerPortalSessionParams,
|
| 340 |
+
options?: RequestOptions,
|
| 341 |
+
) {
|
| 342 |
+
const resp = await this.client.POST(
|
| 343 |
+
'/api/v1/customers/{customerIdOrKey}/stripe/portal',
|
| 344 |
+
{
|
| 345 |
+
body: params,
|
| 346 |
+
params: {
|
| 347 |
+
path: {
|
| 348 |
+
customerIdOrKey,
|
| 349 |
+
},
|
| 350 |
+
},
|
| 351 |
+
...options,
|
| 352 |
+
},
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
return transformResponse(resp)
|
| 356 |
+
}
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
/**
|
| 360 |
+
* Customer Entitlements
|
| 361 |
+
*/
|
| 362 |
+
export class CustomerEntitlements {
|
| 363 |
+
constructor(private client: Client<paths, `${string}/${string}`>) {}
|
| 364 |
+
|
| 365 |
+
/**
|
| 366 |
+
* Get the value of an entitlement for a customer
|
| 367 |
+
* @param customerIdOrKey - The ID or Key of the customer
|
| 368 |
+
* @param featureKey - The key of the feature
|
| 369 |
+
* @param signal - An optional abort signal
|
| 370 |
+
* @returns The value of the entitlement
|
| 371 |
+
*/
|
| 372 |
+
public async value(
|
| 373 |
+
customerIdOrKey: operations['getCustomerEntitlementValue']['parameters']['path']['customerIdOrKey'],
|
| 374 |
+
featureKey: operations['getCustomerEntitlementValue']['parameters']['path']['featureKey'],
|
| 375 |
+
options?: RequestOptions,
|
| 376 |
+
) {
|
| 377 |
+
const resp = await this.client.GET(
|
| 378 |
+
'/api/v1/customers/{customerIdOrKey}/entitlements/{featureKey}/value',
|
| 379 |
+
{
|
| 380 |
+
params: { path: { customerIdOrKey, featureKey } },
|
| 381 |
+
...options,
|
| 382 |
+
},
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
return transformResponse(resp)
|
| 386 |
+
}
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
/**
|
| 390 |
+
* Customer Entitlements V2
|
| 391 |
+
*/
|
| 392 |
+
export class CustomerEntitlementsV2 {
|
| 393 |
+
constructor(private client: Client<paths, `${string}/${string}`>) {}
|
| 394 |
+
|
| 395 |
+
/**
|
| 396 |
+
* List all entitlements for a customer
|
| 397 |
+
* @param customerIdOrKey - The ID or Key of the customer
|
| 398 |
+
* @param options - Request options including query parameters
|
| 399 |
+
* @returns List of customer entitlements
|
| 400 |
+
*/
|
| 401 |
+
public async list(
|
| 402 |
+
customerIdOrKey: operations['listCustomerEntitlementsV2']['parameters']['path']['customerIdOrKey'],
|
| 403 |
+
options?: RequestOptions & {
|
| 404 |
+
query?: operations['listCustomerEntitlementsV2']['parameters']['query']
|
| 405 |
+
},
|
| 406 |
+
) {
|
| 407 |
+
const resp = await this.client.GET(
|
| 408 |
+
'/api/v2/customers/{customerIdOrKey}/entitlements',
|
| 409 |
+
{
|
| 410 |
+
params: {
|
| 411 |
+
path: { customerIdOrKey },
|
| 412 |
+
query: options?.query,
|
| 413 |
+
},
|
| 414 |
+
...options,
|
| 415 |
+
},
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
return transformResponse(resp)
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
/**
|
| 422 |
+
* Create a customer entitlement
|
| 423 |
+
* @param customerIdOrKey - The ID or Key of the customer
|
| 424 |
+
* @param entitlement - The entitlement data to create
|
| 425 |
+
* @param options - Request options
|
| 426 |
+
* @returns The created entitlement
|
| 427 |
+
*/
|
| 428 |
+
public async create(
|
| 429 |
+
customerIdOrKey: operations['createCustomerEntitlementV2']['parameters']['path']['customerIdOrKey'],
|
| 430 |
+
entitlement: operations['createCustomerEntitlementV2']['requestBody']['content']['application/json'],
|
| 431 |
+
options?: RequestOptions,
|
| 432 |
+
) {
|
| 433 |
+
const resp = await this.client.POST(
|
| 434 |
+
'/api/v2/customers/{customerIdOrKey}/entitlements',
|
| 435 |
+
{
|
| 436 |
+
body: entitlement,
|
| 437 |
+
params: {
|
| 438 |
+
path: { customerIdOrKey },
|
| 439 |
+
},
|
| 440 |
+
...options,
|
| 441 |
+
},
|
| 442 |
+
)
|
| 443 |
+
|
| 444 |
+
return transformResponse(resp)
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
/**
|
| 448 |
+
* Get a specific customer entitlement
|
| 449 |
+
* @param customerIdOrKey - The ID or Key of the customer
|
| 450 |
+
* @param entitlementIdOrFeatureKey - The ID or feature key of the entitlement
|
| 451 |
+
* @param options - Request options
|
| 452 |
+
* @returns The entitlement
|
| 453 |
+
*/
|
| 454 |
+
public async get(
|
| 455 |
+
customerIdOrKey: operations['getCustomerEntitlementV2']['parameters']['path']['customerIdOrKey'],
|
| 456 |
+
entitlementIdOrFeatureKey: operations['getCustomerEntitlementV2']['parameters']['path']['entitlementIdOrFeatureKey'],
|
| 457 |
+
options?: RequestOptions,
|
| 458 |
+
) {
|
| 459 |
+
const resp = await this.client.GET(
|
| 460 |
+
'/api/v2/customers/{customerIdOrKey}/entitlements/{entitlementIdOrFeatureKey}',
|
| 461 |
+
{
|
| 462 |
+
params: {
|
| 463 |
+
path: { customerIdOrKey, entitlementIdOrFeatureKey },
|
| 464 |
+
},
|
| 465 |
+
...options,
|
| 466 |
+
},
|
| 467 |
+
)
|
| 468 |
+
|
| 469 |
+
return transformResponse(resp)
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
/**
|
| 473 |
+
* Delete a customer entitlement
|
| 474 |
+
* @param customerIdOrKey - The ID or Key of the customer
|
| 475 |
+
* @param entitlementIdOrFeatureKey - The ID or feature key of the entitlement
|
| 476 |
+
* @param options - Request options
|
| 477 |
+
* @returns The deletion response
|
| 478 |
+
*/
|
| 479 |
+
public async delete(
|
| 480 |
+
customerIdOrKey: operations['deleteCustomerEntitlementV2']['parameters']['path']['customerIdOrKey'],
|
| 481 |
+
entitlementIdOrFeatureKey: operations['deleteCustomerEntitlementV2']['parameters']['path']['entitlementIdOrFeatureKey'],
|
| 482 |
+
options?: RequestOptions,
|
| 483 |
+
) {
|
| 484 |
+
const resp = await this.client.DELETE(
|
| 485 |
+
'/api/v2/customers/{customerIdOrKey}/entitlements/{entitlementIdOrFeatureKey}',
|
| 486 |
+
{
|
| 487 |
+
params: {
|
| 488 |
+
path: { customerIdOrKey, entitlementIdOrFeatureKey },
|
| 489 |
+
},
|
| 490 |
+
...options,
|
| 491 |
+
},
|
| 492 |
+
)
|
| 493 |
+
|
| 494 |
+
return transformResponse(resp)
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
/**
|
| 498 |
+
* Override a customer entitlement
|
| 499 |
+
* @param customerIdOrKey - The ID or Key of the customer
|
| 500 |
+
* @param entitlementIdOrFeatureKey - The ID or feature key of the entitlement
|
| 501 |
+
* @param entitlement - The new entitlement data
|
| 502 |
+
* @param options - Request options
|
| 503 |
+
* @returns The overridden entitlement
|
| 504 |
+
*/
|
| 505 |
+
public async override(
|
| 506 |
+
customerIdOrKey: operations['overrideCustomerEntitlementV2']['parameters']['path']['customerIdOrKey'],
|
| 507 |
+
entitlementIdOrFeatureKey: operations['overrideCustomerEntitlementV2']['parameters']['path']['entitlementIdOrFeatureKey'],
|
| 508 |
+
entitlement: operations['overrideCustomerEntitlementV2']['requestBody']['content']['application/json'],
|
| 509 |
+
options?: RequestOptions,
|
| 510 |
+
) {
|
| 511 |
+
const resp = await this.client.PUT(
|
| 512 |
+
'/api/v2/customers/{customerIdOrKey}/entitlements/{entitlementIdOrFeatureKey}/override',
|
| 513 |
+
{
|
| 514 |
+
body: entitlement,
|
| 515 |
+
params: {
|
| 516 |
+
path: { customerIdOrKey, entitlementIdOrFeatureKey },
|
| 517 |
+
},
|
| 518 |
+
...options,
|
| 519 |
+
},
|
| 520 |
+
)
|
| 521 |
+
|
| 522 |
+
return transformResponse(resp)
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
/**
|
| 526 |
+
* List grants for a customer entitlement
|
| 527 |
+
* @param customerIdOrKey - The ID or Key of the customer
|
| 528 |
+
* @param entitlementIdOrFeatureKey - The ID or feature key of the entitlement
|
| 529 |
+
* @param options - Request options including query parameters
|
| 530 |
+
* @returns List of entitlement grants
|
| 531 |
+
*/
|
| 532 |
+
public async listGrants(
|
| 533 |
+
customerIdOrKey: operations['listCustomerEntitlementGrantsV2']['parameters']['path']['customerIdOrKey'],
|
| 534 |
+
entitlementIdOrFeatureKey: operations['listCustomerEntitlementGrantsV2']['parameters']['path']['entitlementIdOrFeatureKey'],
|
| 535 |
+
options?: RequestOptions & {
|
| 536 |
+
query?: operations['listCustomerEntitlementGrantsV2']['parameters']['query']
|
| 537 |
+
},
|
| 538 |
+
) {
|
| 539 |
+
const resp = await this.client.GET(
|
| 540 |
+
'/api/v2/customers/{customerIdOrKey}/entitlements/{entitlementIdOrFeatureKey}/grants',
|
| 541 |
+
{
|
| 542 |
+
params: {
|
| 543 |
+
path: { customerIdOrKey, entitlementIdOrFeatureKey },
|
| 544 |
+
query: options?.query,
|
| 545 |
+
},
|
| 546 |
+
...options,
|
| 547 |
+
},
|
| 548 |
+
)
|
| 549 |
+
|
| 550 |
+
return transformResponse(resp)
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
/**
|
| 554 |
+
* Create a grant for a customer entitlement
|
| 555 |
+
* @param customerIdOrKey - The ID or Key of the customer
|
| 556 |
+
* @param entitlementIdOrFeatureKey - The ID or feature key of the entitlement
|
| 557 |
+
* @param grant - The grant data to create
|
| 558 |
+
* @param options - Request options
|
| 559 |
+
* @returns The created grant
|
| 560 |
+
*/
|
| 561 |
+
public async createGrant(
|
| 562 |
+
customerIdOrKey: operations['createCustomerEntitlementGrantV2']['parameters']['path']['customerIdOrKey'],
|
| 563 |
+
entitlementIdOrFeatureKey: operations['createCustomerEntitlementGrantV2']['parameters']['path']['entitlementIdOrFeatureKey'],
|
| 564 |
+
grant: operations['createCustomerEntitlementGrantV2']['requestBody']['content']['application/json'],
|
| 565 |
+
options?: RequestOptions,
|
| 566 |
+
) {
|
| 567 |
+
const resp = await this.client.POST(
|
| 568 |
+
'/api/v2/customers/{customerIdOrKey}/entitlements/{entitlementIdOrFeatureKey}/grants',
|
| 569 |
+
{
|
| 570 |
+
body: grant,
|
| 571 |
+
params: {
|
| 572 |
+
path: { customerIdOrKey, entitlementIdOrFeatureKey },
|
| 573 |
+
},
|
| 574 |
+
...options,
|
| 575 |
+
},
|
| 576 |
+
)
|
| 577 |
+
|
| 578 |
+
return transformResponse(resp)
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
/**
|
| 582 |
+
* Get the value of a customer entitlement
|
| 583 |
+
* @param customerIdOrKey - The ID or Key of the customer
|
| 584 |
+
* @param entitlementIdOrFeatureKey - The ID or feature key of the entitlement
|
| 585 |
+
* @param options - Request options including query parameters
|
| 586 |
+
* @returns The entitlement value
|
| 587 |
+
*/
|
| 588 |
+
public async value(
|
| 589 |
+
customerIdOrKey: operations['getCustomerEntitlementValueV2']['parameters']['path']['customerIdOrKey'],
|
| 590 |
+
entitlementIdOrFeatureKey: operations['getCustomerEntitlementValueV2']['parameters']['path']['entitlementIdOrFeatureKey'],
|
| 591 |
+
options?: RequestOptions & {
|
| 592 |
+
query?: operations['getCustomerEntitlementValueV2']['parameters']['query']
|
| 593 |
+
},
|
| 594 |
+
) {
|
| 595 |
+
const resp = await this.client.GET(
|
| 596 |
+
'/api/v2/customers/{customerIdOrKey}/entitlements/{entitlementIdOrFeatureKey}/value',
|
| 597 |
+
{
|
| 598 |
+
params: {
|
| 599 |
+
path: { customerIdOrKey, entitlementIdOrFeatureKey },
|
| 600 |
+
query: options?.query,
|
| 601 |
+
},
|
| 602 |
+
...options,
|
| 603 |
+
},
|
| 604 |
+
)
|
| 605 |
+
|
| 606 |
+
return transformResponse(resp)
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
/**
|
| 610 |
+
* Get the history of a customer entitlement
|
| 611 |
+
* @param customerIdOrKey - The ID or Key of the customer
|
| 612 |
+
* @param entitlementIdOrFeatureKey - The ID or feature key of the entitlement
|
| 613 |
+
* @param windowSize - The window size for the history
|
| 614 |
+
* @param options - Request options including query parameters
|
| 615 |
+
* @returns The entitlement history
|
| 616 |
+
*/
|
| 617 |
+
public async history(
|
| 618 |
+
customerIdOrKey: operations['getCustomerEntitlementHistoryV2']['parameters']['path']['customerIdOrKey'],
|
| 619 |
+
entitlementIdOrFeatureKey: operations['getCustomerEntitlementHistoryV2']['parameters']['path']['entitlementIdOrFeatureKey'],
|
| 620 |
+
windowSize: operations['getCustomerEntitlementHistoryV2']['parameters']['query']['windowSize'],
|
| 621 |
+
options?: RequestOptions & {
|
| 622 |
+
query?: Omit<
|
| 623 |
+
operations['getCustomerEntitlementHistoryV2']['parameters']['query'],
|
| 624 |
+
'windowSize'
|
| 625 |
+
>
|
| 626 |
+
},
|
| 627 |
+
) {
|
| 628 |
+
const resp = await this.client.GET(
|
| 629 |
+
'/api/v2/customers/{customerIdOrKey}/entitlements/{entitlementIdOrFeatureKey}/history',
|
| 630 |
+
{
|
| 631 |
+
params: {
|
| 632 |
+
path: { customerIdOrKey, entitlementIdOrFeatureKey },
|
| 633 |
+
query: {
|
| 634 |
+
windowSize,
|
| 635 |
+
...options?.query,
|
| 636 |
+
},
|
| 637 |
+
},
|
| 638 |
+
...options,
|
| 639 |
+
},
|
| 640 |
+
)
|
| 641 |
+
|
| 642 |
+
return transformResponse(resp)
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
/**
|
| 646 |
+
* Reset the usage of a customer entitlement
|
| 647 |
+
* @param customerIdOrKey - The ID or Key of the customer
|
| 648 |
+
* @param entitlementIdOrFeatureKey - The ID or feature key of the entitlement
|
| 649 |
+
* @param reset - The reset data
|
| 650 |
+
* @param options - Request options
|
| 651 |
+
* @returns The reset response
|
| 652 |
+
*/
|
| 653 |
+
public async resetUsage(
|
| 654 |
+
customerIdOrKey: operations['resetCustomerEntitlementUsageV2']['parameters']['path']['customerIdOrKey'],
|
| 655 |
+
entitlementIdOrFeatureKey: operations['resetCustomerEntitlementUsageV2']['parameters']['path']['entitlementIdOrFeatureKey'],
|
| 656 |
+
reset: operations['resetCustomerEntitlementUsageV2']['requestBody']['content']['application/json'],
|
| 657 |
+
options?: RequestOptions,
|
| 658 |
+
) {
|
| 659 |
+
const resp = await this.client.POST(
|
| 660 |
+
'/api/v2/customers/{customerIdOrKey}/entitlements/{entitlementIdOrFeatureKey}/reset',
|
| 661 |
+
{
|
| 662 |
+
body: reset,
|
| 663 |
+
params: {
|
| 664 |
+
path: { customerIdOrKey, entitlementIdOrFeatureKey },
|
| 665 |
+
},
|
| 666 |
+
...options,
|
| 667 |
+
},
|
| 668 |
+
)
|
| 669 |
+
|
| 670 |
+
return transformResponse(resp)
|
| 671 |
+
}
|
| 672 |
+
}
|
api/client/javascript/src/client/debug.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Client } from 'openapi-fetch'
|
| 2 |
+
import type { RequestOptions } from './common.js'
|
| 3 |
+
import type { paths } from './schemas.js'
|
| 4 |
+
import { transformResponse } from './utils.js'
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* Debug utilities for OpenMeter
|
| 8 |
+
*/
|
| 9 |
+
export class Debug {
|
| 10 |
+
constructor(private client: Client<paths, `${string}/${string}`>) {}
|
| 11 |
+
|
| 12 |
+
/**
|
| 13 |
+
* Get event metrics
|
| 14 |
+
* @description Returns debug metrics (in OpenMetrics format) like the number of ingested events since mindnight UTC.
|
| 15 |
+
* @param options - The request options
|
| 16 |
+
* @returns The debug metrics
|
| 17 |
+
*/
|
| 18 |
+
public async getMetrics(options?: RequestOptions) {
|
| 19 |
+
const resp = await this.client.GET('/api/v1/debug/metrics', {
|
| 20 |
+
...options,
|
| 21 |
+
})
|
| 22 |
+
|
| 23 |
+
return transformResponse(resp)
|
| 24 |
+
}
|
| 25 |
+
}
|