@nodii/approval
v0.16.0
Published
Approval library for the Nodii microservice stack — defineApprovalKinds + requestApproval + bindApprovalHandlers + per-tenant policies + completion-event consumer + migrate-gen. Polyglot ship: TS + Python + Go in parity. Spec: planning hub docKey=approval
Readme
@nodii/approval
Approval library for the Nodii microservice stack. Implements the platform-wide
approval contract locked in 10-approval-doctrine.md. Polyglot ship: TS + Python + Go
at v0.1.0.
Public surface (v0.1.0)
defineApprovalKinds(serviceName, defs)— type-safe kind registry per spec § 5.3.initApproval({...})— boot-time wiring (pg, redis, telemetry, task-tracking).bindApprovalHandlers({...})— registeronApproved/onRejected/onExpired.requestApproval({...})— the canonical 7-step request sequence per spec § 5.2.startApprovalConsumer({...})/stopApprovalConsumer({...})— BullMQ-leased completion-event consumer per spec § 5.8. Passstreams: {...}to also start the native Redis-Streams reader (D263) — see below.streamKeyForTopic(topic)— resolve a completion topic to its canonical 2-segment Redis stream key per01-communication-doctrine § 9.2(see below).getApprovalMigrationSQL(serviceName)— emit per-service<service>_approval_policies<service>_deferred_actionsmigration SQL per spec § 5.4 / § 5.6.
Spec: planning hub feature_doc docKey=approval. See
10-approval-doctrine.md
for the canonical contract.
Boot
import { initApproval, startApprovalConsumer } from "@nodii/approval";
import Redis from "ioredis";
import postgres from "postgres";
const pgPool = postgres(process.env.DATABASE_URL!);
const redis = new Redis(process.env.REDIS_URL!);
await initApproval({
serviceName: "billing",
pgPool,
redis,
taskServiceUrl: process.env.TASK_TRACKING_GRPC_URL!,
});
await startApprovalConsumer({ serviceName: "billing" });See tests/integration.test.ts for an end-to-end example that wires the
library against postgres + redis + an in-process task-tracking gRPC stub.
Native Redis-Streams reader (D263)
Pass a streams config to startApprovalConsumer to have the library run the
native Redis-Streams reader alongside the in-service worker — it XREADGROUPs the
shared completion stream, filters to your service's completions, dedupe-checks
via consumed_events, and bridges new events into the existing worker.
Pass the FULL completion topic as streams.topic. The reader internally
resolves the canonical 2-segment <service>.<domain> Redis stream key per
01-communication-doctrine § 9.2 — because the producer's outbox drainer XADDs
to that 2-segment key, NOT the full 4-segment topic. Do not pre-truncate the
topic yourself.
import { startApprovalConsumer, streamKeyForTopic } from "@nodii/approval";
await startApprovalConsumer({
serviceName: "billing",
streams: {
// Pass the FULL topic. The reader XREADGROUPs streamKeyForTopic(topic).
topic: "tasks.task.completed.v1", // → reader reads the `tasks.task` stream
consumerGroup: "billing-approval-completions",
consumerName: `billing-${process.pid}`,
// Keep only your service's completions (D276 § 4.1).
filter: (e) => e.metadata.originating_service === "billing",
},
});
// The resolver is exported for adopters who need it directly:
streamKeyForTopic("tasks.task.completed.v1"); // "tasks.task"
streamKeyForTopic("tasks.approval_task.resolved.v1"); // "tasks.approval_task"The emitted TaskCompletedEvent.topic and the (event_id, topic) dedup key
always carry the FULL topic (from the wire envelope), never the 2-segment
stream key.
