12 — API Mapping (Audit Module)
- 0. Module-wide request envelope & client policy
- Non-existent surfaces (append-only — do not build against them)
- Loading / streaming / realtime
- Client-side error mapping table (module)
- Optimistic / undo
Exact wire contract for the Audit screens → the single read endpoint. Base
/api/v1; envelope per 00-shared/07. All endpoints fromsrc/modules/audit/audit.controller.ts; business rules fromaudit.service.ts+audit.handler.ts+audit-log.repository.ts. Global guards:RateLimitGuard→JwtAuthGuard→RbacGuard(app.module.ts:129-133). Append-only module: no create/update/delete/export endpoints exist.
0. Module-wide request envelope & client policy
| Aspect | Contract |
|---|---|
| Base | https://api.<domain>/api/v1 |
| Headers | Authorization: Bearer <accessToken>; x-request-id client-generated |
| Success | {success:true, message:"OK", data, meta?, timestamp, requestId} (response-envelope.interceptor.ts:11-18) |
| Error | {success:false, message, error:{code,details?}, timestamp, requestId} (http-exception.filter.ts:16-25) |
| Tenancy | tenantId from JWT claim → TenantContextService.requireTenantId() (audit.controller.ts:30, jwt-auth.guard.ts:44-55); never in query/body |
| Rate limit | default api tier 100/min (rate-limit.constants.ts:6) — no @RateLimit override on the controller |
| Permission | server: JwtAuthGuard only (audit.controller.ts:9); audit.read (permissions.constants.ts:54) unenforced → Phase-5 (planned) (docs/IMPLEMENTATION_PLAN.md:241) |
| Cache | server none; client last-good cache 5 min (volatile list, 00-shared/06 §3.3) |
| Offline | reads from cache + AppOfflineBanner; writes n/a (no writes exist) |
Screen: Audit log list (and filter bar)
| Endpoint | GET /audit-logs (audit.controller.ts:17) |
| Query params | page (default 1), limit (default 50), action?, entityType?, actorId? (audit.controller.ts:20-24) |
| Filters | AND-combined: {action, entityType, actorId} → Mongo filter + tenant scope (audit.controller.ts:26-29, audit.service.ts:45-48) |
| Sort | fixed occurredAt desc (audit-log.repository.ts:23); no sort/q support |
| Auth | Bearer JWT (valid = allowed today; audit.read recommended gate client-side, OQ-5) |
| Errors | 400 VALIDATION_ERROR (non-numeric page/limit → CastError, http-exception.filter.ts:47-55); 401 UNAUTHENTICATED; 429 RATE_LIMITED; 5xx INTERNAL_SERVER_ERROR |
Actual response (exact — derived from code):
{
"success": true,
"message": "OK",
"data": {
"data": [
{
"_id": "66f0…",
"tenantId": "t_springfield",
"actorId": "u_8f2a…",
"actorType": "user",
"action": "StudentUpdated",
"entityType": null,
"entityId": null,
"occurredAt": "2026-08-02T05:02:14.000Z",
"after": { "studentId": "s_9f1…", "changes": ["guardianPhone"] },
"correlationId": "9a1c…",
"before": null,
"metadata": null,
"isDeleted": false,
"version": 0
}
],
"total": 1234
},
"timestamp": "2026-08-02T05:02:15.000Z",
"requestId": "req_…"
}
Pagination shape deviation (critical, OQ-2):
AuditService.query()returns{data, total}withoutmeta(audit.service.ts:41-50). The envelope interceptor treats a payload as paginated only when it has bothdataandmeta(response-envelope.interceptor.ts:25-32), so this response is wrapped as a plain object: client readsenvelope.data.data[]andenvelope.data.total. It does not receivemeta:{page,limit,totalItems,totalPages,hasNext,hasPrevious}per the shared paginated contract (00-shared/07 §2). Client computes paging fromtotal+ requestedpage/limit.
Entry document fields (exact, audit-log.schema.ts:13-56 + inherited base.schema.ts:8-34):
| Field | Type | Notes |
|---|---|---|
_id | string | row key, dedupe key |
tenantId | string | required, index (base.schema.ts:10-11) |
actorId | string | required (audit-log.schema.ts:15-16) |
actorType | user|system|api_key | default user; handler always writes user (audit-log.schema.ts:18-19, audit.handler.ts:35) |
action | string | required = eventType (PascalCase, e.g. UserCreated) (audit-log.schema.ts:21-22, audit.handler.ts:36) |
entityType / entityId | string? | rarely populated today (OQ-1) (audit-log.schema.ts:24-28, audit.handler.ts:37-40) |
ipAddress / device / browser / userAgent | string? | schema fields, never written by current handler (OQ) (audit-log.schema.ts:30-40) |
occurredAt | ISO-8601 UTC | required; the display timestamp (audit-log.schema.ts:42-43); schema timestamps:false → no createdAt |
before / after | object? | masked at write (audit.service.ts:20); before set only when emitter supplies it; after falls back to whole payload (audit.handler.ts:42-43) |
correlationId | string? | trace link (audit-log.schema.ts:51-52) |
metadata | object? | payload.metadata passthrough (audit-log.schema.ts:54-55, audit.handler.ts:44) |
isDeleted/version/createdBy/… | inherited | always false/0/absent — never displayed (base.schema.ts:13-34) |
Client flows: load(page, filters) → map → model; loadMore() → page+1 while
items.length < total; pullToRefresh() → page=1 bypassing cache; changeFilter()
→ page=1.
Endpoint source: audit.controller.ts:17-34, audit.service.ts:37-51, audit-log.repository.ts:19-28
Non-existent surfaces (append-only — do not build against them)
| Surface | Status | Evidence |
|---|---|---|
GET /audit-logs/:id (entry detail) | absent — detail renders in-memory (OQ-4) | controller has one route (audit.controller.ts:17) |
POST/PATCH/DELETE /audit-logs* | absent — append-only | audit-log.repository.ts:6, 15-28; PLAN 19.3 (PLAN.md:191) |
| Export CSV/PDF | (planned) — blueprint only | AUDITING.md:87; no endpoint |
Date-range / q / sort filters | (planned) | controller params only page/limit/action/entityType/actorId (audit.controller.ts:19-25) |
Platform (tenantId:null) entries | (planned) — not implemented | AUDITING.md:70-71 vs audit-log.schema.ts:7-11 |
Loading / streaming / realtime
| Screen | Loading | Streaming | Realtime |
|---|---|---|---|
| audit list | AppSkeleton rows | Load-more pages | WS append (ws-bridge.service.ts:16-22) — topics = eventType strings, joined room tenant:{tenantId} (ws.gateway.ts:50) |
| entry detail | n/a (in-memory) | — | — |
| export | (planned) | (planned) streamed per blueprint | — |
Client-side error mapping table (module)
| Screen | code | UI |
|---|---|---|
| list/detail | 401 | silent refresh → fail → session-expiry (00-shared/06 §3.6) |
| list | 429 | countdown chip; no auto-retry |
| list | 400 (bad page/limit) | reset to page 1, snackbar |
| list | 5xx | AppErrorState generic + requestId |
| list | 403 (future Phase-5) | 403 screen; route hidden pre-emptively |
Optimistic / undo
None — read-only module; no mutations, no optimistic updates, no undo (09 §1).