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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ patch-oapi-templates: ## Patch oapi-codegen chi-middleware template with custom
update-openapi: patch-oapi-templates ## Update OpenAPI spec
$(call print-target)
$(MAKE) -C api/spec generate
go generate ./api/...
go generate ./api/... ./openmeter/.../notificationevents

.PHONY: generate-javascript-sdk
generate-javascript-sdk: ## Generate JavaScript SDK
Expand Down
63 changes: 63 additions & 0 deletions api/notificationevents/billing/openapi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
openapi: 3.0.0
info:
title: Billing.NotificationEvents
description: Notification Event contracts owned by the billing domain.
version: 0.0.0
tags: []
paths: {}
components:
schemas:
BillingInvoiceCreatedV1Payload:
type: object
required:
- invoice_id
- customer_id
- status
- currency
- total
properties:
invoice_id:
allOf:
- $ref: "#/components/schemas/ULID"
description: Invoice that was created.
customer_id:
allOf:
- $ref: "#/components/schemas/ULID"
description: Customer the invoice belongs to.
status:
type: string
description: Invoice status at creation time.
currency:
allOf:
- $ref: "#/components/schemas/CurrencyCode"
description: Invoice currency.
total:
allOf:
- $ref: "#/components/schemas/Numeric"
description: Invoice total at creation time.
additionalProperties: false
description: |-
Payload of `v1.billing.invoice.created`.

