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
4 changes: 4 additions & 0 deletions contributing/sdks.md
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,10 @@ cd ../..
# Regenerate SDK (optional, requires Speakeasy CLI)
cd spec-sdk-tests
./scripts/regenerate-sdk.sh
# For Go: the script unsets GOMODCACHE/GOCACHE so Go uses your default module cache.
# If you run Speakeasy for Go directly and see "module found but does not contain package",
# unset GOMODCACHE and GOCACHE and retry (e.g. in Cursor the sandbox can set a cache path
# that breaks module resolution).

# Run spec-sdk-tests (RECOMMENDED - includes pre-flight checks)
cd spec-sdk-tests
Expand Down
1 change: 1 addition & 0 deletions docs/pages/guides.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Welcome to the Outpost guides section. These guides will help you get the most o
- [Schema Migration](/guides/migration)
- [Upgrade to v0.12](/guides/upgrade-v0.12)
- [Upgrade to v0.13](/guides/upgrade-v0.13)
- [Upgrade to v0.14](/guides/upgrade-v0.14)

## Next Steps

Expand Down
185 changes: 185 additions & 0 deletions docs/pages/guides/upgrade-v0.14.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
---
title: "Upgrade to v0.14"
---

This guide covers breaking changes and migration steps when upgrading from v0.13 to v0.14.

## Breaking Changes Overview

