first commit
This commit is contained in:
119
docs/api-stack/api/api-plugin-event-bus/src/topic/topic.md
Normal file
119
docs/api-stack/api/api-plugin-event-bus/src/topic/topic.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# `topic.ts` — topic validation + tenant scoping
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Pure functions for validating event-bus topics and projecting them into
|
||||
their tenant-scoped form. Every `publish` and `subscribe` going through
|
||||
`eventBusService` calls `assertValidTopic` at the boundary, and the
|
||||
scoped middleware handle uses `scopeTopic` / `unscopeTopic` to keep
|
||||
per-tenant traffic isolated. Pulling this logic into its own module
|
||||
keeps the service-layer code small and lets test code compute scoped
|
||||
topics without spinning up the full plugin.
|
||||
|
||||
## Features
|
||||
|
||||
- Canonical `EVENT_TOPIC_PATTERN` (`/^[a-z0-9_\-:.]+$/`) — single
|
||||
source of truth for topic syntax.
|
||||
- `assertValidTopic(topic, { maxLength })` — throws `EventBusError`
|
||||
(`INVALID_TOPIC`) with `context.reason ∈ "empty" | "exceeds-max-length"
|
||||
| "pattern-mismatch"`.
|
||||
- `scopeTopic(topic, tenantId?)` — prefixes with `tenant:<id>:` when
|
||||
a tenant id is supplied.
|
||||
- `unscopeTopic(scoped, tenantId?)` — inverse, returns `null` if the
|
||||
scoped string does not belong to the supplied tenant.
|
||||
- `topicRoot(topic)` — derives the OTel `topic_root` label.
|
||||
- `DEFAULT_MAX_TOPIC_LENGTH` (256) and `TENANT_TOPIC_PREFIX`
|
||||
(`tenant:`) constants for downstream reuse.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
assertValidTopic,
|
||||
scopeTopic,
|
||||
} from "@suaveplan/api-plugin-event-bus/topic/topic";
|
||||
|
||||
assertValidTopic("order.created"); // OK
|
||||
assertValidTopic("Order Created"); // throws EventBusError(INVALID_TOPIC)
|
||||
|
||||
const scoped = scopeTopic("order.created", "acme");
|
||||
// "tenant:acme:order.created"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
DEFAULT_MAX_TOPIC_LENGTH,
|
||||
scopeTopic,
|
||||
topicRoot,
|
||||
unscopeTopic,
|
||||
} from "@suaveplan/api-plugin-event-bus/topic/topic";
|
||||
|
||||
function project(records: { topic: string }[], tenantId: string) {
|
||||
return records
|
||||
.map((r) => {
|
||||
const original = unscopeTopic(r.topic, tenantId);
|
||||
if (original === null) return null;
|
||||
return { topic: original, root: topicRoot(original) };
|
||||
})
|
||||
.filter((x): x is { topic: string; root: string } => x !== null);
|
||||
}
|
||||
|
||||
// Use the constant when tuning per-app limits — e.g. an app whose
|
||||
// service mesh truncates labels at 128 may pass `maxLength: 128` to
|
||||
// `assertValidTopic`.
|
||||
console.log(DEFAULT_MAX_TOPIC_LENGTH);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `EVENT_TOPIC_PATTERN: RegExp`
|
||||
|
||||
Frozen regex; lowercase letters, digits, `_`, `-`, `:`, `.`.
|
||||
|
||||
### `DEFAULT_MAX_TOPIC_LENGTH: 256`
|
||||
|
||||
### `TENANT_TOPIC_PREFIX: "tenant:"`
|
||||
|
||||
### `assertValidTopic(topic: string, options?: { maxLength?: number }): void`
|
||||
|
||||
Throws `EventBusError(EVENT_BUS.INVALID_TOPIC)` with a `context.reason`
|
||||
discriminator on failure. Returns `void` on success.
|
||||
|
||||
### `scopeTopic(topic: string, tenantId?: string): string`
|
||||
|
||||
Returns `tenant:<id>:<topic>` when `tenantId` is non-empty; otherwise
|
||||
returns `topic` verbatim. Validates `tenantId` against a stricter
|
||||
regex (no `:` allowed) so callers cannot smuggle a foreign tenant
|
||||
prefix through the parameter — a malformed id throws
|
||||
`EventBusError(EVENT_BUS.INVALID_TOPIC)` with
|
||||
`context.reason === "tenant-id-pattern-mismatch"`.
|
||||
|
||||
### `unscopeTopic(scoped: string, tenantId?: string): string | null`
|
||||
|
||||
Returns the original topic if `scoped` matches the tenant prefix.
|
||||
Returns `null` if the prefix does not match (used by
|
||||
`listSubscriptions` to filter foreign records). When `tenantId` is
|
||||
absent the function is the identity.
|
||||
|
||||
### `topicRoot(topic: string): string`
|
||||
|
||||
Returns the substring before the first `.` or `:`. Used as the
|
||||
`topic_root` label on `event_bus.published_total`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The pattern intentionally excludes uppercase letters and whitespace
|
||||
so topics stay safe for use as URL path segments, OTel attribute
|
||||
values, and message-queue routing keys without further escaping.
|
||||
- Tenant ids carried through `scopeTopic` use a stricter sub-regex
|
||||
(no `:` separator) — otherwise a tenant id of `"t1:other"` could
|
||||
emulate `tenant:t1:other:topic` for another tenant.
|
||||
- `topicRoot` truncates at the first separator rather than producing
|
||||
high-cardinality labels — OTel limits the number of distinct label
|
||||
values per metric, so we keep this deliberately coarse.
|
||||
Reference in New Issue
Block a user