Skip to content

Query API ​

Use AuditService.query() for newest-first application views and investigations. For a forward, checkpointed export, use scan() instead.

Query a page ​

typescript
const filters = {
  tenantId: 'tenant-1',
  actorId: 'user-123',
  actorType: 'user',
  action: 'Invoice.*',
  targetType: 'Invoice',
  source: 'auto' as const,
  result: 'success' as const,
  from: new Date('2026-08-01T00:00:00.000Z'),
  to: new Date('2026-09-01T00:00:00.000Z'),
  limit: 50,
  includeTotal: false,
};

let page = await auditService.query(filters);

while (page.hasMore) {
  page = await auditService.query({
    ...filters,
    cursor: page.nextCursor!,
  });
}

Rows are ordered newest-first by (created_at, id). The cursor is opaque and records only that ordering boundary; it does not contain the filters. Reuse the same filter set for every page. Automatic actions preserve the Prisma model name: an Invoice model emits Invoice.created, Invoice.updated, or Invoice.deleted. Matching is case-sensitive, so invoice.* does not match those defaults. A custom @AuditAction() or manual event can deliberately use lowercase actions.

includeTotal defaults to true. Set it to false for feeds that do not need an exact count; this skips the separate COUNT(*) query and omits total from the result. When included, total counts all rows matching the filters, not only rows below the cursor. The count and page are separate queries and can reflect different concurrent database states.

Query options ​

OptionTypeDescription
tenantIdstringExplicitly scope the read to one tenant
allTenantsbooleanDeliberately omit tenant filtering for an authorized admin read
actorIdstringFilter by actor ID
actorTypestringFilter by actor type
actionstringCase-sensitive exact action or * wildcard pattern, such as Invoice.*
targetTypestringFilter by target type
targetIdstringFilter by target ID
source'auto' | 'manual'Filter by audit source
result'success' | 'failure'Filter by outcome
fromDateInclusive lower created_at bound
toDateInclusive upper created_at bound
limitnumberPage size; defaults to 50 and must be a positive integer
cursorstringContinue below the previous page's nextCursor
offsetnumberNon-negative offset for non-cursor pagination
includeTotalbooleanInclude total; defaults to true

cursor and offset are mutually exclusive. Prefer cursors for a changing or large table. Literal SQL wildcard characters in an action filter are escaped; only * has wildcard meaning.

Response ​

typescript
interface AuditQueryResult {
  entries: AuditEntry[];
  nextCursor: string | null;
  hasMore: boolean;
  total?: number;
}

nextCursor is non-null only when another page exists. Treat it as an opaque token and do not parse, edit, or manufacture one.

Tenant boundary ​

query() can use ambient tenant context. An explicit tenantId overrides that ambient context; allTenants: true is the intentional cross-tenant path. tenantId and allTenants are mutually exclusive.

With tenantRequired: true, a call without explicit or ambient tenant scope fails. Without it, an unscoped call is allowed and emits a one-time warning. The package does not authorize admin access, so check cross-tenant permissions before calling allTenants: true.

The optional actorRequired policy in 0.7.0 applies to writes; it does not require an actor for query() or getById() and does not authorize access to audit history. Check the caller's read permissions in your application before invoking either API.

Look up one entry ​

getById() follows the same tenant rules and returns null for an invalid or missing ID:

typescript
const entry = await auditService.getById('12dc5b9e-8e3a-4ec1-b211-f728f924db0f', {
  tenantId: 'tenant-1',
});

Use allTenants: true only after an application-level authorization check.

Released under the MIT License.