From 86acd96d93725d0ffeb915db9a8ce0f499ba0253 Mon Sep 17 00:00:00 2001 From: Derek Gabriel Date: Sun, 12 Apr 2026 17:18:16 -1000 Subject: [PATCH] feat: add admin/touch endpoint to backfill contentHash on existing docs HTTP-triggered (POST /admin/touch), gated by Function App master key. Reads every document and re-upserts it, which triggers the contentHash and contentLength computation in UpsertAsync. Idempotent and safe to run multiple times. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Services/CosmosDocumentStore.cs | 19 +++++++++ .../Services/IDocumentStore.cs | 7 ++++ .../Tools/AdminFunctions.cs | 40 +++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 src/DevBrain.Functions/Tools/AdminFunctions.cs diff --git a/src/DevBrain.Functions/Services/CosmosDocumentStore.cs b/src/DevBrain.Functions/Services/CosmosDocumentStore.cs index b08f770..6d95298 100644 --- a/src/DevBrain.Functions/Services/CosmosDocumentStore.cs +++ b/src/DevBrain.Functions/Services/CosmosDocumentStore.cs @@ -97,6 +97,25 @@ private static string NormalizeForHash(string content) => return null; } + public async Task TouchAllAsync() + { + var queryDefinition = new QueryDefinition("SELECT * FROM c"); + var touched = 0; + + using var iterator = _container.GetItemQueryIterator(queryDefinition); + while (iterator.HasMoreResults) + { + var response = await iterator.ReadNextAsync(); + foreach (var document in response) + { + await UpsertAsync(document); + touched++; + } + } + + return touched; + } + public async Task> ListAsync(string project, string? prefix = null) { var queryText = prefix is not null diff --git a/src/DevBrain.Functions/Services/IDocumentStore.cs b/src/DevBrain.Functions/Services/IDocumentStore.cs index 55b051b..d769852 100644 --- a/src/DevBrain.Functions/Services/IDocumentStore.cs +++ b/src/DevBrain.Functions/Services/IDocumentStore.cs @@ -16,6 +16,13 @@ public interface IDocumentStore /// Task GetMetadataAsync(string key, string project); + /// + /// Re-upserts every document in the store, triggering server-side metadata + /// recomputation (contentHash, contentLength). Returns the number of documents + /// touched. Intended as a one-shot backfill after adding new computed fields. + /// + Task TouchAllAsync(); + /// /// Deletes a single document by key within a project. Idempotent: returns false /// when the document does not exist. Accepts both colon and slash keys to support diff --git a/src/DevBrain.Functions/Tools/AdminFunctions.cs b/src/DevBrain.Functions/Tools/AdminFunctions.cs new file mode 100644 index 0000000..1b4912f --- /dev/null +++ b/src/DevBrain.Functions/Tools/AdminFunctions.cs @@ -0,0 +1,40 @@ +using System.Net; +using DevBrain.Functions.Services; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; + +namespace DevBrain.Functions.Tools; + +/// +/// Administrative HTTP endpoints that are not exposed as MCP tools. +/// Gated by (requires the Function App master key). +/// +public sealed class AdminFunctions +{ + private readonly IDocumentStore _store; + + public AdminFunctions(IDocumentStore store) + { + _store = store; + } + + /// + /// Re-upserts every document to backfill computed metadata fields (contentHash, + /// contentLength). Idempotent — safe to run multiple times. Not exposed as an + /// MCP tool; invoke via HTTP with the Function App master key. + /// + [Function(nameof(TouchAllDocuments))] + public async Task TouchAllDocuments( + [HttpTrigger(AuthorizationLevel.Admin, "post", Route = "admin/touch")] HttpRequestData req) + { + var touched = await _store.TouchAllAsync(); + + var response = req.CreateResponse(HttpStatusCode.OK); + await response.WriteAsJsonAsync(new + { + touched, + message = $"Re-upserted {touched} document(s). contentHash and contentLength are now populated." + }); + return response; + } +}