Skip to content

refactor: v1 deprecation — migrate legacy tables to V4#119

Open
ryceg wants to merge 36 commits intomainfrom
feature/v1-deprecation
Open

refactor: v1 deprecation — migrate legacy tables to V4#119
ryceg wants to merge 36 commits intomainfrom
feature/v1-deprecation

Conversation

@ryceg
Copy link
Copy Markdown
Collaborator

@ryceg ryceg commented Apr 24, 2026

Summary

Incrementally deprecates legacy V1 infrastructure by migrating reads and writes from legacy tables to V4 tables.

Entries table (Phases 1-4 complete)

V1/V3 entry CRUD and most internal consumers now read/write exclusively from V4 tables (sensor_glucose, meter_glucose, calibrations). The legacy entries table remains but is only used by 4 consumers that depend on the MongoDB-style find query parser.

  • EntryProjection static mappers (V4 to Entry shape)
  • EntryReadService replacing DualPathEntryStore for V4-only reads
  • Decomposer promoted to primary write path (no more dual-write)
  • DualPathEntryStore removed
  • PredictionService, AlexaService, CacheWarmingService, DDataService, DebugController migrated to IEntryStore
  • DataSourceService, ConnectorHealthService, ServicesController, DemoServiceHealthMonitor migrated to V4 repos
  • DemoEntryService, DemoDataHostedService migrated to ISensorGlucoseRepository
  • Golden file tests updated to seed V4 entities

Remaining work

See docs/plans/2026-04-24-drop-entries-table-phases.md for the full roadmap.

Test plan

  • 2448 unit tests pass (11 pre-existing failures unrelated)
  • Golden file tests passing with V4 entity seeding
  • Full solution builds with 0 errors
  • V1/V3 entry GET/POST/PUT/DELETE return identical response shapes

ryceg added 21 commits April 24, 2026 21:55
Remove 12 coach marks that just restated obvious UI labels (sidebar nav,
page headings, metric labels). Rewrite 8 onboarding marks to give
actionable guidance instead of repeating card descriptions — e.g.
"Devices → Your CGM, pump, and meter" becomes "Add your current device →
Add the CGM, pump, or meter you use right now and mark it as current."

Consolidate duplicate marks on dashboard (widgets, chart) from paired
arrays down to single marks with better descriptions.
- URL now includes dash-formatted code (ABC-DEFG)
- ValidateSessionAsync uses AsNoTracking for per-request efficiency
- Added test: creator (not just data owner) can revoke guest links
Tasks 7-9: GuestSessionHandler (priority 52) authenticates via encrypted
cookie backed by IDataProtector with 30s validation cache.
GuestLinkController provides CRUD + activation endpoints.
MemberScopeMiddleware grants scopes directly from the guest grant,
bypassing membership lookup since guests have no SubjectId.

Also moves baseUrl from GuestLinkService constructor to a method
parameter on CreateGuestLinkAsync so the service can be registered
normally via DI, with the controller computing the URL from the request.
Brings in unified API key system.
…st banner

- Add /guest/[[code]] entry page for guest link activation
- Detect nocturne-guest-session cookie in authHandle and validate via
  API session endpoint, creating a guest-specific user object
- Add isGuestSession and guestExpiresAt to App.Locals and layout data
- Add GuestBanner component with countdown timer shown in guest mode
- Hide Settings, Members, Tools, Food, Meals, Dev Tools, tenant switcher,
  and notifications in AppSidebar for guest sessions
- Forward guest session cookie through the API proxy handler
- Add /guest to PUBLIC_PREFIXES so the entry page bypasses auth gates
…try shape

Projects SensorGlucose, MeterGlucose, and Calibration models back into
the legacy Entry shape for V1/V3 API compatibility. Handles LegacyId
fallback, timestamp formatting, and type-specific field mapping.
…h V4-only reads

Implements IEntryStore by querying ISensorGlucoseRepository,
IMeterGlucoseRepository, and ICalibrationRepository exclusively,
projecting results into legacy Entry shape via EntryProjection.
Handles type routing, merge-sort across all three types, demo mode
filtering, time range parsing from MongoDB-style find queries,
DateString filtering, pagination, and duplicate detection.
…String priority

- ResolveDemoFilter() now returns (source, excludeDemo) tuple: when demo
  mode is off, post-filter ephemeral records instead of passing null
  (which let demo data leak through)
- Single-type queries (sgv/mbg/cal) push limit/offset directly to the
  database instead of over-fetching count+skip and paginating in memory
- Add comment documenting that DateString takes priority over Find-based
  time range, matching old DualPathEntryStore behavior
- Add tests for demo exclusion in both QueryAsync and GetCurrentAsync
…gacy table