The payload identifies the invoice and carries its state at creation time;
consumers fetch the full invoice through the API when they need it.
CurrencyCode:
type: string
minLength: 3
maxLength: 3
pattern: ^[A-Z]{3}$
description: |-
Three-letter [ISO4217](https://www.iso.org/iso-4217-currency-codes.html)
currency code. Custom three-letter currency codes are also supported for
convenience.
example: USD
Numeric:
type: string
pattern: ^\-?[0-9]+(\.[0-9]+)?$
description: Numeric represents an arbitrary precision number.
ULID:
type: string
pattern: ^[0-7][0-9A-HJKMNP-TV-Z]{25}$
description: ULID (Universally Unique Lexicographically Sortable Identifier).
title: ULID
example: 01G65Z755AFWAKHE12NY0CQ9FH
94 changes: 94 additions & 0 deletions api/notificationevents/entitlement/openapi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
openapi: 3.0.0
info:
title: Entitlements.NotificationEvents
description: Notification Event contracts owned by the entitlements domain.
version: 0.0.0
tags: []
paths: {}
components:
schemas:
EntitlementBalanceThreshold:
type: object
required:
- type
- value
properties:
type:
allOf:
- $ref: "#/components/schemas/EntitlementBalanceThresholdType"
description: Condition kind of the threshold.
value:
type: number
format: double
description: Threshold value, interpreted according to the threshold type.
additionalProperties: false
description: A single entitlement balance threshold.
EntitlementBalanceThresholdTriggeredV1Payload:
type: object
required:
- customer_id
- entitlement_id
- feature_key
- current_balance
- threshold
properties:
customer_id:
allOf:
- $ref: "#/components/schemas/ULID"
description: Customer owning the entitlement.
entitlement_id:
allOf:
- $ref: "#/components/schemas/ULID"
description: Entitlement whose balance crossed the threshold.
feature_key:
allOf:
- $ref: "#/components/schemas/ResourceKey"
description: Feature of the entitlement.
current_balance:
type: number
format: double
description: Balance at evaluation time.
threshold:
allOf:
- $ref: "#/components/schemas/EntitlementBalanceThreshold"
description: Threshold that was crossed.
additionalProperties: false
description: Payload of `v1.entitlement.balance_threshold.triggered`.
EntitlementBalanceThresholdTriggeredV1RuleConfig:
type: object
required:
- thresholds
properties:
thresholds:
type: array
items:
$ref: "#/components/schemas/EntitlementBalanceThreshold"
minItems: 1
description: Thresholds evaluated against the entitlement balance.
features:
type: array
items:
$ref: "#/components/schemas/ResourceKey"
description: Feature keys the Rule is limited to. All features when omitted.
additionalProperties: false
description: Rule configuration of `v1.entitlement.balance_threshold.triggered`.
EntitlementBalanceThresholdType:
type: string
enum:
- balance_at_or_below
- usage_percentage_at_or_above
description: Condition kind of an entitlement balance threshold.
ResourceKey:
type: string
minLength: 1
maxLength: 64
pattern: ^[a-z0-9]+(?:_[a-z0-9]+)*$
description: A key is a unique string that is used to identify a resource.
title: Resource Key
example: resource_key
ULID:
type: string
pattern: ^[0-7][0-9A-HJKMNP-TV-Z]{25}$
description: ULID (Universally Unique Lexicographically Sortable Identifier).
title: ULID
example: 01G65Z755AFWAKHE12NY0CQ9FH
1 change: 1 addition & 0 deletions api/spec/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ test: ## Run AIP TypeScript SDK and emitter tests
pnpm --filter @openmeter/api-spec-aip run test
pnpm run test:sdk:coverage
pnpm --filter @openmeter/typespec-typescript run check
pnpm --filter @openmeter/typespec-notification-events run test

.PHONY: publish-aip-sdk
publish-aip-sdk: ## Publish the AIP TypeScript SDK (@openmeter/client) to npm
Expand Down
10 changes: 10 additions & 0 deletions api/spec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,13 @@ The AIP package defines v3 metering and billing APIs in line with [Kong’s AIP

- **OpenMeter** (`openmeter.tsp`): OpenMeter v3 API.
- **Konnect** (`konnect.tsp`): Konnect metering & billing API, same surface with Konnect-specific auth and servers.

---

## Notification Event contracts (`packages/aip/src/<domain>/notification-events/`)

The producing domain owns the payload, and for rule-produced events the Rule configuration, of each Notification Event type. It declares them in a `<Domain>.NotificationEvents` namespace, mapped to a Go package in `packages/aip/src/notification-events.tsp`. `make update-openapi` generates the Go structs, JSON Schemas and event type values into `openmeter/<domain>/notificationevents`, and the events-only OpenAPI spec into `api/notificationevents/<domain>/`.

- A released contract is immutable. Change it by adding the next version (`v2.<name>`, `...V2Payload`) next to it, never by editing it in place. Nested types follow the same rule: a breaking change needs a new type. Reviewers enforce this; generation does not.
- Payloads are dedicated models. They may use `Shared` types but must not reuse API resource models, so that API changes cannot change a contract.
- [`packages/typespec-notification-events`](packages/typespec-notification-events) enforces the naming and `@id` conventions at compile time.
12 changes: 12 additions & 0 deletions api/spec/packages/aip/oapi-codegen.notification-events.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/oapi-codegen/oapi-codegen/HEAD/configuration-schema.json
# Shared oapi-codegen configuration of every domain notificationevents package.
# The generated go:generate directive in each package (generate.gen.go) passes
# the package name (-package) and the output file (-o), so neither is set here.
generate:
models: true
compatibility:
disable-required-readonly-as-pointer: true
always-prefix-enum-values: true
output-options:
nullable-type: true
skip-prune: true
3 changes: 2 additions & 1 deletion api/spec/packages/aip/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.1.0",
"type": "module",
"scripts": {
"generate": "tsp compile --config tspconfig.yaml ./src && node ./scripts/flatten-allof.mjs ./output/definitions/metering-and-billing/v3/openapi.MeteringAndBilling.yaml && node ./scripts/seal-object-schemas.mjs ./output/definitions/metering-and-billing/v3/*.yaml",
"generate": "tsp compile --config tspconfig.yaml ./src && node ./scripts/flatten-allof.mjs ./output/definitions/metering-and-billing/v3/openapi.MeteringAndBilling.yaml && node ./scripts/seal-object-schemas.mjs ./output/definitions/metering-and-billing/v3/*.yaml && tsp compile --config tspconfig.notification-events.yaml ./src/notification-events.tsp && node ./scripts/generate-notification-events.mjs",
"bundle": "openapi bundle output/definitions/metering-and-billing/v3/openapi.OpenMeter.yaml",
"watch": "tsp compile --watch --config tspconfig.yaml ./src",
"test": "node --test test/*.test.js",
Expand All @@ -19,6 +19,7 @@
},
"devDependencies": {
"@openmeter/typespec-go": "workspace:*",
"@openmeter/typespec-notification-events": "workspace:*",
"@openmeter/typespec-typescript": "workspace:*",
"@redocly/cli": "2.40.0",
"@types/node": "26.1.1",
Expand Down
84 changes: 84 additions & 0 deletions api/spec/packages/aip/scripts/generate-notification-events.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Generates the per-domain Notification Event artifacts and copies them into
// the Go module.
//
// Run after `tsp compile --config tspconfig.notification-events.yaml`, which
// emits the JSON Schemas, the generated Go files and manifest.json. For every @goPackage
// namespace in the manifest this script:
// 1. compiles a generated entry point that makes the namespace an OpenAPI
// service, producing the events-only OpenAPI document of its types;
// 2. copies that document to its committed location under api/, and the
// emitted generate.gen.go, events.gen.go and JSON Schemas into the Go
// package directory. `go generate` then runs oapi-codegen through the
// directive in generate.gen.go, like every other oapi-codegen target.
import { execFileSync } from 'node:child_process'
import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { dirname, join, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'

const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const repoRoot = resolve(pkgRoot, '../../../..')
const out = join(pkgRoot, 'output/notification-events')
const manifest = JSON.parse(
readFileSync(join(out, '_go/manifest.json'), 'utf8'),
)

for (const entry of manifest) {
const entryDir = join(out, 'entries')
const entryFile = join(entryDir, `${entry.dir.replaceAll('/', '_')}.tsp`)
const openapiDir = join(out, 'openapi', entry.dir)
mkdirSync(entryDir, { recursive: true })
writeFileSync(
entryFile,
[
`import "${relative(entryDir, join(pkgRoot, 'src/notification-events.tsp'))}";`,
'',
`@@TypeSpec.service(${entry.namespace}, #{ title: "${entry.namespace}" });`,
'',
].join('\n'),
)
execFileSync(
'tsp',
[
'compile',
'--config',
join(pkgRoot, 'tspconfig.notification-events-openapi.yaml'),
entryFile,
'--option',
`@typespec/openapi3.emitter-output-dir=${openapiDir}`,
],
{ cwd: pkgRoot, stdio: 'inherit' },
)
// Same post-processing as v3: oapi-codegen and kin-openapi need
// additionalProperties: false instead of the emitter's { not: {} }.
execFileSync(
'node',
[
join(pkgRoot, 'scripts/seal-object-schemas.mjs'),
join(openapiDir, 'openapi.yaml'),
],
{
stdio: 'inherit',
},
)

const goDir = join(repoRoot, entry.dir)
mkdirSync(goDir, { recursive: true })
mkdirSync(dirname(join(repoRoot, entry.spec)), { recursive: true })
cpSync(join(openapiDir, 'openapi.yaml'), join(repoRoot, entry.spec))
cpSync(
join(out, '_go', entry.dir, 'generate.gen.go'),
join(goDir, 'generate.gen.go'),
)
rmSync(join(goDir, 'events.gen.go'), { force: true })
rmSync(join(goDir, 'schemas'), { recursive: true, force: true })
if (entry.schemas.length > 0) {
cpSync(
join(out, '_go', entry.dir, 'events.gen.go'),
join(goDir, 'events.gen.go'),
)
mkdirSync(join(goDir, 'schemas'))
for (const { source, target } of entry.schemas) {
cpSync(join(out, 'jsonschema', source), join(goDir, 'schemas', target))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "./invoice.tsp";
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import "@typespec/json-schema";
import "@openmeter/typespec-notification-events";
import "../../shared/index.tsp";

using TypeSpec.JsonSchema;
using NotificationCodegen;

/**
* Notification Event contracts owned by the billing domain.
*/
namespace Billing.NotificationEvents;

/**
* Payload of `v1.billing.invoice.created`.
*
* The payload identifies the invoice and carries its state at creation time;
* consumers fetch the full invoice through the API when they need it.
*/
@jsonSchema
@id("v1.billing.invoice.created.payload")
@eventType("v1.billing.invoice.created")
@friendlyName("BillingInvoiceCreatedV1Payload")
model InvoiceCreatedV1Payload {
Comment thread
gergely-kurucz-konghq marked this conversation as resolved.
/**
* Invoice that was created.
*/
invoice_id: Shared.ULID;

/**
* Customer the invoice belongs to.
*/
customer_id: Shared.ULID;

/**
* Invoice status at creation time.
*/
status: string;

/**
* Invoice currency.
*/
currency: Shared.CurrencyCode;

/**
* Invoice total at creation time.
*/
total: Shared.Numeric;
}
Loading
Loading