Skip to content

Add resource changes feed API#817

Open
TheTechArch wants to merge 4 commits into
mainfrom
feature/resource-changes-feed-812
Open

Add resource changes feed API#817
TheTechArch wants to merge 4 commits into
mainfrom
feature/resource-changes-feed-812

Conversation

@TheTechArch

@TheTechArch TheTechArch commented Jul 10, 2026

Copy link
Copy Markdown
Member

Description

Implements the resource change feed designed in #812.

Adds GET /resourceregistry/api/v1/resource/changes — a keyset-paginated feed of resources that have changed, ordered by change. Consumers (e.g. downstream sync/cache jobs) can poll this endpoint to find out which resources changed since their last poll, without re-fetching or diffing the full resource list.

Design (per #812, updated after review discussion)

Changes are tracked in a dedicated resource_change_log table (migration v0.10: seq BIGSERIAL, identifier, changed_at, change_source), written atomically by every mutation path:

  • CreateResource and UpdateResource log a 'metadata' entry inside their own transaction/statement (data-modifying CTE).
  • Policy uploads log a 'policy' entry via a new LogPolicyChanged repository method, called from StorePolicy after SetResourceSubjects.
  • DeleteResource logs a 'deleted' entry in the same statement as the delete.

The migration backfills one entry per existing resource at its latest version, in version order, so pre-existing resources are served by the feed from the start.

Feed semantics:

  • Only resources with an active policy are listed — resources with no non-deleted rows in resourcesubjects are filtered out.
  • Each resource appears at most once, at its latest changeROW_NUMBER() OVER (PARTITION BY identifier ORDER BY seq DESC) ... rn = 1, keyset-paged on seq with the cursor predicate pushed into the CTE for PK-index pruning.
  • Deleted resources are excluded — a resource whose latest log entry is 'deleted' drops out of the feed. Delete events are recorded in the log, so exposing them to consumers later (e.g. a changeType field) is just a matter of lifting this filter — no new bookkeeping needed.
  • Pagination follows the existing resource/updated endpoint pattern: Opaque<long> continuation token, Paginated<T> envelope, limit 1–1000 (default 1000), fetching limit + 1 rows to detect the next page.

Why a change-log table instead of touching the resource row? An earlier revision of this PR copied the latest resource version forward on policy updates. That worked, but duplicated serviceresourcejson per policy write and polluted the resource version history with no-op versions. The change-log table keeps version history meaning "metadata versions" only, adds no payload duplication, and records deletes for free. Writes are atomic with their triggering statement, so the log cannot drift from the actual data.

API

GET /resourceregistry/api/v1/resource/changes?limit=1000
GET /resourceregistry/api/v1/resource/changes?token=<opaque>&limit=1000
{
  "links": { "next": ".../resource/changes?token=<opaque>&limit=1000" },
  "items": [
    { "resourceId": "some-resource", "changedAt": "2026-07-08T10:15:30Z" }
  ]
}

The seq cursor is internal ([JsonIgnore] on the model) — clients only see the opaque token.

Tests

  • Mock-based: pagination walk across pages, invalid limit → 400 (run locally, green).
  • DB-backed (testcontainers, run in CI): pagination, resources without policy excluded, resource listed once at its latest change after metadata update, policy upload bumps the resource into/through the feed, deleted resource drops out of the feed, invalid limit theory.

Closes #812.

🤖 Generated with Claude Code

Adds GET /resourceregistry/api/v1/resource/changes - a keyset-paginated
feed of resources that have changed, ordered by version_id. Only
resources with an active policy (non-deleted resource subjects) are
included, and each resource appears at most once, at its latest change.

Policy updates are reflected in the feed via a new
TouchResourceModified repository method called from StorePolicy. It
copies the latest resource version forward entirely inside SQL with an
updated modified timestamp, so a concurrent metadata update can never
be overwritten by stale in-memory state.

Pagination follows the existing resource/updated endpoint pattern:
Opaque<long> continuation token over version_id, Paginated<T> response
envelope, limit 1-1000 fetching limit+1 rows to detect the next page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new keyset-paginated “resource changes” feed endpoint to the Resource Registry so downstream consumers can poll for changed resources (metadata or policy-driven “touches”) without refetching the full list.

Changes:

  • Added GET /resourceregistry/api/v1/resource/changes with opaque continuation token + limit validation and Paginated<T> response.
  • Implemented persistence/service support for querying “latest change per resource” (filtered to resources with active policy) and for “touching” a resource on policy updates.
  • Added both mock-based and DB-backed tests covering pagination, filtering, and policy-update bumping behavior.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/Altinn.ResourceRegistry.Tests/ResourceControllerWithDbTests.cs Adds DB-backed integration tests for the changes feed, including policy bump behavior.
test/Altinn.ResourceRegistry.Tests/ResourceControllerTest.cs Adds mock-based controller tests for pagination and invalid limit handling.
test/Altinn.ResourceRegistry.Tests/Mocks/RegisterResourceRepositoryMock.cs Extends the repository mock with FindChangedResources and TouchResourceModified.
src/Altinn.ResourceRegistry/Controllers/ResourceController.cs Adds the new /resource/changes endpoint, limit validation, and next-link generation.
src/Altinn.ResourceRegistry.Persistence/ResourceRegistryRepository.cs Implements SQL for change-feed reads and the “touch resource modified” insert-select.
src/Altinn.ResourceRegistry.Core/Services/ResourceRegistryService.cs Calls the new “touch” method after storing policy-derived subjects; exposes FindChangedResources.
src/Altinn.ResourceRegistry.Core/Services/Interfaces/IResourceRegistry.cs Adds service interface contract for FindChangedResources.
src/Altinn.ResourceRegistry.Core/Models/ResourceChange.cs Introduces the API model for change feed items (with internal VersionId cursor).
src/Altinn.ResourceRegistry.Core/IResourceRegistryRepository.cs Adds repository interface methods for change-feed querying and “touching” resources.
src/Altinn.ResourceRegistry.Core/Errors/ValidationErrors.cs Adds validation descriptor for invalid limit on the changes endpoint.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +145 to +146
await _repository.SetResourceSubjects(resourceSubjects, CancellationToken.None);
await _repository.TouchResourceModified(serviceResource.Identifier, CancellationToken.None);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional, and consistent with the pre-existing pattern: SetResourceSubjects was already called with CancellationToken.None here before this PR. Once the policy blob has been written to storage, cancelling the follow-up DB updates midway would leave the blob and the database inconsistent (policy stored but subjects/feed not updated). Letting the DB updates run to completion keeps the write atomic-ish; a client that cancels simply retries the idempotent upload. TouchResourceModified follows the same rule for the same reason.

Rune T. Larsen and others added 2 commits July 10, 2026 16:22
Review feedback: filtering version_id > @version_id inside the CTE lets
PostgreSQL prune via the primary key index before the window function
runs, making incremental polls with a high cursor cheap. Results are
unchanged: rows at or below the cursor can never be the per-resource
max when that max is above the cursor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the copy-forward touch approach with a change-log table
(migration v0.10) written atomically by every mutation path:
CreateResource, UpdateResource and DeleteResource log inside their own
statements/transaction, and policy updates log via LogPolicyChanged
from StorePolicy. The feed pages on the log's seq column instead of
resources.version_id.

Compared to copying the full resource row forward on policy updates,
this avoids duplicating serviceresourcejson and keeps the resource
version history free of no-op versions. It also records deletes: the
feed excludes resources whose latest change is a delete (previously
deleted resources silently disappeared because their version rows were
hard-deleted), and exposing delete events later is now just a matter of
lifting that filter.

The migration backfills one log entry per existing resource at its
latest version, in version order, so pre-existing resources are
served by the feed from the start.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shared GetServiceResource row-mapper reads version_id, but
DeleteResource's RETURNING clause never included it - a latent bug from
the v0.09 table split that no test exercised until the new
change-feed delete test hit it in CI. Also collapse the result to the
latest version row: deleting a multi-version resource returns one row
per version, which would make SingleOrDefaultAsync throw.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Comment on lines +164 to +174
await using (var cmd3 = new NpgsqlCommand(
@"
INSERT INTO resourceregistry.resource_change_log(identifier, changed_at, change_source)
VALUES (@identifier, @changedat, 'metadata');",
conn,
tx))
{
cmd3.Parameters.AddWithValue("identifier", NpgsqlDbType.Text, resource.Identifier);
cmd3.Parameters.AddWithValue("changedat", NpgsqlDbType.TimestampTz, modified);
await cmd3.ExecuteNonQueryAsync(cancellationToken);
}
Comment on lines 145 to +146
await _repository.SetResourceSubjects(resourceSubjects, CancellationToken.None);
await _repository.LogPolicyChanged(serviceResource.Identifier, CancellationToken.None);
Comment on lines +5 to +10
(
seq BIGSERIAL NOT NULL PRIMARY KEY,
identifier text NOT NULL,
changed_at timestamp with time zone NOT NULL DEFAULT now(),
change_source text NOT NULL
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/feature-request New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add change feed API for resources (paged, ordered by change time)

2 participants