EntryService.CreateEntriesAsync now validates entry types (sgv/mbg/cal)
and writes directly to V4 tables via IEntryDecomposer.DecomposeAsync
instead of IEntryRepository.CreateEntriesAsync. UpdateEntryAsync uses
the decomposer's idempotent LegacyId-based upsert. DeleteEntryAsync
deletes from V4 tables via DeleteByLegacyIdAsync.

SignalREntryEventSink sets DecomposeToV4 = false since decomposition
now happens before side effects fire. IEntryRepository is retained
only for the bulk delete path (DeleteEntriesAsync).
…vice layer

Replace DualPathEntryStore with EntryReadService in DI registration so
all entry reads go through V4 tables exclusively. Remove IEntryRepository
from V3 EntriesController and route all operations through IEntryService,
which delegates to the new EntryReadService for reads and IEntryDecomposer
for writes. V1 controller already used IEntryService exclusively (no-op).
Delete DualPathEntryStore (replaced by EntryReadService), and remove
MergeAndDeduplicate, SelectMostRecent, and ShouldProject from
EntryDomainLogic — these were only needed for the dual-path merge
between legacy entries and V4 projections. Clean up stale seealso
references and comments in EntryReadService and V4ToLegacyProjectionService.
…ntries

Golden file tests were seeding EntryEntity (legacy entries table) but
EntryReadService now reads from V4 tables (sensor_glucose, meter_glucose).
Updated all entries golden tests to seed SensorGlucoseEntity and
MeterGlucoseEntity directly, added SeedSensorGlucose/SeedMeterGlucose
helpers to GoldenFileTestBase, and accepted updated snapshot files.
…tore (Phase 2)

Swap PredictionService, AlexaService, CacheWarmingService, DDataService,
and DebugController from IEntryRepository to IEntryStore.QueryAsync /
GetCurrentAsync. Update corresponding unit test mocks.
…repos (Phase 3)

Replace IEntryRepository usage in DataSourceService, ConnectorHealthService,
ServicesController, and DemoServiceHealthMonitor with V4 repository methods
and new IDataSourceService aggregation methods.

- Add GetLatestTimestampAsync, GetOldestTimestampAsync, DeleteBySourceAsync
  to ISensorGlucoseRepository, IMeterGlucoseRepository, ICalibrationRepository
- Add GetDataSourceStatsAsync, GetLatestGlucoseTimestampBySourceAsync,
  GetOldestGlucoseTimestampBySourceAsync, DeleteGlucoseDataBySourceAsync
  to IDataSourceService
- Move GetEntryStatsBySourceAsync logic from EntryRepository to DataSourceService
  (querying V4 tables instead of legacy entries)
- DataSourceService.GetActiveDataSourcesAsync now queries sensor_glucose
  instead of legacy entries table
- DataSourceService delete methods use V4 repo DeleteBySourceAsync
- ConnectorHealthService depends on IDataSourceService instead of IEntryRepository
- ServicesController uses IDataSourceService for glucose timestamps
- DemoServiceHealthMonitor uses IDataSourceService.DeleteDemoDataAsync
…s (Phase 4)

Replace all IEntryRepository usage in the demo service with
ISensorGlucoseRepository:

- DemoEntryService: creates SensorGlucose records directly via
  BulkCreateAsync instead of legacy Entry objects through IEntryRepository
- DemoDataHostedService: uses ISensorGlucoseRepository.DeleteBySourceAsync
  for clearing demo data instead of IEntryRepository.DeleteEntriesByDataSourceAsync
- Program.cs /stats endpoint: uses CountBySourceAsync instead of
  CountEntriesAsync with JSON find query
- Program.cs /clear endpoint: uses DeleteBySourceAsync instead of
  DeleteEntriesByDataSourceAsync

Also adds CountBySourceAsync to ISensorGlucoseRepository interface and
implementation, and registers ISensorGlucoseRepository + IAuditContext in
the demo service DI container.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 24, 2026

Preview Container Images

Published for commit dee32a9 with tag pr-119-dee32a9.

Image Status Package URI
nocturne-api ✅ Published package ghcr.io/nightscout/nocturne/nocturne-api:pr-119-dee32a9
nocturne-web ✅ Published package ghcr.io/nightscout/nocturne/nocturne-web:pr-119-dee32a9

This comment is updated on each push to this PR.

ryceg added 6 commits April 25, 2026 18:03
…ase 6g)

- Delete RecordType.Entry branch in GetOrCreateCanonicalIdAsync (dead code)
- Delete "entry" case in RecordExistsAsync
- Delete GetUnifiedEntryAsync and MergeEntries (zero callers)
- Remove GetUnifiedEntryAsync from IDeduplicationService interface
- Remove entryCount from DeduplicateAllAsync total; add meterGlucoseCount
- Refactor DeduplicateEntriesAsync to scan SensorGlucose + MeterGlucose
  V4 tables instead of legacy Entries table, using appropriate RecordType
  strings (sensorglucose/meterglucose) for linked records
