You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
> **List endpoints with filtering:** if the operation supports `?filter[...]` query parameters, use the `/api-filters` skill for the decoder and adapter wiring. It covers `api/v3/filters.Parse`, the typed filter structs, `Convert*` helpers, range splitting, and the Ent `.Select(field)` application — everything this skill does not cover.
Follow the API design standards in `api/spec/packages/aip/README.md` (symlinked as `AIP.md` next to this file). That document is the single source of truth for all AIP conventions including naming, enums, resources, visibility, CRUD templates, pagination, filtering, labels, errors, time/duration, content-type, bulk operations, versioning, and empty fields.
330
+
OpenMeter v3 APIs follow [Kong's AIP](https://kong-aip.netlify.app/list/) conventions. Each rule lives in its own file under `rules/` next to this SKILL — open the rule file you need for the task at hand.
|`rules/aip-3106-empty-fields.md`| Always return all fields, `null` / `[]` / `{}` for empty |
353
+
354
+
For filtering specifically, `rules/aip-160-filtering.md` covers the **TypeSpec side** (which `Common.*FieldFilter` to pick, `Shared.ResourceFilters`, label dot-notation, `deepObject` exposure). The **Go implementation side** — parsing deepObject query params into typed filters, converting to `pkg/filter`, and applying Ent predicates — is in the `/api-filters` skill.
AIP-122 is a narrow rule about **resource names, URL paths, and field names** — nothing more:
8
+
9
+
-**URL paths** use `kebab-case` for multi-word resource names: `/v1/konnect-services/domestic-cats`
10
+
- Prefer lowercase resource names like `services`, `users`
11
+
-**Field names** (JSON property names) are lowercase and use `snake_case` for multi-word names: `primary_color`, `billing_profile_id`
12
+
- Uppercase, title case, and `camelCase` are exceptions and require justification
13
+
14
+
That's it. AIP-122 says nothing about TypeSpec model names, enum names, path parameter casing, or operation IDs.
15
+
16
+
## OpenMeter-local naming conventions (not from AIP-122)
17
+
18
+
These are locally enforced by linter rules in `api/spec/packages/aip/lib/rules/` but are **not** rules from AIP-122 itself. They are collected here for convenience.
| Model names (TypeSpec) |`PascalCase`|`BillingProfile`| OpenMeter linter only |
27
+
| Enum type names (TypeSpec) |`PascalCase`|`MeterAggregation`| OpenMeter linter only |
28
+
| Enum member names (TypeSpec) |`PascalCase`|`UniqueCount`| OpenMeter linter only |
29
+
| Path parameters (TypeSpec) |`camelCase`|`meterId`, `customerId`| OpenMeter linter only |
30
+
31
+
The TypeSpec-facing casing (`PascalCase` model names, `camelCase` path parameters) is an OpenMeter convention because TypeSpec model identifiers and path parameter identifiers are separate from the JSON wire format. The wire format still complies with AIP-122.
32
+
33
+
## Base resource models (not from AIP-122)
34
+
35
+
These are OpenMeter `Shared.Resource` conventions defined in `api/spec/packages/aip/src/shared/resource.tsp`, not AIP-122 rules. They use AIP-122-compliant snake_case field names on the wire.
- List endpoints use `GET` on the entity root (e.g., `GET /meters`).
8
+
- Responses must contain a `data` key holding the array of requested entities — this comes from the pagination response templates (`Shared.PagePaginatedResponse<T>`, etc.).
9
+
-**Trailing slash returns `404`** — `GET /meters/` must 404, `GET /meters` must 200.
10
+
- List endpoints must follow the pagination AIP (see `aip-158-pagination.md`) unless the resource will provably never need pagination.
11
+
12
+
## Default sort order is mandatory and deterministic
13
+
14
+
AIP-132 requires a default sort order: *"Multiple requests to retrieve the same list must result in the same ordering of items."* Pick a field that is both stable and unique enough to produce a total order.
15
+
16
+
**OpenMeter convention** (not AIP-132 itself): prefer `name asc`, `created_at asc`, or `updated_at desc` as the default. Avoid UUIDs as the sort key — they are unique but randomly ordered, which makes the default sort feel arbitrary to clients. Always pair a non-unique sort column (like `name`) with a tie-breaker (like `id`) to preserve determinism.
17
+
18
+
## `sort` query parameter
19
+
20
+
Expose sorting via `@query(#{ name: "sort" }) sort?: Common.SortQuery`.
21
+
22
+
Sort values are a **comma-delimited list of attributes with optional `asc`/`desc` suffixes**, with the following syntax rules (from AIP-132):
23
+
24
+
- Ascending is the default — the `asc` suffix is optional
25
+
- Descending requires the `desc` suffix, separated by a space
26
+
- Multiple attributes sort left-to-right
27
+
- Extra whitespace around delimiters is insignificant
Sortable attributes should include all filterable attributes (so clients can sort on anything they can filter on). If the server cannot execute a particular sort expression, return `400 Bad Request`.
| Page list | — |`Shared.PagePaginatedResponse<T>`| 200 |
18
+
| Cursor list | — |`Shared.CursorPaginatedResponse<T>`| 200 |
19
+
20
+
## AIP-134 update rules
21
+
22
+
Both `PATCH` and `PUT` are **required** for all entities (mandate introduced 2025-04-08). The sole exception: `PATCH` may be omitted when the full entity representation is needed to validate an update.
23
+
24
+
### PATCH (partial update)
25
+
26
+
- Implements JSON Merge Patch (RFC 7396) with **mandatory recursive patching** of nested objects.
27
+
- Operation ID: `update-<entity>` (kebab-case).
28
+
- Rejects requests with `Content-Type` other than `application/json` with `400 Bad Request`.
29
+
- Rejects unknown fields and read-only fields with `400 Bad Request`, naming them in `invalid_parameters`.
30
+
- Null-value semantics:
31
+
- For non-required properties: removes the property
32
+
- For schema-less object properties: removes the property
33
+
- For required-nullable properties: sets them to null
34
+
- For required non-nullable properties: `400 Bad Request`
35
+
36
+
### PUT (upsert)
37
+
38
+
- Returns `201 Created` when creating an entity, `200 OK` when replacing an existing one.
39
+
- Operation ID: `upsert-<entity>` or `update-<entity>` (kebab-case).
40
+
-**Creation via PUT only works when the entity uses customer-supplied IDs** (unique within the organization or parent scope, not globally).
41
+
- For entities with system-generated globally-unique IDs, PUT only supports **replacement**; missing entities must return `404 Not Found`.
42
+
43
+
## AIP-135 delete rules
44
+
45
+
- DELETE returns `204 No Content` on success.
46
+
-**No request body is accepted**; all parameters go in the URL path, not the query string.
47
+
- Return `403 Forbidden` before `404 Not Found` — check permissions before existence to prevent resource enumeration.
48
+
- Soft-deleted resources return `404 Not Found` on subsequent DELETE calls.
49
+
- Cascading deletes:
50
+
- If no protected resources would be affected, proceed as a normal DELETE.
51
+
- If protected resources would be affected and `?force=true` is **not** supplied, return `400 Bad Request` listing the affected resource types in the error detail.
52
+
- If `?force=true` is supplied, delete all child resources and associations, returning `204`.
When the `Content-Type` header is absent, it defaults to `application/json; charset=utf-8` (note: the default **includes** a charset). APIs must not reject valid JSON payloads that omit the header.
8
+
9
+
## Validation
10
+
11
+
Validate the request body's `Content-Type` on `POST`, `PUT`, and `PATCH`. Validation fails when:
12
+
13
+
- The `Content-Type` header specifies an unsupported type, **or**
14
+
- The body does not match the declared type
15
+
16
+
Either failure mode returns `415 Unsupported Media Type`. When responding with 415, include an `Accept-{METHOD}` header listing supported types (e.g., `Accept-POST: application/json`). If the rejection was caused by a charset mismatch, the charset directive must appear in the `Accept-{METHOD}` value.
17
+
18
+
## Exclusions
19
+
20
+
**Endpoints without request bodies are exempt** from content-type validation — there is nothing to validate.
-**Field name pattern**: `<past_participle>_at` for fields representing a point in time — e.g., `created_at`, `updated_at`, `deleted_at`, `expires_at`
8
+
-**Wire format**: RFC-3339 string in UTC with `Z` suffix, e.g., `"2023-02-27T02:15:00Z"`
9
+
-`T` separates date and time; fractional seconds are optional
10
+
11
+
## Durations (OpenMeter deviates from AIP-142)
12
+
13
+
AIP-142 specifies durations as **integer values with a unit suffix in the field name**: `ttl_ms`, `flight_duration_mins`, `lifespan_yrs`. Permitted suffixes: `ns`, `ms`, `secs`, `mins`, `hrs`, `days`, `yrs`. Values must be non-negative integers within `0 <= N < 2^53`.
14
+
15
+
**OpenMeter does not follow AIP-142 for durations.** Instead, OpenMeter uses the **ISO-8601 duration format** as an opaque string:
16
+
17
+
-`PT1M` — one minute
18
+
-`PT1H` — one hour
19
+
-`P1D` — one day
20
+
-`P1M` — one month
21
+
22
+
Components: years (`Y`), months (`M`), weeks (`W`), days (`D`), time separator `T`, hours (`H`), minutes (`M` after `T`), seconds (`S`). When authoring a new duration field in OpenMeter, use the ISO-8601 form and do **not** append a unit suffix to the field name. This deviation is intentional and documented here so reviewers know to allow it.
0 commit comments