| Change | Impact | Action Required |
| --- | --- | --- |
| [PostgreSQL data columns migrated to TEXT](#postgresql-data-columns-migrated-to-text) | Direct queries against Postgres `events` and `attempts` tables | Update any SQL that relies on JSONB operators for `data` / `event_data` columns |
| [Array query parameters](#array-query-parameters) | API query filters & SDK method signatures | Use bracket notation for array filters; update SDK calls |
| [Python SDK: request body and method signatures](#python-sdk-request-body-and-method-signatures) | Python SDK methods that send a request body | Change `params=` to `body=`; use new positional + `body=` signature |
| [List tenants: method rename and request object](#list-tenants-method-rename-and-request-object) | All SDKs — tenants list API | Replace `listTenants(...)` / `ListTenants(...)` with `list(request)`; use a single request object |

## PostgreSQL Data Columns Migrated to TEXT

The `events.data` and `attempts.event_data` columns have been migrated from `JSONB` to `TEXT`. This change preserves the original JSON key ordering of webhook payloads, which JSONB normalizes alphabetically.

**The migration runs automatically when Outpost starts.** No manual migration steps are required.

If you only interact with Outpost through the API or SDKs, **no action is needed** — the API response format is unchanged.

If you have custom SQL queries, dashboards, or tooling that reads directly from the Outpost PostgreSQL database, you will need to update any queries that use JSONB-specific operators on these columns.

## Array Query Parameters

API query parameters now accept arrays using bracket notation. This affects filters like `tenant_id`, `status`, and `topic` on list endpoints.

**Single values continue to work as before.** This is a non-breaking addition for API users who don't use arrays. However, the SDK method signatures have changed to accommodate the new array types.

### API usage

```
# Single value (unchanged)
GET /events?tenant_id=tenant_123&topic=user.created

# Array filter (new)
GET /events?tenant_id[]=tenant_123&tenant_id[]=tenant_456&topic[]=user.created&topic[]=user.updated
```

### SDK changes

The SDKs have been updated to accept both single values and arrays for filter parameters.

**TypeScript:**

```ts
// Single value (unchanged)
const events = await outpost.events.list({
tenantId: "tenant_123",
topic: "user.created",
});

// Array filter (new)
const events = await outpost.events.list({
tenantId: ["tenant_123", "tenant_456"],
topic: ["user.created", "user.updated"],
});
```

**Go:**

Filter parameters that accept single or multiple values are represented as `[]string`. Pass a slice with one or more values:

```go
// Single value
res, err := s.Events.List(ctx, operations.ListEventsRequest{
TenantID: []string{"tenant_123"},
Topic: []string{"user.created"},
Limit: outpostgo.Int64(50),
})

// Array filter
res, err := s.Events.List(ctx, operations.ListEventsRequest{
TenantID: []string{"tenant_123", "tenant_456"},
Topic: []string{"user.created", "user.updated"},
Limit: outpostgo.Int64(50),
})
```

**Action:** Update SDK dependencies to the latest version. If you pass filter parameters that previously accepted only a single string, verify that your code still works with the updated type signatures.

## Python SDK: request body and method signatures

The Python SDK has two breaking changes for methods that send a request body (`destinations.create`, `destinations.update`, `tenants.upsert`):

1. **Request body parameter renamed:** The keyword argument for the request body is now `body` instead of `params`. You must update every call site.
2. **Method signature shape:** Methods now take path parameters positionally and the request body as the named argument `body=`, instead of a single request object containing `params`.

**Before (v0.13):**

```py
# Keyword argument was params=
sdk.destinations.create(tenant_id="acme", params=models.DestinationCreateWebhook(...))
sdk.destinations.update(tenant_id="acme", destination_id="des_123", params=models.DestinationUpdate(...))
```

**After (v0.14):**

```py
# Use body= and (for create/update) positional path params + body=
sdk.destinations.create(tenant_id="acme", body=models.DestinationCreateWebhook(...))
sdk.destinations.update(tenant_id="acme", destination_id="des_123", body=models.DestinationUpdate(...))
```

**TypeScript and Go:** The request body parameter in the method signature is now named `body` (was `params`). This is a type/signature rename only; call sites that pass the body positionally do not need to change.

## List tenants: method rename and request object

The "list tenants" API is now exposed as `tenants.list` (or `Tenants.List` / `tenants.list`) with a **single request object** in all SDKs, consistent with `events.list` and `attempts.list`. The previous method names and flattened parameters are no longer available.

**Before (v0.13):**

**TypeScript:**

```ts
const result = await outpost.tenants.listTenants(20, "desc");
```

**Go:**

```go
res, err := client.Tenants.ListTenants(ctx, outpostgo.Pointer(int64(20)), operations.ListTenantsDirDesc.ToPointer(), nil, nil)
```

**Python:**

```py
res = sdk.tenants.list_tenants(limit=20, direction=models.ListTenantsDir.DESC)
```

**After (v0.14):**

**TypeScript:**

```ts
const result = await outpost.tenants.list({ limit: 20, dir: "desc" });
```

**Go:**

```go
res, err := client.Tenants.List(ctx, operations.ListTenantsRequest{
Limit: outpostgo.Pointer(int64(20)),
Dir: operations.ListTenantsDirDesc.ToPointer(),
})
```

**Python:**

```py
res = sdk.tenants.list(request=models.ListTenantsRequest(limit=20, direction=models.ListTenantsDir.DESC))
```

**Action:** Replace any call to `listTenants` / `ListTenants` / `list_tenants` with `list` (or `List` in Go) and pass a single request object with `limit`, `dir`, `next`, and `prev` as needed.

## Other Notable Changes

These changes are **not breaking** but may be useful to know about.

### Relaxed destination URL validation

Destination URL validation has been relaxed to allow:
- URLs with Basic Auth credentials (e.g., `https://user:pass@example.com/webhook`)
- RabbitMQ server URLs with Docker service names (e.g., `amqp://guest:guest@rabbitmq:5672`)

### Empty `custom_headers` accepted

Empty `custom_headers` on webhook destinations are now treated as absent instead of triggering validation errors. If you previously worked around the v0.13 validation by omitting `custom_headers`, you can now safely pass an empty object again.

## Upgrade Checklist

1. **Before upgrading:**
- [ ] Back up your PostgreSQL database
- [ ] Audit any direct SQL queries against `events.data` or `attempts.event_data` for JSONB operators
- [ ] Update SDK dependencies to the latest version
- [ ] **(Python only)** Replace `params=` with `body=` for `destinations.create`, `destinations.update`, and `tenants.upsert`
- [ ] Replace `tenants.listTenants` / `Tenants.ListTenants` / `tenants.list_tenants` with `tenants.list` (or `Tenants.List`) and pass a single request object

2. **Upgrade:**
- [ ] Update Outpost to v0.14 and restart — the PostgreSQL migration runs automatically on startup

3. **After upgrading:**
- [ ] Verify any direct SQL queries against Outpost tables are working as expected
11 changes: 11 additions & 0 deletions examples/sdk-go/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"

outpostgo "github.com/hookdeck/outpost/sdks/outpost-go"
"github.com/hookdeck/outpost/sdks/outpost-go/models/operations"
"github.com/joho/godotenv"
)

Expand Down Expand Up @@ -68,6 +69,16 @@ func withAdminApiKey(ctx context.Context, apiServerURL string, adminAPIKey strin
log.Println("List destinations with Admin Key returned no data or an unexpected response structure.")
}

// List tenants (v0.14+: Tenants.List with request object)
tenantsRes, err := adminClient.Tenants.List(ctx, operations.ListTenantsRequest{
Limit: outpostgo.Pointer(int64(5)),
})
if err != nil {
log.Printf("List tenants failed (e.g. 501 if RediSearch not available): %v", err)
} else if tenantsRes != nil && tenantsRes.TenantPaginatedResult != nil {
log.Printf("Successfully listed %d tenant(s) (first page).", len(tenantsRes.TenantPaginatedResult.Models))
}

tokenRes, err := adminClient.Tenants.GetToken(ctx, tenantID)
if err != nil {
log.Fatalf("Failed to get tenant token: %v", err)
Expand Down
1 change: 0 additions & 1 deletion examples/sdk-go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,5 @@ replace github.com/hookdeck/outpost/sdks/outpost-go => ../../sdks/outpost-go

require (
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 // indirect
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b // indirect
)
12 changes: 8 additions & 4 deletions examples/sdk-go/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 h1:R/ZjJpjQKsZ6L/+Gf9WHbt31GG8NMVcpRqUE+1mMIyo=
github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731/go.mod h1:M9R1FoZ3y//hwwnJtO51ypFGwm8ZfpxPT/ZLtO1mcgQ=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hookdeck/outpost/sdks/outpost-go v0.3.0 h1:jN+Pwg7TiIvScnR5aoeHgKItDO+jpA0xn+t2NALOWhU=
github.com/hookdeck/outpost/sdks/outpost-go v0.3.0/go.mod h1:Ljmw6AK9r1rm9U17BBEnLQW1pRYMrutthCTRFTufNCE=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b h1:MQE+LT/ABUuuvEZ+YQAMSXindAdUh7slEmAkup74op4=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
25 changes: 16 additions & 9 deletions examples/sdk-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@ The source code for the Python SDK can be found in the [`sdks/outpost-python/`](

### Setup

1. **Build the local SDK** (this example uses a path dependency pointing at the local SDK source):
```bash
cd ../../sdks/outpost-python && pip install -e .
cd ../../examples/sdk-python
```
**Option A — Using the run script (recommended if Poetry has issues)**
From `examples/sdk-python`, create a `.env` (see below), then:
```bash
./run-auth.sh
```
This creates a `.venv`, installs the local SDK and dependencies, and runs the auth example. For other commands (e.g. create-destination), activate the venv and run:
```bash
.venv/bin/python app.py create-destination
```

**Option B — Using Poetry**

2. **Install dependencies:**
1. **Install dependencies:**
```bash
poetry install
```
Expand All @@ -42,10 +48,11 @@ The source code for the Python SDK can be found in the [`sdks/outpost-python/`](
```
Use `API_BASE_URL` for the full API base, or `SERVER_URL` for local. (Note: `.env` is gitignored.)

2. **Run the example script:**
*(Ensure you are inside the Poetry shell activated in the setup step)*
2. **Run the example script:**
If you used **Option A** (run script), use `./run-auth.sh` or `.venv/bin/python app.py auth`.
If you used **Option B** (Poetry), ensure you are inside the Poetry shell, then use `python app.py auth`.

The `app.py` script is now a command-line interface (CLI) that accepts different commands to run specific examples.
The `app.py` script is a command-line interface (CLI) that accepts different commands:

* **To run the API Key and tenant-scoped API key auth example:**
```bash
Expand Down
12 changes: 11 additions & 1 deletion examples/sdk-python/example/auth.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
import sys
from dotenv import load_dotenv
from outpost_sdk import Outpost
from outpost_sdk import Outpost, models


def with_tenant_api_key(outpost: Outpost, tenant_api_key: str, tenant_id: str):
Expand Down Expand Up @@ -41,6 +41,16 @@ def with_admin_api_key(outpost: Outpost, tenant_id: str):
)
print(destinations_res)

# List tenants (tenants.list(request) with request object)
try:
tenants_res = outpost.tenants.list(request=models.ListTenantsRequest(limit=5))
if tenants_res and tenants_res.result and tenants_res.result.models is not None:
print(f"Tenants (first page): {len(tenants_res.result.models)} tenant(s)")
else:
print("Tenants (first page): (no tenants or 501 if RediSearch not available)")
except Exception as list_err:
print(f"List tenants skipped or failed: {list_err}")

token_res = outpost.tenants.get_token(tenant_id=tenant_id)
print(f"Tenant token obtained for tenant {tenant_id}:")
print(token_res)
Expand Down
9 changes: 9 additions & 0 deletions examples/sdk-python/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Dependencies for the Python example (install after the local SDK).
# From examples/sdk-python:
# python3 -m venv .venv
# .venv/bin/pip install -e ../../sdks/outpost-python
# .venv/bin/pip install -r requirements.txt
# .venv/bin/python app.py auth
python-dotenv>=1.1.0
typer>=0.16.0
questionary>=2.1.0
18 changes: 18 additions & 0 deletions examples/sdk-python/run-auth.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Run the auth example using a venv (avoids Poetry when lock/metadata issues occur).
# From repo root or examples/sdk-python: ./examples/sdk-python/run-auth.sh
# Or from examples/sdk-python: ./run-auth.sh
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
SDK_DIR="$REPO_ROOT/sdks/outpost-python"

if [[ ! -d .venv ]]; then
echo "Creating .venv..."
python3 -m venv .venv
fi
.venv/bin/pip install -q -e "$SDK_DIR"
.venv/bin/pip install -q -r requirements.txt
echo "Running auth example..."
.venv/bin/python app.py auth
4 changes: 4 additions & 0 deletions examples/sdk-typescript/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ const withAdminApiKey = async () => {

console.log(destinations);

// List tenants (v0.14+: tenants.list(request) with a single request object)
const tenantsPage = await outpost.tenants.list({ limit: 5 });
console.log("Tenants (first page):", tenantsPage?.models ?? []);

// Get portal URL (Admin API Key required)
try {
const portal = await outpost.tenants.getPortalUrl(tenantId);
Expand Down
10 changes: 10 additions & 0 deletions sdks/schemas/speakeasy-modifications-overlay.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,16 @@ actions:
created_at: 1745611620645
reviewed_at: 1745611624395
type: method-name
- target: $["paths"]["/tenants"]["get"]
update:
x-speakeasy-name-override: list
x-speakeasy-max-method-params: 0
x-speakeasy-metadata:
after: sdk.tenants.list()
before: sdk.Tenants.listTenants()
created_at: 1745611620645
reviewed_at: 1745611624395
type: method-name
Comment on lines +209 to +218
- target: $["paths"]["/tenants/{tenant_id}"]["get"]
update:
x-speakeasy-name-override: get
Expand Down
5 changes: 5 additions & 0 deletions spec-sdk-tests/scripts/regenerate-sdk.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ run_ts() {

run_go() {
echo "Regenerating Go SDK..."
# Use default Go module/cache dirs so go mod tidy and go build succeed. When run inside
# Cursor/sandbox, GOMODCACHE can point at a sandbox path and module resolution fails
# with "module found but does not contain package"; unsetting fixes that.
unset GOMODCACHE
unset GOCACHE
speakeasy run -t outpost-go
echo "Building Go SDK..."
(cd "$REPO_ROOT/sdks/outpost-go" && go build ./...)
Expand Down
Loading
Loading