…yMapper, V4BackfillService (Phase 6h)

Delete dead code now that all consumers have been migrated to V4 tables:
- IEntryRepository, EntryRepository, EntryEntity, EntryMapper
- V4BackfillService and BackfillController
- EntryRepositoryTests, entry-specific soft-delete tests
- All EntryEntity model configuration from NocturneDbContext
- DefaultEntryConverters from QueryParser
- IEntryRepository DI registrations from ServiceCollectionExtensions
- Legacy entry migration methods from MigrationJobService

Fix all compilation errors in test files by converting EntryEntity
seeding to SensorGlucoseEntity and removing EntryRepository references
from integration/performance benchmarks.

if (hasFind && !hasTimeBounds)
{
_logger.LogWarning("BulkDelete refused: find query has no parseable time range, would delete all records. find={Find}", find);

var total = (long)sgDeleted + mgDeleted + calDeleted;
_logger.LogInformation("BulkDelete: removed {Total} v4 records (sg={Sg}, mg={Mg}, cal={Cal}) for find={Find}",
total, sgDeleted, mgDeleted, calDeleted, find);
if (bolusCalcsTotal > 0) { typeBreakdown["BolusCalculations"] = bolusCalcsTotal; typeBreakdown24h["BolusCalculations"] = bolusCalcs24h; }
if (notesTotal > 0) { typeBreakdown["Notes"] = notesTotal; typeBreakdown24h["Notes"] = notes24h; }
if (deviceEventsTotal > 0) { typeBreakdown["DeviceEvents"] = deviceEventsTotal; typeBreakdown24h["DeviceEvents"] = deviceEvents24h; }
if ((stateSpanStats?.TotalStateSpans ?? 0) > 0) { typeBreakdown["StateSpans"] = stateSpanStats!.TotalStateSpans; typeBreakdown24h["StateSpans"] = stateSpanStats.StateSpansLast24Hours; }
if (deviceEventsTotal > 0) { typeBreakdown["DeviceEvents"] = deviceEventsTotal; typeBreakdown24h["DeviceEvents"] = deviceEvents24h; }
if ((stateSpanStats?.TotalStateSpans ?? 0) > 0) { typeBreakdown["StateSpans"] = stateSpanStats!.TotalStateSpans; typeBreakdown24h["StateSpans"] = stateSpanStats.StateSpansLast24Hours; }
if (deviceStatusTotal > 0) { typeBreakdown["DeviceStatus"] = deviceStatusTotal; typeBreakdown24h["DeviceStatus"] = deviceStatus24h; }
if ((treatmentStats?.TotalTreatments ?? 0) > 0) { typeBreakdown["Treatments"] = treatmentStats!.TotalTreatments; typeBreakdown24h["Treatments"] = treatmentStats.TreatmentsLast24Hours; }
if ((treatmentStats?.TotalTreatments ?? 0) > 0) { typeBreakdown["Treatments"] = treatmentStats!.TotalTreatments; typeBreakdown24h["Treatments"] = treatmentStats.TreatmentsLast24Hours; }

var lastTreatmentTime = treatmentStats?.LastTreatmentMills.HasValue == true
? DateTimeOffset.FromUnixTimeMilliseconds(treatmentStats.LastTreatmentMills.Value).UtcDateTime
? DateTimeOffset.FromUnixTimeMilliseconds(treatmentStats.LastTreatmentMills.Value).UtcDateTime
: (DateTime?)null;
var firstTreatmentTime = treatmentStats?.FirstTreatmentMills.HasValue == true
? DateTimeOffset.FromUnixTimeMilliseconds(treatmentStats.FirstTreatmentMills.Value).UtcDateTime
{
var results = await _sgRepo.GetAsync(from, to, device, source: null, limit: 100, offset: 0, descending: true, nativeOnly: false, ct: ct);
var match = sgv.HasValue
? results.FirstOrDefault(r => Math.Abs(r.Mgdl - sgv.Value) < 0.01)
{
var results = await _mgRepo.GetAsync(from, to, device, source: null, limit: 100, offset: 0, descending: true, ct: ct);
var match = mbg.HasValue
? results.FirstOrDefault(r => Math.Abs(r.Mgdl - mbg.Value) < 0.01)
var query = _context.Calibrations.AsQueryable();

if (from.HasValue)
query = query.Where(e => e.Timestamp >= from.Value);
if (from.HasValue)
query = query.Where(e => e.Timestamp >= from.Value);
if (to.HasValue)
query = query.Where(e => e.Timestamp < to.Value);
var query = _context.MeterGlucose.AsQueryable();

if (from.HasValue)
query = query.Where(e => e.Timestamp >= from.Value);
if (from.HasValue)
query = query.Where(e => e.Timestamp >= from.Value);
if (to.HasValue)
query = query.Where(e => e.Timestamp < to.Value);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants