From 98224f526a31694e89f8af0f70d1a29886bd102c Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Fri, 16 Oct 2026 09:55:34 -0400 Subject: [PATCH 1/7] =?UTF-8?q?docs(plano):=20plano=20da=20issue=20#75=20?= =?UTF-8?q?=E2=80=94=20colunas=20de=20risco=20em=20texto=20plano?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-10-16-issue-75-colunas-de-risco.md | 1012 +++++++++++++++++ 1 file changed, 1012 insertions(+) create mode 100644 docs/superpowers/plans/2026-10-16-issue-75-colunas-de-risco.md diff --git a/docs/superpowers/plans/2026-10-16-issue-75-colunas-de-risco.md b/docs/superpowers/plans/2026-10-16-issue-75-colunas-de-risco.md new file mode 100644 index 0000000..39c6c6d --- /dev/null +++ b/docs/superpowers/plans/2026-10-16-issue-75-colunas-de-risco.md @@ -0,0 +1,1012 @@ +# Issue #75 — Colunas de risco em texto plano (INV-04) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fechar a lacuna P0 da issue #75 — cifrar em repouso as classificações de risco de `triage_sessions` e `visits`, eliminar o rótulo derivado `resultDisplay` e registrar (com teste que a sustenta) a decisão de manter `alerts.riskLevel` legível. + +**Architecture:** A cifragem segue o padrão já existente da Track E: AES-256-GCM na borda do repositório ORM (`HealthDataCipher` + par `*Encrypted`/`*KeyVersion`), com `application/` trafegando sempre o valor em claro. Um codec único (`EncryptedRiskLevel`) serializa o risco com **largura fixa**, para que o tamanho do ciphertext não revele a classe. `alerts.riskLevel` fica em claro porque só existe um valor gravável (`red`) e a coluna é usada pelos predicados SQL que garantem a INV-03. + +**Tech Stack:** Dart, Serverpod 2 (models `.spy.yaml`, `serverpod generate`, `serverpod create-migration`), PostgreSQL, pacote `cryptography` (AES-GCM, já usado), `package:test` com o harness `withServerpod`. + +**Spec:** Issue #75 (`gh issue view 75`), origem `docs/auditoria-prd/2026-10-15-lacunas-prd.md` (item #1, linhas INV-04/RNF03/LGPD-RF09); `spec/PRD_system.md` §1.2 (INV-03, INV-04); `spec/lgpd_data_audit.md` §1 (`:50-51`, `:64-65`, `:82`) e §2.3; decisão §6 de `docs/superpowers/specs/2026-09-16-decisoes-produto-pos-validacao.md` (cifra na aplicação, não `pgcrypto`). + +## Verificação feita ao escrever o plano (2026-10-16, `develop` em `9c7bca6`) + +A lacuna procede. Leitores e escritores das cinco colunas, medidos por `grep` em `backend/sinalacs_server/lib`: + +| Coluna | Escrita | Leitura | Usada em SQL cru? | Decisão | +|---|---|---|---|---| +| `triage_sessions.resultRisk` | `OrmTriageSessionStore.insert` | `OrmPatientDataOverviewStore.loadFor` (histórico do "Meus Dados") | Não | **Cifrar** | +| `triage_sessions.resultDisplay` | `OrmTriageSessionStore.insert` (valor de `_displayFor(risk)`) | Ninguém | Não | **Remover** (dado derivado, minimização) | +| `visits.riskLevelBefore` / `riskLevelAfter` | `OrmVisitStore._toRow` | `OrmVisitStore._toRecord` (sync e `pull` do ACS) | Não | **Cifrar** | +| `alerts.riskLevel` | Só `OrmAlertStore.save`, que grava **sempre** `RiskLevel.red` (`orm_alert_store.dart:47`) | ORM e SQL cru | **Sim**: `alert_overdue_sql.dart:22`, `orm_alert_escalation_store.dart:93`, `orm_retention_store.dart:25,76,154`, `orm_admin_read_store.dart:54-60,160` | **Manter em claro e registrar a decisão** | + +Por que `alerts.riskLevel` fica legível: (1) como todo alerta gravado é vermelho, a coluna não revela nada além do que a própria existência da linha em `alerts` já revela; (2) ela está nos predicados SQL que impedem o descarte de alerta vermelho pendente (INV-03: varredura de escalonamento e guarda do expurgo) — cifrá-la obrigaria a trazer esses filtros para a memória do processo, trocando uma invariante crítica por uma proteção nula. A decisão **cai** no dia em que algum caminho gravar alerta amarelo/verde; a Task 4 transforma essa condição num teste. + +## Global Constraints + +- Cifra só na borda do repositório ORM; `application/` nunca sabe de ciphertext (`backend/CLAUDE.md`, parágrafo "The three clinical columns are encrypted at the ORM boundary only"). +- Serialização de valor cifrado passa por `EncryptedJson` (`lib/src/infrastructure/crypto/encrypted_json.dart`) — ponto único de mudança da rotação de chave. +- Nunca editar à mão `lib/src/generated/` nem `migrations/`: usar `serverpod generate` e `serverpod create-migration`. +- Ciphertext vazio (`''`) significa "linha nunca escrita pelo caminho Dart"; o JSON `null` cifrado significa "ausência real". +- Nunca dado real de paciente em teste, log ou fixture (LGPD); mensagens de erro e log só com identificadores sintéticos, nunca o risco nem o conteúdo clínico (VAZ-01). +- Classificação de risco é determinística (INV-02): nenhum caminho de leitura pode **inventar** um risco para preencher um valor ausente. +- Commits em português, no estilo `tipo(escopo): descrição` do repositório, **sem** `Co-Authored-By` nem menção a IA (`CLAUDE.md`). +- Trabalhar num branch a partir de `develop`: `git switch -c fix/issue-75-colunas-de-risco`. +- Testes de integração precisam do Postgres de teste em `localhost:9090` (`docker compose up -d postgres-test`; ver `backend/CLAUDE.md`, início) e rodam com `-j 1`. Os valores de `POSTGRES_USER`/`POSTGRES_DB` da pilha local valem `sinalacs_user`/`sinalacs_db` salvo override no `.env`. + +## Review Focus + +1. **Tamanho do ciphertext revelando a classe** — AES-GCM preserva o tamanho do texto claro; `"red"` cifrado sairia mais curto que `"yellow"`, e quem lê o Postgres separaria os vermelhos pelo comprimento do base64. Esperado: os três níveis e a ausência produzem ciphertexts do mesmo tamanho. Teste na Task 1. +2. **Visita gravada antes da migração (ciphertext de risco vazio)** — um `pg_data/` antigo fica com `riskLevelBeforeEncrypted = ''`. Esperado: o `pull` falha alto (`StateError` com só o id da visita), nunca devolve um risco inventado. Teste na Task 3. +3. **Triagem antiga sem risco cifrado no "Meus Dados"** — esperado: o painel abre e essa triagem fica fora do histórico, sem risco inventado e sem derrubar o painel inteiro. Teste na Task 2. +4. **`riskLevelAfter` nulo e depois preenchido na conclusão da visita** — esperado: o nulo faz ida e volta como nulo, e a atualização (versão 2) regrava o risco cifrado novo, que volta legível no `pull`. Teste na Task 3. +5. **Um novo escritor de `alerts` gravando amarelo/verde** — derrubaria a justificativa de `alerts.riskLevel` em claro sem ninguém perceber. Esperado: a suíte falha apontando a decisão. Teste na Task 4. + +--- + +### Task 1: Codec de risco cifrado com largura fixa + +**Files:** +- Create: `backend/sinalacs_server/lib/src/infrastructure/crypto/encrypted_risk_level.dart` +- Create: `backend/sinalacs_server/test/unit/encrypted_risk_level_test.dart` + +**Interfaces:** +- Consumes: `HealthDataCipher` (`encrypt`, `decrypt`, `keyVersion`), extensão `EncryptedJson` (`encryptJson(Object?)`, `decryptJson(String, int) → Future`), enum gerado `RiskLevel { red, yellow, green }`. +- Produces: extensão `EncryptedRiskLevel on HealthDataCipher` com + - `Future encryptRiskLevel(RiskLevel? risk)` + - `Future decryptRiskLevel(String ciphertextBase64, int keyVersion)` — `null` para ciphertext vazio **e** para ausência cifrada; nome desconhecido lança `ArgumentError`. + +- [ ] **Step 1: Criar o branch** + +```bash +cd /home/rock/Documents/Dev/APPs/SinalACS +git switch develop && git pull --ff-only +git switch -c fix/issue-75-colunas-de-risco +``` + +- [ ] **Step 2: Escrever o teste que falha** + +`backend/sinalacs_server/test/unit/encrypted_risk_level_test.dart`: + +```dart +import 'package:sinalacs_server/src/config/app_config.dart'; +import 'package:sinalacs_server/src/generated/protocol.dart'; +import 'package:sinalacs_server/src/infrastructure/crypto/encrypted_json.dart'; +import 'package:sinalacs_server/src/infrastructure/crypto/encrypted_risk_level.dart'; +import 'package:sinalacs_server/src/infrastructure/crypto/health_data_cipher.dart'; +import 'package:test/test.dart'; + +/// Codec do risco cifrado (issue #75, INV-04). Hermético: só a cifra, sem banco. +void main() { + final cipher = HealthDataCipher( + keyHex: AppConfig.developmentHealthDataEncryptionKey, + keyVersion: 1, + ); + + test('cada nível e a ausência fazem ida e volta', () async { + for (final risk in [...RiskLevel.values, null]) { + final encrypted = await cipher.encryptRiskLevel(risk); + expect( + await cipher.decryptRiskLevel(encrypted.ciphertextBase64, encrypted.keyVersion), + risk, + reason: 'ida e volta de $risk', + ); + } + }); + + test('o tamanho do ciphertext não revela a classe: todos têm o mesmo ' + 'comprimento, inclusive a ausência', () async { + final lengths = { + for (final risk in [...RiskLevel.values, null]) + risk: (await cipher.encryptRiskLevel(risk)).ciphertextBase64.length, + }; + expect(lengths.values.toSet(), hasLength(1), reason: 'comprimentos: $lengths'); + }); + + test('o mesmo risco cifrado duas vezes dá ciphertexts diferentes (nonce ' + 'aleatório: igualdade de coluna não agrupa pacientes por risco)', () async { + final a = await cipher.encryptRiskLevel(RiskLevel.red); + final b = await cipher.encryptRiskLevel(RiskLevel.red); + expect(a.ciphertextBase64, isNot(b.ciphertextBase64)); + }); + + test('ciphertext vazio (linha nunca escrita pelo caminho Dart) vira null', () async { + expect(await cipher.decryptRiskLevel('', 1), isNull); + }); + + test('nome desconhecido lança em vez de inventar um risco (INV-02)', () async { + final forjado = await cipher.encryptJson('purple'); + expect( + () => cipher.decryptRiskLevel(forjado.ciphertextBase64, forjado.keyVersion), + throwsArgumentError, + ); + }); +} +``` + +- [ ] **Step 3: Rodar e ver falhar** + +Run: `cd backend/sinalacs_server && dart test test/unit/encrypted_risk_level_test.dart` +Expected: FAIL na compilação — `encrypted_risk_level.dart` não existe / `encryptRiskLevel` não definido. + +- [ ] **Step 4: Implementar o codec** + +`backend/sinalacs_server/lib/src/infrastructure/crypto/encrypted_risk_level.dart`: + +```dart +import 'package:sinalacs_server/src/generated/protocol.dart'; +import 'package:sinalacs_server/src/infrastructure/crypto/encrypted_json.dart'; +import 'package:sinalacs_server/src/infrastructure/crypto/health_data_cipher.dart'; + +/// Cifra de uma classificação de risco em repouso (INV-04, issue #75). +/// +/// O AES-GCM não esconde o TAMANHO do texto claro: `"red"` cifrado sairia +/// mais curto que `"yellow"`, e quem lê a coluna bruta separaria os vermelhos +/// pelo comprimento do base64. Por isso o nome vai preenchido com espaços até +/// a largura do maior nome de [RiskLevel], e a ausência (`null`, o +/// `riskLevelAfter` de uma visita ainda não concluída) vai como só espaços, +/// do mesmo tamanho. +/// +/// Usada pelos stores ORM de triagem e de visita; vive em `infrastructure/` +/// pelo mesmo motivo de [EncryptedJson]. +extension EncryptedRiskLevel on HealthDataCipher { + static final int _width = + RiskLevel.values.map((r) => r.name.length).reduce((a, b) => a > b ? a : b); + + Future encryptRiskLevel(RiskLevel? risk) => + encryptJson((risk?.name ?? '').padRight(_width)); + + /// `null` para ciphertext vazio (linha nunca escrita pelo caminho Dart) e + /// para a ausência cifrada; quem chama decide se `null` é aceitável. Um + /// nome desconhecido lança `ArgumentError` — nunca vira um risco inventado. + Future decryptRiskLevel(String ciphertextBase64, int keyVersion) async { + final decoded = await decryptJson(ciphertextBase64, keyVersion); + if (decoded == null) return null; + final name = (decoded as String).trim(); + if (name.isEmpty) return null; + return RiskLevel.values.byName(name); + } +} +``` + +- [ ] **Step 5: Rodar e ver passar** + +Run: `cd backend/sinalacs_server && dart test test/unit/encrypted_risk_level_test.dart && dart analyze lib/src/infrastructure/crypto test/unit/encrypted_risk_level_test.dart` +Expected: 5 testes PASS; `No issues found!` + +- [ ] **Step 6: Commit** + +```bash +git add backend/sinalacs_server/lib/src/infrastructure/crypto/encrypted_risk_level.dart \ + backend/sinalacs_server/test/unit/encrypted_risk_level_test.dart +git commit -m "feat(backend): codec de risco cifrado com largura fixa (#75)" +``` + +--- + +### Task 2: `triage_sessions` — cifrar `resultRisk` e remover `resultDisplay` + +**Files:** +- Modify: `backend/sinalacs_server/lib/src/models/triage_session.spy.yaml` +- Modify: `backend/sinalacs_server/lib/src/application/triage/triage_session_service.dart` (`TriageSessionRecord` linhas ~32-55, chamada linhas ~150-160, `_displayFor` linhas ~183-187) +- Modify: `backend/sinalacs_server/lib/src/infrastructure/database/orm_triage_session_store.dart:62-74` +- Modify: `backend/sinalacs_server/lib/src/infrastructure/database/orm_patient_data_overview_store.dart:79-84` +- Modify: `backend/sinalacs_server/test/support/health_data_fixtures.dart` +- Modify (testes que constroem `TriageSession`/`TriageSessionRecord`): `test/integration/health_data_encryption_test.dart`, `test/integration/patient_data_overview_endpoint_test.dart:108-117`, `test/integration/retention_expurge_test.dart:180-198`, `test/integration/data_subject_case_store_test.dart:255-265,493`, `test/integration/triage_session_persistence_test.dart:127-128,233`, `test/unit/triage_session_service_test.dart:118-119` +- Generated (por comando): `lib/src/generated/**`, `backend/sinalacs_client/lib/src/protocol/**`, `migrations//` + +**Interfaces:** +- Consumes: `encryptRiskLevel`/`decryptRiskLevel` (Task 1). +- Produces: + - Modelo `TriageSession` com `resultRiskEncrypted: String` (default `''`) e `resultRiskKeyVersion: int` (default `1`); **sem** `resultRisk` nem `resultDisplay`. + - `TriageSessionRecord` **sem** `resultDisplay` (continua com `resultRisk: RiskLevel`). + - Fixtures de teste: `Future encryptedRiskLevel(RiskLevel? risk)` e `Future decryptedTriageRisk(TriageSession session)` em `test/support/health_data_fixtures.dart`. + +- [ ] **Step 1: Escrever o teste que verifica a lacuna (e falha hoje)** + +Em `test/integration/health_data_encryption_test.dart`, acrescentar depois de `_colunaBruta` (linha ~140): + +```dart +/// A linha inteira como mapa coluna → valor, lida por SQL cru (#75). Mostra +/// TODAS as colunas, inclusive as que alguém acrescentar depois. +Future> _linhaBruta(Session session, {required String table}) async { + final rows = await session.db.unsafeQuery('SELECT row_to_json(t)::text FROM "$table" t;'); + expect(rows, hasLength(1), reason: 'o seed deve ter gravado exatamente 1 linha'); + return jsonDecode(rows.single.first as String) as Map; +} + +/// Nenhuma coluna pode guardar a classificação legível — nem o nome do enum +/// nem o rótulo de tela. Igualdade, não substring: um base64 pode conter +/// "red" por acaso. +void _expectSemRiscoLegivel(Map linha) { + const legiveis = {'red', 'yellow', 'green', 'Vermelho', 'Amarelo', 'Verde'}; + for (final entry in linha.entries) { + expect( + legiveis.contains('${entry.value}'.trim()), + isFalse, + reason: 'a coluna ${entry.key} guarda o risco em claro (INV-04, #75)', + ); + } +} +``` + +E, dentro do mesmo `group`, depois do teste de `triage_sessions.answersEncrypted`: + +```dart + test('#75: nenhuma coluna de triage_sessions guarda a classificação de ' + 'risco em claro', () async { + final session = sessionBuilder.build(); + await _seed(session); + + final service = TriageSessionService( + store: OrmTriageSessionStore( + session: () => session, + cipher: testHealthDataCipher(), + ), + audit: _SilentAuditTrail(), + ); + final risco = await service.evaluateAndRecord( + user: _paciente, + chestPain: true, + difficultyBreathing: false, + fever: false, + persistentVomiting: false, + bleeding: false, + severeWeakness: false, + ); + expect(risco, RiskLevel.red); + + final linha = await _linhaBruta(session, table: 'triage_sessions'); + _expectSemRiscoLegivel(linha); + }); +``` + +- [ ] **Step 2: Rodar e confirmar que a lacuna existe** + +Run: `cd backend/sinalacs_server && dart test test/integration/health_data_encryption_test.dart -j 1 -N '#75'` +Expected: FAIL com `a coluna resultRisk guarda o risco em claro (INV-04, #75)` — é a evidência da issue no código atual. + +- [ ] **Step 3: Mudar o modelo** + +`lib/src/models/triage_session.spy.yaml` — substituir as linhas `resultRisk: RiskLevel` e `resultDisplay: String` por: + +```yaml + ### Classificação do TriageEngine, cifrada (INV-04, #75): nome do RiskLevel + ### com largura fixa, ver `encrypted_risk_level.dart`. O rótulo de tela + ### (`resultDisplay`) deixou de ser persistido: era derivado do risco. + resultRiskEncrypted: String, default='' + resultRiskKeyVersion: int, default=1 +``` + +E trocar o comentário do topo (linha 1) por: + +```yaml +### Sessão de triagem estruturada. O risco é produzido pelo TriageEngine +``` + +Run: `cd backend/sinalacs_server && serverpod generate` +Expected: termina sem erro; `dart analyze` agora acusa os usos antigos de `resultRisk`/`resultDisplay` — são os passos seguintes. + +- [ ] **Step 4: Tirar `resultDisplay` do serviço** + +Em `lib/src/application/triage/triage_session_service.dart`: +- remover `required this.resultDisplay,` do construtor e `final String resultDisplay;` de `TriageSessionRecord`; +- remover a linha `resultDisplay: _displayFor(risk),` da construção do record (linha ~157); +- remover o método `static String _displayFor(RiskLevel risk) => switch (risk) { … };` (linhas ~183-187), que fica sem uso. + +- [ ] **Step 5: Cifrar no store de gravação** + +Em `lib/src/infrastructure/database/orm_triage_session_store.dart`, importar o codec: + +```dart +import 'package:sinalacs_server/src/infrastructure/crypto/encrypted_risk_level.dart'; +``` + +Em `insert`, cifrar o risco junto das respostas e passar ao `_insert`: + +```dart + @override + Future insert(TriageSessionRecord session) async { + final encrypted = await _cipher.encryptJson( + [for (final answer in session.answers) answer.toJson()], + ); + final risk = await _cipher.encryptRiskLevel(session.resultRisk); + final db = _session(); + final external = _transaction; + if (external != null) return _insert(db, external, session, encrypted, risk); + return db.db.transaction((transaction) => _insert(db, transaction, session, encrypted, risk)); + } + + Future _insert( + Session db, + Transaction transaction, + TriageSessionRecord session, + EncryptedValue encrypted, + EncryptedValue risk, + ) async { +``` + +e, no `TriageSession(...)` do `_insert`, trocar `resultRisk:`/`resultDisplay:` por: + +```dart + resultRiskEncrypted: risk.ciphertextBase64, + resultRiskKeyVersion: risk.keyVersion, +``` + +Atualizar o comentário da classe (linhas 15-19): "É aqui que as respostas e a classificação de risco da triagem viram ciphertext (RNF03, INV-04, #75) …". + +- [ ] **Step 6: Decifrar no histórico do "Meus Dados"** + +Em `lib/src/infrastructure/database/orm_patient_data_overview_store.dart`, importar `encrypted_risk_level.dart` e trocar o bloco `final riskHistory = [ … ]..sort(…);` por: + +```dart + final riskHistory = []; + for (final row in triageRows) { + // Ciphertext vazio = triagem gravada antes da migração da #75: sem risco + // legível, a linha fica fora do histórico — nunca um risco inventado + // (INV-02), e o painel do titular continua abrindo. + final risk = await _cipher.decryptRiskLevel( + row.resultRiskEncrypted, + row.resultRiskKeyVersion, + ); + if (risk == null) continue; + riskHistory.add( + RiskEventSnapshot(source: 'triage', riskLevel: risk, recordedAt: row.createdAt), + ); + } + for (final row in alertRows) { + riskHistory.add( + RiskEventSnapshot(source: 'alert', riskLevel: row.riskLevel, recordedAt: row.triggeredAt), + ); + } + riskHistory.sort((a, b) => b.recordedAt.compareTo(a.recordedAt)); +``` + +- [ ] **Step 7: Fixtures de teste** + +Em `test/support/health_data_fixtures.dart`, importar `package:sinalacs_server/src/infrastructure/crypto/encrypted_risk_level.dart` e acrescentar no fim: + +```dart +/// Cifra uma classificação de risco do jeito que os stores gravam (#75). +Future encryptedRiskLevel(RiskLevel? risk) => + testHealthDataCipher().encryptRiskLevel(risk); + +/// Decifra o risco de uma sessão de triagem lida pelo ORM (#75). +Future decryptedTriageRisk(TriageSession session) => + testHealthDataCipher().decryptRiskLevel( + session.resultRiskEncrypted, + session.resultRiskKeyVersion, + ); +``` + +- [ ] **Step 8: Ajustar os testes que montavam a linha em claro** + +Cada `TriageSession(...)` de teste troca `resultRisk: X, resultDisplay: '…',` por um par cifrado. Padrão (exemplo de `patient_data_overview_endpoint_test.dart:108`): + +```dart + final riscoTriagem = await encryptedRiskLevel(RiskLevel.yellow); + await TriageSession.db.insertRow( + session, + TriageSession( + patientId: UuidValue.fromString(_patientId), + resultRiskEncrypted: riscoTriagem.ciphertextBase64, + resultRiskKeyVersion: riscoTriagem.keyVersion, + createdAt: DateTime.utc(2026, 2, 1), + deviceId: 'device-de-teste', + ), + ); +``` + +Aplicar em: +- `test/integration/patient_data_overview_endpoint_test.dart` (seed, `RiskLevel.yellow`; importar `../support/health_data_fixtures.dart` se ainda não importa); +- `test/integration/data_subject_case_store_test.dart:255-265` (`RiskLevel.yellow`); na asserção da linha ~493 trocar `expect(triagens.single.resultRisk, RiskLevel.yellow);` por `expect(await decryptedTriageRisk(triagens.single), RiskLevel.yellow);` — prova que a anonimização (#42) mantém o risco para a estatística; +- `test/integration/retention_expurge_test.dart:180-198` — o expurgo não lê o risco: basta **remover** as linhas `resultRisk:`/`resultDisplay:` (o default `''` vale); +- `test/integration/triage_session_persistence_test.dart:127-128,233` — trocar `expect(gravadas.single.resultRisk, RiskLevel.red);` por `expect(await decryptedTriageRisk(gravadas.single), RiskLevel.red);` (idem `green` na 233) e apagar a asserção de `resultDisplay`; +- `test/unit/triage_session_service_test.dart:119` — apagar `expect(store.saved.single.resultDisplay, 'Vermelho');` (o `resultRisk` do record em claro continua). + +Run: `cd backend/sinalacs_server && dart analyze` +Expected: `No issues found!` (se sobrar algum uso de `resultRisk`/`resultDisplay` em `test/`, ajustar pelo mesmo padrão). + +- [ ] **Step 9: Ida e volta e triagem antiga no painel** + +No teste `#75` do Step 1, acrescentar ao fim: + +```dart + expect( + linha.containsKey('resultDisplay'), + isFalse, + reason: 'o rótulo derivado do risco não é mais persistido', + ); + final gravada = await TriageSession.db.findFirstRow(session); + expect(await decryptedTriageRisk(gravada!), RiskLevel.red); +``` + +Em `test/integration/patient_data_overview_endpoint_test.dart`, dentro do `withServerpod`, acrescentar: + +```dart + test('#75: triagem gravada antes da cifra do risco fica fora do histórico ' + 'e o painel abre mesmo assim', () async { + final session = sessionBuilder.build(); + await _seed(session); + // Linha "antiga": o default '' da coluna, como a migração deixa as + // linhas que já existiam. + await TriageSession.db.insertRow( + session, + TriageSession( + patientId: UuidValue.fromString(_patientId), + createdAt: DateTime.utc(2026, 4, 1), + deviceId: 'device-de-teste', + ), + ); + final login = await endpoints.auth.developmentLogin(sessionBuilder, role: 'patient'); + + final result = await endpoints.patients.myData( + sessionBuilder, + accessToken: login.accessToken, + ); + + expect(result.riskHistory, hasLength(2), reason: 'só as duas linhas do seed'); + expect( + result.riskHistory.every((e) => e.recordedAt != DateTime.utc(2026, 4, 1)), + isTrue, + ); + }); +``` + +- [ ] **Step 10: Gerar a migração** + +Run: `cd backend/sinalacs_server && serverpod create-migration` +Expected: novo diretório em `migrations/` com `DROP COLUMN "resultRisk"`, `DROP COLUMN "resultDisplay"` e `ADD COLUMN "resultRiskEncrypted" text NOT NULL DEFAULT ''`/`"resultRiskKeyVersion" bigint NOT NULL DEFAULT 1` em `migration.sql`. Se o comando recusar por mudança destrutiva, rodar `serverpod create-migration --force` — o descarte é a mudança pretendida e fica documentado na Task 5. + +- [ ] **Step 11: Rodar os testes tocados** + +Run: +```bash +cd backend/sinalacs_server +dart test test/unit/triage_session_service_test.dart +dart test -j 1 test/integration/health_data_encryption_test.dart \ + test/integration/patient_data_overview_endpoint_test.dart \ + test/integration/data_subject_case_store_test.dart \ + test/integration/retention_expurge_test.dart \ + test/integration/triage_session_persistence_test.dart +``` +Expected: tudo PASS, incluindo os dois testes `#75`. + +- [ ] **Step 12: Commit** + +```bash +git add backend/sinalacs_server backend/sinalacs_client +git commit -m "fix(backend): cifra o risco da triagem e deixa de gravar resultDisplay (#75)" +``` + +--- + +### Task 3: `visits` — cifrar `riskLevelBefore` e `riskLevelAfter` + +**Files:** +- Modify: `backend/sinalacs_server/lib/src/models/visit.spy.yaml:19-20` +- Modify: `backend/sinalacs_server/lib/src/infrastructure/database/orm_visit_store.dart:44-64,119-167` +- Modify (testes que constroem `Visit` com risco): `test/integration/patient_directory_and_territory_test.dart:362-386`, `test/integration/visit_legacy_sync_test.dart:469-484`, `test/integration/retention_expurge_test.dart:153-177`, `test/integration/data_subject_case_store_test.dart:282-295`, `test/integration/admin_endpoint_accounts_test.dart:~320-335`, `test/integration/upload_token_test.dart:~125-140`, `test/integration/health_data_encryption_test.dart` +- Generated (por comando): `lib/src/generated/**`, `backend/sinalacs_client/lib/src/protocol/**`, `migrations//` + +**Interfaces:** +- Consumes: `encryptRiskLevel`/`decryptRiskLevel` (Task 1); fixtures `encryptedRiskLevel`, `encryptedVisitNotes`, `_linhaBruta`, `_expectSemRiscoLegivel` (Task 2). +- Produces: modelo `Visit` com `riskLevelBeforeEncrypted`/`riskLevelBeforeKeyVersion` e `riskLevelAfterEncrypted`/`riskLevelAfterKeyVersion` (defaults `''`/`1`); `VisitRecord` **não muda** (continua `riskLevelBefore: RiskLevel`, `riskLevelAfter: RiskLevel?`). + +- [ ] **Step 1: Escrever o teste que verifica a lacuna (e falha hoje)** + +Em `test/integration/health_data_encryption_test.dart`, no mesmo `group`, acrescentar: + +```dart + test('#75: nenhuma coluna de visits guarda a classificação de risco em ' + 'claro', () async { + final session = sessionBuilder.build(); + await _seed(session); + final relogio = DateTime.utc(2026, 9, 17, 12); + final service = VisitSyncService( + store: OrmVisitStore(session: () => session, cipher: testHealthDataCipher()), + audit: _SilentAuditTrail(), + clock: () => relogio, + ); + + await service.sync( + user: _acs, + entries: [ + VisitSyncEntry( + localId: _localId, + patientId: _patientId, + scheduledAt: DateTime.utc(2026, 9, 17, 9), + completedAt: DateTime.utc(2026, 9, 17, 10), + status: 'realizada', + riskLevelBefore: RiskLevel.yellow, + riskLevelAfter: RiskLevel.green, + notes: const {}, + version: 1, + arrivalMethod: ArrivalMethod.manual, + ), + ], + ); + + final linha = await _linhaBruta(session, table: 'visits'); + _expectSemRiscoLegivel(linha); + }); +``` + +- [ ] **Step 2: Rodar e confirmar que a lacuna existe** + +Run: `cd backend/sinalacs_server && dart test test/integration/health_data_encryption_test.dart -j 1 -N 'visits guarda a classificação'` +Expected: FAIL com `a coluna riskLevelBefore guarda o risco em claro (INV-04, #75)`. + +- [ ] **Step 3: Mudar o modelo** + +`lib/src/models/visit.spy.yaml` — substituir `riskLevelBefore: RiskLevel` e `riskLevelAfter: RiskLevel?` por: + +```yaml + ### Risco antes e depois da visita, cifrados (INV-04, #75): nome do + ### RiskLevel com largura fixa, ver `encrypted_risk_level.dart`. O "depois" + ### ausente (visita não concluída) é cifrado também, com o mesmo tamanho. + riskLevelBeforeEncrypted: String, default='' + riskLevelBeforeKeyVersion: int, default=1 + riskLevelAfterEncrypted: String, default='' + riskLevelAfterKeyVersion: int, default=1 +``` + +Run: `cd backend/sinalacs_server && serverpod generate` + +- [ ] **Step 4: Cifrar e decifrar no `OrmVisitStore`** + +Importar `package:sinalacs_server/src/infrastructure/crypto/encrypted_risk_level.dart`. Trocar `insert`/`update` para repassar o record em claro inteiro: + +```dart + @override + Future insert(VisitRecord visit) async { + final row = await Visit.db.insertRow( + _session(), + await _toRow(visit), + transaction: _transaction, + ); + // Reaproveita o texto claro que o chamador já tem (notas e riscos): + // decifrar de volta o que acabamos de cifrar seria ida e volta à toa. + return _toRecord(row, plain: visit); + } + + @override + Future update(VisitRecord visit) async { + final row = await Visit.db.updateRow( + _session(), + await _toRow(visit), + transaction: _transaction, + ); + return _toRecord(row, plain: visit); + } +``` + +Em `_toRow`, cifrar os dois riscos e trocar as linhas `riskLevelBefore:`/`riskLevelAfter:` do `Visit(...)`: + +```dart + Future _toRow(VisitRecord record) async { + final encrypted = await _cipher.encryptJson(record.notes); + final before = await _cipher.encryptRiskLevel(record.riskLevelBefore); + final after = await _cipher.encryptRiskLevel(record.riskLevelAfter); + return Visit( + // … campos inalterados … + riskLevelBeforeEncrypted: before.ciphertextBase64, + riskLevelBeforeKeyVersion: before.keyVersion, + riskLevelAfterEncrypted: after.ciphertextBase64, + riskLevelAfterKeyVersion: after.keyVersion, + notesEncrypted: encrypted.ciphertextBase64, + // … campos inalterados … + ); + } +``` + +Substituir `_toRecord` por: + +```dart + Future _toRecord(Visit row, {VisitRecord? plain}) async { + final notes = plain?.notes ?? + ((await _cipher.decryptJson(row.notesEncrypted, row.notesKeyVersion)) + as Map?) + ?.cast() ?? + const {}; + final before = plain?.riskLevelBefore ?? + await _cipher.decryptRiskLevel( + row.riskLevelBeforeEncrypted, + row.riskLevelBeforeKeyVersion, + ); + // Ciphertext vazio = linha anterior à migração da #75 ou gravada fora + // deste store. Falha alto: a fila do ACS não pode receber um risco + // inventado (INV-02). Só o id sintético vai na mensagem (VAZ-01). + if (before == null) { + throw StateError('Visita ${row.id} sem riskLevelBefore cifrado (#75).'); + } + final after = plain != null + ? plain.riskLevelAfter + : await _cipher.decryptRiskLevel( + row.riskLevelAfterEncrypted, + row.riskLevelAfterKeyVersion, + ); + return VisitRecord( + id: row.id, + patientId: row.patientId, + acsId: row.acsId, + authorship: row.authorship, + originDeviceId: row.originDeviceId, + scheduledAt: row.scheduledAt, + startedAt: row.startedAt, + completedAt: row.completedAt, + status: row.status, + riskLevelBefore: before, + riskLevelAfter: after, + notes: notes, + syncStatus: row.syncStatus, + arrivalMethod: row.arrivalMethod, + localId: row.localId, + syncAt: row.syncAt, + version: row.version, + ); + } +``` + +Atualizar o comentário da classe (linhas 13-17): "É também a única fronteira que conhece a cifragem de `visits.notes` e dos riscos `riskLevelBefore`/`riskLevelAfter` (RNF03, INV-04, #75) …". + +- [ ] **Step 5: Ajustar os testes que montavam `Visit` em claro** + +Testes que **leem a visita de volta** (via `pull`/store) precisam do par cifrado. Padrão (exemplo de `patient_directory_and_territory_test.dart:362`): + +```dart + final notasVazias = await encryptedVisitNotes(const {}); + final riscoVerde = await encryptedRiskLevel(RiskLevel.green); + + Visit visita({…}) => Visit( + // … + riskLevelBeforeEncrypted: riscoVerde.ciphertextBase64, + riskLevelBeforeKeyVersion: riscoVerde.keyVersion, + riskLevelAfterEncrypted: riscoVerde.ciphertextBase64, + riskLevelAfterKeyVersion: riscoVerde.keyVersion, + notesEncrypted: notasVazias.ciphertextBase64, + // … + ); +``` + +Aplicar em `patient_directory_and_territory_test.dart` (antes `green`/`green`) e `visit_legacy_sync_test.dart:469-484` (antes só `riskLevelBefore: RiskLevel.green` → só o par `Before`; o `After` fica no default `''`, que decifra como `null`, igual ao `riskLevelAfter` ausente de antes). + +Testes que **só inserem** a visita (contagem, FK, expurgo, autoria) e nunca a leem pelo store: **remover** as linhas `riskLevelBefore:`/`riskLevelAfter:` — `retention_expurge_test.dart:153-177`, `data_subject_case_store_test.dart:282-295`, `admin_endpoint_accounts_test.dart` (~327), `upload_token_test.dart` (~132). Se `dart test` mostrar `StateError … sem riskLevelBefore cifrado` em algum deles, é porque o teste lê a visita: trocar pelo par cifrado do padrão acima. + +Run: `cd backend/sinalacs_server && dart analyze` +Expected: `No issues found!` + +- [ ] **Step 6: Ida e volta, `After` nulo e atualização na conclusão** + +No teste `#75` de visitas do Step 1, acrescentar ao fim: + +```dart + final puxadas = await service.pull( + user: _acs, + since: relogio.subtract(const Duration(days: 1)), + ); + expect(puxadas.single.riskLevelBefore, RiskLevel.yellow); + expect(puxadas.single.riskLevelAfter, RiskLevel.green); +``` + +E acrescentar ao `group`: + +```dart + test('#75: riskLevelAfter nulo volta nulo e, na conclusão (versão 2), o ' + 'risco novo é regravado cifrado', () async { + final session = sessionBuilder.build(); + await _seed(session); + var relogio = DateTime.utc(2026, 9, 17, 12); + final service = VisitSyncService( + store: OrmVisitStore(session: () => session, cipher: testHealthDataCipher()), + audit: _SilentAuditTrail(), + clock: () => relogio, + ); + VisitSyncEntry entrada({required int version, RiskLevel? depois, DateTime? fim}) => + VisitSyncEntry( + localId: _localId, + patientId: _patientId, + scheduledAt: DateTime.utc(2026, 9, 17, 9), + completedAt: fim, + status: fim == null ? 'agendada' : 'realizada', + riskLevelBefore: RiskLevel.red, + riskLevelAfter: depois, + notes: const {}, + version: version, + arrivalMethod: ArrivalMethod.manual, + ); + + await service.sync(user: _acs, entries: [entrada(version: 1)]); + var puxadas = await service.pull( + user: _acs, + since: relogio.subtract(const Duration(days: 1)), + ); + expect(puxadas.single.riskLevelAfter, isNull); + + relogio = relogio.add(const Duration(hours: 1)); + final resultado = await service.sync( + user: _acs, + entries: [ + entrada(version: 2, depois: RiskLevel.yellow, fim: DateTime.utc(2026, 9, 17, 10)), + ], + ); + expect(resultado.single.syncStatus, SyncStatus.synced); + _expectSemRiscoLegivel(await _linhaBruta(session, table: 'visits')); + puxadas = await service.pull( + user: _acs, + since: relogio.subtract(const Duration(days: 1)), + ); + expect(puxadas.single.riskLevelBefore, RiskLevel.red); + expect(puxadas.single.riskLevelAfter, RiskLevel.yellow); + }); + + test('#75: visita sem risco cifrado (anterior à migração) faz o pull ' + 'falhar alto, sem inventar risco', () async { + final session = sessionBuilder.build(); + await _seed(session); + final notas = await encryptedVisitNotes(const {}); + await Visit.db.insertRow( + session, + Visit( + patientId: UuidValue.fromString(_patientId), + acsId: UuidValue.fromString(_acsId), + scheduledAt: DateTime.utc(2026, 9, 17, 9), + status: 'realizada', + notesEncrypted: notas.ciphertextBase64, + notesKeyVersion: notas.keyVersion, + syncStatus: SyncStatus.synced, + localId: UuidValue.fromString(_localId), + syncAt: DateTime.utc(2026, 9, 17, 11), + version: 1, + ), + ); + final service = VisitSyncService( + store: OrmVisitStore(session: () => session, cipher: testHealthDataCipher()), + audit: _SilentAuditTrail(), + clock: () => DateTime.utc(2026, 9, 17, 12), + ); + + expect( + () => service.pull(user: _acs, since: DateTime.utc(2026, 9, 16)), + throwsA(isA()), + ); + }); +``` + +Se a versão 2 voltar `conflict` em vez de `synced`, ler a regra de versão em `VisitSyncService._syncOne` (`lib/src/application/visits/visit_sync_service.dart`, ~linha 500) e ajustar só a entrada do teste (versão/campos) para o caminho de atualização aceito — **não** mudar a regra de sincronização (`CLAUDE.md`: preservar semântica de retry/fila/conflito). + +- [ ] **Step 7: Gerar a migração** + +Run: `cd backend/sinalacs_server && serverpod create-migration` (com `--force` se recusar por mudança destrutiva, mesmo motivo da Task 2) +Expected: `migration.sql` com `DROP COLUMN "riskLevelBefore"`, `DROP COLUMN "riskLevelAfter"` e os quatro `ADD COLUMN` com default. + +- [ ] **Step 8: Rodar os testes tocados** + +Run: +```bash +cd backend/sinalacs_server +dart test test/unit/visit_sync_service_test.dart +dart test -j 1 test/integration/health_data_encryption_test.dart \ + test/integration/patient_directory_and_territory_test.dart \ + test/integration/visit_legacy_sync_test.dart \ + test/integration/retention_expurge_test.dart \ + test/integration/data_subject_case_store_test.dart \ + test/integration/admin_endpoint_accounts_test.dart \ + test/integration/upload_token_test.dart +``` +Expected: tudo PASS. + +- [ ] **Step 9: Commit** + +```bash +git add backend/sinalacs_server backend/sinalacs_client +git commit -m "fix(backend): cifra o risco antes e depois da visita (#75)" +``` + +--- + +### Task 4: `alerts.riskLevel` — teste que sustenta a decisão de mantê-lo legível + +**Files:** +- Modify: `backend/sinalacs_server/test/unit/red_alert_discard_paths_test.dart` + +**Interfaces:** +- Consumes: o inventário de escritores `_allowedWriters` e `_grepSources` do mesmo arquivo (já existentes). +- Produces: nenhum símbolo novo; um teste que falha se algum `Alert(…)`/`copyWith(…)` em `lib/src` gravar `riskLevel` diferente de `RiskLevel.red`. + +- [ ] **Step 1: Escrever o teste** + +No `main()` de `test/unit/red_alert_discard_paths_test.dart`, junto dos testes de varredura do fonte, acrescentar: + +```dart + test('INV-04 (#75): todo alerta gravado é vermelho — é o que justifica ' + 'alerts.riskLevel em claro (spec/lgpd_data_audit.md §2.3)', () { + // `(?{}; + for (final entry in _grepSources(pattern).entries) { + for (final hit in entry.value) { + valores.add(pattern.firstMatch(hit)!.group(1)!); + } + } + expect(valores, isNotEmpty, reason: 'a varredura precisa achar o save() do OrmAlertStore'); + expect( + valores, + {'RiskLevel.red'}, + reason: 'Se um alerta amarelo/verde passou a ser gravado, alerts.riskLevel ' + 'deixou de ser igual à existência da linha: cifre a coluna ou ' + 'revise a decisão da #75 antes de mudar este teste.', + ); + }); +``` + +Observação para quem implementa: `_grepSources` devolve o trecho casado com espaços colapsados; `pattern.firstMatch(hit)` reaplica o regex nesse trecho para extrair o grupo. Se `_grepSources` não aceitar um `RegExp` com `dotAll` (ele só repassa o `pattern`), o teste funciona como está — `dotAll` é necessário porque o `Alert(` e o `riskLevel:` ficam em linhas diferentes. + +- [ ] **Step 2: Rodar e ver passar** + +Run: `cd backend/sinalacs_server && dart test test/unit/red_alert_discard_paths_test.dart` +Expected: PASS (o único casamento é `RiskLevel.red`, de `orm_alert_store.dart`). + +- [ ] **Step 3: Provar que o teste morde** + +Trocar temporariamente `riskLevel: RiskLevel.red,` por `riskLevel: RiskLevel.yellow,` em `lib/src/infrastructure/database/orm_alert_store.dart:47` e rodar o mesmo comando. +Expected: FAIL com a `reason` acima. Desfazer a troca (`git checkout -- lib/src/infrastructure/database/orm_alert_store.dart`) e rodar de novo: PASS. + +- [ ] **Step 4: Commit** + +```bash +git add backend/sinalacs_server/test/unit/red_alert_discard_paths_test.dart +git commit -m "test(backend): amarra alerts.riskLevel em claro a só gravar vermelho (#75)" +``` + +--- + +### Task 5: Documentação da decisão e do descarte de dados + +**Files:** +- Modify: `spec/PRD_system.md:54` (linha da INV-04) +- Modify: `spec/lgpd_data_audit.md` (§1 linhas ~50-51, ~64-65, ~82; §2.3) +- Modify: `backend/CLAUDE.md` (parágrafo "The three clinical columns are encrypted at the ORM boundary only", ~linha 85) +- Modify: `CLAUDE.md` (parágrafo "Updating an existing `pg_data/` to this branch loses dev clinical data silently") +- Modify: `PROGRESS.md` (nova seção) +- Modify: `docs/auditoria-prd/2026-10-15-lacunas-prd.md` (linhas INV-04 ~75, RNF03 ~106, LGPD-RF09 ~144, item #1 ~228) + +**Interfaces:** +- Consumes: os nomes de coluna das Tasks 2-3 e os ids das duas migrações geradas (`ls backend/sinalacs_server/migrations | tail -3`). +- Produces: nada de código. + +- [ ] **Step 1: PRD — INV-04** + +Na tabela de §1.2, substituir a terceira célula da linha **INV-04** por: + +```markdown +Criptografia AES-256 em `sqflite` e PostgreSQL. **Exceção registrada (#75, 2026-10-16):** `alerts.riskLevel` fica legível — todo alerta gravado é vermelho (a coluna não diz nada além da existência da linha) e ela sustenta os predicados SQL da INV-03; ver `spec/lgpd_data_audit.md` §2.3 +``` + +- [ ] **Step 2: Auditoria LGPD** + +Em `spec/lgpd_data_audit.md` §1: +- trocar as linhas `resultRisk` e `resultDisplay` de `triage_sessions` por uma linha só: + `| | `resultRiskEncrypted` + `resultRiskKeyVersion` | `text` (ciphertext) + `bigint` | Sensível (Saúde - Art. 5º, II) | **Cifrado em repouso (AES-256-GCM, aplicação)** — ver §2.3 | **Atualizado (2026-10-16, #75):** nome do `RiskLevel` com largura fixa (o tamanho do ciphertext não revela a classe), cifrado em `OrmTriageSessionStore` e decifrado só no histórico do "Meus Dados". `resultDisplay` deixou de existir: era o rótulo derivado do risco (minimização). |` +- trocar as linhas `riskLevelBefore`/`riskLevelAfter` de `visits` por: + `| | `riskLevelBeforeEncrypted`/`riskLevelAfterEncrypted` + `*KeyVersion` | `text` (ciphertext) + `bigint` | Sensível (Saúde - Art. 5º, II) | **Cifrado em repouso (AES-256-GCM, aplicação)** — ver §2.3 | **Atualizado (2026-10-16, #75):** mesmo codec de largura fixa; o "depois" ausente também é cifrado, com o mesmo tamanho. Linha sem risco cifrado faz o `pull` falhar alto em vez de inventar risco. |` +- na linha `riskLevel` de `alerts` (~82), acrescentar à última célula: ` **Decisão (2026-10-16, #75): permanece em texto plano** — só `red` é gravado (teste `INV-04 (#75)` em `test/unit/red_alert_discard_paths_test.dart`), e a coluna é usada nos predicados SQL de escalonamento, guarda do expurgo e indicadores (INV-03). Se alerta não vermelho passar a existir, cifrar.` + +Em §2.3, acrescentar um parágrafo ao fim: + +```markdown +**Classificações de risco (2026-10-16, issue #75).** `triage_sessions.resultRisk` e `visits.riskLevelBefore`/`riskLevelAfter` passaram ao mesmo padrão (`*Encrypted`/`*KeyVersion`, codec `encrypted_risk_level.dart`). Como o AES-GCM preserva o tamanho do texto claro, o nome do nível é preenchido até a largura do maior nome (a ausência também), e o teste `test/unit/encrypted_risk_level_test.dart` prova que os quatro casos dão ciphertexts do mesmo comprimento. `triage_sessions.resultDisplay` foi removida. `alerts.riskLevel` fica legível por decisão registrada no PRD (INV-04): é constante (`red`) e é chave dos filtros SQL que impedem o descarte de alerta vermelho. O `payload` de `alert_outbox` carrega o mesmo `risk_level` constante; o risco dele é o `locationHash`, tratado em §2.2. Migrações sem backfill: um `pg_data/` anterior perde o risco das triagens (somem do histórico do "Meus Dados") e das visitas (o `pull` do ACS falha até recriar o volume). +``` + +- [ ] **Step 3: `backend/CLAUDE.md`** + +No parágrafo "The three clinical columns are encrypted at the ORM boundary only: …", trocar o início por: + +```markdown +The clinical columns are encrypted at the ORM boundary only (since #75 also the risk classifications `triage_sessions.resultRiskEncrypted` and `visits.riskLevelBefore/AfterEncrypted`, through the fixed-width codec `infrastructure/crypto/encrypted_risk_level.dart`; `alerts.riskLevel` stays plaintext by a recorded decision — see `spec/lgpd_data_audit.md` §2.3): +``` + +mantendo o resto do parágrafo. + +- [ ] **Step 4: `CLAUDE.md` raiz — aviso do `pg_data/`** + +No parágrafo **"Updating an existing `pg_data/` to this branch loses dev clinical data silently."**, acrescentar ao fim (com os ids reais das duas migrações geradas nas Tasks 2-3): + +```markdown +Issue #75 repeats it: migrations `` and `` `DROP COLUMN` `triage_sessions.resultRisk`/`resultDisplay` and `visits.riskLevelBefore`/`riskLevelAfter` with no backfill. On an old volume, old triages drop out of the patient's "Meus Dados" risk history and the ACS `visits.pull` fails with `StateError` on any pre-migration visit until `docker compose down && rm -rf pg_data/`. +``` + +Substituir ``/`` pelos nomes dos diretórios criados (`ls backend/sinalacs_server/migrations | tail -3`) antes de salvar. + +- [ ] **Step 5: `PROGRESS.md`** + +Acrescentar uma seção no padrão das demais (título, data, o que foi entregue, provas, o que fica aberto): + +```markdown +## Colunas de risco cifradas (#75) — 2026-10-16 + +- Entregue: `triage_sessions.resultRisk` e `visits.riskLevelBefore`/`riskLevelAfter` cifrados em AES-256-GCM na borda ORM, com codec de largura fixa (o tamanho do ciphertext não revela a classe); `triage_sessions.resultDisplay` removida (derivada). +- Decisão: `alerts.riskLevel` permanece legível (só `red` é gravado; chave dos filtros SQL da INV-03). Registrada no PRD (INV-04) e em `spec/lgpd_data_audit.md` §2.3; amarrada pelo teste `INV-04 (#75)` de `test/unit/red_alert_discard_paths_test.dart`. +- Provas: `test/unit/encrypted_risk_level_test.dart`; testes `#75` em `test/integration/health_data_encryption_test.dart` (linha bruta por `row_to_json` sem risco legível, ida e volta, `After` nulo e conclusão, linha antiga falha alto) e em `test/integration/patient_data_overview_endpoint_test.dart`. +- Aberto: migrações sem backfill (volume antigo precisa ser recriado); rotação de chave continua inexistente (`keyVersion` só marcador), como nas demais colunas cifradas. +``` + +- [ ] **Step 6: Backlog da auditoria** + +Em `docs/auditoria-prd/2026-10-15-lacunas-prd.md`: +- linha **INV-04**: status `**entregue**`; na coluna de evidência acrescentar "riscos de triagem e visita cifrados (#75); `alerts.riskLevel` legível por decisão registrada no PRD"; coluna "falta": `—`; +- linha **RNF03**: coluna "falta" passa a `Ver CRIPTO-APP-CACHE.` (status segue **parcial**); +- linha **LGPD-RF09**: coluna "falta" passa a `Ver CRIPTO-APP-CACHE e RNF06.` (status segue **parcial**); +- linha do item **1** do backlog priorizado: acrescentar `**Resolvido (2026-10-16, #75).**` no início da descrição. + +- [ ] **Step 7: Commit** + +```bash +git add spec/PRD_system.md spec/lgpd_data_audit.md backend/CLAUDE.md CLAUDE.md PROGRESS.md docs/auditoria-prd/2026-10-15-lacunas-prd.md +git commit -m "docs: registra a cifra dos riscos e a exceção de alerts.riskLevel (#75)" +``` + +--- + +### Task 6: Verificação completa e fechamento + +**Files:** nenhum novo (só `graphify-out/` regenerado). + +- [ ] **Step 1: Suíte do backend inteira** + +Run: `cd backend/sinalacs_server && dart analyze && dart test -j 1` +Expected: `No issues found!` e todos os testes PASS (o total cresce em ~10 em relação a `develop`). + +- [ ] **Step 2: Cliente regenerado não quebra os apps** + +Run: +```bash +cd /home/rock/Documents/Dev/APPs/SinalACS +for app in acs patient admin; do (cd apps/$app && flutter analyze) || echo "FALHOU: $app"; done +``` +Expected: nenhum `FALHOU:` (os apps usam os DTOs de API, não `Visit`/`TriageSession`; se algum usar, adaptar ao par cifrado não é o caminho — o app deve usar o DTO de API). + +- [ ] **Step 3: Pilha local do zero** + +Run: +```bash +docker compose down && sudo rm -rf pg_data/ && docker compose up --build -d +docker compose logs serverpod | grep -i -E "migrat|error" | tail -20 +``` +Expected: as duas migrações novas aplicadas no boot, sem erro. Depois, uma rodada da validação ponta a ponta (skill `validacao-e2e`, `scripts/qa/e2e.sh`) para confirmar triagem → "Meus Dados" e sync/pull do ACS contra o backend real. + +Conferir no banco que nada ficou legível: +```bash +docker compose exec postgres psql -U sinalacs_user -d sinalacs_db -c \ + 'SELECT "resultRiskEncrypted" FROM triage_sessions LIMIT 3;' -c \ + 'SELECT "riskLevelBeforeEncrypted", "riskLevelAfterEncrypted" FROM visits LIMIT 3;' +``` +Expected: só base64, todos do mesmo comprimento. + +- [ ] **Step 4: Atualizar o grafo** + +Run: `cd /home/rock/Documents/Dev/APPs/SinalACS && graphify update .` + +- [ ] **Step 5: PR** + +```bash +git push -u origin fix/issue-75-colunas-de-risco +gh pr create --base develop --title "fix: cifra as colunas de risco e registra a exceção de alerts.riskLevel (#75)" --body "$(cat <<'EOF' +Fecha #75. + +- Cifra `triage_sessions.resultRisk` e `visits.riskLevelBefore`/`riskLevelAfter` (AES-256-GCM na borda ORM, codec de largura fixa para o tamanho não revelar a classe). +- Remove `triage_sessions.resultDisplay` (derivada do risco, ninguém lia). +- Mantém `alerts.riskLevel` legível por decisão registrada no PRD (INV-04): só `red` é gravado e a coluna sustenta os filtros SQL da INV-03; teste `INV-04 (#75)` amarra a condição. +- Migrações sem backfill: `pg_data/` antigo precisa ser recriado (ver `CLAUDE.md`). + +Testes: `dart test -j 1` verde; `flutter analyze` dos três apps limpo; pilha local recriada do zero. +EOF +)" +``` + +Os 8 checks obrigatórios da CI precisam passar antes do merge. From 59f083c123f52406347449b698022e090e6586a2 Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Fri, 16 Oct 2026 09:56:44 -0400 Subject: [PATCH 2/7] feat(backend): codec de risco cifrado com largura fixa (#75) --- .../crypto/encrypted_risk_level.dart | 33 ++++++++++++ .../test/unit/encrypted_risk_level_test.dart | 53 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 backend/sinalacs_server/lib/src/infrastructure/crypto/encrypted_risk_level.dart create mode 100644 backend/sinalacs_server/test/unit/encrypted_risk_level_test.dart diff --git a/backend/sinalacs_server/lib/src/infrastructure/crypto/encrypted_risk_level.dart b/backend/sinalacs_server/lib/src/infrastructure/crypto/encrypted_risk_level.dart new file mode 100644 index 0000000..7a0ca3d --- /dev/null +++ b/backend/sinalacs_server/lib/src/infrastructure/crypto/encrypted_risk_level.dart @@ -0,0 +1,33 @@ +import 'package:sinalacs_server/src/generated/protocol.dart'; +import 'package:sinalacs_server/src/infrastructure/crypto/encrypted_json.dart'; +import 'package:sinalacs_server/src/infrastructure/crypto/health_data_cipher.dart'; + +/// Cifra de uma classificação de risco em repouso (INV-04, issue #75). +/// +/// O AES-GCM não esconde o TAMANHO do texto claro: `"red"` cifrado sairia +/// mais curto que `"yellow"`, e quem lê a coluna bruta separaria os vermelhos +/// pelo comprimento do base64. Por isso o nome vai preenchido com espaços até +/// a largura do maior nome de [RiskLevel], e a ausência (`null`, o +/// `riskLevelAfter` de uma visita ainda não concluída) vai como só espaços, +/// do mesmo tamanho. +/// +/// Usada pelos stores ORM de triagem e de visita; vive em `infrastructure/` +/// pelo mesmo motivo de [EncryptedJson]. +extension EncryptedRiskLevel on HealthDataCipher { + static final int _width = + RiskLevel.values.map((r) => r.name.length).reduce((a, b) => a > b ? a : b); + + Future encryptRiskLevel(RiskLevel? risk) => + encryptJson((risk?.name ?? '').padRight(_width)); + + /// `null` para ciphertext vazio (linha nunca escrita pelo caminho Dart) e + /// para a ausência cifrada; quem chama decide se `null` é aceitável. Um + /// nome desconhecido lança `ArgumentError` — nunca vira um risco inventado. + Future decryptRiskLevel(String ciphertextBase64, int keyVersion) async { + final decoded = await decryptJson(ciphertextBase64, keyVersion); + if (decoded == null) return null; + final name = (decoded as String).trim(); + if (name.isEmpty) return null; + return RiskLevel.values.byName(name); + } +} diff --git a/backend/sinalacs_server/test/unit/encrypted_risk_level_test.dart b/backend/sinalacs_server/test/unit/encrypted_risk_level_test.dart new file mode 100644 index 0000000..d9bb597 --- /dev/null +++ b/backend/sinalacs_server/test/unit/encrypted_risk_level_test.dart @@ -0,0 +1,53 @@ +import 'package:sinalacs_server/src/config/app_config.dart'; +import 'package:sinalacs_server/src/generated/protocol.dart'; +import 'package:sinalacs_server/src/infrastructure/crypto/encrypted_json.dart'; +import 'package:sinalacs_server/src/infrastructure/crypto/encrypted_risk_level.dart'; +import 'package:sinalacs_server/src/infrastructure/crypto/health_data_cipher.dart'; +import 'package:test/test.dart'; + +/// Codec do risco cifrado (issue #75, INV-04). Hermético: só a cifra, sem banco. +void main() { + final cipher = HealthDataCipher( + keyHex: AppConfig.developmentHealthDataEncryptionKey, + keyVersion: 1, + ); + + test('cada nível e a ausência fazem ida e volta', () async { + for (final risk in [...RiskLevel.values, null]) { + final encrypted = await cipher.encryptRiskLevel(risk); + expect( + await cipher.decryptRiskLevel(encrypted.ciphertextBase64, encrypted.keyVersion), + risk, + reason: 'ida e volta de $risk', + ); + } + }); + + test('o tamanho do ciphertext não revela a classe: todos têm o mesmo ' + 'comprimento, inclusive a ausência', () async { + final lengths = { + for (final risk in [...RiskLevel.values, null]) + risk: (await cipher.encryptRiskLevel(risk)).ciphertextBase64.length, + }; + expect(lengths.values.toSet(), hasLength(1), reason: 'comprimentos: $lengths'); + }); + + test('o mesmo risco cifrado duas vezes dá ciphertexts diferentes (nonce ' + 'aleatório: igualdade de coluna não agrupa pacientes por risco)', () async { + final a = await cipher.encryptRiskLevel(RiskLevel.red); + final b = await cipher.encryptRiskLevel(RiskLevel.red); + expect(a.ciphertextBase64, isNot(b.ciphertextBase64)); + }); + + test('ciphertext vazio (linha nunca escrita pelo caminho Dart) vira null', () async { + expect(await cipher.decryptRiskLevel('', 1), isNull); + }); + + test('nome desconhecido lança em vez de inventar um risco (INV-02)', () async { + final forjado = await cipher.encryptJson('purple'); + expect( + () => cipher.decryptRiskLevel(forjado.ciphertextBase64, forjado.keyVersion), + throwsArgumentError, + ); + }); +} From 699b072da13526f47620b9e72dced1f733a2ee3e Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Fri, 16 Oct 2026 09:59:46 -0400 Subject: [PATCH 3/7] fix(backend): cifra o risco da triagem e deixa de gravar resultDisplay (#75) --- .../lib/src/protocol/triage_session.dart | 52 +- .../triage/triage_session_service.dart | 9 - .../lib/src/generated/protocol.dart | 12 +- .../lib/src/generated/triage_session.dart | 94 +- .../orm_patient_data_overview_store.dart | 25 +- .../database/orm_triage_session_store.dart | 18 +- .../lib/src/models/triage_session.spy.yaml | 9 +- .../20261016135918697/definition.json | 4504 +++++++++++++++++ .../20261016135918697/definition.sql | 936 ++++ .../20261016135918697/definition_project.json | 3168 ++++++++++++ .../20261016135918697/migration.json | 86 + .../20261016135918697/migration.sql | 28 + .../migrations/migration_registry.txt | 1 + .../data_subject_case_store_test.dart | 7 +- .../health_data_encryption_test.dart | 56 + .../patient_data_overview_endpoint_test.dart | 33 +- .../integration/retention_expurge_test.dart | 4 - .../triage_session_persistence_test.dart | 5 +- .../test/support/health_data_fixtures.dart | 12 + .../unit/triage_session_service_test.dart | 1 - 20 files changed, 8948 insertions(+), 112 deletions(-) create mode 100644 backend/sinalacs_server/migrations/20261016135918697/definition.json create mode 100644 backend/sinalacs_server/migrations/20261016135918697/definition.sql create mode 100644 backend/sinalacs_server/migrations/20261016135918697/definition_project.json create mode 100644 backend/sinalacs_server/migrations/20261016135918697/migration.json create mode 100644 backend/sinalacs_server/migrations/20261016135918697/migration.sql diff --git a/backend/sinalacs_client/lib/src/protocol/triage_session.dart b/backend/sinalacs_client/lib/src/protocol/triage_session.dart index f32bded..a27410d 100644 --- a/backend/sinalacs_client/lib/src/protocol/triage_session.dart +++ b/backend/sinalacs_client/lib/src/protocol/triage_session.dart @@ -12,9 +12,8 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:serverpod_client/serverpod_client.dart' as _i1; -import 'enums/risk_level.dart' as _i2; -/// Sessão de triagem estruturada. resultRisk é produzido pelo TriageEngine +/// Sessão de triagem estruturada. O risco é produzido pelo TriageEngine /// de forma determinística e não é editável (INV-02). abstract class TriageSession implements _i1.SerializableModel { TriageSession._({ @@ -22,20 +21,22 @@ abstract class TriageSession implements _i1.SerializableModel { required this.patientId, String? answersEncrypted, int? answersKeyVersion, - required this.resultRisk, - required this.resultDisplay, + String? resultRiskEncrypted, + int? resultRiskKeyVersion, required this.createdAt, required this.deviceId, }) : answersEncrypted = answersEncrypted ?? '', - answersKeyVersion = answersKeyVersion ?? 1; + answersKeyVersion = answersKeyVersion ?? 1, + resultRiskEncrypted = resultRiskEncrypted ?? '', + resultRiskKeyVersion = resultRiskKeyVersion ?? 1; factory TriageSession({ _i1.UuidValue? id, required _i1.UuidValue patientId, String? answersEncrypted, int? answersKeyVersion, - required _i2.RiskLevel resultRisk, - required String resultDisplay, + String? resultRiskEncrypted, + int? resultRiskKeyVersion, required DateTime createdAt, required String deviceId, }) = _TriageSessionImpl; @@ -50,10 +51,8 @@ abstract class TriageSession implements _i1.SerializableModel { ), answersEncrypted: jsonSerialization['answersEncrypted'] as String?, answersKeyVersion: jsonSerialization['answersKeyVersion'] as int?, - resultRisk: _i2.RiskLevel.fromJson( - (jsonSerialization['resultRisk'] as String), - ), - resultDisplay: jsonSerialization['resultDisplay'] as String, + resultRiskEncrypted: jsonSerialization['resultRiskEncrypted'] as String?, + resultRiskKeyVersion: jsonSerialization['resultRiskKeyVersion'] as int?, createdAt: _i1.DateTimeJsonExtension.fromJson( jsonSerialization['createdAt'], ), @@ -73,9 +72,12 @@ abstract class TriageSession implements _i1.SerializableModel { int answersKeyVersion; - _i2.RiskLevel resultRisk; + /// Classificação do TriageEngine, cifrada (INV-04, #75): nome do RiskLevel + /// com largura fixa, ver `encrypted_risk_level.dart`. O rótulo de tela + /// (`resultDisplay`) deixou de ser persistido: era derivado do risco. + String resultRiskEncrypted; - String resultDisplay; + int resultRiskKeyVersion; DateTime createdAt; @@ -89,8 +91,8 @@ abstract class TriageSession implements _i1.SerializableModel { _i1.UuidValue? patientId, String? answersEncrypted, int? answersKeyVersion, - _i2.RiskLevel? resultRisk, - String? resultDisplay, + String? resultRiskEncrypted, + int? resultRiskKeyVersion, DateTime? createdAt, String? deviceId, }); @@ -102,8 +104,8 @@ abstract class TriageSession implements _i1.SerializableModel { 'patientId': patientId.toJson(), 'answersEncrypted': answersEncrypted, 'answersKeyVersion': answersKeyVersion, - 'resultRisk': resultRisk.toJson(), - 'resultDisplay': resultDisplay, + 'resultRiskEncrypted': resultRiskEncrypted, + 'resultRiskKeyVersion': resultRiskKeyVersion, 'createdAt': createdAt.toJson(), 'deviceId': deviceId, }; @@ -123,8 +125,8 @@ class _TriageSessionImpl extends TriageSession { required _i1.UuidValue patientId, String? answersEncrypted, int? answersKeyVersion, - required _i2.RiskLevel resultRisk, - required String resultDisplay, + String? resultRiskEncrypted, + int? resultRiskKeyVersion, required DateTime createdAt, required String deviceId, }) : super._( @@ -132,8 +134,8 @@ class _TriageSessionImpl extends TriageSession { patientId: patientId, answersEncrypted: answersEncrypted, answersKeyVersion: answersKeyVersion, - resultRisk: resultRisk, - resultDisplay: resultDisplay, + resultRiskEncrypted: resultRiskEncrypted, + resultRiskKeyVersion: resultRiskKeyVersion, createdAt: createdAt, deviceId: deviceId, ); @@ -147,8 +149,8 @@ class _TriageSessionImpl extends TriageSession { _i1.UuidValue? patientId, String? answersEncrypted, int? answersKeyVersion, - _i2.RiskLevel? resultRisk, - String? resultDisplay, + String? resultRiskEncrypted, + int? resultRiskKeyVersion, DateTime? createdAt, String? deviceId, }) { @@ -157,8 +159,8 @@ class _TriageSessionImpl extends TriageSession { patientId: patientId ?? this.patientId, answersEncrypted: answersEncrypted ?? this.answersEncrypted, answersKeyVersion: answersKeyVersion ?? this.answersKeyVersion, - resultRisk: resultRisk ?? this.resultRisk, - resultDisplay: resultDisplay ?? this.resultDisplay, + resultRiskEncrypted: resultRiskEncrypted ?? this.resultRiskEncrypted, + resultRiskKeyVersion: resultRiskKeyVersion ?? this.resultRiskKeyVersion, createdAt: createdAt ?? this.createdAt, deviceId: deviceId ?? this.deviceId, ); diff --git a/backend/sinalacs_server/lib/src/application/triage/triage_session_service.dart b/backend/sinalacs_server/lib/src/application/triage/triage_session_service.dart index 5fcc932..dbb76d3 100644 --- a/backend/sinalacs_server/lib/src/application/triage/triage_session_service.dart +++ b/backend/sinalacs_server/lib/src/application/triage/triage_session_service.dart @@ -34,7 +34,6 @@ class TriageSessionRecord { required this.patientId, required this.answers, required this.resultRisk, - required this.resultDisplay, required this.createdAt, required this.deviceId, }); @@ -42,7 +41,6 @@ class TriageSessionRecord { final UuidValue patientId; final List answers; final RiskLevel resultRisk; - final String resultDisplay; final DateTime createdAt; final String deviceId; } @@ -154,7 +152,6 @@ class TriageSessionService { ), ], resultRisk: risk, - resultDisplay: _displayFor(risk), createdAt: _clock(), deviceId: user.deviceId, )); @@ -179,10 +176,4 @@ class TriageSessionService { return risk; } - - static String _displayFor(RiskLevel risk) => switch (risk) { - RiskLevel.red => 'Vermelho', - RiskLevel.yellow => 'Amarelo', - RiskLevel.green => 'Verde', - }; } diff --git a/backend/sinalacs_server/lib/src/generated/protocol.dart b/backend/sinalacs_server/lib/src/generated/protocol.dart index 1bda673..e18cf9c 100644 --- a/backend/sinalacs_server/lib/src/generated/protocol.dart +++ b/backend/sinalacs_server/lib/src/generated/protocol.dart @@ -2419,16 +2419,18 @@ class Protocol extends _i1.SerializationManagerServer { columnDefault: '1', ), _i2.ColumnDefinition( - name: 'resultRisk', + name: 'resultRiskEncrypted', columnType: _i2.ColumnType.text, isNullable: false, - dartType: 'protocol:RiskLevel', + dartType: 'String', + columnDefault: '\'\'::text', ), _i2.ColumnDefinition( - name: 'resultDisplay', - columnType: _i2.ColumnType.text, + name: 'resultRiskKeyVersion', + columnType: _i2.ColumnType.bigint, isNullable: false, - dartType: 'String', + dartType: 'int', + columnDefault: '1', ), _i2.ColumnDefinition( name: 'createdAt', diff --git a/backend/sinalacs_server/lib/src/generated/triage_session.dart b/backend/sinalacs_server/lib/src/generated/triage_session.dart index a1a9b47..9c22a35 100644 --- a/backend/sinalacs_server/lib/src/generated/triage_session.dart +++ b/backend/sinalacs_server/lib/src/generated/triage_session.dart @@ -12,9 +12,8 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:serverpod/serverpod.dart' as _i1; -import 'enums/risk_level.dart' as _i2; -/// Sessão de triagem estruturada. resultRisk é produzido pelo TriageEngine +/// Sessão de triagem estruturada. O risco é produzido pelo TriageEngine /// de forma determinística e não é editável (INV-02). abstract class TriageSession implements _i1.TableRow<_i1.UuidValue?>, _i1.ProtocolSerialization { @@ -23,20 +22,22 @@ abstract class TriageSession required this.patientId, String? answersEncrypted, int? answersKeyVersion, - required this.resultRisk, - required this.resultDisplay, + String? resultRiskEncrypted, + int? resultRiskKeyVersion, required this.createdAt, required this.deviceId, }) : answersEncrypted = answersEncrypted ?? '', - answersKeyVersion = answersKeyVersion ?? 1; + answersKeyVersion = answersKeyVersion ?? 1, + resultRiskEncrypted = resultRiskEncrypted ?? '', + resultRiskKeyVersion = resultRiskKeyVersion ?? 1; factory TriageSession({ _i1.UuidValue? id, required _i1.UuidValue patientId, String? answersEncrypted, int? answersKeyVersion, - required _i2.RiskLevel resultRisk, - required String resultDisplay, + String? resultRiskEncrypted, + int? resultRiskKeyVersion, required DateTime createdAt, required String deviceId, }) = _TriageSessionImpl; @@ -51,10 +52,8 @@ abstract class TriageSession ), answersEncrypted: jsonSerialization['answersEncrypted'] as String?, answersKeyVersion: jsonSerialization['answersKeyVersion'] as int?, - resultRisk: _i2.RiskLevel.fromJson( - (jsonSerialization['resultRisk'] as String), - ), - resultDisplay: jsonSerialization['resultDisplay'] as String, + resultRiskEncrypted: jsonSerialization['resultRiskEncrypted'] as String?, + resultRiskKeyVersion: jsonSerialization['resultRiskKeyVersion'] as int?, createdAt: _i1.DateTimeJsonExtension.fromJson( jsonSerialization['createdAt'], ), @@ -76,9 +75,12 @@ abstract class TriageSession int answersKeyVersion; - _i2.RiskLevel resultRisk; + /// Classificação do TriageEngine, cifrada (INV-04, #75): nome do RiskLevel + /// com largura fixa, ver `encrypted_risk_level.dart`. O rótulo de tela + /// (`resultDisplay`) deixou de ser persistido: era derivado do risco. + String resultRiskEncrypted; - String resultDisplay; + int resultRiskKeyVersion; DateTime createdAt; @@ -95,8 +97,8 @@ abstract class TriageSession _i1.UuidValue? patientId, String? answersEncrypted, int? answersKeyVersion, - _i2.RiskLevel? resultRisk, - String? resultDisplay, + String? resultRiskEncrypted, + int? resultRiskKeyVersion, DateTime? createdAt, String? deviceId, }); @@ -108,8 +110,8 @@ abstract class TriageSession 'patientId': patientId.toJson(), 'answersEncrypted': answersEncrypted, 'answersKeyVersion': answersKeyVersion, - 'resultRisk': resultRisk.toJson(), - 'resultDisplay': resultDisplay, + 'resultRiskEncrypted': resultRiskEncrypted, + 'resultRiskKeyVersion': resultRiskKeyVersion, 'createdAt': createdAt.toJson(), 'deviceId': deviceId, }; @@ -123,8 +125,8 @@ abstract class TriageSession 'patientId': patientId.toJson(), 'answersEncrypted': answersEncrypted, 'answersKeyVersion': answersKeyVersion, - 'resultRisk': resultRisk.toJson(), - 'resultDisplay': resultDisplay, + 'resultRiskEncrypted': resultRiskEncrypted, + 'resultRiskKeyVersion': resultRiskKeyVersion, 'createdAt': createdAt.toJson(), 'deviceId': deviceId, }; @@ -168,8 +170,8 @@ class _TriageSessionImpl extends TriageSession { required _i1.UuidValue patientId, String? answersEncrypted, int? answersKeyVersion, - required _i2.RiskLevel resultRisk, - required String resultDisplay, + String? resultRiskEncrypted, + int? resultRiskKeyVersion, required DateTime createdAt, required String deviceId, }) : super._( @@ -177,8 +179,8 @@ class _TriageSessionImpl extends TriageSession { patientId: patientId, answersEncrypted: answersEncrypted, answersKeyVersion: answersKeyVersion, - resultRisk: resultRisk, - resultDisplay: resultDisplay, + resultRiskEncrypted: resultRiskEncrypted, + resultRiskKeyVersion: resultRiskKeyVersion, createdAt: createdAt, deviceId: deviceId, ); @@ -192,8 +194,8 @@ class _TriageSessionImpl extends TriageSession { _i1.UuidValue? patientId, String? answersEncrypted, int? answersKeyVersion, - _i2.RiskLevel? resultRisk, - String? resultDisplay, + String? resultRiskEncrypted, + int? resultRiskKeyVersion, DateTime? createdAt, String? deviceId, }) { @@ -202,8 +204,8 @@ class _TriageSessionImpl extends TriageSession { patientId: patientId ?? this.patientId, answersEncrypted: answersEncrypted ?? this.answersEncrypted, answersKeyVersion: answersKeyVersion ?? this.answersKeyVersion, - resultRisk: resultRisk ?? this.resultRisk, - resultDisplay: resultDisplay ?? this.resultDisplay, + resultRiskEncrypted: resultRiskEncrypted ?? this.resultRiskEncrypted, + resultRiskKeyVersion: resultRiskKeyVersion ?? this.resultRiskKeyVersion, createdAt: createdAt ?? this.createdAt, deviceId: deviceId ?? this.deviceId, ); @@ -231,19 +233,17 @@ class TriageSessionUpdateTable extends _i1.UpdateTable { value, ); - _i1.ColumnValue<_i2.RiskLevel, _i2.RiskLevel> resultRisk( - _i2.RiskLevel value, - ) => _i1.ColumnValue( - table.resultRisk, - value, - ); - - _i1.ColumnValue resultDisplay(String value) => + _i1.ColumnValue resultRiskEncrypted(String value) => _i1.ColumnValue( - table.resultDisplay, + table.resultRiskEncrypted, value, ); + _i1.ColumnValue resultRiskKeyVersion(int value) => _i1.ColumnValue( + table.resultRiskKeyVersion, + value, + ); + _i1.ColumnValue createdAt(DateTime value) => _i1.ColumnValue( table.createdAt, @@ -274,14 +274,15 @@ class TriageSessionTable extends _i1.Table<_i1.UuidValue?> { this, hasDefault: true, ); - resultRisk = _i1.ColumnEnum( - 'resultRisk', + resultRiskEncrypted = _i1.ColumnString( + 'resultRiskEncrypted', this, - _i1.EnumSerialization.byName, + hasDefault: true, ); - resultDisplay = _i1.ColumnString( - 'resultDisplay', + resultRiskKeyVersion = _i1.ColumnInt( + 'resultRiskKeyVersion', this, + hasDefault: true, ); createdAt = _i1.ColumnDateTime( 'createdAt', @@ -302,9 +303,12 @@ class TriageSessionTable extends _i1.Table<_i1.UuidValue?> { late final _i1.ColumnInt answersKeyVersion; - late final _i1.ColumnEnum<_i2.RiskLevel> resultRisk; + /// Classificação do TriageEngine, cifrada (INV-04, #75): nome do RiskLevel + /// com largura fixa, ver `encrypted_risk_level.dart`. O rótulo de tela + /// (`resultDisplay`) deixou de ser persistido: era derivado do risco. + late final _i1.ColumnString resultRiskEncrypted; - late final _i1.ColumnString resultDisplay; + late final _i1.ColumnInt resultRiskKeyVersion; late final _i1.ColumnDateTime createdAt; @@ -316,8 +320,8 @@ class TriageSessionTable extends _i1.Table<_i1.UuidValue?> { patientId, answersEncrypted, answersKeyVersion, - resultRisk, - resultDisplay, + resultRiskEncrypted, + resultRiskKeyVersion, createdAt, deviceId, ]; diff --git a/backend/sinalacs_server/lib/src/infrastructure/database/orm_patient_data_overview_store.dart b/backend/sinalacs_server/lib/src/infrastructure/database/orm_patient_data_overview_store.dart index c476bdb..7a60d4d 100644 --- a/backend/sinalacs_server/lib/src/infrastructure/database/orm_patient_data_overview_store.dart +++ b/backend/sinalacs_server/lib/src/infrastructure/database/orm_patient_data_overview_store.dart @@ -2,6 +2,7 @@ import 'package:serverpod/serverpod.dart'; import 'package:sinalacs_server/src/application/patients/patient_data_overview_service.dart'; import 'package:sinalacs_server/src/generated/protocol.dart'; import 'package:sinalacs_server/src/infrastructure/crypto/encrypted_json.dart'; +import 'package:sinalacs_server/src/infrastructure/crypto/encrypted_risk_level.dart'; import 'package:sinalacs_server/src/infrastructure/crypto/health_data_cipher.dart'; import 'package:sinalacs_server/src/infrastructure/database/orm_data_subject_rights_store.dart'; @@ -76,12 +77,26 @@ class OrmPatientDataOverviewStore implements PatientDataOverviewStore { for (final row in requestRows) await dataSubjectRequestSnapshotOf(row, _cipher), ]; - final riskHistory = [ - for (final row in triageRows) - RiskEventSnapshot(source: 'triage', riskLevel: row.resultRisk, recordedAt: row.createdAt), - for (final row in alertRows) + final riskHistory = []; + for (final row in triageRows) { + // Ciphertext vazio = triagem gravada antes da migração da #75: sem risco + // legível, a linha fica fora do histórico — nunca um risco inventado + // (INV-02), e o painel do titular continua abrindo. + final risk = await _cipher.decryptRiskLevel( + row.resultRiskEncrypted, + row.resultRiskKeyVersion, + ); + if (risk == null) continue; + riskHistory.add( + RiskEventSnapshot(source: 'triage', riskLevel: risk, recordedAt: row.createdAt), + ); + } + for (final row in alertRows) { + riskHistory.add( RiskEventSnapshot(source: 'alert', riskLevel: row.riskLevel, recordedAt: row.triggeredAt), - ]..sort((a, b) => b.recordedAt.compareTo(a.recordedAt)); + ); + } + riskHistory.sort((a, b) => b.recordedAt.compareTo(a.recordedAt)); return PatientDataSnapshot( // `patients.id` É `users.id` (mesma chave, ver `patient.spy.yaml`), então diff --git a/backend/sinalacs_server/lib/src/infrastructure/database/orm_triage_session_store.dart b/backend/sinalacs_server/lib/src/infrastructure/database/orm_triage_session_store.dart index 085a957..cf8f08f 100644 --- a/backend/sinalacs_server/lib/src/infrastructure/database/orm_triage_session_store.dart +++ b/backend/sinalacs_server/lib/src/infrastructure/database/orm_triage_session_store.dart @@ -2,6 +2,7 @@ import 'package:serverpod/serverpod.dart'; import 'package:sinalacs_server/src/application/triage/triage_session_service.dart'; import 'package:sinalacs_server/src/generated/protocol.dart'; import 'package:sinalacs_server/src/infrastructure/crypto/encrypted_json.dart'; +import 'package:sinalacs_server/src/infrastructure/crypto/encrypted_risk_level.dart'; import 'package:sinalacs_server/src/infrastructure/crypto/health_data_cipher.dart'; import 'package:sinalacs_server/src/infrastructure/database/removed_account.dart'; import 'package:sinalacs_server/src/infrastructure/database/subject_lock.dart'; @@ -12,9 +13,10 @@ import 'package:sinalacs_server/src/infrastructure/database/subject_lock.dart'; /// gravação falha se o paciente do token não existir no banco — é o que /// garante que uma triagem nunca fique órfã de prontuário. /// -/// É aqui que as respostas da triagem viram ciphertext: o serviço entrega um -/// [TriageSessionRecord] em claro e este store serializa para JSON, cifra e -/// monta a linha persistida (RNF03, INV-04). `application/` não constrói mais +/// É aqui que as respostas e a classificação de risco da triagem viram +/// ciphertext: o serviço entrega um [TriageSessionRecord] em claro e este +/// store serializa para JSON, cifra e monta a linha persistida (RNF03, +/// INV-04, #75). `application/` não constrói mais /// o `TriageSession` gerado justamente para não precisar conhecer o par /// `answersEncrypted`/`answersKeyVersion`. class OrmTriageSessionStore implements TriageSessionStore { @@ -42,10 +44,11 @@ class OrmTriageSessionStore implements TriageSessionStore { final encrypted = await _cipher.encryptJson( [for (final answer in session.answers) answer.toJson()], ); + final risk = await _cipher.encryptRiskLevel(session.resultRisk); final db = _session(); final external = _transaction; - if (external != null) return _insert(db, external, session, encrypted); - return db.db.transaction((transaction) => _insert(db, transaction, session, encrypted)); + if (external != null) return _insert(db, external, session, encrypted, risk); + return db.db.transaction((transaction) => _insert(db, transaction, session, encrypted, risk)); } Future _insert( @@ -53,6 +56,7 @@ class OrmTriageSessionStore implements TriageSessionStore { Transaction transaction, TriageSessionRecord session, EncryptedValue encrypted, + EncryptedValue risk, ) async { await lockPerSubject(db, transaction, namespace: lockNamespaceDeletion, key: session.patientId.uuid); @@ -65,8 +69,8 @@ class OrmTriageSessionStore implements TriageSessionStore { patientId: session.patientId, answersEncrypted: encrypted.ciphertextBase64, answersKeyVersion: encrypted.keyVersion, - resultRisk: session.resultRisk, - resultDisplay: session.resultDisplay, + resultRiskEncrypted: risk.ciphertextBase64, + resultRiskKeyVersion: risk.keyVersion, createdAt: session.createdAt, deviceId: session.deviceId, ), diff --git a/backend/sinalacs_server/lib/src/models/triage_session.spy.yaml b/backend/sinalacs_server/lib/src/models/triage_session.spy.yaml index 2bf471b..c493d75 100644 --- a/backend/sinalacs_server/lib/src/models/triage_session.spy.yaml +++ b/backend/sinalacs_server/lib/src/models/triage_session.spy.yaml @@ -1,4 +1,4 @@ -### Sessão de triagem estruturada. resultRisk é produzido pelo TriageEngine +### Sessão de triagem estruturada. O risco é produzido pelo TriageEngine ### de forma determinística e não é editável (INV-02). class: TriageSession table: triage_sessions @@ -8,7 +8,10 @@ fields: ### JSON de List, cifrado. Ver patient.spy.yaml para o padrão. answersEncrypted: String, default='' answersKeyVersion: int, default=1 - resultRisk: RiskLevel - resultDisplay: String + ### Classificação do TriageEngine, cifrada (INV-04, #75): nome do RiskLevel + ### com largura fixa, ver `encrypted_risk_level.dart`. O rótulo de tela + ### (`resultDisplay`) deixou de ser persistido: era derivado do risco. + resultRiskEncrypted: String, default='' + resultRiskKeyVersion: int, default=1 createdAt: DateTime deviceId: String diff --git a/backend/sinalacs_server/migrations/20261016135918697/definition.json b/backend/sinalacs_server/migrations/20261016135918697/definition.json new file mode 100644 index 0000000..aecd790 --- /dev/null +++ b/backend/sinalacs_server/migrations/20261016135918697/definition.json @@ -0,0 +1,4504 @@ +{ + "__className__": "serverpod.DatabaseDefinition", + "moduleName": "sinalacs", + "tables": [ + { + "__className__": "serverpod.TableDefinition", + "name": "acs", + "dartName": "Acs", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enrollmentId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "active", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastSyncAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_ubs_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ubsId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_enrollment_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "enrollmentId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "acs_refresh_tokens", + "dartName": "AcsRefreshToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "familyId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tokenHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "issuedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "idleExpiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "absoluteExpiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rotatedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revokedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "acs_refresh_tokens_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "tokenHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_family_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "familyId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "acs_upload_tokens", + "dartName": "AcsUploadToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tokenHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "issuedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revokedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "acs_upload_tokens_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_upload_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_upload_tokens_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "tokenHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_upload_tokens_user_device_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "deviceId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_deliveries", + "dartName": "AlertDeliveryRecord", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acknowledgedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_deliveries_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_deliveries_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_deliveries_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_deliveries_alert_acs_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "alertId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "acsId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_idempotency_keys", + "dartName": "AlertIdempotencyKey", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "key", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_idempotency_keys_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_idempotency_keys_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_idempotency_keys_key_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "key" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_outbox", + "dartName": "AlertOutboxEntry", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "topic", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "payload", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "nextAttemptAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastError", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_outbox_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_outbox_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_outbox_pending_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "publishedAt" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "nextAttemptAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alerts", + "dartName": "Alert", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triggeredAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "receivedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "respondedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acknowledgedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevel", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:RiskLevel" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationCell", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:AlertStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "mqttTopic", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "retryCount", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "escalatedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "escalatedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "escalationReason", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:EscalationReason?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alerts_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alerts_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alerts_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alerts_micro_area_status_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "triggeredAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "audit_chain_checkpoints", + "dartName": "AuditChainCheckpoint", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sequence", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "entryHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "signature", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_chain_checkpoints_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_chain_checkpoints_sequence_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "audit_exports", + "dartName": "AuditExport", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestedBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "filtersJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "format", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rowCount", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "firstSequence", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastSequence", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentSha256", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chainState", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "audit_exports_fk_0", + "columns": [ + "requestedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_exports_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_exports_requested_at_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "requestedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "audit_logs", + "dartName": "AuditLog", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actionType", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resourceType", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resourceId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ipHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "result", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sequence", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "previousHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "entryHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "audit_logs_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_sequence_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_timestamp_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "timestamp" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_user_sequence_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "consent_logs", + "dartName": "ConsentLog", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "purpose", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "action", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ipHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userAgent", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "signature", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "consent_logs_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "consent_logs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "data_subject_requests", + "dartName": "DataSubjectRequest", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestType", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:DataSubjectRequestType" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "detailsEncrypted", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "detailsKeyVersion", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:DataSubjectRequestStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "dueAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "decidedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "decidedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resolutionEncrypted", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resolutionKeyVersion", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "data_subject_requests_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "data_subject_requests_fk_1", + "columns": [ + "decidedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "data_subject_requests_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "data_subject_requests_user_id_type_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "requestType" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "enrollment_tokens", + "dartName": "EnrollmentToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tokenHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdByAcsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "consumedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "enrollment_tokens_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "enrollment_tokens_fk_1", + "columns": [ + "createdByAcsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "enrollment_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "enrollment_tokens_token_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "tokenHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "micro_areas", + "dartName": "MicroArea", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "geoJsonBoundary", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "micro_areas_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "micro_areas_ubs_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ubsId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "otp_challenges", + "dartName": "OtpChallenge", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "codeHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "consumedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "otp_challenges_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "otp_challenges_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "otp_challenges_user_id_created_at_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "patients", + "dartName": "Patient", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "emergencyContactEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "emergencyContactKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isChronic", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chronicConditionsEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chronicConditionsKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastLocationHash", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastTriageAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "patients_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "push_tokens", + "dartName": "PushToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "token", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "platform", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "push_tokens_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "push_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "push_tokens_token_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "token" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "push_tokens_user_id_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "scheduled_notices", + "dartName": "ScheduledNotice", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "audience", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "message", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scheduledFor", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ScheduledNoticeStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "recipients", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "accepted", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "failureReason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sentAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "scheduled_notices_fk_0", + "columns": [ + "createdBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "scheduled_notices_fk_1", + "columns": [ + "microAreaId" + ], + "referenceTable": "micro_areas", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "scheduled_notices_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "scheduled_notices_due_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "scheduledFor" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "scheduled_notices_area_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "staff_accounts", + "dartName": "StaffAccount", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enrollmentId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "active", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeHash", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeExpiresAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeIssuedBy", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeIssuedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "staff_accounts_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "staff_accounts_enrollment_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "enrollmentId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "terms_versions", + "dartName": "TermsVersion", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "summary", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "effectiveFrom", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "legallyReviewedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "legallyReviewedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "terms_versions_fk_0", + "columns": [ + "createdBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "terms_versions_fk_1", + "columns": [ + "legallyReviewedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "terms_versions_fk_2", + "columns": [ + "publishedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "terms_versions_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "terms_versions_version_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "version" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "triage_sessions", + "dartName": "TriageSession", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "answersEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "answersKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resultRiskEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resultRiskKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "triage_sessions_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "triage_sessions_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "ubs", + "dartName": "Ubs", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "address", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "city", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "state", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contactPhone", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ubs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "user_credentials", + "dartName": "UserCredential", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "passwordHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "passwordSalt", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "memoryKb", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "iterations", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "parallelism", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "failedAttempts", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lockedUntil", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lockStreak", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpSecretEncrypted", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpKeyVersion", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpEnabledAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpLastStep", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "user_credentials_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_credentials_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_credentials_user_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "users", + "dartName": "User", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cpfHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "birthDate", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "role", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:UserRole" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_cpf_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "cpfHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_micro_area_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "visits", + "dartName": "Visit", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorship", + "columnType": 0, + "isNullable": false, + "columnDefault": "'acs'::text", + "dartType": "protocol:VisitAuthorship" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "originDeviceId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scheduledAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "startedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "completedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelBefore", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:RiskLevel" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelAfter", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:RiskLevel?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "notesEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "notesKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "syncStatus", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:SyncStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "arrivalMethod", + "columnType": 0, + "isNullable": false, + "columnDefault": "'manual'::text", + "dartType": "protocol:ArrivalMethod" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "localId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "syncAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "visits_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "visits_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "visits_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "visits_local_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "localId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_cloud_storage", + "dartName": "CloudStorageEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_cloud_storage_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "storageId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "path", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "addedTime", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiration", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "byteData", + "columnType": 5, + "isNullable": false, + "dartType": "dart:typed_data:ByteData" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "verified", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_path_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "storageId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "path" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_expiration", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "expiration" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_cloud_storage_direct_upload", + "dartName": "CloudStorageDirectUploadEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_cloud_storage_direct_upload_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "storageId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "path", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiration", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_direct_upload_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_direct_upload_storage_path", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "storageId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "path" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_future_call", + "dartName": "FutureCallEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_future_call_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "time", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serializedObject", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "identifier", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_future_call_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_future_call_time_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "time" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_future_call_serverId_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "serverId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_future_call_identifier_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "identifier" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_health_connection_info", + "dartName": "ServerHealthConnectionInfo", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_health_connection_info_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "active", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "closing", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "idle", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "granularity", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_health_connection_info_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_health_connection_info_timestamp_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "timestamp" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "serverId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "granularity" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_health_metric", + "dartName": "ServerHealthMetric", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_health_metric_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isHealthy", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "value", + "columnType": 3, + "isNullable": false, + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "granularity", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_health_metric_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_health_metric_timestamp_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "timestamp" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "serverId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "name" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "granularity" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_log", + "dartName": "LogEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_log_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sessionLogId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "messageId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reference", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "time", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logLevel", + "columnType": 6, + "isNullable": false, + "dartType": "protocol:LogLevel" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "message", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "order", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_log_fk_0", + "columns": [ + "sessionLogId" + ], + "referenceTable": "serverpod_session_log", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_log_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_log_sessionLogId_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sessionLogId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_message_log", + "dartName": "MessageLogEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_message_log_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sessionLogId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "messageId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "endpoint", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "messageName", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "duration", + "columnType": 3, + "isNullable": false, + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slow", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "order", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_message_log_fk_0", + "columns": [ + "sessionLogId" + ], + "referenceTable": "serverpod_session_log", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_message_log_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_method", + "dartName": "MethodInfo", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_method_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "endpoint", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "method", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_method_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_method_endpoint_method_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "endpoint" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "method" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_migrations", + "dartName": "DatabaseMigrationVersion", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_migrations_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "module", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_migrations_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_migrations_ids", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "module" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_query_log", + "dartName": "QueryLogEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_query_log_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sessionLogId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "messageId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "query", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "duration", + "columnType": 3, + "isNullable": false, + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "numRows", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slow", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "order", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_query_log_fk_0", + "columns": [ + "sessionLogId" + ], + "referenceTable": "serverpod_session_log", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_query_log_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_query_log_sessionLogId_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sessionLogId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_readwrite_test", + "dartName": "ReadWriteTestEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_readwrite_test_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "number", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_readwrite_test_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_runtime_settings", + "dartName": "RuntimeSettings", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_runtime_settings_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logSettings", + "columnType": 8, + "isNullable": false, + "dartType": "protocol:LogSettings" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logSettingsOverrides", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logServiceCalls", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logMalformedCalls", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_runtime_settings_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_session_log", + "dartName": "SessionLogEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_session_log_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "time", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "module", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "endpoint", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "method", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "duration", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "numQueries", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slow", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authenticatedUserId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isOpen", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "touched", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_serverid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "serverId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_time_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "time" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_touched_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "touched" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_isopen_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "isOpen" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + } + ], + "installedModules": [ + { + "__className__": "serverpod.DatabaseMigrationVersion", + "module": "sinalacs", + "version": "20261016135918697" + }, + { + "__className__": "serverpod.DatabaseMigrationVersion", + "module": "serverpod", + "version": "20260129180959368" + } + ], + "migrationApiVersion": 1 +} \ No newline at end of file diff --git a/backend/sinalacs_server/migrations/20261016135918697/definition.sql b/backend/sinalacs_server/migrations/20261016135918697/definition.sql new file mode 100644 index 0000000..e559d31 --- /dev/null +++ b/backend/sinalacs_server/migrations/20261016135918697/definition.sql @@ -0,0 +1,936 @@ +BEGIN; + +-- +-- Class Acs as table acs +-- +CREATE TABLE "acs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "enrollmentId" text NOT NULL, + "ubsId" uuid NOT NULL, + "active" boolean NOT NULL, + "lastSyncAt" timestamp without time zone +); + +-- Indexes +CREATE INDEX "acs_ubs_idx" ON "acs" USING btree ("ubsId"); +CREATE UNIQUE INDEX "acs_enrollment_id_key" ON "acs" USING btree ("enrollmentId"); + +-- +-- Class AcsRefreshToken as table acs_refresh_tokens +-- +CREATE TABLE "acs_refresh_tokens" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "familyId" uuid NOT NULL, + "tokenHash" text NOT NULL, + "deviceId" text NOT NULL, + "issuedAt" timestamp without time zone NOT NULL, + "idleExpiresAt" timestamp without time zone NOT NULL, + "absoluteExpiresAt" timestamp without time zone NOT NULL, + "rotatedAt" timestamp without time zone, + "revokedAt" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "acs_refresh_tokens_hash_key" ON "acs_refresh_tokens" USING btree ("tokenHash"); +CREATE INDEX "acs_refresh_tokens_family_idx" ON "acs_refresh_tokens" USING btree ("familyId"); +CREATE INDEX "acs_refresh_tokens_user_idx" ON "acs_refresh_tokens" USING btree ("userId"); + +-- +-- Class AcsUploadToken as table acs_upload_tokens +-- +CREATE TABLE "acs_upload_tokens" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "tokenHash" text NOT NULL, + "deviceId" text NOT NULL, + "issuedAt" timestamp without time zone NOT NULL, + "expiresAt" timestamp without time zone NOT NULL, + "revokedAt" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "acs_upload_tokens_hash_key" ON "acs_upload_tokens" USING btree ("tokenHash"); +CREATE INDEX "acs_upload_tokens_user_device_idx" ON "acs_upload_tokens" USING btree ("userId", "deviceId"); + +-- +-- Class AlertDeliveryRecord as table alert_deliveries +-- +CREATE TABLE "alert_deliveries" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "alertId" uuid NOT NULL, + "acsId" uuid NOT NULL, + "acknowledgedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "alert_deliveries_alert_acs_key" ON "alert_deliveries" USING btree ("alertId", "acsId"); + +-- +-- Class AlertIdempotencyKey as table alert_idempotency_keys +-- +CREATE TABLE "alert_idempotency_keys" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "key" text NOT NULL, + "alertId" uuid NOT NULL, + "locationHash" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "alert_idempotency_keys_key_key" ON "alert_idempotency_keys" USING btree ("key"); + +-- +-- Class AlertOutboxEntry as table alert_outbox +-- +CREATE TABLE "alert_outbox" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "alertId" uuid NOT NULL, + "topic" text NOT NULL, + "payload" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "publishedAt" timestamp without time zone, + "attempts" bigint NOT NULL, + "nextAttemptAt" timestamp without time zone NOT NULL, + "lastError" text +); + +-- Indexes +CREATE INDEX "alert_outbox_pending_idx" ON "alert_outbox" USING btree ("publishedAt", "nextAttemptAt"); + +-- +-- Class Alert as table alerts +-- +CREATE TABLE "alerts" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "patientId" uuid NOT NULL, + "acsId" uuid, + "microAreaId" uuid, + "triggeredAt" timestamp without time zone NOT NULL, + "receivedAt" timestamp without time zone, + "respondedAt" timestamp without time zone, + "acknowledgedAt" timestamp without time zone, + "riskLevel" text NOT NULL, + "locationHash" text NOT NULL, + "locationCell" text, + "status" text NOT NULL, + "mqttTopic" text NOT NULL, + "deviceId" text NOT NULL, + "retryCount" bigint NOT NULL, + "version" bigint NOT NULL, + "escalatedAt" timestamp without time zone, + "escalatedBy" uuid, + "escalationReason" text +); + +-- Indexes +CREATE INDEX "alerts_micro_area_status_idx" ON "alerts" USING btree ("microAreaId", "status", "triggeredAt"); + +-- +-- Class AuditChainCheckpoint as table audit_chain_checkpoints +-- +CREATE TABLE "audit_chain_checkpoints" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "sequence" bigint NOT NULL, + "entryHash" text NOT NULL, + "signature" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "audit_chain_checkpoints_sequence_idx" ON "audit_chain_checkpoints" USING btree ("sequence"); + +-- +-- Class AuditExport as table audit_exports +-- +CREATE TABLE "audit_exports" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "requestedBy" uuid NOT NULL, + "requestedAt" timestamp without time zone NOT NULL, + "filtersJson" text NOT NULL, + "format" text NOT NULL, + "rowCount" bigint NOT NULL, + "firstSequence" bigint, + "lastSequence" bigint, + "contentSha256" text NOT NULL, + "chainState" text NOT NULL +); + +-- Indexes +CREATE INDEX "audit_exports_requested_at_idx" ON "audit_exports" USING btree ("requestedAt"); + +-- +-- Class AuditLog as table audit_logs +-- +CREATE TABLE "audit_logs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "actionType" text NOT NULL, + "resourceType" text NOT NULL, + "resourceId" uuid, + "timestamp" timestamp without time zone NOT NULL, + "ipHash" text NOT NULL, + "result" text NOT NULL, + "sequence" bigint NOT NULL, + "previousHash" text NOT NULL, + "entryHash" text NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "audit_logs_sequence_idx" ON "audit_logs" USING btree ("sequence"); +CREATE INDEX "audit_logs_timestamp_idx" ON "audit_logs" USING btree ("timestamp"); +CREATE INDEX "audit_logs_user_sequence_idx" ON "audit_logs" USING btree ("userId", "sequence"); + +-- +-- Class ConsentLog as table consent_logs +-- +CREATE TABLE "consent_logs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "purpose" text NOT NULL, + "action" text NOT NULL, + "version" text NOT NULL, + "timestamp" timestamp without time zone NOT NULL, + "ipHash" text NOT NULL, + "userAgent" text NOT NULL, + "signature" text NOT NULL +); + +-- +-- Class DataSubjectRequest as table data_subject_requests +-- +CREATE TABLE "data_subject_requests" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "requestType" text NOT NULL, + "detailsEncrypted" text NOT NULL, + "detailsKeyVersion" bigint NOT NULL, + "status" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "dueAt" timestamp without time zone NOT NULL, + "decidedAt" timestamp without time zone, + "decidedBy" uuid, + "resolutionEncrypted" text, + "resolutionKeyVersion" bigint +); + +-- Indexes +CREATE INDEX "data_subject_requests_user_id_type_idx" ON "data_subject_requests" USING btree ("userId", "requestType"); + +-- +-- Class EnrollmentToken as table enrollment_tokens +-- +CREATE TABLE "enrollment_tokens" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "tokenHash" text NOT NULL, + "patientId" uuid NOT NULL, + "microAreaId" uuid NOT NULL, + "createdByAcsId" uuid NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "expiresAt" timestamp without time zone NOT NULL, + "consumedAt" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "enrollment_tokens_token_hash_key" ON "enrollment_tokens" USING btree ("tokenHash"); + +-- +-- Class MicroArea as table micro_areas +-- +CREATE TABLE "micro_areas" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "name" text NOT NULL, + "ubsId" uuid NOT NULL, + "geoJsonBoundary" text NOT NULL +); + +-- Indexes +CREATE INDEX "micro_areas_ubs_idx" ON "micro_areas" USING btree ("ubsId"); + +-- +-- Class OtpChallenge as table otp_challenges +-- +CREATE TABLE "otp_challenges" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "codeHash" text NOT NULL, + "attempts" bigint NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "expiresAt" timestamp without time zone NOT NULL, + "consumedAt" timestamp without time zone +); + +-- Indexes +CREATE INDEX "otp_challenges_user_id_created_at_idx" ON "otp_challenges" USING btree ("userId", "createdAt"); + +-- +-- Class Patient as table patients +-- +CREATE TABLE "patients" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "emergencyContactEncrypted" text NOT NULL DEFAULT ''::text, + "emergencyContactKeyVersion" bigint NOT NULL DEFAULT 1, + "isChronic" boolean NOT NULL, + "chronicConditionsEncrypted" text NOT NULL DEFAULT ''::text, + "chronicConditionsKeyVersion" bigint NOT NULL DEFAULT 1, + "lastLocationHash" text, + "lastTriageAt" timestamp without time zone +); + +-- +-- Class PushToken as table push_tokens +-- +CREATE TABLE "push_tokens" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "microAreaId" uuid, + "token" text NOT NULL, + "platform" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "updatedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "push_tokens_token_key" ON "push_tokens" USING btree ("token"); +CREATE INDEX "push_tokens_user_id_idx" ON "push_tokens" USING btree ("userId"); + +-- +-- Class ScheduledNotice as table scheduled_notices +-- +CREATE TABLE "scheduled_notices" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "createdBy" uuid NOT NULL, + "microAreaId" uuid NOT NULL, + "audience" text NOT NULL, + "title" text NOT NULL, + "message" text NOT NULL, + "scheduledFor" timestamp without time zone NOT NULL, + "status" text NOT NULL, + "recipients" bigint, + "accepted" bigint, + "failureReason" text, + "createdAt" timestamp without time zone NOT NULL, + "sentAt" timestamp without time zone +); + +-- Indexes +CREATE INDEX "scheduled_notices_due_idx" ON "scheduled_notices" USING btree ("status", "scheduledFor"); +CREATE INDEX "scheduled_notices_area_idx" ON "scheduled_notices" USING btree ("microAreaId", "createdAt"); + +-- +-- Class StaffAccount as table staff_accounts +-- +CREATE TABLE "staff_accounts" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "enrollmentId" text NOT NULL, + "active" boolean NOT NULL, + "ubsId" uuid, + "activationCodeHash" text, + "activationCodeExpiresAt" timestamp without time zone, + "activationCodeIssuedBy" text, + "activationCodeIssuedAt" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "staff_accounts_enrollment_id_key" ON "staff_accounts" USING btree ("enrollmentId"); + +-- +-- Class TermsVersion as table terms_versions +-- +CREATE TABLE "terms_versions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "version" text NOT NULL, + "summary" text NOT NULL, + "effectiveFrom" timestamp without time zone NOT NULL, + "createdBy" uuid NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "legallyReviewedAt" timestamp without time zone, + "legallyReviewedBy" uuid, + "publishedAt" timestamp without time zone, + "publishedBy" uuid +); + +-- Indexes +CREATE UNIQUE INDEX "terms_versions_version_key" ON "terms_versions" USING btree ("version"); + +-- +-- Class TriageSession as table triage_sessions +-- +CREATE TABLE "triage_sessions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "patientId" uuid NOT NULL, + "answersEncrypted" text NOT NULL DEFAULT ''::text, + "answersKeyVersion" bigint NOT NULL DEFAULT 1, + "resultRiskEncrypted" text NOT NULL DEFAULT ''::text, + "resultRiskKeyVersion" bigint NOT NULL DEFAULT 1, + "createdAt" timestamp without time zone NOT NULL, + "deviceId" text NOT NULL +); + +-- +-- Class Ubs as table ubs +-- +CREATE TABLE "ubs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "name" text NOT NULL, + "address" text NOT NULL, + "city" text NOT NULL, + "state" text NOT NULL, + "contactPhone" text +); + +-- +-- Class UserCredential as table user_credentials +-- +CREATE TABLE "user_credentials" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "passwordHash" text NOT NULL, + "passwordSalt" text NOT NULL, + "memoryKb" bigint NOT NULL, + "iterations" bigint NOT NULL, + "parallelism" bigint NOT NULL, + "failedAttempts" bigint NOT NULL, + "lockedUntil" timestamp without time zone, + "lockStreak" bigint NOT NULL DEFAULT 0, + "createdAt" timestamp without time zone NOT NULL, + "updatedAt" timestamp without time zone NOT NULL, + "totpSecretEncrypted" text, + "totpKeyVersion" bigint, + "totpEnabledAt" timestamp without time zone, + "totpLastStep" bigint +); + +-- Indexes +CREATE UNIQUE INDEX "user_credentials_user_id_key" ON "user_credentials" USING btree ("userId"); + +-- +-- Class User as table users +-- +CREATE TABLE "users" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "cpfHash" text NOT NULL, + "name" text NOT NULL, + "birthDate" timestamp without time zone NOT NULL, + "role" text NOT NULL, + "microAreaId" uuid, + "createdAt" timestamp without time zone NOT NULL, + "updatedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "users_cpf_hash_key" ON "users" USING btree ("cpfHash"); +CREATE INDEX "users_micro_area_idx" ON "users" USING btree ("microAreaId"); + +-- +-- Class Visit as table visits +-- +CREATE TABLE "visits" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "patientId" uuid NOT NULL, + "acsId" uuid, + "authorship" text NOT NULL DEFAULT 'acs'::text, + "originDeviceId" text, + "scheduledAt" timestamp without time zone NOT NULL, + "startedAt" timestamp without time zone, + "completedAt" timestamp without time zone, + "status" text NOT NULL, + "riskLevelBefore" text NOT NULL, + "riskLevelAfter" text, + "notesEncrypted" text NOT NULL DEFAULT ''::text, + "notesKeyVersion" bigint NOT NULL DEFAULT 1, + "syncStatus" text NOT NULL, + "arrivalMethod" text NOT NULL DEFAULT 'manual'::text, + "localId" uuid NOT NULL, + "syncAt" timestamp without time zone, + "version" bigint NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "visits_local_id_key" ON "visits" USING btree ("localId"); + +-- +-- Class CloudStorageEntry as table serverpod_cloud_storage +-- +CREATE TABLE "serverpod_cloud_storage" ( + "id" bigserial PRIMARY KEY, + "storageId" text NOT NULL, + "path" text NOT NULL, + "addedTime" timestamp without time zone NOT NULL, + "expiration" timestamp without time zone, + "byteData" bytea NOT NULL, + "verified" boolean NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_cloud_storage_path_idx" ON "serverpod_cloud_storage" USING btree ("storageId", "path"); +CREATE INDEX "serverpod_cloud_storage_expiration" ON "serverpod_cloud_storage" USING btree ("expiration"); + +-- +-- Class CloudStorageDirectUploadEntry as table serverpod_cloud_storage_direct_upload +-- +CREATE TABLE "serverpod_cloud_storage_direct_upload" ( + "id" bigserial PRIMARY KEY, + "storageId" text NOT NULL, + "path" text NOT NULL, + "expiration" timestamp without time zone NOT NULL, + "authKey" text NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_cloud_storage_direct_upload_storage_path" ON "serverpod_cloud_storage_direct_upload" USING btree ("storageId", "path"); + +-- +-- Class FutureCallEntry as table serverpod_future_call +-- +CREATE TABLE "serverpod_future_call" ( + "id" bigserial PRIMARY KEY, + "name" text NOT NULL, + "time" timestamp without time zone NOT NULL, + "serializedObject" text, + "serverId" text NOT NULL, + "identifier" text +); + +-- Indexes +CREATE INDEX "serverpod_future_call_time_idx" ON "serverpod_future_call" USING btree ("time"); +CREATE INDEX "serverpod_future_call_serverId_idx" ON "serverpod_future_call" USING btree ("serverId"); +CREATE INDEX "serverpod_future_call_identifier_idx" ON "serverpod_future_call" USING btree ("identifier"); + +-- +-- Class ServerHealthConnectionInfo as table serverpod_health_connection_info +-- +CREATE TABLE "serverpod_health_connection_info" ( + "id" bigserial PRIMARY KEY, + "serverId" text NOT NULL, + "timestamp" timestamp without time zone NOT NULL, + "active" bigint NOT NULL, + "closing" bigint NOT NULL, + "idle" bigint NOT NULL, + "granularity" bigint NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_health_connection_info_timestamp_idx" ON "serverpod_health_connection_info" USING btree ("timestamp", "serverId", "granularity"); + +-- +-- Class ServerHealthMetric as table serverpod_health_metric +-- +CREATE TABLE "serverpod_health_metric" ( + "id" bigserial PRIMARY KEY, + "name" text NOT NULL, + "serverId" text NOT NULL, + "timestamp" timestamp without time zone NOT NULL, + "isHealthy" boolean NOT NULL, + "value" double precision NOT NULL, + "granularity" bigint NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_health_metric_timestamp_idx" ON "serverpod_health_metric" USING btree ("timestamp", "serverId", "name", "granularity"); + +-- +-- Class LogEntry as table serverpod_log +-- +CREATE TABLE "serverpod_log" ( + "id" bigserial PRIMARY KEY, + "sessionLogId" bigint NOT NULL, + "messageId" bigint, + "reference" text, + "serverId" text NOT NULL, + "time" timestamp without time zone NOT NULL, + "logLevel" bigint NOT NULL, + "message" text NOT NULL, + "error" text, + "stackTrace" text, + "order" bigint NOT NULL +); + +-- Indexes +CREATE INDEX "serverpod_log_sessionLogId_idx" ON "serverpod_log" USING btree ("sessionLogId"); + +-- +-- Class MessageLogEntry as table serverpod_message_log +-- +CREATE TABLE "serverpod_message_log" ( + "id" bigserial PRIMARY KEY, + "sessionLogId" bigint NOT NULL, + "serverId" text NOT NULL, + "messageId" bigint NOT NULL, + "endpoint" text NOT NULL, + "messageName" text NOT NULL, + "duration" double precision NOT NULL, + "error" text, + "stackTrace" text, + "slow" boolean NOT NULL, + "order" bigint NOT NULL +); + +-- +-- Class MethodInfo as table serverpod_method +-- +CREATE TABLE "serverpod_method" ( + "id" bigserial PRIMARY KEY, + "endpoint" text NOT NULL, + "method" text NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_method_endpoint_method_idx" ON "serverpod_method" USING btree ("endpoint", "method"); + +-- +-- Class DatabaseMigrationVersion as table serverpod_migrations +-- +CREATE TABLE "serverpod_migrations" ( + "id" bigserial PRIMARY KEY, + "module" text NOT NULL, + "version" text NOT NULL, + "timestamp" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_migrations_ids" ON "serverpod_migrations" USING btree ("module"); + +-- +-- Class QueryLogEntry as table serverpod_query_log +-- +CREATE TABLE "serverpod_query_log" ( + "id" bigserial PRIMARY KEY, + "serverId" text NOT NULL, + "sessionLogId" bigint NOT NULL, + "messageId" bigint, + "query" text NOT NULL, + "duration" double precision NOT NULL, + "numRows" bigint, + "error" text, + "stackTrace" text, + "slow" boolean NOT NULL, + "order" bigint NOT NULL +); + +-- Indexes +CREATE INDEX "serverpod_query_log_sessionLogId_idx" ON "serverpod_query_log" USING btree ("sessionLogId"); + +-- +-- Class ReadWriteTestEntry as table serverpod_readwrite_test +-- +CREATE TABLE "serverpod_readwrite_test" ( + "id" bigserial PRIMARY KEY, + "number" bigint NOT NULL +); + +-- +-- Class RuntimeSettings as table serverpod_runtime_settings +-- +CREATE TABLE "serverpod_runtime_settings" ( + "id" bigserial PRIMARY KEY, + "logSettings" json NOT NULL, + "logSettingsOverrides" json NOT NULL, + "logServiceCalls" boolean NOT NULL, + "logMalformedCalls" boolean NOT NULL +); + +-- +-- Class SessionLogEntry as table serverpod_session_log +-- +CREATE TABLE "serverpod_session_log" ( + "id" bigserial PRIMARY KEY, + "serverId" text NOT NULL, + "time" timestamp without time zone NOT NULL, + "module" text, + "endpoint" text, + "method" text, + "duration" double precision, + "numQueries" bigint, + "slow" boolean, + "error" text, + "stackTrace" text, + "authenticatedUserId" bigint, + "userId" text, + "isOpen" boolean, + "touched" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "serverpod_session_log_serverid_idx" ON "serverpod_session_log" USING btree ("serverId"); +CREATE INDEX "serverpod_session_log_time_idx" ON "serverpod_session_log" USING btree ("time"); +CREATE INDEX "serverpod_session_log_touched_idx" ON "serverpod_session_log" USING btree ("touched"); +CREATE INDEX "serverpod_session_log_isopen_idx" ON "serverpod_session_log" USING btree ("isOpen"); + +-- +-- Foreign relations for "acs_refresh_tokens" table +-- +ALTER TABLE ONLY "acs_refresh_tokens" + ADD CONSTRAINT "acs_refresh_tokens_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "acs_upload_tokens" table +-- +ALTER TABLE ONLY "acs_upload_tokens" + ADD CONSTRAINT "acs_upload_tokens_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "alert_deliveries" table +-- +ALTER TABLE ONLY "alert_deliveries" + ADD CONSTRAINT "alert_deliveries_fk_0" + FOREIGN KEY("alertId") + REFERENCES "alerts"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "alert_deliveries" + ADD CONSTRAINT "alert_deliveries_fk_1" + FOREIGN KEY("acsId") + REFERENCES "acs"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "alert_idempotency_keys" table +-- +ALTER TABLE ONLY "alert_idempotency_keys" + ADD CONSTRAINT "alert_idempotency_keys_fk_0" + FOREIGN KEY("alertId") + REFERENCES "alerts"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "alert_outbox" table +-- +ALTER TABLE ONLY "alert_outbox" + ADD CONSTRAINT "alert_outbox_fk_0" + FOREIGN KEY("alertId") + REFERENCES "alerts"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "alerts" table +-- +ALTER TABLE ONLY "alerts" + ADD CONSTRAINT "alerts_fk_0" + FOREIGN KEY("patientId") + REFERENCES "patients"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "alerts" + ADD CONSTRAINT "alerts_fk_1" + FOREIGN KEY("acsId") + REFERENCES "acs"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "audit_exports" table +-- +ALTER TABLE ONLY "audit_exports" + ADD CONSTRAINT "audit_exports_fk_0" + FOREIGN KEY("requestedBy") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "audit_logs" table +-- +ALTER TABLE ONLY "audit_logs" + ADD CONSTRAINT "audit_logs_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "consent_logs" table +-- +ALTER TABLE ONLY "consent_logs" + ADD CONSTRAINT "consent_logs_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "data_subject_requests" table +-- +ALTER TABLE ONLY "data_subject_requests" + ADD CONSTRAINT "data_subject_requests_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "data_subject_requests" + ADD CONSTRAINT "data_subject_requests_fk_1" + FOREIGN KEY("decidedBy") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "enrollment_tokens" table +-- +ALTER TABLE ONLY "enrollment_tokens" + ADD CONSTRAINT "enrollment_tokens_fk_0" + FOREIGN KEY("patientId") + REFERENCES "patients"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "enrollment_tokens" + ADD CONSTRAINT "enrollment_tokens_fk_1" + FOREIGN KEY("createdByAcsId") + REFERENCES "acs"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "otp_challenges" table +-- +ALTER TABLE ONLY "otp_challenges" + ADD CONSTRAINT "otp_challenges_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "push_tokens" table +-- +ALTER TABLE ONLY "push_tokens" + ADD CONSTRAINT "push_tokens_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "scheduled_notices" table +-- +ALTER TABLE ONLY "scheduled_notices" + ADD CONSTRAINT "scheduled_notices_fk_0" + FOREIGN KEY("createdBy") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "scheduled_notices" + ADD CONSTRAINT "scheduled_notices_fk_1" + FOREIGN KEY("microAreaId") + REFERENCES "micro_areas"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "terms_versions" table +-- +ALTER TABLE ONLY "terms_versions" + ADD CONSTRAINT "terms_versions_fk_0" + FOREIGN KEY("createdBy") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "terms_versions" + ADD CONSTRAINT "terms_versions_fk_1" + FOREIGN KEY("legallyReviewedBy") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "terms_versions" + ADD CONSTRAINT "terms_versions_fk_2" + FOREIGN KEY("publishedBy") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "triage_sessions" table +-- +ALTER TABLE ONLY "triage_sessions" + ADD CONSTRAINT "triage_sessions_fk_0" + FOREIGN KEY("patientId") + REFERENCES "patients"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "user_credentials" table +-- +ALTER TABLE ONLY "user_credentials" + ADD CONSTRAINT "user_credentials_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "visits" table +-- +ALTER TABLE ONLY "visits" + ADD CONSTRAINT "visits_fk_0" + FOREIGN KEY("patientId") + REFERENCES "patients"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "visits" + ADD CONSTRAINT "visits_fk_1" + FOREIGN KEY("acsId") + REFERENCES "acs"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_log" table +-- +ALTER TABLE ONLY "serverpod_log" + ADD CONSTRAINT "serverpod_log_fk_0" + FOREIGN KEY("sessionLogId") + REFERENCES "serverpod_session_log"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_message_log" table +-- +ALTER TABLE ONLY "serverpod_message_log" + ADD CONSTRAINT "serverpod_message_log_fk_0" + FOREIGN KEY("sessionLogId") + REFERENCES "serverpod_session_log"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_query_log" table +-- +ALTER TABLE ONLY "serverpod_query_log" + ADD CONSTRAINT "serverpod_query_log_fk_0" + FOREIGN KEY("sessionLogId") + REFERENCES "serverpod_session_log"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + + +-- +-- MIGRATION VERSION FOR sinalacs +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('sinalacs', '20261016135918697', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20261016135918697', "timestamp" = now(); + +-- +-- MIGRATION VERSION FOR serverpod +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('serverpod', '20260129180959368', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20260129180959368', "timestamp" = now(); + + +COMMIT; diff --git a/backend/sinalacs_server/migrations/20261016135918697/definition_project.json b/backend/sinalacs_server/migrations/20261016135918697/definition_project.json new file mode 100644 index 0000000..c519974 --- /dev/null +++ b/backend/sinalacs_server/migrations/20261016135918697/definition_project.json @@ -0,0 +1,3168 @@ +{ + "__className__": "serverpod.DatabaseDefinition", + "moduleName": "sinalacs", + "tables": [ + { + "__className__": "serverpod.TableDefinition", + "name": "acs", + "dartName": "Acs", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enrollmentId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "active", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastSyncAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_ubs_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ubsId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_enrollment_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "enrollmentId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "acs_refresh_tokens", + "dartName": "AcsRefreshToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "familyId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tokenHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "issuedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "idleExpiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "absoluteExpiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rotatedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revokedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "acs_refresh_tokens_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "tokenHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_family_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "familyId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "acs_upload_tokens", + "dartName": "AcsUploadToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tokenHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "issuedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revokedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "acs_upload_tokens_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_upload_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_upload_tokens_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "tokenHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_upload_tokens_user_device_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "deviceId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_deliveries", + "dartName": "AlertDeliveryRecord", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acknowledgedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_deliveries_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_deliveries_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_deliveries_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_deliveries_alert_acs_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "alertId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "acsId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_idempotency_keys", + "dartName": "AlertIdempotencyKey", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "key", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_idempotency_keys_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_idempotency_keys_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_idempotency_keys_key_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "key" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_outbox", + "dartName": "AlertOutboxEntry", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "topic", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "payload", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "nextAttemptAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastError", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_outbox_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_outbox_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_outbox_pending_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "publishedAt" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "nextAttemptAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alerts", + "dartName": "Alert", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triggeredAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "receivedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "respondedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acknowledgedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevel", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:RiskLevel" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationCell", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:AlertStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "mqttTopic", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "retryCount", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "escalatedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "escalatedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "escalationReason", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:EscalationReason?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alerts_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alerts_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alerts_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alerts_micro_area_status_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "triggeredAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "audit_chain_checkpoints", + "dartName": "AuditChainCheckpoint", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sequence", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "entryHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "signature", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_chain_checkpoints_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_chain_checkpoints_sequence_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "audit_exports", + "dartName": "AuditExport", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestedBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "filtersJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "format", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rowCount", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "firstSequence", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastSequence", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentSha256", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chainState", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "audit_exports_fk_0", + "columns": [ + "requestedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_exports_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_exports_requested_at_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "requestedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "audit_logs", + "dartName": "AuditLog", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actionType", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resourceType", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resourceId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ipHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "result", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sequence", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "previousHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "entryHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "audit_logs_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_sequence_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_timestamp_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "timestamp" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_user_sequence_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "consent_logs", + "dartName": "ConsentLog", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "purpose", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "action", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ipHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userAgent", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "signature", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "consent_logs_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "consent_logs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "data_subject_requests", + "dartName": "DataSubjectRequest", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestType", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:DataSubjectRequestType" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "detailsEncrypted", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "detailsKeyVersion", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:DataSubjectRequestStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "dueAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "decidedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "decidedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resolutionEncrypted", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resolutionKeyVersion", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "data_subject_requests_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "data_subject_requests_fk_1", + "columns": [ + "decidedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "data_subject_requests_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "data_subject_requests_user_id_type_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "requestType" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "enrollment_tokens", + "dartName": "EnrollmentToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tokenHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdByAcsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "consumedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "enrollment_tokens_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "enrollment_tokens_fk_1", + "columns": [ + "createdByAcsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "enrollment_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "enrollment_tokens_token_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "tokenHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "micro_areas", + "dartName": "MicroArea", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "geoJsonBoundary", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "micro_areas_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "micro_areas_ubs_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ubsId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "otp_challenges", + "dartName": "OtpChallenge", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "codeHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "consumedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "otp_challenges_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "otp_challenges_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "otp_challenges_user_id_created_at_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "patients", + "dartName": "Patient", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "emergencyContactEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "emergencyContactKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isChronic", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chronicConditionsEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chronicConditionsKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastLocationHash", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastTriageAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "patients_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "push_tokens", + "dartName": "PushToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "token", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "platform", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "push_tokens_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "push_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "push_tokens_token_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "token" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "push_tokens_user_id_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "scheduled_notices", + "dartName": "ScheduledNotice", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "audience", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "message", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scheduledFor", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ScheduledNoticeStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "recipients", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "accepted", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "failureReason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sentAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "scheduled_notices_fk_0", + "columns": [ + "createdBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "scheduled_notices_fk_1", + "columns": [ + "microAreaId" + ], + "referenceTable": "micro_areas", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "scheduled_notices_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "scheduled_notices_due_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "scheduledFor" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "scheduled_notices_area_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "staff_accounts", + "dartName": "StaffAccount", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enrollmentId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "active", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeHash", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeExpiresAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeIssuedBy", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeIssuedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "staff_accounts_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "staff_accounts_enrollment_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "enrollmentId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "terms_versions", + "dartName": "TermsVersion", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "summary", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "effectiveFrom", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "legallyReviewedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "legallyReviewedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "terms_versions_fk_0", + "columns": [ + "createdBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "terms_versions_fk_1", + "columns": [ + "legallyReviewedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "terms_versions_fk_2", + "columns": [ + "publishedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "terms_versions_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "terms_versions_version_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "version" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "triage_sessions", + "dartName": "TriageSession", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "answersEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "answersKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resultRiskEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resultRiskKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "triage_sessions_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "triage_sessions_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "ubs", + "dartName": "Ubs", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "address", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "city", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "state", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contactPhone", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ubs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "user_credentials", + "dartName": "UserCredential", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "passwordHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "passwordSalt", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "memoryKb", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "iterations", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "parallelism", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "failedAttempts", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lockedUntil", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lockStreak", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpSecretEncrypted", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpKeyVersion", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpEnabledAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpLastStep", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "user_credentials_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_credentials_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_credentials_user_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "users", + "dartName": "User", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cpfHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "birthDate", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "role", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:UserRole" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_cpf_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "cpfHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_micro_area_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "visits", + "dartName": "Visit", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorship", + "columnType": 0, + "isNullable": false, + "columnDefault": "'acs'::text", + "dartType": "protocol:VisitAuthorship" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "originDeviceId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scheduledAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "startedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "completedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelBefore", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:RiskLevel" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelAfter", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:RiskLevel?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "notesEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "notesKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "syncStatus", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:SyncStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "arrivalMethod", + "columnType": 0, + "isNullable": false, + "columnDefault": "'manual'::text", + "dartType": "protocol:ArrivalMethod" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "localId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "syncAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "visits_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "visits_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "visits_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "visits_local_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "localId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + } + ], + "installedModules": [ + { + "__className__": "serverpod.DatabaseMigrationVersion", + "module": "serverpod", + "version": "20260129180959368" + } + ], + "migrationApiVersion": 1 +} \ No newline at end of file diff --git a/backend/sinalacs_server/migrations/20261016135918697/migration.json b/backend/sinalacs_server/migrations/20261016135918697/migration.json new file mode 100644 index 0000000..3cd4412 --- /dev/null +++ b/backend/sinalacs_server/migrations/20261016135918697/migration.json @@ -0,0 +1,86 @@ +{ + "__className__": "serverpod.DatabaseMigration", + "actions": [ + { + "__className__": "serverpod.DatabaseMigrationAction", + "type": "alterTable", + "alterTable": { + "__className__": "serverpod.TableMigration", + "name": "triage_sessions", + "schema": "public", + "addColumns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "resultRiskEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resultRiskKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + } + ], + "deleteColumns": [ + "resultRisk", + "resultDisplay" + ], + "modifyColumns": [], + "addIndexes": [], + "deleteIndexes": [], + "addForeignKeys": [], + "deleteForeignKeys": [], + "warnings": [ + { + "__className__": "serverpod.DatabaseMigrationWarning", + "type": "columnDropped", + "message": "Column \"resultRisk\" of table \"triage_sessions\" will be dropped.", + "table": "triage_sessions", + "columns": [ + "resultRisk" + ], + "destrucive": true + }, + { + "__className__": "serverpod.DatabaseMigrationWarning", + "type": "columnDropped", + "message": "Column \"resultDisplay\" of table \"triage_sessions\" will be dropped.", + "table": "triage_sessions", + "columns": [ + "resultDisplay" + ], + "destrucive": true + } + ] + } + } + ], + "warnings": [ + { + "__className__": "serverpod.DatabaseMigrationWarning", + "type": "columnDropped", + "message": "Column \"resultRisk\" of table \"triage_sessions\" will be dropped.", + "table": "triage_sessions", + "columns": [ + "resultRisk" + ], + "destrucive": true + }, + { + "__className__": "serverpod.DatabaseMigrationWarning", + "type": "columnDropped", + "message": "Column \"resultDisplay\" of table \"triage_sessions\" will be dropped.", + "table": "triage_sessions", + "columns": [ + "resultDisplay" + ], + "destrucive": true + } + ], + "migrationApiVersion": 1 +} \ No newline at end of file diff --git a/backend/sinalacs_server/migrations/20261016135918697/migration.sql b/backend/sinalacs_server/migrations/20261016135918697/migration.sql new file mode 100644 index 0000000..59d8b9b --- /dev/null +++ b/backend/sinalacs_server/migrations/20261016135918697/migration.sql @@ -0,0 +1,28 @@ +BEGIN; + +-- +-- ACTION ALTER TABLE +-- +ALTER TABLE "triage_sessions" DROP COLUMN "resultRisk"; +ALTER TABLE "triage_sessions" DROP COLUMN "resultDisplay"; +ALTER TABLE "triage_sessions" ADD COLUMN "resultRiskEncrypted" text NOT NULL DEFAULT ''::text; +ALTER TABLE "triage_sessions" ADD COLUMN "resultRiskKeyVersion" bigint NOT NULL DEFAULT 1; + +-- +-- MIGRATION VERSION FOR sinalacs +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('sinalacs', '20261016135918697', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20261016135918697', "timestamp" = now(); + +-- +-- MIGRATION VERSION FOR serverpod +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('serverpod', '20260129180959368', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20260129180959368', "timestamp" = now(); + + +COMMIT; diff --git a/backend/sinalacs_server/migrations/migration_registry.txt b/backend/sinalacs_server/migrations/migration_registry.txt index 294c3e7..992b20e 100644 --- a/backend/sinalacs_server/migrations/migration_registry.txt +++ b/backend/sinalacs_server/migrations/migration_registry.txt @@ -30,3 +30,4 @@ 20261013151127052 20261014152407324 20261014155304871 +20261016135918697 diff --git a/backend/sinalacs_server/test/integration/data_subject_case_store_test.dart b/backend/sinalacs_server/test/integration/data_subject_case_store_test.dart index f617afb..518fa15 100644 --- a/backend/sinalacs_server/test/integration/data_subject_case_store_test.dart +++ b/backend/sinalacs_server/test/integration/data_subject_case_store_test.dart @@ -252,14 +252,15 @@ Future _prontuarioA(Session s) async { final respostas = await testHealthDataCipher().encryptJson([ TriageAnswer(question: 'fever', answer: 'sim').toJson(), ]); + final riscoTriagem = await encryptedRiskLevel(RiskLevel.yellow); await TriageSession.db.insertRow( s, TriageSession( patientId: a, answersEncrypted: respostas.ciphertextBase64, answersKeyVersion: respostas.keyVersion, - resultRisk: RiskLevel.yellow, - resultDisplay: 'Amarelo', + resultRiskEncrypted: riscoTriagem.ciphertextBase64, + resultRiskKeyVersion: riscoTriagem.keyVersion, createdAt: _t, deviceId: 'device-sintetico', ), @@ -490,7 +491,7 @@ void main() { final triagens = await TriageSession.db.find(session, where: (t) => t.patientId.equals(a)); expect(triagens, hasLength(1), reason: 'a sessão fica para estatística'); expect(await decryptedTriageAnswers(triagens.single), isEmpty); - expect(triagens.single.resultRisk, RiskLevel.yellow); + expect(await decryptedTriageRisk(triagens.single), RiskLevel.yellow); expect(await Alert.db.count(session, where: (t) => t.patientId.equals(a)), 1); expect(await Visit.db.count(session, where: (t) => t.patientId.equals(a)), 1); diff --git a/backend/sinalacs_server/test/integration/health_data_encryption_test.dart b/backend/sinalacs_server/test/integration/health_data_encryption_test.dart index 9c27906..225a0a1 100644 --- a/backend/sinalacs_server/test/integration/health_data_encryption_test.dart +++ b/backend/sinalacs_server/test/integration/health_data_encryption_test.dart @@ -139,6 +139,28 @@ Future _colunaBruta( return rows.single.first.toString(); } +/// A linha inteira como mapa coluna → valor, lida por SQL cru (#75). Mostra +/// TODAS as colunas, inclusive as que alguém acrescentar depois. +Future> _linhaBruta(Session session, {required String table}) async { + final rows = await session.db.unsafeQuery('SELECT row_to_json(t)::text FROM "$table" t;'); + expect(rows, hasLength(1), reason: 'o seed deve ter gravado exatamente 1 linha'); + return jsonDecode(rows.single.first as String) as Map; +} + +/// Nenhuma coluna pode guardar a classificação legível — nem o nome do enum +/// nem o rótulo de tela. Igualdade, não substring: um base64 pode conter +/// "red" por acaso. +void _expectSemRiscoLegivel(Map linha) { + const legiveis = {'red', 'yellow', 'green', 'Vermelho', 'Amarelo', 'Verde'}; + for (final entry in linha.entries) { + expect( + legiveis.contains('${entry.value}'.trim()), + isFalse, + reason: 'a coluna ${entry.key} guarda o risco em claro (INV-04, #75)', + ); + } +} + /// Falha se QUALQUER palavra do texto claro aparecer na coluna bruta. /// /// Comparar a string inteira seria fraco demais: um JSON parcialmente cifrado, @@ -266,6 +288,40 @@ void main() { ); }); + test('#75: nenhuma coluna de triage_sessions guarda a classificação de ' + 'risco em claro', () async { + final session = sessionBuilder.build(); + await _seed(session); + + final service = TriageSessionService( + store: OrmTriageSessionStore( + session: () => session, + cipher: testHealthDataCipher(), + ), + audit: _SilentAuditTrail(), + ); + final risco = await service.evaluateAndRecord( + user: _paciente, + chestPain: true, + difficultyBreathing: false, + fever: false, + persistentVomiting: false, + bleeding: false, + severeWeakness: false, + ); + expect(risco, RiskLevel.red); + + final linha = await _linhaBruta(session, table: 'triage_sessions'); + _expectSemRiscoLegivel(linha); + expect( + linha.containsKey('resultDisplay'), + isFalse, + reason: 'o rótulo derivado do risco não é mais persistido', + ); + final gravada = await TriageSession.db.findFirstRow(session); + expect(await decryptedTriageRisk(gravada!), RiskLevel.red); + }); + test('visits.notesEncrypted não guarda as notas em claro, e o pull do ACS ' 'as devolve legíveis', () async { final session = sessionBuilder.build(); diff --git a/backend/sinalacs_server/test/integration/patient_data_overview_endpoint_test.dart b/backend/sinalacs_server/test/integration/patient_data_overview_endpoint_test.dart index 75dc930..cdc592f 100644 --- a/backend/sinalacs_server/test/integration/patient_data_overview_endpoint_test.dart +++ b/backend/sinalacs_server/test/integration/patient_data_overview_endpoint_test.dart @@ -105,12 +105,13 @@ Future _seed(Session session) async { signature: 'assinatura-de-teste', ), ); + final riscoTriagem = await encryptedRiskLevel(RiskLevel.yellow); await TriageSession.db.insertRow( session, TriageSession( patientId: UuidValue.fromString(_patientId), - resultRisk: RiskLevel.yellow, - resultDisplay: 'Risco: Amarelo', + resultRiskEncrypted: riscoTriagem.ciphertextBase64, + resultRiskKeyVersion: riscoTriagem.keyVersion, createdAt: DateTime.utc(2026, 2, 1), deviceId: 'device-de-teste', ), @@ -166,6 +167,34 @@ void main() { expect(result.riskHistory[1].riskLevel, RiskLevel.yellow); }); + test('#75: triagem gravada antes da cifra do risco fica fora do histórico ' + 'e o painel abre mesmo assim', () async { + final session = sessionBuilder.build(); + await _seed(session); + // Linha "antiga": o default '' da coluna, como a migração deixa as + // linhas que já existiam. + await TriageSession.db.insertRow( + session, + TriageSession( + patientId: UuidValue.fromString(_patientId), + createdAt: DateTime.utc(2026, 4, 1), + deviceId: 'device-de-teste', + ), + ); + final login = await endpoints.auth.developmentLogin(sessionBuilder, role: 'patient'); + + final result = await endpoints.patients.myData( + sessionBuilder, + accessToken: login.accessToken, + ); + + expect(result.riskHistory, hasLength(2), reason: 'só as duas linhas do seed'); + expect( + result.riskHistory.every((e) => e.recordedAt != DateTime.utc(2026, 4, 1)), + isTrue, + ); + }); + test('um ACS não pode consultar o painel "Meus Dados" de um paciente', () async { await _seed(sessionBuilder.build()); final login = await endpoints.auth.developmentLogin(sessionBuilder, role: 'acs'); diff --git a/backend/sinalacs_server/test/integration/retention_expurge_test.dart b/backend/sinalacs_server/test/integration/retention_expurge_test.dart index 2c095df..4ff2918 100644 --- a/backend/sinalacs_server/test/integration/retention_expurge_test.dart +++ b/backend/sinalacs_server/test/integration/retention_expurge_test.dart @@ -181,8 +181,6 @@ Future _seed(Session session) async { session, TriageSession( patientId: UuidValue.fromString(_patientId), - resultRisk: RiskLevel.green, - resultDisplay: 'Verde', createdAt: _cutoffTriage.subtract(const Duration(days: 1)), deviceId: 'device-retencao', ), @@ -191,8 +189,6 @@ Future _seed(Session session) async { session, TriageSession( patientId: UuidValue.fromString(_patientId), - resultRisk: RiskLevel.yellow, - resultDisplay: 'Amarelo', createdAt: _now, deviceId: 'device-retencao', ), diff --git a/backend/sinalacs_server/test/integration/triage_session_persistence_test.dart b/backend/sinalacs_server/test/integration/triage_session_persistence_test.dart index 4aeb8cf..4ee12f8 100644 --- a/backend/sinalacs_server/test/integration/triage_session_persistence_test.dart +++ b/backend/sinalacs_server/test/integration/triage_session_persistence_test.dart @@ -124,8 +124,7 @@ void main() { final gravadas = await TriageSession.db.find(session); expect(gravadas, hasLength(1)); expect(gravadas.single.patientId, UuidValue.fromString(_patientId)); - expect(gravadas.single.resultRisk, RiskLevel.red); - expect(gravadas.single.resultDisplay, 'Vermelho'); + expect(await decryptedTriageRisk(gravadas.single), RiskLevel.red); // A coluna guarda ciphertext (RNF03): as respostas só voltam a ser // legíveis passando pela cifra, e é isso que este bloco prova — que a // ida e volta pelo store preserva as seis respostas. @@ -230,7 +229,7 @@ void main() { final gravadas = await TriageSession.db.find(session); expect(gravadas, hasLength(1)); - expect(gravadas.single.resultRisk, RiskLevel.green); + expect(await decryptedTriageRisk(gravadas.single), RiskLevel.green); }); }); } diff --git a/backend/sinalacs_server/test/support/health_data_fixtures.dart b/backend/sinalacs_server/test/support/health_data_fixtures.dart index a1d9ad2..3c1f975 100644 --- a/backend/sinalacs_server/test/support/health_data_fixtures.dart +++ b/backend/sinalacs_server/test/support/health_data_fixtures.dart @@ -2,6 +2,7 @@ import 'package:serverpod/serverpod.dart' show UuidValue; import 'package:sinalacs_server/src/config/app_config.dart'; import 'package:sinalacs_server/src/generated/protocol.dart'; import 'package:sinalacs_server/src/infrastructure/crypto/encrypted_json.dart'; +import 'package:sinalacs_server/src/infrastructure/crypto/encrypted_risk_level.dart'; import 'package:sinalacs_server/src/infrastructure/crypto/health_data_cipher.dart'; /// Ajudantes para os testes que precisam SEMEAR linhas cifradas. @@ -62,3 +63,14 @@ Future> decryptedTriageAnswers(TriageSession session) async { TriageAnswer.fromJson(json as Map), ]; } + +/// Cifra uma classificação de risco do jeito que os stores gravam (#75). +Future encryptedRiskLevel(RiskLevel? risk) => + testHealthDataCipher().encryptRiskLevel(risk); + +/// Decifra o risco de uma sessão de triagem lida pelo ORM (#75). +Future decryptedTriageRisk(TriageSession session) => + testHealthDataCipher().decryptRiskLevel( + session.resultRiskEncrypted, + session.resultRiskKeyVersion, + ); diff --git a/backend/sinalacs_server/test/unit/triage_session_service_test.dart b/backend/sinalacs_server/test/unit/triage_session_service_test.dart index 7f2b218..c34f8be 100644 --- a/backend/sinalacs_server/test/unit/triage_session_service_test.dart +++ b/backend/sinalacs_server/test/unit/triage_session_service_test.dart @@ -116,7 +116,6 @@ void main() { expect(risco, RiskLevel.red); expect(store.saved.single.resultRisk, RiskLevel.red); - expect(store.saved.single.resultDisplay, 'Vermelho'); }); test('grava as seis respostas com chave estável, não com texto de UI', () async { From 31998329a5e955902a70a0993c7a38ad7f477ad9 Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Fri, 16 Oct 2026 10:03:31 -0400 Subject: [PATCH 4/7] fix(backend): cifra o risco antes e depois da visita (#75) --- .../lib/src/protocol/visit.dart | 100 +- .../lib/src/generated/protocol.dart | 26 +- .../lib/src/generated/visit.dart | 185 +- .../database/orm_visit_store.dart | 51 +- .../lib/src/models/visit.spy.yaml | 9 +- .../20261016140159775/definition.json | 4522 +++++++++++++++++ .../20261016140159775/definition.sql | 938 ++++ .../20261016140159775/definition_project.json | 3186 ++++++++++++ .../20261016140159775/migration.json | 102 + .../20261016140159775/migration.sql | 30 + .../migrations/migration_registry.txt | 1 + .../data_subject_case_store_test.dart | 1 - .../health_data_encryption_test.dart | 123 + .../patient_directory_and_territory_test.dart | 7 +- .../integration/retention_expurge_test.dart | 2 - .../integration/visit_legacy_sync_test.dart | 4 +- 16 files changed, 9161 insertions(+), 126 deletions(-) create mode 100644 backend/sinalacs_server/migrations/20261016140159775/definition.json create mode 100644 backend/sinalacs_server/migrations/20261016140159775/definition.sql create mode 100644 backend/sinalacs_server/migrations/20261016140159775/definition_project.json create mode 100644 backend/sinalacs_server/migrations/20261016140159775/migration.json create mode 100644 backend/sinalacs_server/migrations/20261016140159775/migration.sql diff --git a/backend/sinalacs_client/lib/src/protocol/visit.dart b/backend/sinalacs_client/lib/src/protocol/visit.dart index 9eeceb0..108baba 100644 --- a/backend/sinalacs_client/lib/src/protocol/visit.dart +++ b/backend/sinalacs_client/lib/src/protocol/visit.dart @@ -14,8 +14,7 @@ import 'package:serverpod_client/serverpod_client.dart' as _i1; import 'enums/visit_authorship.dart' as _i2; import 'enums/arrival_method.dart' as _i3; -import 'enums/risk_level.dart' as _i4; -import 'enums/sync_status.dart' as _i5; +import 'enums/sync_status.dart' as _i4; /// Visita domiciliar. Registrada offline e sincronizada depois. abstract class Visit implements _i1.SerializableModel { @@ -29,8 +28,10 @@ abstract class Visit implements _i1.SerializableModel { this.startedAt, this.completedAt, required this.status, - required this.riskLevelBefore, - this.riskLevelAfter, + String? riskLevelBeforeEncrypted, + int? riskLevelBeforeKeyVersion, + String? riskLevelAfterEncrypted, + int? riskLevelAfterKeyVersion, String? notesEncrypted, int? notesKeyVersion, required this.syncStatus, @@ -39,6 +40,10 @@ abstract class Visit implements _i1.SerializableModel { this.syncAt, required this.version, }) : authorship = authorship ?? _i2.VisitAuthorship.acs, + riskLevelBeforeEncrypted = riskLevelBeforeEncrypted ?? '', + riskLevelBeforeKeyVersion = riskLevelBeforeKeyVersion ?? 1, + riskLevelAfterEncrypted = riskLevelAfterEncrypted ?? '', + riskLevelAfterKeyVersion = riskLevelAfterKeyVersion ?? 1, notesEncrypted = notesEncrypted ?? '', notesKeyVersion = notesKeyVersion ?? 1, arrivalMethod = arrivalMethod ?? _i3.ArrivalMethod.manual; @@ -53,11 +58,13 @@ abstract class Visit implements _i1.SerializableModel { DateTime? startedAt, DateTime? completedAt, required String status, - required _i4.RiskLevel riskLevelBefore, - _i4.RiskLevel? riskLevelAfter, + String? riskLevelBeforeEncrypted, + int? riskLevelBeforeKeyVersion, + String? riskLevelAfterEncrypted, + int? riskLevelAfterKeyVersion, String? notesEncrypted, int? notesKeyVersion, - required _i5.SyncStatus syncStatus, + required _i4.SyncStatus syncStatus, _i3.ArrivalMethod? arrivalMethod, required _i1.UuidValue localId, DateTime? syncAt, @@ -93,17 +100,17 @@ abstract class Visit implements _i1.SerializableModel { jsonSerialization['completedAt'], ), status: jsonSerialization['status'] as String, - riskLevelBefore: _i4.RiskLevel.fromJson( - (jsonSerialization['riskLevelBefore'] as String), - ), - riskLevelAfter: jsonSerialization['riskLevelAfter'] == null - ? null - : _i4.RiskLevel.fromJson( - (jsonSerialization['riskLevelAfter'] as String), - ), + riskLevelBeforeEncrypted: + jsonSerialization['riskLevelBeforeEncrypted'] as String?, + riskLevelBeforeKeyVersion: + jsonSerialization['riskLevelBeforeKeyVersion'] as int?, + riskLevelAfterEncrypted: + jsonSerialization['riskLevelAfterEncrypted'] as String?, + riskLevelAfterKeyVersion: + jsonSerialization['riskLevelAfterKeyVersion'] as int?, notesEncrypted: jsonSerialization['notesEncrypted'] as String?, notesKeyVersion: jsonSerialization['notesKeyVersion'] as int?, - syncStatus: _i5.SyncStatus.fromJson( + syncStatus: _i4.SyncStatus.fromJson( (jsonSerialization['syncStatus'] as String), ), arrivalMethod: jsonSerialization['arrivalMethod'] == null @@ -147,16 +154,23 @@ abstract class Visit implements _i1.SerializableModel { String status; - _i4.RiskLevel riskLevelBefore; + /// Risco antes e depois da visita, cifrados (INV-04, #75): nome do + /// RiskLevel com largura fixa, ver `encrypted_risk_level.dart`. O "depois" + /// ausente (visita não concluída) é cifrado também, com o mesmo tamanho. + String riskLevelBeforeEncrypted; + + int riskLevelBeforeKeyVersion; + + String riskLevelAfterEncrypted; - _i4.RiskLevel? riskLevelAfter; + int riskLevelAfterKeyVersion; /// JSON de Map, cifrado. Ver patient.spy.yaml para o padrão. String notesEncrypted; int notesKeyVersion; - _i5.SyncStatus syncStatus; + _i4.SyncStatus syncStatus; /// Como o check-in desta visita foi registrado (RF12, decisão §4). Default /// `manual` até a integração nativa de geofencing existir — hoje nenhum @@ -183,11 +197,13 @@ abstract class Visit implements _i1.SerializableModel { DateTime? startedAt, DateTime? completedAt, String? status, - _i4.RiskLevel? riskLevelBefore, - _i4.RiskLevel? riskLevelAfter, + String? riskLevelBeforeEncrypted, + int? riskLevelBeforeKeyVersion, + String? riskLevelAfterEncrypted, + int? riskLevelAfterKeyVersion, String? notesEncrypted, int? notesKeyVersion, - _i5.SyncStatus? syncStatus, + _i4.SyncStatus? syncStatus, _i3.ArrivalMethod? arrivalMethod, _i1.UuidValue? localId, DateTime? syncAt, @@ -206,8 +222,10 @@ abstract class Visit implements _i1.SerializableModel { if (startedAt != null) 'startedAt': startedAt?.toJson(), if (completedAt != null) 'completedAt': completedAt?.toJson(), 'status': status, - 'riskLevelBefore': riskLevelBefore.toJson(), - if (riskLevelAfter != null) 'riskLevelAfter': riskLevelAfter?.toJson(), + 'riskLevelBeforeEncrypted': riskLevelBeforeEncrypted, + 'riskLevelBeforeKeyVersion': riskLevelBeforeKeyVersion, + 'riskLevelAfterEncrypted': riskLevelAfterEncrypted, + 'riskLevelAfterKeyVersion': riskLevelAfterKeyVersion, 'notesEncrypted': notesEncrypted, 'notesKeyVersion': notesKeyVersion, 'syncStatus': syncStatus.toJson(), @@ -237,11 +255,13 @@ class _VisitImpl extends Visit { DateTime? startedAt, DateTime? completedAt, required String status, - required _i4.RiskLevel riskLevelBefore, - _i4.RiskLevel? riskLevelAfter, + String? riskLevelBeforeEncrypted, + int? riskLevelBeforeKeyVersion, + String? riskLevelAfterEncrypted, + int? riskLevelAfterKeyVersion, String? notesEncrypted, int? notesKeyVersion, - required _i5.SyncStatus syncStatus, + required _i4.SyncStatus syncStatus, _i3.ArrivalMethod? arrivalMethod, required _i1.UuidValue localId, DateTime? syncAt, @@ -256,8 +276,10 @@ class _VisitImpl extends Visit { startedAt: startedAt, completedAt: completedAt, status: status, - riskLevelBefore: riskLevelBefore, - riskLevelAfter: riskLevelAfter, + riskLevelBeforeEncrypted: riskLevelBeforeEncrypted, + riskLevelBeforeKeyVersion: riskLevelBeforeKeyVersion, + riskLevelAfterEncrypted: riskLevelAfterEncrypted, + riskLevelAfterKeyVersion: riskLevelAfterKeyVersion, notesEncrypted: notesEncrypted, notesKeyVersion: notesKeyVersion, syncStatus: syncStatus, @@ -281,11 +303,13 @@ class _VisitImpl extends Visit { Object? startedAt = _Undefined, Object? completedAt = _Undefined, String? status, - _i4.RiskLevel? riskLevelBefore, - Object? riskLevelAfter = _Undefined, + String? riskLevelBeforeEncrypted, + int? riskLevelBeforeKeyVersion, + String? riskLevelAfterEncrypted, + int? riskLevelAfterKeyVersion, String? notesEncrypted, int? notesKeyVersion, - _i5.SyncStatus? syncStatus, + _i4.SyncStatus? syncStatus, _i3.ArrivalMethod? arrivalMethod, _i1.UuidValue? localId, Object? syncAt = _Undefined, @@ -303,10 +327,14 @@ class _VisitImpl extends Visit { startedAt: startedAt is DateTime? ? startedAt : this.startedAt, completedAt: completedAt is DateTime? ? completedAt : this.completedAt, status: status ?? this.status, - riskLevelBefore: riskLevelBefore ?? this.riskLevelBefore, - riskLevelAfter: riskLevelAfter is _i4.RiskLevel? - ? riskLevelAfter - : this.riskLevelAfter, + riskLevelBeforeEncrypted: + riskLevelBeforeEncrypted ?? this.riskLevelBeforeEncrypted, + riskLevelBeforeKeyVersion: + riskLevelBeforeKeyVersion ?? this.riskLevelBeforeKeyVersion, + riskLevelAfterEncrypted: + riskLevelAfterEncrypted ?? this.riskLevelAfterEncrypted, + riskLevelAfterKeyVersion: + riskLevelAfterKeyVersion ?? this.riskLevelAfterKeyVersion, notesEncrypted: notesEncrypted ?? this.notesEncrypted, notesKeyVersion: notesKeyVersion ?? this.notesKeyVersion, syncStatus: syncStatus ?? this.syncStatus, diff --git a/backend/sinalacs_server/lib/src/generated/protocol.dart b/backend/sinalacs_server/lib/src/generated/protocol.dart index e18cf9c..ea3fd68 100644 --- a/backend/sinalacs_server/lib/src/generated/protocol.dart +++ b/backend/sinalacs_server/lib/src/generated/protocol.dart @@ -2846,16 +2846,32 @@ class Protocol extends _i1.SerializationManagerServer { dartType: 'String', ), _i2.ColumnDefinition( - name: 'riskLevelBefore', + name: 'riskLevelBeforeEncrypted', columnType: _i2.ColumnType.text, isNullable: false, - dartType: 'protocol:RiskLevel', + dartType: 'String', + columnDefault: '\'\'::text', + ), + _i2.ColumnDefinition( + name: 'riskLevelBeforeKeyVersion', + columnType: _i2.ColumnType.bigint, + isNullable: false, + dartType: 'int', + columnDefault: '1', ), _i2.ColumnDefinition( - name: 'riskLevelAfter', + name: 'riskLevelAfterEncrypted', columnType: _i2.ColumnType.text, - isNullable: true, - dartType: 'protocol:RiskLevel?', + isNullable: false, + dartType: 'String', + columnDefault: '\'\'::text', + ), + _i2.ColumnDefinition( + name: 'riskLevelAfterKeyVersion', + columnType: _i2.ColumnType.bigint, + isNullable: false, + dartType: 'int', + columnDefault: '1', ), _i2.ColumnDefinition( name: 'notesEncrypted', diff --git a/backend/sinalacs_server/lib/src/generated/visit.dart b/backend/sinalacs_server/lib/src/generated/visit.dart index 9ef0beb..8cce952 100644 --- a/backend/sinalacs_server/lib/src/generated/visit.dart +++ b/backend/sinalacs_server/lib/src/generated/visit.dart @@ -14,8 +14,7 @@ import 'package:serverpod/serverpod.dart' as _i1; import 'enums/visit_authorship.dart' as _i2; import 'enums/arrival_method.dart' as _i3; -import 'enums/risk_level.dart' as _i4; -import 'enums/sync_status.dart' as _i5; +import 'enums/sync_status.dart' as _i4; /// Visita domiciliar. Registrada offline e sincronizada depois. abstract class Visit @@ -30,8 +29,10 @@ abstract class Visit this.startedAt, this.completedAt, required this.status, - required this.riskLevelBefore, - this.riskLevelAfter, + String? riskLevelBeforeEncrypted, + int? riskLevelBeforeKeyVersion, + String? riskLevelAfterEncrypted, + int? riskLevelAfterKeyVersion, String? notesEncrypted, int? notesKeyVersion, required this.syncStatus, @@ -40,6 +41,10 @@ abstract class Visit this.syncAt, required this.version, }) : authorship = authorship ?? _i2.VisitAuthorship.acs, + riskLevelBeforeEncrypted = riskLevelBeforeEncrypted ?? '', + riskLevelBeforeKeyVersion = riskLevelBeforeKeyVersion ?? 1, + riskLevelAfterEncrypted = riskLevelAfterEncrypted ?? '', + riskLevelAfterKeyVersion = riskLevelAfterKeyVersion ?? 1, notesEncrypted = notesEncrypted ?? '', notesKeyVersion = notesKeyVersion ?? 1, arrivalMethod = arrivalMethod ?? _i3.ArrivalMethod.manual; @@ -54,11 +59,13 @@ abstract class Visit DateTime? startedAt, DateTime? completedAt, required String status, - required _i4.RiskLevel riskLevelBefore, - _i4.RiskLevel? riskLevelAfter, + String? riskLevelBeforeEncrypted, + int? riskLevelBeforeKeyVersion, + String? riskLevelAfterEncrypted, + int? riskLevelAfterKeyVersion, String? notesEncrypted, int? notesKeyVersion, - required _i5.SyncStatus syncStatus, + required _i4.SyncStatus syncStatus, _i3.ArrivalMethod? arrivalMethod, required _i1.UuidValue localId, DateTime? syncAt, @@ -94,17 +101,17 @@ abstract class Visit jsonSerialization['completedAt'], ), status: jsonSerialization['status'] as String, - riskLevelBefore: _i4.RiskLevel.fromJson( - (jsonSerialization['riskLevelBefore'] as String), - ), - riskLevelAfter: jsonSerialization['riskLevelAfter'] == null - ? null - : _i4.RiskLevel.fromJson( - (jsonSerialization['riskLevelAfter'] as String), - ), + riskLevelBeforeEncrypted: + jsonSerialization['riskLevelBeforeEncrypted'] as String?, + riskLevelBeforeKeyVersion: + jsonSerialization['riskLevelBeforeKeyVersion'] as int?, + riskLevelAfterEncrypted: + jsonSerialization['riskLevelAfterEncrypted'] as String?, + riskLevelAfterKeyVersion: + jsonSerialization['riskLevelAfterKeyVersion'] as int?, notesEncrypted: jsonSerialization['notesEncrypted'] as String?, notesKeyVersion: jsonSerialization['notesKeyVersion'] as int?, - syncStatus: _i5.SyncStatus.fromJson( + syncStatus: _i4.SyncStatus.fromJson( (jsonSerialization['syncStatus'] as String), ), arrivalMethod: jsonSerialization['arrivalMethod'] == null @@ -150,16 +157,23 @@ abstract class Visit String status; - _i4.RiskLevel riskLevelBefore; + /// Risco antes e depois da visita, cifrados (INV-04, #75): nome do + /// RiskLevel com largura fixa, ver `encrypted_risk_level.dart`. O "depois" + /// ausente (visita não concluída) é cifrado também, com o mesmo tamanho. + String riskLevelBeforeEncrypted; + + int riskLevelBeforeKeyVersion; + + String riskLevelAfterEncrypted; - _i4.RiskLevel? riskLevelAfter; + int riskLevelAfterKeyVersion; /// JSON de Map, cifrado. Ver patient.spy.yaml para o padrão. String notesEncrypted; int notesKeyVersion; - _i5.SyncStatus syncStatus; + _i4.SyncStatus syncStatus; /// Como o check-in desta visita foi registrado (RF12, decisão §4). Default /// `manual` até a integração nativa de geofencing existir — hoje nenhum @@ -189,11 +203,13 @@ abstract class Visit DateTime? startedAt, DateTime? completedAt, String? status, - _i4.RiskLevel? riskLevelBefore, - _i4.RiskLevel? riskLevelAfter, + String? riskLevelBeforeEncrypted, + int? riskLevelBeforeKeyVersion, + String? riskLevelAfterEncrypted, + int? riskLevelAfterKeyVersion, String? notesEncrypted, int? notesKeyVersion, - _i5.SyncStatus? syncStatus, + _i4.SyncStatus? syncStatus, _i3.ArrivalMethod? arrivalMethod, _i1.UuidValue? localId, DateTime? syncAt, @@ -212,8 +228,10 @@ abstract class Visit if (startedAt != null) 'startedAt': startedAt?.toJson(), if (completedAt != null) 'completedAt': completedAt?.toJson(), 'status': status, - 'riskLevelBefore': riskLevelBefore.toJson(), - if (riskLevelAfter != null) 'riskLevelAfter': riskLevelAfter?.toJson(), + 'riskLevelBeforeEncrypted': riskLevelBeforeEncrypted, + 'riskLevelBeforeKeyVersion': riskLevelBeforeKeyVersion, + 'riskLevelAfterEncrypted': riskLevelAfterEncrypted, + 'riskLevelAfterKeyVersion': riskLevelAfterKeyVersion, 'notesEncrypted': notesEncrypted, 'notesKeyVersion': notesKeyVersion, 'syncStatus': syncStatus.toJson(), @@ -237,8 +255,10 @@ abstract class Visit if (startedAt != null) 'startedAt': startedAt?.toJson(), if (completedAt != null) 'completedAt': completedAt?.toJson(), 'status': status, - 'riskLevelBefore': riskLevelBefore.toJson(), - if (riskLevelAfter != null) 'riskLevelAfter': riskLevelAfter?.toJson(), + 'riskLevelBeforeEncrypted': riskLevelBeforeEncrypted, + 'riskLevelBeforeKeyVersion': riskLevelBeforeKeyVersion, + 'riskLevelAfterEncrypted': riskLevelAfterEncrypted, + 'riskLevelAfterKeyVersion': riskLevelAfterKeyVersion, 'notesEncrypted': notesEncrypted, 'notesKeyVersion': notesKeyVersion, 'syncStatus': syncStatus.toJson(), @@ -292,11 +312,13 @@ class _VisitImpl extends Visit { DateTime? startedAt, DateTime? completedAt, required String status, - required _i4.RiskLevel riskLevelBefore, - _i4.RiskLevel? riskLevelAfter, + String? riskLevelBeforeEncrypted, + int? riskLevelBeforeKeyVersion, + String? riskLevelAfterEncrypted, + int? riskLevelAfterKeyVersion, String? notesEncrypted, int? notesKeyVersion, - required _i5.SyncStatus syncStatus, + required _i4.SyncStatus syncStatus, _i3.ArrivalMethod? arrivalMethod, required _i1.UuidValue localId, DateTime? syncAt, @@ -311,8 +333,10 @@ class _VisitImpl extends Visit { startedAt: startedAt, completedAt: completedAt, status: status, - riskLevelBefore: riskLevelBefore, - riskLevelAfter: riskLevelAfter, + riskLevelBeforeEncrypted: riskLevelBeforeEncrypted, + riskLevelBeforeKeyVersion: riskLevelBeforeKeyVersion, + riskLevelAfterEncrypted: riskLevelAfterEncrypted, + riskLevelAfterKeyVersion: riskLevelAfterKeyVersion, notesEncrypted: notesEncrypted, notesKeyVersion: notesKeyVersion, syncStatus: syncStatus, @@ -336,11 +360,13 @@ class _VisitImpl extends Visit { Object? startedAt = _Undefined, Object? completedAt = _Undefined, String? status, - _i4.RiskLevel? riskLevelBefore, - Object? riskLevelAfter = _Undefined, + String? riskLevelBeforeEncrypted, + int? riskLevelBeforeKeyVersion, + String? riskLevelAfterEncrypted, + int? riskLevelAfterKeyVersion, String? notesEncrypted, int? notesKeyVersion, - _i5.SyncStatus? syncStatus, + _i4.SyncStatus? syncStatus, _i3.ArrivalMethod? arrivalMethod, _i1.UuidValue? localId, Object? syncAt = _Undefined, @@ -358,10 +384,14 @@ class _VisitImpl extends Visit { startedAt: startedAt is DateTime? ? startedAt : this.startedAt, completedAt: completedAt is DateTime? ? completedAt : this.completedAt, status: status ?? this.status, - riskLevelBefore: riskLevelBefore ?? this.riskLevelBefore, - riskLevelAfter: riskLevelAfter is _i4.RiskLevel? - ? riskLevelAfter - : this.riskLevelAfter, + riskLevelBeforeEncrypted: + riskLevelBeforeEncrypted ?? this.riskLevelBeforeEncrypted, + riskLevelBeforeKeyVersion: + riskLevelBeforeKeyVersion ?? this.riskLevelBeforeKeyVersion, + riskLevelAfterEncrypted: + riskLevelAfterEncrypted ?? this.riskLevelAfterEncrypted, + riskLevelAfterKeyVersion: + riskLevelAfterKeyVersion ?? this.riskLevelAfterKeyVersion, notesEncrypted: notesEncrypted ?? this.notesEncrypted, notesKeyVersion: notesKeyVersion ?? this.notesKeyVersion, syncStatus: syncStatus ?? this.syncStatus, @@ -425,19 +455,29 @@ class VisitUpdateTable extends _i1.UpdateTable { value, ); - _i1.ColumnValue<_i4.RiskLevel, _i4.RiskLevel> riskLevelBefore( - _i4.RiskLevel value, - ) => _i1.ColumnValue( - table.riskLevelBefore, - value, - ); + _i1.ColumnValue riskLevelBeforeEncrypted(String value) => + _i1.ColumnValue( + table.riskLevelBeforeEncrypted, + value, + ); - _i1.ColumnValue<_i4.RiskLevel, _i4.RiskLevel> riskLevelAfter( - _i4.RiskLevel? value, - ) => _i1.ColumnValue( - table.riskLevelAfter, - value, - ); + _i1.ColumnValue riskLevelBeforeKeyVersion(int value) => + _i1.ColumnValue( + table.riskLevelBeforeKeyVersion, + value, + ); + + _i1.ColumnValue riskLevelAfterEncrypted(String value) => + _i1.ColumnValue( + table.riskLevelAfterEncrypted, + value, + ); + + _i1.ColumnValue riskLevelAfterKeyVersion(int value) => + _i1.ColumnValue( + table.riskLevelAfterKeyVersion, + value, + ); _i1.ColumnValue notesEncrypted(String value) => _i1.ColumnValue( @@ -450,8 +490,8 @@ class VisitUpdateTable extends _i1.UpdateTable { value, ); - _i1.ColumnValue<_i5.SyncStatus, _i5.SyncStatus> syncStatus( - _i5.SyncStatus value, + _i1.ColumnValue<_i4.SyncStatus, _i4.SyncStatus> syncStatus( + _i4.SyncStatus value, ) => _i1.ColumnValue( table.syncStatus, value, @@ -519,15 +559,25 @@ class VisitTable extends _i1.Table<_i1.UuidValue?> { 'status', this, ); - riskLevelBefore = _i1.ColumnEnum( - 'riskLevelBefore', + riskLevelBeforeEncrypted = _i1.ColumnString( + 'riskLevelBeforeEncrypted', this, - _i1.EnumSerialization.byName, + hasDefault: true, ); - riskLevelAfter = _i1.ColumnEnum( - 'riskLevelAfter', + riskLevelBeforeKeyVersion = _i1.ColumnInt( + 'riskLevelBeforeKeyVersion', this, - _i1.EnumSerialization.byName, + hasDefault: true, + ); + riskLevelAfterEncrypted = _i1.ColumnString( + 'riskLevelAfterEncrypted', + this, + hasDefault: true, + ); + riskLevelAfterKeyVersion = _i1.ColumnInt( + 'riskLevelAfterKeyVersion', + this, + hasDefault: true, ); notesEncrypted = _i1.ColumnString( 'notesEncrypted', @@ -587,16 +637,23 @@ class VisitTable extends _i1.Table<_i1.UuidValue?> { late final _i1.ColumnString status; - late final _i1.ColumnEnum<_i4.RiskLevel> riskLevelBefore; + /// Risco antes e depois da visita, cifrados (INV-04, #75): nome do + /// RiskLevel com largura fixa, ver `encrypted_risk_level.dart`. O "depois" + /// ausente (visita não concluída) é cifrado também, com o mesmo tamanho. + late final _i1.ColumnString riskLevelBeforeEncrypted; + + late final _i1.ColumnInt riskLevelBeforeKeyVersion; + + late final _i1.ColumnString riskLevelAfterEncrypted; - late final _i1.ColumnEnum<_i4.RiskLevel> riskLevelAfter; + late final _i1.ColumnInt riskLevelAfterKeyVersion; /// JSON de Map, cifrado. Ver patient.spy.yaml para o padrão. late final _i1.ColumnString notesEncrypted; late final _i1.ColumnInt notesKeyVersion; - late final _i1.ColumnEnum<_i5.SyncStatus> syncStatus; + late final _i1.ColumnEnum<_i4.SyncStatus> syncStatus; /// Como o check-in desta visita foi registrado (RF12, decisão §4). Default /// `manual` até a integração nativa de geofencing existir — hoje nenhum @@ -621,8 +678,10 @@ class VisitTable extends _i1.Table<_i1.UuidValue?> { startedAt, completedAt, status, - riskLevelBefore, - riskLevelAfter, + riskLevelBeforeEncrypted, + riskLevelBeforeKeyVersion, + riskLevelAfterEncrypted, + riskLevelAfterKeyVersion, notesEncrypted, notesKeyVersion, syncStatus, diff --git a/backend/sinalacs_server/lib/src/infrastructure/database/orm_visit_store.dart b/backend/sinalacs_server/lib/src/infrastructure/database/orm_visit_store.dart index 6c1904d..b6796c2 100644 --- a/backend/sinalacs_server/lib/src/infrastructure/database/orm_visit_store.dart +++ b/backend/sinalacs_server/lib/src/infrastructure/database/orm_visit_store.dart @@ -2,6 +2,7 @@ import 'package:serverpod/serverpod.dart'; import 'package:sinalacs_server/src/application/visits/visit_sync_service.dart'; import 'package:sinalacs_server/src/generated/protocol.dart'; import 'package:sinalacs_server/src/infrastructure/crypto/encrypted_json.dart'; +import 'package:sinalacs_server/src/infrastructure/crypto/encrypted_risk_level.dart'; import 'package:sinalacs_server/src/infrastructure/crypto/health_data_cipher.dart'; /// Implementação de [VisitStore] sobre o ORM do Serverpod. @@ -10,8 +11,8 @@ import 'package:sinalacs_server/src/infrastructure/crypto/health_data_cipher.dar /// não guardada no construtor, porque o Serverpod amarra o ciclo de vida da /// conexão à `Session` da requisição. /// -/// É também a única fronteira que conhece a cifragem de `visits.notes` -/// (RNF03, INV-04): [VisitSyncService] trafega [VisitRecord] com `notes` em +/// É também a única fronteira que conhece a cifragem de `visits.notes` e dos +/// riscos `riskLevelBefore`/`riskLevelAfter` (RNF03, INV-04, #75): [VisitSyncService] trafega [VisitRecord] com `notes` em /// claro nos dois sentidos — inclusive no `pull`, porque o ACS do território é /// justamente quem tem legitimidade para lê-las — e este store converte de e /// para o par `notesEncrypted`/`notesKeyVersion`. @@ -48,9 +49,9 @@ class OrmVisitStore implements VisitStore { await _toRow(visit), transaction: _transaction, ); - // Reaproveita as notas em claro que o chamador já tem: decifrar de volta o - // que acabamos de cifrar seria trabalho puro de ida e volta. - return _toRecord(row, notes: visit.notes); + // Reaproveita o texto claro que o chamador já tem (notas e riscos): + // decifrar de volta o que acabamos de cifrar seria ida e volta à toa. + return _toRecord(row, plain: visit); } @override @@ -60,7 +61,7 @@ class OrmVisitStore implements VisitStore { await _toRow(visit), transaction: _transaction, ); - return _toRecord(row, notes: visit.notes); + return _toRecord(row, plain: visit); } @override @@ -118,6 +119,8 @@ class OrmVisitStore implements VisitStore { Future _toRow(VisitRecord record) async { final encrypted = await _cipher.encryptJson(record.notes); + final before = await _cipher.encryptRiskLevel(record.riskLevelBefore); + final after = await _cipher.encryptRiskLevel(record.riskLevelAfter); return Visit( id: record.id, patientId: record.patientId, @@ -128,8 +131,10 @@ class OrmVisitStore implements VisitStore { startedAt: record.startedAt, completedAt: record.completedAt, status: record.status, - riskLevelBefore: record.riskLevelBefore, - riskLevelAfter: record.riskLevelAfter, + riskLevelBeforeEncrypted: before.ciphertextBase64, + riskLevelBeforeKeyVersion: before.keyVersion, + riskLevelAfterEncrypted: after.ciphertextBase64, + riskLevelAfterKeyVersion: after.keyVersion, notesEncrypted: encrypted.ciphertextBase64, notesKeyVersion: encrypted.keyVersion, syncStatus: record.syncStatus, @@ -140,11 +145,29 @@ class OrmVisitStore implements VisitStore { ); } - Future _toRecord(Visit row, {Map? notes}) async { - final decoded = notes ?? + Future _toRecord(Visit row, {VisitRecord? plain}) async { + final notes = plain?.notes ?? ((await _cipher.decryptJson(row.notesEncrypted, row.notesKeyVersion)) as Map?) - ?.cast(); + ?.cast() ?? + const {}; + final before = plain?.riskLevelBefore ?? + await _cipher.decryptRiskLevel( + row.riskLevelBeforeEncrypted, + row.riskLevelBeforeKeyVersion, + ); + // Ciphertext vazio = linha anterior à migração da #75 ou gravada fora + // deste store. Falha alto: a fila do ACS não pode receber um risco + // inventado (INV-02). Só o id sintético vai na mensagem (VAZ-01). + if (before == null) { + throw StateError('Visita ${row.id} sem riskLevelBefore cifrado (#75).'); + } + final after = plain != null + ? plain.riskLevelAfter + : await _cipher.decryptRiskLevel( + row.riskLevelAfterEncrypted, + row.riskLevelAfterKeyVersion, + ); return VisitRecord( id: row.id, patientId: row.patientId, @@ -155,9 +178,9 @@ class OrmVisitStore implements VisitStore { startedAt: row.startedAt, completedAt: row.completedAt, status: row.status, - riskLevelBefore: row.riskLevelBefore, - riskLevelAfter: row.riskLevelAfter, - notes: decoded ?? const {}, + riskLevelBefore: before, + riskLevelAfter: after, + notes: notes, syncStatus: row.syncStatus, arrivalMethod: row.arrivalMethod, localId: row.localId, diff --git a/backend/sinalacs_server/lib/src/models/visit.spy.yaml b/backend/sinalacs_server/lib/src/models/visit.spy.yaml index 3cf8c47..18c9bb7 100644 --- a/backend/sinalacs_server/lib/src/models/visit.spy.yaml +++ b/backend/sinalacs_server/lib/src/models/visit.spy.yaml @@ -16,8 +16,13 @@ fields: startedAt: DateTime? completedAt: DateTime? status: String - riskLevelBefore: RiskLevel - riskLevelAfter: RiskLevel? + ### Risco antes e depois da visita, cifrados (INV-04, #75): nome do + ### RiskLevel com largura fixa, ver `encrypted_risk_level.dart`. O "depois" + ### ausente (visita não concluída) é cifrado também, com o mesmo tamanho. + riskLevelBeforeEncrypted: String, default='' + riskLevelBeforeKeyVersion: int, default=1 + riskLevelAfterEncrypted: String, default='' + riskLevelAfterKeyVersion: int, default=1 ### JSON de Map, cifrado. Ver patient.spy.yaml para o padrão. notesEncrypted: String, default='' notesKeyVersion: int, default=1 diff --git a/backend/sinalacs_server/migrations/20261016140159775/definition.json b/backend/sinalacs_server/migrations/20261016140159775/definition.json new file mode 100644 index 0000000..469e724 --- /dev/null +++ b/backend/sinalacs_server/migrations/20261016140159775/definition.json @@ -0,0 +1,4522 @@ +{ + "__className__": "serverpod.DatabaseDefinition", + "moduleName": "sinalacs", + "tables": [ + { + "__className__": "serverpod.TableDefinition", + "name": "acs", + "dartName": "Acs", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enrollmentId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "active", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastSyncAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_ubs_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ubsId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_enrollment_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "enrollmentId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "acs_refresh_tokens", + "dartName": "AcsRefreshToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "familyId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tokenHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "issuedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "idleExpiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "absoluteExpiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rotatedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revokedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "acs_refresh_tokens_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "tokenHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_family_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "familyId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "acs_upload_tokens", + "dartName": "AcsUploadToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tokenHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "issuedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revokedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "acs_upload_tokens_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_upload_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_upload_tokens_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "tokenHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_upload_tokens_user_device_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "deviceId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_deliveries", + "dartName": "AlertDeliveryRecord", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acknowledgedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_deliveries_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_deliveries_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_deliveries_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_deliveries_alert_acs_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "alertId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "acsId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_idempotency_keys", + "dartName": "AlertIdempotencyKey", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "key", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_idempotency_keys_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_idempotency_keys_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_idempotency_keys_key_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "key" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_outbox", + "dartName": "AlertOutboxEntry", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "topic", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "payload", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "nextAttemptAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastError", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_outbox_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_outbox_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_outbox_pending_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "publishedAt" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "nextAttemptAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alerts", + "dartName": "Alert", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triggeredAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "receivedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "respondedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acknowledgedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevel", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:RiskLevel" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationCell", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:AlertStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "mqttTopic", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "retryCount", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "escalatedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "escalatedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "escalationReason", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:EscalationReason?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alerts_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alerts_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alerts_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alerts_micro_area_status_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "triggeredAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "audit_chain_checkpoints", + "dartName": "AuditChainCheckpoint", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sequence", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "entryHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "signature", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_chain_checkpoints_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_chain_checkpoints_sequence_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "audit_exports", + "dartName": "AuditExport", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestedBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "filtersJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "format", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rowCount", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "firstSequence", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastSequence", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentSha256", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chainState", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "audit_exports_fk_0", + "columns": [ + "requestedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_exports_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_exports_requested_at_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "requestedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "audit_logs", + "dartName": "AuditLog", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actionType", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resourceType", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resourceId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ipHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "result", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sequence", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "previousHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "entryHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "audit_logs_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_sequence_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_timestamp_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "timestamp" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_user_sequence_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "consent_logs", + "dartName": "ConsentLog", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "purpose", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "action", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ipHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userAgent", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "signature", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "consent_logs_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "consent_logs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "data_subject_requests", + "dartName": "DataSubjectRequest", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestType", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:DataSubjectRequestType" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "detailsEncrypted", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "detailsKeyVersion", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:DataSubjectRequestStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "dueAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "decidedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "decidedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resolutionEncrypted", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resolutionKeyVersion", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "data_subject_requests_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "data_subject_requests_fk_1", + "columns": [ + "decidedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "data_subject_requests_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "data_subject_requests_user_id_type_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "requestType" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "enrollment_tokens", + "dartName": "EnrollmentToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tokenHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdByAcsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "consumedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "enrollment_tokens_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "enrollment_tokens_fk_1", + "columns": [ + "createdByAcsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "enrollment_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "enrollment_tokens_token_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "tokenHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "micro_areas", + "dartName": "MicroArea", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "geoJsonBoundary", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "micro_areas_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "micro_areas_ubs_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ubsId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "otp_challenges", + "dartName": "OtpChallenge", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "codeHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "consumedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "otp_challenges_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "otp_challenges_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "otp_challenges_user_id_created_at_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "patients", + "dartName": "Patient", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "emergencyContactEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "emergencyContactKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isChronic", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chronicConditionsEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chronicConditionsKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastLocationHash", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastTriageAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "patients_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "push_tokens", + "dartName": "PushToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "token", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "platform", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "push_tokens_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "push_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "push_tokens_token_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "token" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "push_tokens_user_id_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "scheduled_notices", + "dartName": "ScheduledNotice", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "audience", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "message", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scheduledFor", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ScheduledNoticeStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "recipients", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "accepted", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "failureReason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sentAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "scheduled_notices_fk_0", + "columns": [ + "createdBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "scheduled_notices_fk_1", + "columns": [ + "microAreaId" + ], + "referenceTable": "micro_areas", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "scheduled_notices_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "scheduled_notices_due_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "scheduledFor" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "scheduled_notices_area_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "staff_accounts", + "dartName": "StaffAccount", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enrollmentId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "active", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeHash", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeExpiresAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeIssuedBy", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeIssuedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "staff_accounts_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "staff_accounts_enrollment_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "enrollmentId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "terms_versions", + "dartName": "TermsVersion", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "summary", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "effectiveFrom", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "legallyReviewedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "legallyReviewedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "terms_versions_fk_0", + "columns": [ + "createdBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "terms_versions_fk_1", + "columns": [ + "legallyReviewedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "terms_versions_fk_2", + "columns": [ + "publishedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "terms_versions_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "terms_versions_version_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "version" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "triage_sessions", + "dartName": "TriageSession", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "answersEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "answersKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resultRiskEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resultRiskKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "triage_sessions_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "triage_sessions_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "ubs", + "dartName": "Ubs", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "address", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "city", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "state", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contactPhone", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ubs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "user_credentials", + "dartName": "UserCredential", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "passwordHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "passwordSalt", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "memoryKb", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "iterations", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "parallelism", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "failedAttempts", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lockedUntil", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lockStreak", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpSecretEncrypted", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpKeyVersion", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpEnabledAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpLastStep", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "user_credentials_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_credentials_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_credentials_user_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "users", + "dartName": "User", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cpfHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "birthDate", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "role", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:UserRole" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_cpf_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "cpfHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_micro_area_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "visits", + "dartName": "Visit", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorship", + "columnType": 0, + "isNullable": false, + "columnDefault": "'acs'::text", + "dartType": "protocol:VisitAuthorship" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "originDeviceId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scheduledAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "startedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "completedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelBeforeEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelBeforeKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelAfterEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelAfterKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "notesEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "notesKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "syncStatus", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:SyncStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "arrivalMethod", + "columnType": 0, + "isNullable": false, + "columnDefault": "'manual'::text", + "dartType": "protocol:ArrivalMethod" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "localId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "syncAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "visits_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "visits_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "visits_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "visits_local_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "localId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_cloud_storage", + "dartName": "CloudStorageEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_cloud_storage_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "storageId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "path", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "addedTime", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiration", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "byteData", + "columnType": 5, + "isNullable": false, + "dartType": "dart:typed_data:ByteData" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "verified", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_path_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "storageId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "path" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_expiration", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "expiration" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_cloud_storage_direct_upload", + "dartName": "CloudStorageDirectUploadEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_cloud_storage_direct_upload_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "storageId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "path", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiration", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_direct_upload_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_direct_upload_storage_path", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "storageId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "path" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_future_call", + "dartName": "FutureCallEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_future_call_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "time", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serializedObject", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "identifier", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_future_call_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_future_call_time_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "time" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_future_call_serverId_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "serverId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_future_call_identifier_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "identifier" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_health_connection_info", + "dartName": "ServerHealthConnectionInfo", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_health_connection_info_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "active", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "closing", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "idle", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "granularity", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_health_connection_info_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_health_connection_info_timestamp_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "timestamp" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "serverId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "granularity" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_health_metric", + "dartName": "ServerHealthMetric", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_health_metric_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isHealthy", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "value", + "columnType": 3, + "isNullable": false, + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "granularity", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_health_metric_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_health_metric_timestamp_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "timestamp" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "serverId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "name" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "granularity" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_log", + "dartName": "LogEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_log_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sessionLogId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "messageId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reference", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "time", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logLevel", + "columnType": 6, + "isNullable": false, + "dartType": "protocol:LogLevel" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "message", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "order", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_log_fk_0", + "columns": [ + "sessionLogId" + ], + "referenceTable": "serverpod_session_log", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_log_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_log_sessionLogId_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sessionLogId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_message_log", + "dartName": "MessageLogEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_message_log_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sessionLogId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "messageId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "endpoint", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "messageName", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "duration", + "columnType": 3, + "isNullable": false, + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slow", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "order", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_message_log_fk_0", + "columns": [ + "sessionLogId" + ], + "referenceTable": "serverpod_session_log", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_message_log_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_method", + "dartName": "MethodInfo", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_method_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "endpoint", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "method", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_method_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_method_endpoint_method_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "endpoint" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "method" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_migrations", + "dartName": "DatabaseMigrationVersion", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_migrations_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "module", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_migrations_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_migrations_ids", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "module" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_query_log", + "dartName": "QueryLogEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_query_log_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sessionLogId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "messageId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "query", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "duration", + "columnType": 3, + "isNullable": false, + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "numRows", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slow", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "order", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_query_log_fk_0", + "columns": [ + "sessionLogId" + ], + "referenceTable": "serverpod_session_log", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_query_log_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_query_log_sessionLogId_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sessionLogId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_readwrite_test", + "dartName": "ReadWriteTestEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_readwrite_test_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "number", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_readwrite_test_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_runtime_settings", + "dartName": "RuntimeSettings", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_runtime_settings_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logSettings", + "columnType": 8, + "isNullable": false, + "dartType": "protocol:LogSettings" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logSettingsOverrides", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logServiceCalls", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logMalformedCalls", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_runtime_settings_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_session_log", + "dartName": "SessionLogEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_session_log_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "time", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "module", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "endpoint", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "method", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "duration", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "numQueries", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slow", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authenticatedUserId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isOpen", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "touched", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_serverid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "serverId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_time_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "time" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_touched_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "touched" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_isopen_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "isOpen" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + } + ], + "installedModules": [ + { + "__className__": "serverpod.DatabaseMigrationVersion", + "module": "sinalacs", + "version": "20261016140159775" + }, + { + "__className__": "serverpod.DatabaseMigrationVersion", + "module": "serverpod", + "version": "20260129180959368" + } + ], + "migrationApiVersion": 1 +} \ No newline at end of file diff --git a/backend/sinalacs_server/migrations/20261016140159775/definition.sql b/backend/sinalacs_server/migrations/20261016140159775/definition.sql new file mode 100644 index 0000000..872eda0 --- /dev/null +++ b/backend/sinalacs_server/migrations/20261016140159775/definition.sql @@ -0,0 +1,938 @@ +BEGIN; + +-- +-- Class Acs as table acs +-- +CREATE TABLE "acs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "enrollmentId" text NOT NULL, + "ubsId" uuid NOT NULL, + "active" boolean NOT NULL, + "lastSyncAt" timestamp without time zone +); + +-- Indexes +CREATE INDEX "acs_ubs_idx" ON "acs" USING btree ("ubsId"); +CREATE UNIQUE INDEX "acs_enrollment_id_key" ON "acs" USING btree ("enrollmentId"); + +-- +-- Class AcsRefreshToken as table acs_refresh_tokens +-- +CREATE TABLE "acs_refresh_tokens" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "familyId" uuid NOT NULL, + "tokenHash" text NOT NULL, + "deviceId" text NOT NULL, + "issuedAt" timestamp without time zone NOT NULL, + "idleExpiresAt" timestamp without time zone NOT NULL, + "absoluteExpiresAt" timestamp without time zone NOT NULL, + "rotatedAt" timestamp without time zone, + "revokedAt" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "acs_refresh_tokens_hash_key" ON "acs_refresh_tokens" USING btree ("tokenHash"); +CREATE INDEX "acs_refresh_tokens_family_idx" ON "acs_refresh_tokens" USING btree ("familyId"); +CREATE INDEX "acs_refresh_tokens_user_idx" ON "acs_refresh_tokens" USING btree ("userId"); + +-- +-- Class AcsUploadToken as table acs_upload_tokens +-- +CREATE TABLE "acs_upload_tokens" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "tokenHash" text NOT NULL, + "deviceId" text NOT NULL, + "issuedAt" timestamp without time zone NOT NULL, + "expiresAt" timestamp without time zone NOT NULL, + "revokedAt" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "acs_upload_tokens_hash_key" ON "acs_upload_tokens" USING btree ("tokenHash"); +CREATE INDEX "acs_upload_tokens_user_device_idx" ON "acs_upload_tokens" USING btree ("userId", "deviceId"); + +-- +-- Class AlertDeliveryRecord as table alert_deliveries +-- +CREATE TABLE "alert_deliveries" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "alertId" uuid NOT NULL, + "acsId" uuid NOT NULL, + "acknowledgedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "alert_deliveries_alert_acs_key" ON "alert_deliveries" USING btree ("alertId", "acsId"); + +-- +-- Class AlertIdempotencyKey as table alert_idempotency_keys +-- +CREATE TABLE "alert_idempotency_keys" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "key" text NOT NULL, + "alertId" uuid NOT NULL, + "locationHash" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "alert_idempotency_keys_key_key" ON "alert_idempotency_keys" USING btree ("key"); + +-- +-- Class AlertOutboxEntry as table alert_outbox +-- +CREATE TABLE "alert_outbox" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "alertId" uuid NOT NULL, + "topic" text NOT NULL, + "payload" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "publishedAt" timestamp without time zone, + "attempts" bigint NOT NULL, + "nextAttemptAt" timestamp without time zone NOT NULL, + "lastError" text +); + +-- Indexes +CREATE INDEX "alert_outbox_pending_idx" ON "alert_outbox" USING btree ("publishedAt", "nextAttemptAt"); + +-- +-- Class Alert as table alerts +-- +CREATE TABLE "alerts" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "patientId" uuid NOT NULL, + "acsId" uuid, + "microAreaId" uuid, + "triggeredAt" timestamp without time zone NOT NULL, + "receivedAt" timestamp without time zone, + "respondedAt" timestamp without time zone, + "acknowledgedAt" timestamp without time zone, + "riskLevel" text NOT NULL, + "locationHash" text NOT NULL, + "locationCell" text, + "status" text NOT NULL, + "mqttTopic" text NOT NULL, + "deviceId" text NOT NULL, + "retryCount" bigint NOT NULL, + "version" bigint NOT NULL, + "escalatedAt" timestamp without time zone, + "escalatedBy" uuid, + "escalationReason" text +); + +-- Indexes +CREATE INDEX "alerts_micro_area_status_idx" ON "alerts" USING btree ("microAreaId", "status", "triggeredAt"); + +-- +-- Class AuditChainCheckpoint as table audit_chain_checkpoints +-- +CREATE TABLE "audit_chain_checkpoints" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "sequence" bigint NOT NULL, + "entryHash" text NOT NULL, + "signature" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "audit_chain_checkpoints_sequence_idx" ON "audit_chain_checkpoints" USING btree ("sequence"); + +-- +-- Class AuditExport as table audit_exports +-- +CREATE TABLE "audit_exports" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "requestedBy" uuid NOT NULL, + "requestedAt" timestamp without time zone NOT NULL, + "filtersJson" text NOT NULL, + "format" text NOT NULL, + "rowCount" bigint NOT NULL, + "firstSequence" bigint, + "lastSequence" bigint, + "contentSha256" text NOT NULL, + "chainState" text NOT NULL +); + +-- Indexes +CREATE INDEX "audit_exports_requested_at_idx" ON "audit_exports" USING btree ("requestedAt"); + +-- +-- Class AuditLog as table audit_logs +-- +CREATE TABLE "audit_logs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "actionType" text NOT NULL, + "resourceType" text NOT NULL, + "resourceId" uuid, + "timestamp" timestamp without time zone NOT NULL, + "ipHash" text NOT NULL, + "result" text NOT NULL, + "sequence" bigint NOT NULL, + "previousHash" text NOT NULL, + "entryHash" text NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "audit_logs_sequence_idx" ON "audit_logs" USING btree ("sequence"); +CREATE INDEX "audit_logs_timestamp_idx" ON "audit_logs" USING btree ("timestamp"); +CREATE INDEX "audit_logs_user_sequence_idx" ON "audit_logs" USING btree ("userId", "sequence"); + +-- +-- Class ConsentLog as table consent_logs +-- +CREATE TABLE "consent_logs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "purpose" text NOT NULL, + "action" text NOT NULL, + "version" text NOT NULL, + "timestamp" timestamp without time zone NOT NULL, + "ipHash" text NOT NULL, + "userAgent" text NOT NULL, + "signature" text NOT NULL +); + +-- +-- Class DataSubjectRequest as table data_subject_requests +-- +CREATE TABLE "data_subject_requests" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "requestType" text NOT NULL, + "detailsEncrypted" text NOT NULL, + "detailsKeyVersion" bigint NOT NULL, + "status" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "dueAt" timestamp without time zone NOT NULL, + "decidedAt" timestamp without time zone, + "decidedBy" uuid, + "resolutionEncrypted" text, + "resolutionKeyVersion" bigint +); + +-- Indexes +CREATE INDEX "data_subject_requests_user_id_type_idx" ON "data_subject_requests" USING btree ("userId", "requestType"); + +-- +-- Class EnrollmentToken as table enrollment_tokens +-- +CREATE TABLE "enrollment_tokens" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "tokenHash" text NOT NULL, + "patientId" uuid NOT NULL, + "microAreaId" uuid NOT NULL, + "createdByAcsId" uuid NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "expiresAt" timestamp without time zone NOT NULL, + "consumedAt" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "enrollment_tokens_token_hash_key" ON "enrollment_tokens" USING btree ("tokenHash"); + +-- +-- Class MicroArea as table micro_areas +-- +CREATE TABLE "micro_areas" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "name" text NOT NULL, + "ubsId" uuid NOT NULL, + "geoJsonBoundary" text NOT NULL +); + +-- Indexes +CREATE INDEX "micro_areas_ubs_idx" ON "micro_areas" USING btree ("ubsId"); + +-- +-- Class OtpChallenge as table otp_challenges +-- +CREATE TABLE "otp_challenges" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "codeHash" text NOT NULL, + "attempts" bigint NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "expiresAt" timestamp without time zone NOT NULL, + "consumedAt" timestamp without time zone +); + +-- Indexes +CREATE INDEX "otp_challenges_user_id_created_at_idx" ON "otp_challenges" USING btree ("userId", "createdAt"); + +-- +-- Class Patient as table patients +-- +CREATE TABLE "patients" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "emergencyContactEncrypted" text NOT NULL DEFAULT ''::text, + "emergencyContactKeyVersion" bigint NOT NULL DEFAULT 1, + "isChronic" boolean NOT NULL, + "chronicConditionsEncrypted" text NOT NULL DEFAULT ''::text, + "chronicConditionsKeyVersion" bigint NOT NULL DEFAULT 1, + "lastLocationHash" text, + "lastTriageAt" timestamp without time zone +); + +-- +-- Class PushToken as table push_tokens +-- +CREATE TABLE "push_tokens" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "microAreaId" uuid, + "token" text NOT NULL, + "platform" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "updatedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "push_tokens_token_key" ON "push_tokens" USING btree ("token"); +CREATE INDEX "push_tokens_user_id_idx" ON "push_tokens" USING btree ("userId"); + +-- +-- Class ScheduledNotice as table scheduled_notices +-- +CREATE TABLE "scheduled_notices" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "createdBy" uuid NOT NULL, + "microAreaId" uuid NOT NULL, + "audience" text NOT NULL, + "title" text NOT NULL, + "message" text NOT NULL, + "scheduledFor" timestamp without time zone NOT NULL, + "status" text NOT NULL, + "recipients" bigint, + "accepted" bigint, + "failureReason" text, + "createdAt" timestamp without time zone NOT NULL, + "sentAt" timestamp without time zone +); + +-- Indexes +CREATE INDEX "scheduled_notices_due_idx" ON "scheduled_notices" USING btree ("status", "scheduledFor"); +CREATE INDEX "scheduled_notices_area_idx" ON "scheduled_notices" USING btree ("microAreaId", "createdAt"); + +-- +-- Class StaffAccount as table staff_accounts +-- +CREATE TABLE "staff_accounts" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "enrollmentId" text NOT NULL, + "active" boolean NOT NULL, + "ubsId" uuid, + "activationCodeHash" text, + "activationCodeExpiresAt" timestamp without time zone, + "activationCodeIssuedBy" text, + "activationCodeIssuedAt" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "staff_accounts_enrollment_id_key" ON "staff_accounts" USING btree ("enrollmentId"); + +-- +-- Class TermsVersion as table terms_versions +-- +CREATE TABLE "terms_versions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "version" text NOT NULL, + "summary" text NOT NULL, + "effectiveFrom" timestamp without time zone NOT NULL, + "createdBy" uuid NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "legallyReviewedAt" timestamp without time zone, + "legallyReviewedBy" uuid, + "publishedAt" timestamp without time zone, + "publishedBy" uuid +); + +-- Indexes +CREATE UNIQUE INDEX "terms_versions_version_key" ON "terms_versions" USING btree ("version"); + +-- +-- Class TriageSession as table triage_sessions +-- +CREATE TABLE "triage_sessions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "patientId" uuid NOT NULL, + "answersEncrypted" text NOT NULL DEFAULT ''::text, + "answersKeyVersion" bigint NOT NULL DEFAULT 1, + "resultRiskEncrypted" text NOT NULL DEFAULT ''::text, + "resultRiskKeyVersion" bigint NOT NULL DEFAULT 1, + "createdAt" timestamp without time zone NOT NULL, + "deviceId" text NOT NULL +); + +-- +-- Class Ubs as table ubs +-- +CREATE TABLE "ubs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "name" text NOT NULL, + "address" text NOT NULL, + "city" text NOT NULL, + "state" text NOT NULL, + "contactPhone" text +); + +-- +-- Class UserCredential as table user_credentials +-- +CREATE TABLE "user_credentials" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "passwordHash" text NOT NULL, + "passwordSalt" text NOT NULL, + "memoryKb" bigint NOT NULL, + "iterations" bigint NOT NULL, + "parallelism" bigint NOT NULL, + "failedAttempts" bigint NOT NULL, + "lockedUntil" timestamp without time zone, + "lockStreak" bigint NOT NULL DEFAULT 0, + "createdAt" timestamp without time zone NOT NULL, + "updatedAt" timestamp without time zone NOT NULL, + "totpSecretEncrypted" text, + "totpKeyVersion" bigint, + "totpEnabledAt" timestamp without time zone, + "totpLastStep" bigint +); + +-- Indexes +CREATE UNIQUE INDEX "user_credentials_user_id_key" ON "user_credentials" USING btree ("userId"); + +-- +-- Class User as table users +-- +CREATE TABLE "users" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "cpfHash" text NOT NULL, + "name" text NOT NULL, + "birthDate" timestamp without time zone NOT NULL, + "role" text NOT NULL, + "microAreaId" uuid, + "createdAt" timestamp without time zone NOT NULL, + "updatedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "users_cpf_hash_key" ON "users" USING btree ("cpfHash"); +CREATE INDEX "users_micro_area_idx" ON "users" USING btree ("microAreaId"); + +-- +-- Class Visit as table visits +-- +CREATE TABLE "visits" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "patientId" uuid NOT NULL, + "acsId" uuid, + "authorship" text NOT NULL DEFAULT 'acs'::text, + "originDeviceId" text, + "scheduledAt" timestamp without time zone NOT NULL, + "startedAt" timestamp without time zone, + "completedAt" timestamp without time zone, + "status" text NOT NULL, + "riskLevelBeforeEncrypted" text NOT NULL DEFAULT ''::text, + "riskLevelBeforeKeyVersion" bigint NOT NULL DEFAULT 1, + "riskLevelAfterEncrypted" text NOT NULL DEFAULT ''::text, + "riskLevelAfterKeyVersion" bigint NOT NULL DEFAULT 1, + "notesEncrypted" text NOT NULL DEFAULT ''::text, + "notesKeyVersion" bigint NOT NULL DEFAULT 1, + "syncStatus" text NOT NULL, + "arrivalMethod" text NOT NULL DEFAULT 'manual'::text, + "localId" uuid NOT NULL, + "syncAt" timestamp without time zone, + "version" bigint NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "visits_local_id_key" ON "visits" USING btree ("localId"); + +-- +-- Class CloudStorageEntry as table serverpod_cloud_storage +-- +CREATE TABLE "serverpod_cloud_storage" ( + "id" bigserial PRIMARY KEY, + "storageId" text NOT NULL, + "path" text NOT NULL, + "addedTime" timestamp without time zone NOT NULL, + "expiration" timestamp without time zone, + "byteData" bytea NOT NULL, + "verified" boolean NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_cloud_storage_path_idx" ON "serverpod_cloud_storage" USING btree ("storageId", "path"); +CREATE INDEX "serverpod_cloud_storage_expiration" ON "serverpod_cloud_storage" USING btree ("expiration"); + +-- +-- Class CloudStorageDirectUploadEntry as table serverpod_cloud_storage_direct_upload +-- +CREATE TABLE "serverpod_cloud_storage_direct_upload" ( + "id" bigserial PRIMARY KEY, + "storageId" text NOT NULL, + "path" text NOT NULL, + "expiration" timestamp without time zone NOT NULL, + "authKey" text NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_cloud_storage_direct_upload_storage_path" ON "serverpod_cloud_storage_direct_upload" USING btree ("storageId", "path"); + +-- +-- Class FutureCallEntry as table serverpod_future_call +-- +CREATE TABLE "serverpod_future_call" ( + "id" bigserial PRIMARY KEY, + "name" text NOT NULL, + "time" timestamp without time zone NOT NULL, + "serializedObject" text, + "serverId" text NOT NULL, + "identifier" text +); + +-- Indexes +CREATE INDEX "serverpod_future_call_time_idx" ON "serverpod_future_call" USING btree ("time"); +CREATE INDEX "serverpod_future_call_serverId_idx" ON "serverpod_future_call" USING btree ("serverId"); +CREATE INDEX "serverpod_future_call_identifier_idx" ON "serverpod_future_call" USING btree ("identifier"); + +-- +-- Class ServerHealthConnectionInfo as table serverpod_health_connection_info +-- +CREATE TABLE "serverpod_health_connection_info" ( + "id" bigserial PRIMARY KEY, + "serverId" text NOT NULL, + "timestamp" timestamp without time zone NOT NULL, + "active" bigint NOT NULL, + "closing" bigint NOT NULL, + "idle" bigint NOT NULL, + "granularity" bigint NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_health_connection_info_timestamp_idx" ON "serverpod_health_connection_info" USING btree ("timestamp", "serverId", "granularity"); + +-- +-- Class ServerHealthMetric as table serverpod_health_metric +-- +CREATE TABLE "serverpod_health_metric" ( + "id" bigserial PRIMARY KEY, + "name" text NOT NULL, + "serverId" text NOT NULL, + "timestamp" timestamp without time zone NOT NULL, + "isHealthy" boolean NOT NULL, + "value" double precision NOT NULL, + "granularity" bigint NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_health_metric_timestamp_idx" ON "serverpod_health_metric" USING btree ("timestamp", "serverId", "name", "granularity"); + +-- +-- Class LogEntry as table serverpod_log +-- +CREATE TABLE "serverpod_log" ( + "id" bigserial PRIMARY KEY, + "sessionLogId" bigint NOT NULL, + "messageId" bigint, + "reference" text, + "serverId" text NOT NULL, + "time" timestamp without time zone NOT NULL, + "logLevel" bigint NOT NULL, + "message" text NOT NULL, + "error" text, + "stackTrace" text, + "order" bigint NOT NULL +); + +-- Indexes +CREATE INDEX "serverpod_log_sessionLogId_idx" ON "serverpod_log" USING btree ("sessionLogId"); + +-- +-- Class MessageLogEntry as table serverpod_message_log +-- +CREATE TABLE "serverpod_message_log" ( + "id" bigserial PRIMARY KEY, + "sessionLogId" bigint NOT NULL, + "serverId" text NOT NULL, + "messageId" bigint NOT NULL, + "endpoint" text NOT NULL, + "messageName" text NOT NULL, + "duration" double precision NOT NULL, + "error" text, + "stackTrace" text, + "slow" boolean NOT NULL, + "order" bigint NOT NULL +); + +-- +-- Class MethodInfo as table serverpod_method +-- +CREATE TABLE "serverpod_method" ( + "id" bigserial PRIMARY KEY, + "endpoint" text NOT NULL, + "method" text NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_method_endpoint_method_idx" ON "serverpod_method" USING btree ("endpoint", "method"); + +-- +-- Class DatabaseMigrationVersion as table serverpod_migrations +-- +CREATE TABLE "serverpod_migrations" ( + "id" bigserial PRIMARY KEY, + "module" text NOT NULL, + "version" text NOT NULL, + "timestamp" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_migrations_ids" ON "serverpod_migrations" USING btree ("module"); + +-- +-- Class QueryLogEntry as table serverpod_query_log +-- +CREATE TABLE "serverpod_query_log" ( + "id" bigserial PRIMARY KEY, + "serverId" text NOT NULL, + "sessionLogId" bigint NOT NULL, + "messageId" bigint, + "query" text NOT NULL, + "duration" double precision NOT NULL, + "numRows" bigint, + "error" text, + "stackTrace" text, + "slow" boolean NOT NULL, + "order" bigint NOT NULL +); + +-- Indexes +CREATE INDEX "serverpod_query_log_sessionLogId_idx" ON "serverpod_query_log" USING btree ("sessionLogId"); + +-- +-- Class ReadWriteTestEntry as table serverpod_readwrite_test +-- +CREATE TABLE "serverpod_readwrite_test" ( + "id" bigserial PRIMARY KEY, + "number" bigint NOT NULL +); + +-- +-- Class RuntimeSettings as table serverpod_runtime_settings +-- +CREATE TABLE "serverpod_runtime_settings" ( + "id" bigserial PRIMARY KEY, + "logSettings" json NOT NULL, + "logSettingsOverrides" json NOT NULL, + "logServiceCalls" boolean NOT NULL, + "logMalformedCalls" boolean NOT NULL +); + +-- +-- Class SessionLogEntry as table serverpod_session_log +-- +CREATE TABLE "serverpod_session_log" ( + "id" bigserial PRIMARY KEY, + "serverId" text NOT NULL, + "time" timestamp without time zone NOT NULL, + "module" text, + "endpoint" text, + "method" text, + "duration" double precision, + "numQueries" bigint, + "slow" boolean, + "error" text, + "stackTrace" text, + "authenticatedUserId" bigint, + "userId" text, + "isOpen" boolean, + "touched" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "serverpod_session_log_serverid_idx" ON "serverpod_session_log" USING btree ("serverId"); +CREATE INDEX "serverpod_session_log_time_idx" ON "serverpod_session_log" USING btree ("time"); +CREATE INDEX "serverpod_session_log_touched_idx" ON "serverpod_session_log" USING btree ("touched"); +CREATE INDEX "serverpod_session_log_isopen_idx" ON "serverpod_session_log" USING btree ("isOpen"); + +-- +-- Foreign relations for "acs_refresh_tokens" table +-- +ALTER TABLE ONLY "acs_refresh_tokens" + ADD CONSTRAINT "acs_refresh_tokens_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "acs_upload_tokens" table +-- +ALTER TABLE ONLY "acs_upload_tokens" + ADD CONSTRAINT "acs_upload_tokens_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "alert_deliveries" table +-- +ALTER TABLE ONLY "alert_deliveries" + ADD CONSTRAINT "alert_deliveries_fk_0" + FOREIGN KEY("alertId") + REFERENCES "alerts"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "alert_deliveries" + ADD CONSTRAINT "alert_deliveries_fk_1" + FOREIGN KEY("acsId") + REFERENCES "acs"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "alert_idempotency_keys" table +-- +ALTER TABLE ONLY "alert_idempotency_keys" + ADD CONSTRAINT "alert_idempotency_keys_fk_0" + FOREIGN KEY("alertId") + REFERENCES "alerts"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "alert_outbox" table +-- +ALTER TABLE ONLY "alert_outbox" + ADD CONSTRAINT "alert_outbox_fk_0" + FOREIGN KEY("alertId") + REFERENCES "alerts"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "alerts" table +-- +ALTER TABLE ONLY "alerts" + ADD CONSTRAINT "alerts_fk_0" + FOREIGN KEY("patientId") + REFERENCES "patients"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "alerts" + ADD CONSTRAINT "alerts_fk_1" + FOREIGN KEY("acsId") + REFERENCES "acs"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "audit_exports" table +-- +ALTER TABLE ONLY "audit_exports" + ADD CONSTRAINT "audit_exports_fk_0" + FOREIGN KEY("requestedBy") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "audit_logs" table +-- +ALTER TABLE ONLY "audit_logs" + ADD CONSTRAINT "audit_logs_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "consent_logs" table +-- +ALTER TABLE ONLY "consent_logs" + ADD CONSTRAINT "consent_logs_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "data_subject_requests" table +-- +ALTER TABLE ONLY "data_subject_requests" + ADD CONSTRAINT "data_subject_requests_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "data_subject_requests" + ADD CONSTRAINT "data_subject_requests_fk_1" + FOREIGN KEY("decidedBy") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "enrollment_tokens" table +-- +ALTER TABLE ONLY "enrollment_tokens" + ADD CONSTRAINT "enrollment_tokens_fk_0" + FOREIGN KEY("patientId") + REFERENCES "patients"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "enrollment_tokens" + ADD CONSTRAINT "enrollment_tokens_fk_1" + FOREIGN KEY("createdByAcsId") + REFERENCES "acs"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "otp_challenges" table +-- +ALTER TABLE ONLY "otp_challenges" + ADD CONSTRAINT "otp_challenges_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "push_tokens" table +-- +ALTER TABLE ONLY "push_tokens" + ADD CONSTRAINT "push_tokens_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "scheduled_notices" table +-- +ALTER TABLE ONLY "scheduled_notices" + ADD CONSTRAINT "scheduled_notices_fk_0" + FOREIGN KEY("createdBy") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "scheduled_notices" + ADD CONSTRAINT "scheduled_notices_fk_1" + FOREIGN KEY("microAreaId") + REFERENCES "micro_areas"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "terms_versions" table +-- +ALTER TABLE ONLY "terms_versions" + ADD CONSTRAINT "terms_versions_fk_0" + FOREIGN KEY("createdBy") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "terms_versions" + ADD CONSTRAINT "terms_versions_fk_1" + FOREIGN KEY("legallyReviewedBy") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "terms_versions" + ADD CONSTRAINT "terms_versions_fk_2" + FOREIGN KEY("publishedBy") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "triage_sessions" table +-- +ALTER TABLE ONLY "triage_sessions" + ADD CONSTRAINT "triage_sessions_fk_0" + FOREIGN KEY("patientId") + REFERENCES "patients"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "user_credentials" table +-- +ALTER TABLE ONLY "user_credentials" + ADD CONSTRAINT "user_credentials_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "visits" table +-- +ALTER TABLE ONLY "visits" + ADD CONSTRAINT "visits_fk_0" + FOREIGN KEY("patientId") + REFERENCES "patients"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "visits" + ADD CONSTRAINT "visits_fk_1" + FOREIGN KEY("acsId") + REFERENCES "acs"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_log" table +-- +ALTER TABLE ONLY "serverpod_log" + ADD CONSTRAINT "serverpod_log_fk_0" + FOREIGN KEY("sessionLogId") + REFERENCES "serverpod_session_log"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_message_log" table +-- +ALTER TABLE ONLY "serverpod_message_log" + ADD CONSTRAINT "serverpod_message_log_fk_0" + FOREIGN KEY("sessionLogId") + REFERENCES "serverpod_session_log"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_query_log" table +-- +ALTER TABLE ONLY "serverpod_query_log" + ADD CONSTRAINT "serverpod_query_log_fk_0" + FOREIGN KEY("sessionLogId") + REFERENCES "serverpod_session_log"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + + +-- +-- MIGRATION VERSION FOR sinalacs +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('sinalacs', '20261016140159775', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20261016140159775', "timestamp" = now(); + +-- +-- MIGRATION VERSION FOR serverpod +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('serverpod', '20260129180959368', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20260129180959368', "timestamp" = now(); + + +COMMIT; diff --git a/backend/sinalacs_server/migrations/20261016140159775/definition_project.json b/backend/sinalacs_server/migrations/20261016140159775/definition_project.json new file mode 100644 index 0000000..a420ad7 --- /dev/null +++ b/backend/sinalacs_server/migrations/20261016140159775/definition_project.json @@ -0,0 +1,3186 @@ +{ + "__className__": "serverpod.DatabaseDefinition", + "moduleName": "sinalacs", + "tables": [ + { + "__className__": "serverpod.TableDefinition", + "name": "acs", + "dartName": "Acs", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enrollmentId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "active", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastSyncAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_ubs_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ubsId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_enrollment_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "enrollmentId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "acs_refresh_tokens", + "dartName": "AcsRefreshToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "familyId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tokenHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "issuedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "idleExpiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "absoluteExpiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rotatedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revokedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "acs_refresh_tokens_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "tokenHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_family_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "familyId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "acs_upload_tokens", + "dartName": "AcsUploadToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tokenHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "issuedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revokedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "acs_upload_tokens_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_upload_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_upload_tokens_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "tokenHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_upload_tokens_user_device_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "deviceId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_deliveries", + "dartName": "AlertDeliveryRecord", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acknowledgedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_deliveries_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_deliveries_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_deliveries_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_deliveries_alert_acs_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "alertId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "acsId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_idempotency_keys", + "dartName": "AlertIdempotencyKey", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "key", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_idempotency_keys_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_idempotency_keys_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_idempotency_keys_key_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "key" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_outbox", + "dartName": "AlertOutboxEntry", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "topic", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "payload", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "nextAttemptAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastError", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_outbox_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_outbox_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_outbox_pending_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "publishedAt" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "nextAttemptAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alerts", + "dartName": "Alert", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triggeredAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "receivedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "respondedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acknowledgedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevel", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:RiskLevel" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationCell", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:AlertStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "mqttTopic", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "retryCount", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "escalatedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "escalatedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "escalationReason", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:EscalationReason?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alerts_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alerts_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alerts_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alerts_micro_area_status_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "triggeredAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "audit_chain_checkpoints", + "dartName": "AuditChainCheckpoint", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sequence", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "entryHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "signature", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_chain_checkpoints_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_chain_checkpoints_sequence_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "audit_exports", + "dartName": "AuditExport", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestedBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "filtersJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "format", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rowCount", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "firstSequence", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastSequence", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentSha256", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chainState", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "audit_exports_fk_0", + "columns": [ + "requestedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_exports_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_exports_requested_at_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "requestedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "audit_logs", + "dartName": "AuditLog", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actionType", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resourceType", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resourceId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ipHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "result", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sequence", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "previousHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "entryHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "audit_logs_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_sequence_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_timestamp_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "timestamp" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_user_sequence_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "consent_logs", + "dartName": "ConsentLog", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "purpose", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "action", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ipHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userAgent", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "signature", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "consent_logs_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "consent_logs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "data_subject_requests", + "dartName": "DataSubjectRequest", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestType", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:DataSubjectRequestType" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "detailsEncrypted", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "detailsKeyVersion", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:DataSubjectRequestStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "dueAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "decidedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "decidedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resolutionEncrypted", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resolutionKeyVersion", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "data_subject_requests_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "data_subject_requests_fk_1", + "columns": [ + "decidedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "data_subject_requests_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "data_subject_requests_user_id_type_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "requestType" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "enrollment_tokens", + "dartName": "EnrollmentToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tokenHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdByAcsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "consumedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "enrollment_tokens_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "enrollment_tokens_fk_1", + "columns": [ + "createdByAcsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "enrollment_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "enrollment_tokens_token_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "tokenHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "micro_areas", + "dartName": "MicroArea", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "geoJsonBoundary", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "micro_areas_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "micro_areas_ubs_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ubsId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "otp_challenges", + "dartName": "OtpChallenge", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "codeHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "consumedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "otp_challenges_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "otp_challenges_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "otp_challenges_user_id_created_at_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "patients", + "dartName": "Patient", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "emergencyContactEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "emergencyContactKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isChronic", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chronicConditionsEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chronicConditionsKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastLocationHash", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastTriageAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "patients_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "push_tokens", + "dartName": "PushToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "token", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "platform", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "push_tokens_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "push_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "push_tokens_token_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "token" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "push_tokens_user_id_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "scheduled_notices", + "dartName": "ScheduledNotice", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "audience", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "message", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scheduledFor", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ScheduledNoticeStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "recipients", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "accepted", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "failureReason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sentAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "scheduled_notices_fk_0", + "columns": [ + "createdBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "scheduled_notices_fk_1", + "columns": [ + "microAreaId" + ], + "referenceTable": "micro_areas", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "scheduled_notices_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "scheduled_notices_due_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "scheduledFor" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "scheduled_notices_area_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "staff_accounts", + "dartName": "StaffAccount", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enrollmentId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "active", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeHash", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeExpiresAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeIssuedBy", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeIssuedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "staff_accounts_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "staff_accounts_enrollment_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "enrollmentId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "terms_versions", + "dartName": "TermsVersion", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "summary", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "effectiveFrom", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "legallyReviewedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "legallyReviewedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "terms_versions_fk_0", + "columns": [ + "createdBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "terms_versions_fk_1", + "columns": [ + "legallyReviewedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "terms_versions_fk_2", + "columns": [ + "publishedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "terms_versions_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "terms_versions_version_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "version" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "triage_sessions", + "dartName": "TriageSession", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "answersEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "answersKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resultRiskEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resultRiskKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "triage_sessions_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "triage_sessions_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "ubs", + "dartName": "Ubs", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "address", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "city", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "state", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contactPhone", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ubs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "user_credentials", + "dartName": "UserCredential", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "passwordHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "passwordSalt", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "memoryKb", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "iterations", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "parallelism", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "failedAttempts", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lockedUntil", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lockStreak", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpSecretEncrypted", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpKeyVersion", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpEnabledAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpLastStep", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "user_credentials_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_credentials_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_credentials_user_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "users", + "dartName": "User", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cpfHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "birthDate", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "role", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:UserRole" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_cpf_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "cpfHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_micro_area_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "visits", + "dartName": "Visit", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorship", + "columnType": 0, + "isNullable": false, + "columnDefault": "'acs'::text", + "dartType": "protocol:VisitAuthorship" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "originDeviceId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scheduledAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "startedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "completedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelBeforeEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelBeforeKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelAfterEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelAfterKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "notesEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "notesKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "syncStatus", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:SyncStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "arrivalMethod", + "columnType": 0, + "isNullable": false, + "columnDefault": "'manual'::text", + "dartType": "protocol:ArrivalMethod" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "localId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "syncAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "visits_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "visits_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "visits_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "visits_local_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "localId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + } + ], + "installedModules": [ + { + "__className__": "serverpod.DatabaseMigrationVersion", + "module": "serverpod", + "version": "20260129180959368" + } + ], + "migrationApiVersion": 1 +} \ No newline at end of file diff --git a/backend/sinalacs_server/migrations/20261016140159775/migration.json b/backend/sinalacs_server/migrations/20261016140159775/migration.json new file mode 100644 index 0000000..d34ff82 --- /dev/null +++ b/backend/sinalacs_server/migrations/20261016140159775/migration.json @@ -0,0 +1,102 @@ +{ + "__className__": "serverpod.DatabaseMigration", + "actions": [ + { + "__className__": "serverpod.DatabaseMigrationAction", + "type": "alterTable", + "alterTable": { + "__className__": "serverpod.TableMigration", + "name": "visits", + "schema": "public", + "addColumns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelBeforeEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelBeforeKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelAfterEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelAfterKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + } + ], + "deleteColumns": [ + "riskLevelBefore", + "riskLevelAfter" + ], + "modifyColumns": [], + "addIndexes": [], + "deleteIndexes": [], + "addForeignKeys": [], + "deleteForeignKeys": [], + "warnings": [ + { + "__className__": "serverpod.DatabaseMigrationWarning", + "type": "columnDropped", + "message": "Column \"riskLevelBefore\" of table \"visits\" will be dropped.", + "table": "visits", + "columns": [ + "riskLevelBefore" + ], + "destrucive": true + }, + { + "__className__": "serverpod.DatabaseMigrationWarning", + "type": "columnDropped", + "message": "Column \"riskLevelAfter\" of table \"visits\" will be dropped.", + "table": "visits", + "columns": [ + "riskLevelAfter" + ], + "destrucive": true + } + ] + } + } + ], + "warnings": [ + { + "__className__": "serverpod.DatabaseMigrationWarning", + "type": "columnDropped", + "message": "Column \"riskLevelBefore\" of table \"visits\" will be dropped.", + "table": "visits", + "columns": [ + "riskLevelBefore" + ], + "destrucive": true + }, + { + "__className__": "serverpod.DatabaseMigrationWarning", + "type": "columnDropped", + "message": "Column \"riskLevelAfter\" of table \"visits\" will be dropped.", + "table": "visits", + "columns": [ + "riskLevelAfter" + ], + "destrucive": true + } + ], + "migrationApiVersion": 1 +} \ No newline at end of file diff --git a/backend/sinalacs_server/migrations/20261016140159775/migration.sql b/backend/sinalacs_server/migrations/20261016140159775/migration.sql new file mode 100644 index 0000000..377d737 --- /dev/null +++ b/backend/sinalacs_server/migrations/20261016140159775/migration.sql @@ -0,0 +1,30 @@ +BEGIN; + +-- +-- ACTION ALTER TABLE +-- +ALTER TABLE "visits" DROP COLUMN "riskLevelBefore"; +ALTER TABLE "visits" DROP COLUMN "riskLevelAfter"; +ALTER TABLE "visits" ADD COLUMN "riskLevelBeforeEncrypted" text NOT NULL DEFAULT ''::text; +ALTER TABLE "visits" ADD COLUMN "riskLevelBeforeKeyVersion" bigint NOT NULL DEFAULT 1; +ALTER TABLE "visits" ADD COLUMN "riskLevelAfterEncrypted" text NOT NULL DEFAULT ''::text; +ALTER TABLE "visits" ADD COLUMN "riskLevelAfterKeyVersion" bigint NOT NULL DEFAULT 1; + +-- +-- MIGRATION VERSION FOR sinalacs +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('sinalacs', '20261016140159775', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20261016140159775', "timestamp" = now(); + +-- +-- MIGRATION VERSION FOR serverpod +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('serverpod', '20260129180959368', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20260129180959368', "timestamp" = now(); + + +COMMIT; diff --git a/backend/sinalacs_server/migrations/migration_registry.txt b/backend/sinalacs_server/migrations/migration_registry.txt index 992b20e..4a874d6 100644 --- a/backend/sinalacs_server/migrations/migration_registry.txt +++ b/backend/sinalacs_server/migrations/migration_registry.txt @@ -31,3 +31,4 @@ 20261014152407324 20261014155304871 20261016135918697 +20261016140159775 diff --git a/backend/sinalacs_server/test/integration/data_subject_case_store_test.dart b/backend/sinalacs_server/test/integration/data_subject_case_store_test.dart index 518fa15..1fc1f70 100644 --- a/backend/sinalacs_server/test/integration/data_subject_case_store_test.dart +++ b/backend/sinalacs_server/test/integration/data_subject_case_store_test.dart @@ -287,7 +287,6 @@ Future _prontuarioA(Session s) async { acsId: UuidValue.fromString(_acsId), scheduledAt: _t, status: 'realizada', - riskLevelBefore: RiskLevel.green, syncStatus: SyncStatus.synced, localId: UuidValue.fromString('00000000-0000-4000-8000-000000004231'), version: 1, diff --git a/backend/sinalacs_server/test/integration/health_data_encryption_test.dart b/backend/sinalacs_server/test/integration/health_data_encryption_test.dart index 225a0a1..49792e7 100644 --- a/backend/sinalacs_server/test/integration/health_data_encryption_test.dart +++ b/backend/sinalacs_server/test/integration/health_data_encryption_test.dart @@ -380,5 +380,128 @@ void main() { expect(puxadas, hasLength(1)); expect(puxadas.single.notes, const {_notaChave: _notaValor}); }); + + test('#75: nenhuma coluna de visits guarda a classificação de risco em ' + 'claro', () async { + final session = sessionBuilder.build(); + await _seed(session); + final relogio = DateTime.utc(2026, 9, 17, 12); + final service = VisitSyncService( + store: OrmVisitStore(session: () => session, cipher: testHealthDataCipher()), + audit: _SilentAuditTrail(), + clock: () => relogio, + ); + + await service.sync( + user: _acs, + entries: [ + VisitSyncEntry( + localId: _localId, + patientId: _patientId, + scheduledAt: DateTime.utc(2026, 9, 17, 9), + completedAt: DateTime.utc(2026, 9, 17, 10), + status: 'realizada', + riskLevelBefore: RiskLevel.yellow, + riskLevelAfter: RiskLevel.green, + notes: const {}, + version: 1, + arrivalMethod: ArrivalMethod.manual, + ), + ], + ); + + final linha = await _linhaBruta(session, table: 'visits'); + _expectSemRiscoLegivel(linha); + + final puxadas = await service.pull( + user: _acs, + since: relogio.subtract(const Duration(days: 1)), + ); + expect(puxadas.single.riskLevelBefore, RiskLevel.yellow); + expect(puxadas.single.riskLevelAfter, RiskLevel.green); + }); + + test('#75: riskLevelAfter nulo volta nulo e, na conclusão (servidor vai à ' + 'versão 2), o risco novo é regravado cifrado', () async { + final session = sessionBuilder.build(); + await _seed(session); + var relogio = DateTime.utc(2026, 9, 17, 12); + final service = VisitSyncService( + store: OrmVisitStore(session: () => session, cipher: testHealthDataCipher()), + audit: _SilentAuditTrail(), + clock: () => relogio, + ); + VisitSyncEntry entrada({required int version, RiskLevel? depois, DateTime? fim}) => + VisitSyncEntry( + localId: _localId, + patientId: _patientId, + scheduledAt: DateTime.utc(2026, 9, 17, 9), + completedAt: fim, + status: fim == null ? 'agendada' : 'realizada', + riskLevelBefore: RiskLevel.red, + riskLevelAfter: depois, + notes: const {}, + version: version, + arrivalMethod: ArrivalMethod.manual, + ); + + // O aparelho envia a versão-base: 0 na criação e de novo 0 na conclusão, + // que o servidor (já na 1) aceita como `existing.version - 1`. + await service.sync(user: _acs, entries: [entrada(version: 0)]); + var puxadas = await service.pull( + user: _acs, + since: relogio.subtract(const Duration(days: 1)), + ); + expect(puxadas.single.riskLevelAfter, isNull); + + relogio = relogio.add(const Duration(hours: 1)); + final resultado = await service.sync( + user: _acs, + entries: [ + entrada(version: 0, depois: RiskLevel.yellow, fim: DateTime.utc(2026, 9, 17, 10)), + ], + ); + expect(resultado.single.syncStatus, SyncStatus.synced); + expect(resultado.single.serverVersion, 2); + _expectSemRiscoLegivel(await _linhaBruta(session, table: 'visits')); + puxadas = await service.pull( + user: _acs, + since: relogio.subtract(const Duration(days: 1)), + ); + expect(puxadas.single.riskLevelBefore, RiskLevel.red); + expect(puxadas.single.riskLevelAfter, RiskLevel.yellow); + }); + + test('#75: visita sem risco cifrado (anterior à migração) faz o pull ' + 'falhar alto, sem inventar risco', () async { + final session = sessionBuilder.build(); + await _seed(session); + final notas = await encryptedVisitNotes(const {}); + await Visit.db.insertRow( + session, + Visit( + patientId: UuidValue.fromString(_patientId), + acsId: UuidValue.fromString(_acsId), + scheduledAt: DateTime.utc(2026, 9, 17, 9), + status: 'realizada', + notesEncrypted: notas.ciphertextBase64, + notesKeyVersion: notas.keyVersion, + syncStatus: SyncStatus.synced, + localId: UuidValue.fromString(_localId), + syncAt: DateTime.utc(2026, 9, 17, 11), + version: 1, + ), + ); + final service = VisitSyncService( + store: OrmVisitStore(session: () => session, cipher: testHealthDataCipher()), + audit: _SilentAuditTrail(), + clock: () => DateTime.utc(2026, 9, 17, 12), + ); + + expect( + () => service.pull(user: _acs, since: DateTime.utc(2026, 9, 16)), + throwsA(isA()), + ); + }); }); } diff --git a/backend/sinalacs_server/test/integration/patient_directory_and_territory_test.dart b/backend/sinalacs_server/test/integration/patient_directory_and_territory_test.dart index 8fe22c6..6d55e29 100644 --- a/backend/sinalacs_server/test/integration/patient_directory_and_territory_test.dart +++ b/backend/sinalacs_server/test/integration/patient_directory_and_territory_test.dart @@ -362,6 +362,7 @@ void main() { // `notesEncrypted`/`notesKeyVersion` com a mesma chave de // desenvolvimento que o `AlertRuntime` usa aqui. final notasVazias = await encryptedVisitNotes(const {}); + final riscoVerde = await encryptedRiskLevel(RiskLevel.green); Visit visita({ required String localId, @@ -374,8 +375,10 @@ void main() { scheduledAt: DateTime.utc(2026, 9, 12, 9), completedAt: DateTime.utc(2026, 9, 12, 10), status: 'realizada', - riskLevelBefore: RiskLevel.green, - riskLevelAfter: RiskLevel.green, + riskLevelBeforeEncrypted: riscoVerde.ciphertextBase64, + riskLevelBeforeKeyVersion: riscoVerde.keyVersion, + riskLevelAfterEncrypted: riscoVerde.ciphertextBase64, + riskLevelAfterKeyVersion: riscoVerde.keyVersion, notesEncrypted: notasVazias.ciphertextBase64, notesKeyVersion: notasVazias.keyVersion, syncStatus: SyncStatus.synced, diff --git a/backend/sinalacs_server/test/integration/retention_expurge_test.dart b/backend/sinalacs_server/test/integration/retention_expurge_test.dart index 4ff2918..64e943e 100644 --- a/backend/sinalacs_server/test/integration/retention_expurge_test.dart +++ b/backend/sinalacs_server/test/integration/retention_expurge_test.dart @@ -156,7 +156,6 @@ Future _seed(Session session) async { patientId: UuidValue.fromString(_patientId), scheduledAt: _cutoffVisits.subtract(const Duration(days: 10)), status: 'agendada', - riskLevelBefore: RiskLevel.green, syncStatus: SyncStatus.synced, localId: UuidValue.fromString('00000000-0000-4000-8000-0000000000d1'), version: 1, @@ -169,7 +168,6 @@ Future _seed(Session session) async { scheduledAt: _now, completedAt: _now, status: 'realizada', - riskLevelBefore: RiskLevel.green, syncStatus: SyncStatus.synced, localId: UuidValue.fromString('00000000-0000-4000-8000-0000000000d2'), version: 1, diff --git a/backend/sinalacs_server/test/integration/visit_legacy_sync_test.dart b/backend/sinalacs_server/test/integration/visit_legacy_sync_test.dart index 4fab66e..5e0d3bf 100644 --- a/backend/sinalacs_server/test/integration/visit_legacy_sync_test.dart +++ b/backend/sinalacs_server/test/integration/visit_legacy_sync_test.dart @@ -466,6 +466,7 @@ void main() { const localId = '00000000-0000-4000-8000-0000000001f1'; // Visita com autor de um paciente da microárea vizinha, gravada direto. final notas = await encryptedVisitNotes(const {}); + final riscoVerde = await encryptedRiskLevel(RiskLevel.green); await Visit.db.insertRow( session, Visit( @@ -473,7 +474,8 @@ void main() { acsId: UuidValue.fromString(_acsId), scheduledAt: DateTime.utc(2026, 9, 12, 9), status: 'realizada', - riskLevelBefore: RiskLevel.green, + riskLevelBeforeEncrypted: riscoVerde.ciphertextBase64, + riskLevelBeforeKeyVersion: riscoVerde.keyVersion, notesEncrypted: notas.ciphertextBase64, notesKeyVersion: notas.keyVersion, syncStatus: SyncStatus.synced, From 355d2a05832e2e1d177cd819573c4cd1e0f5317a Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Fri, 16 Oct 2026 10:04:55 -0400 Subject: [PATCH 5/7] =?UTF-8?q?test(backend):=20amarra=20alerts.riskLevel?= =?UTF-8?q?=20em=20claro=20a=20s=C3=B3=20gravar=20vermelho=20(#75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unit/red_alert_discard_paths_test.dart | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/backend/sinalacs_server/test/unit/red_alert_discard_paths_test.dart b/backend/sinalacs_server/test/unit/red_alert_discard_paths_test.dart index dbad059..5b40387 100644 --- a/backend/sinalacs_server/test/unit/red_alert_discard_paths_test.dart +++ b/backend/sinalacs_server/test/unit/red_alert_discard_paths_test.dart @@ -230,6 +230,30 @@ void main() { ); expect(adminWriters, isEmpty); }); + + test('INV-04 (#75): todo alerta gravado é vermelho — é o que justifica ' + 'alerts.riskLevel em claro (spec/lgpd_data_audit.md §2.3)', () { + // `(?{}; + for (final entry in _grepSources(pattern).entries) { + for (final hit in entry.value) { + valores.add(pattern.firstMatch(hit)!.group(1)!); + } + } + expect(valores, isNotEmpty, reason: 'a varredura precisa achar o save() do OrmAlertStore'); + expect( + valores, + {'RiskLevel.red'}, + reason: 'Se um alerta amarelo/verde passou a ser gravado, alerts.riskLevel ' + 'deixou de ser igual à existência da linha: cifre a coluna ou ' + 'revise a decisão da #75 antes de mudar este teste.', + ); + }); }); group('alerts.createRedAlert repetido (idempotência)', () { From b070e2abbfdab625c7a6b70f097c6e9cc6f9d9d0 Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Fri, 16 Oct 2026 10:06:55 -0400 Subject: [PATCH 6/7] =?UTF-8?q?docs:=20registra=20a=20cifra=20dos=20riscos?= =?UTF-8?q?=20e=20a=20exce=C3=A7=C3=A3o=20de=20alerts.riskLevel=20(#75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 2 ++ PROGRESS.md | 7 +++++++ backend/CLAUDE.md | 2 +- docs/auditoria-prd/2026-10-15-lacunas-prd.md | 8 ++++---- spec/PRD_system.md | 2 +- spec/lgpd_data_audit.md | 10 +++++----- 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e536696..86e6018 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,6 +72,8 @@ existing content with no warning. Either `docker compose down && rm -rf pg_data/` before starting on the new schema, or accept the loss of synthetic dev data in those three fields. +Issue #75 repeats it: migrations `20261016135918697` and `20261016140159775` `DROP COLUMN` `triage_sessions.resultRisk`/`resultDisplay` and `visits.riskLevelBefore`/`riskLevelAfter` with no backfill. On an old volume, old triages drop out of the patient's "Meus Dados" risk history and the ACS `visits.pull` fails with `StateError` on any pre-migration visit until `docker compose down && rm -rf pg_data/`. + Rotating the MQTT passwords is enough on its own: `infra/docker/mosquitto/init.sh` rewrites the broker's `passwordfile` on every boot. It used to only create it when absent, so a changed password silently did nothing and the broker kept diff --git a/PROGRESS.md b/PROGRESS.md index 556908c..1d82c43 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -1655,3 +1655,10 @@ Relatório item a item de `spec/PRD_system.md` contra o código, com evidência Contagem dos 64 IDs do PRD: 24 entregue, 27 parcial, 4 ausente, 9 externo (mais 57 linhas sem ID próprio, para SLOs, STRIDE, criptografia, acessibilidade, telemetria, métricas do §1.3 e módulos dos apps). Os três P0 do backlog: colunas de risco em texto plano (INV-04), o estado "Atendido" que o paciente nunca vê (RF05) e o SMS real do OTP (RF01). Backlog priorizado na seção final do relatório; conferência por `python3 scripts/qa/cobertura_prd.py --final`. + +## Colunas de risco cifradas (#75) — 2026-10-16 + +- Entregue: `triage_sessions.resultRisk` e `visits.riskLevelBefore`/`riskLevelAfter` cifrados em AES-256-GCM na borda ORM, com codec de largura fixa (o tamanho do ciphertext não revela a classe); `triage_sessions.resultDisplay` removida (derivada). Migrações `20261016135918697` (triagem) e `20261016140159775` (visitas). +- Decisão: `alerts.riskLevel` permanece legível (só `red` é gravado; chave dos filtros SQL da INV-03). Registrada no PRD (INV-04) e em `spec/lgpd_data_audit.md` §2.3; amarrada pelo teste `INV-04 (#75)` de `test/unit/red_alert_discard_paths_test.dart`. +- Provas: `test/unit/encrypted_risk_level_test.dart`; testes `#75` em `test/integration/health_data_encryption_test.dart` (linha bruta por `row_to_json` sem risco legível, ida e volta, `After` nulo e conclusão, linha antiga falha alto) e em `test/integration/patient_data_overview_endpoint_test.dart`. +- Aberto: migrações sem backfill (volume antigo precisa ser recriado); rotação de chave continua inexistente (`keyVersion` só marcador), como nas demais colunas cifradas. diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md index d0ef076..5d15814 100644 --- a/backend/CLAUDE.md +++ b/backend/CLAUDE.md @@ -82,7 +82,7 @@ The seed runs in **four** steps. `development.sql` is plain SQL over `psql`, so Each encrypted column stores a `keyVersion` next to the ciphertext, but **nothing reads it back**: `HealthDataCipher.decrypt` always uses the one key the process was configured with. The column is a marker for a future rewrite routine; until that routine exists, changing `HEALTH_DATA_ENCRYPTION_KEY` makes every previously encrypted row unreadable (GCM authentication failure). Real key rotation is a future production improvement, not an MVP requirement. -The three clinical columns are encrypted at the ORM boundary only: `OrmPatientDirectoryStore`, `OrmTriageSessionStore` and `OrmVisitStore` take a `HealthDataCipher` and translate to/from the plaintext types (`PatientDirectoryEntry`, `TriageSessionRecord`, `VisitRecord`) that `application/` works with. Keep it that way — a service that decides whether to encrypt is the failure mode this design exists to prevent. +The clinical columns are encrypted at the ORM boundary only (since #75 also the risk classifications `triage_sessions.resultRiskEncrypted` and `visits.riskLevelBefore/AfterEncrypted`, through the fixed-width codec `infrastructure/crypto/encrypted_risk_level.dart`; `alerts.riskLevel` stays plaintext by a recorded decision — see `spec/lgpd_data_audit.md` §2.3): `OrmPatientDirectoryStore`, `OrmTriageSessionStore` and `OrmVisitStore` take a `HealthDataCipher` and translate to/from the plaintext types (`PatientDirectoryEntry`, `TriageSessionRecord`, `VisitRecord`) that `application/` works with. Keep it that way — a service that decides whether to encrypt is the failure mode this design exists to prevent. ### Backend (`backend/`) — Serverpod workspace diff --git a/docs/auditoria-prd/2026-10-15-lacunas-prd.md b/docs/auditoria-prd/2026-10-15-lacunas-prd.md index 281dc39..d09c732 100644 --- a/docs/auditoria-prd/2026-10-15-lacunas-prd.md +++ b/docs/auditoria-prd/2026-10-15-lacunas-prd.md @@ -72,7 +72,7 @@ próprio; o conferidor exige apenas os IDs do PRD. | INV-01 | ACS só vê pacientes da própria microárea | **entregue** | Recusa de outra microárea testada por endpoint: `patients.listMicroArea` e `visits.pull` (`patient_directory_and_territory_test.dart`), `visits.sync` (`visit_sync_service_test.dart`, rejeição de território), `onboarding.generateEnrollmentToken` (`onboarding_service_test.dart:75`), `alerts.acknowledge` (`red_alert_discard_paths_test.dart`), `notices.sendSegmented` (`notices_endpoint_test.dart`), staff por UBS (`admin_scope_test.dart`); todos passam (2026-10-15). | | | INV-02 | Classificação de risco não alterável por humano | **entregue** | Nenhum endpoint recebe risco do cliente: `createRedAlert` aceita só `idempotencyKey`, `locationHash` e `locationCell` (`alerts_endpoint.dart`); `triage.evaluate` recebe os sinais e o servidor classifica; o app do paciente não calcula risco (`apps/patient/lib/app/app.dart:1424-1428`). | | | INV-03 | Alerta vermelho nunca perdido | **entregue** | Outbox MQTT com ACK, retry e reforço; publicação confirmada por PUBACK (`mqtt_alert_dispatcher_resilience_test.dart`, #56), caminhos de descarte cobertos (`red_alert_discard_paths_test.dart`), `red_alert_cycle_test.dart`; e2e `scripts/qa/mqtt_resiliencia_e2e.sh`; 10/10 alertas entregues na medição de latência. | O ACS só recebe alerta por MQTT: não há reserva por HTTP se o broker ficar inalcançável (ver R2). | -| INV-04 | Dado de saúde nunca em texto plano | **parcial** | Colunas clínicas cifradas em AES-256-GCM na aplicação (`*Encrypted`/`*KeyVersion` para condições crônicas, respostas de triagem e notas de visita); SQLCipher no ACS (`apps/acs/lib/core/database/encrypted_database.dart`). | Seguem em texto plano, e `spec/lgpd_data_audit.md` os classifica como dado sensível de saúde: `triage_sessions.resultRisk` e `resultDisplay` (`:50-51`), `visits.riskLevelBefore` e `riskLevelAfter` (`:64-65`) e `alerts.riskLevel` (`definition.sql` da migração `20261014155304871`). Cifrar ou registrar a decisão de deixá-los legíveis para a fila. | +| INV-04 | Dado de saúde nunca em texto plano | **entregue** | Colunas clínicas cifradas em AES-256-GCM na aplicação (`*Encrypted`/`*KeyVersion` para condições crônicas, respostas de triagem e notas de visita); SQLCipher no ACS (`apps/acs/lib/core/database/encrypted_database.dart`). Desde 2026-10-16 (#75): riscos de triagem e visita cifrados (`resultRiskEncrypted`, `riskLevelBefore/AfterEncrypted`); `alerts.riskLevel` legível por decisão registrada no PRD (INV-04). | — | | INV-05 | Paciente nunca acessa dado de outro paciente | **entregue** | Token do paciente carrega o `patientId` e os endpoints de paciente leem só dele: `myData`, consentimentos e pedidos (`patient_data_overview_endpoint_test.dart`, `data_subject_rights_endpoint_test.dart`), `statusFor` (`authorization_test.dart`, `red_alert_cycle_test.dart`), token de push por dono (`push_token_endpoint_test.dart`); passam (2026-10-15). | | ### STRIDE (§4.2.1) @@ -103,7 +103,7 @@ próprio; o conferidor exige apenas os IDs do PRD. |----|-----------|--------|-----------|-------------| | RNF01 | Latência MQTT < 500 ms (p95) | **parcial** | Entrega de alerta p95 212 ms, 10/10 amostras (meta < 500 ms); `measure_latency.dart --samples 10 --visit-count 100` contra a stack local (2026-10-16 01:27 UTC). | Só stack local, 1 cliente sequencial, 10 amostras (p95 de 10 amostras é quase o máximo): repetir em produção, com carga e mais amostras. | | RNF02 | Sincronização offline > 99,5 % | **parcial** | Sync 10/10 lotes de 100 visitas sem perda (`measure_latency.dart --samples 10 --visit-count 100` contra a stack local (2026-10-16 01:27 UTC)); `offline_burst_test.dart` prova que nenhuma de 100 se perde em reinício. | A taxa > 99,5 % em campo não é medida: nenhum evento `sync_attempt`/`sync_result` é emitido (ver EVT-*). | -| RNF03 | Criptografia AES-256 em repouso | **parcial** | Ver INV-04 e CRIPTO-*. | Ver INV-04 (cinco colunas de risco em texto plano) e CRIPTO-APP-CACHE. | +| RNF03 | Criptografia AES-256 em repouso | **parcial** | Ver INV-04 e CRIPTO-*. | Ver CRIPTO-APP-CACHE. | | RNF04 | TLS 1.3 em todas as comunicações | **parcial** | RPC em TLS 1.3 obrigatório (`tls_invariants.sh` OK); broker MQTT só em TLS na 8883 com mTLS (`mtls_invariants.sh` OK). | O broker aceita TLS 1.2: `openssl s_client -tls1_2 -connect localhost:8883` com certificado de cliente negociou TLSv1.2 (2026-10-15). Falta `tls_version tlsv1.3` no `mosquitto.conf` e o caso no `tls_invariants.sh`; Traefik-backend em texto claro (CRIPTO-TRAEFIK-BACKEND). | | RNF05 | Acessibilidade WCAG 2.1 AA | **parcial** | Matriz de contraste e `meetsGuideline` nos três apps (`apps/*/test/contrast_tokens_test.dart`, `touch_targets_test.dart` no admin), avaliação em `spec/ux_accessibility_assessment.md` (WCAG 2.2 AA, superconjunto do 2.1); linha de base verde. | Foco visível no admin (A11Y-2.4.7) e métricas de interface (A11Y-LCP/FID/CLS) não medidas. | @@ -141,7 +141,7 @@ próprio; o conferidor exige apenas os IDs do PRD. | LGPD-RF02 | Consentimento granular | **entregue** | Consentimento por finalidade com histórico (`consent_logs`, `patients.updateConsent`, `hasGrantedConsent`); token de push só com consentimento; testes de endpoint passam na linha de base. | Validação jurídica do texto dos termos (ver M3.4). | | LGPD-RF07 | Retenção e exclusão (job de expurgo) | **parcial** | Expurgo diário no servidor por `ExpurgeFutureCall` (`backend/sinalacs_server/lib/src/application/retention/expurge_future_call.dart`, agendado em `server.dart:158`): alertas 2 anos, visitas e triagens 5 anos (`RETENTION_*_DAYS`); `expurge_service_test.dart` e `retention_expurge_test.dart` passam (12 testes, 2026-10-15). | Expurgo no aparelho da fila de um dono que nunca volta (ACS); prazos de OTP, convites e tokens não entram na política. | | LGPD-RF08 | Direitos do titular (Meus Dados + exportação JSON) | **entregue** | Painel Meus Dados (`patients.myData`, `PatientDataOverview`) e exportação em JSON copiada para a área de transferência (`apps/patient/lib/app/app.dart:2040-2084`, botão `Copiar meus dados (JSON)`); pedidos de exclusão/correção atendidos no backoffice (#42). | Validar as regras de anonimização com o encarregado (DPO); exportação como arquivo, não só área de transferência. | -| LGPD-RF09 | Proteção de dados sensíveis | **parcial** | Ver INV-04, CRIPTO-* e RNF06. | Colunas de risco em texto plano em `triage_sessions`, `visits` e `alerts` (INV-04). | +| LGPD-RF09 | Proteção de dados sensíveis | **parcial** | Ver INV-04, CRIPTO-* e RNF06. | Ver CRIPTO-APP-CACHE e RNF06. | | LGPD-RF11 | Controle de acesso RBAC | **parcial** | Ver RNF06. | Matriz §4.2.2: paciente lendo os próprios logs e justificativa do coordenador. | ### Modelo de dados e telemetria (§1.3, §3) @@ -225,7 +225,7 @@ Critério: **P0** toca invariante ou o núcleo do MVP (§6.1) sem critério atin | # | Prioridade | Lacuna | IDs | Tipo | Justificativa | |---|---|---|---|---|---| -| 1 | P0 | Colunas de risco da triagem e do alerta em texto plano (`triage_sessions.resultRisk`/`resultDisplay`, `visits.riskLevelBefore`/`riskLevelAfter`, `alerts.riskLevel`) | INV-04, RNF03, LGPD-RF09 | código | Viola uma invariante; ou cifrar, ou registrar no PRD por que o risco precisa ficar legível para a fila. | +| 1 | P0 | **Resolvido (2026-10-16, #75).** Colunas de risco da triagem e do alerta em texto plano (`triage_sessions.resultRisk`/`resultDisplay`, `visits.riskLevelBefore`/`riskLevelAfter`, `alerts.riskLevel`) | INV-04, RNF03, LGPD-RF09 | código | Viola uma invariante; ou cifrar, ou registrar no PRD por que o risco precisa ficar legível para a fila. | | 2 | P0 | Paciente nunca vê Atendido: nenhum caminho leva o alerta a `resolved` | RF05, MVP-STATUS, M2.1 | código | Critério do núcleo do MVP (§6.1) não atingido; precisa da ação do ACS de encerrar o atendimento. | | 3 | P0 | Envio de OTP por SMS real (provedor + telefone do paciente) | RF01, MVP-LOGIN-60S, M2.1, R6 | código + externo | Sem SMS o paciente só entra com o ACS presente gerando um QR (R6): não há login por conta própria fora do ambiente de dev; a escolha de provedor/contrato é de produto. | | 4 | P1 | Broker MQTT aceita TLS 1.2; Traefik → backend em HTTP | RNF04, M2.3, CRIPTO-TRAEFIK-BACKEND | infra | Risco Crítico; `tls_version tlsv1.3` no `mosquitto.conf` e o caso no `tls_invariants.sh` é pequeno; mTLS interno exige decisão. | diff --git a/spec/PRD_system.md b/spec/PRD_system.md index b11bb64..68c5773 100644 --- a/spec/PRD_system.md +++ b/spec/PRD_system.md @@ -51,7 +51,7 @@ O SinalACS transforma dados clínicos estruturados em ação imediata através d | **INV-01** | Um ACS **nunca** pode visualizar dados de pacientes fora de sua microárea | Verificação em todas as queries: `WHERE micro_area_id = user.micro_area_id` | | **INV-02** | A classificação de risco **nunca** pode ser alterada por um humano no momento da triagem | O cálculo é determinístico e imutável no frontend | | **INV-03** | Um alerta vermelho **nunca** pode ser perdido ou silenciosamente descartado | Confirmação de entrega (ACK) no MQTT com retry até confirmação | -| **INV-04** | Dados de saúde sensíveis **nunca** podem ser persistidos em texto plano | Criptografia AES-256 em `sqflite` e PostgreSQL | +| **INV-04** | Dados de saúde sensíveis **nunca** podem ser persistidos em texto plano | Criptografia AES-256 em `sqflite` e PostgreSQL. **Exceção registrada (#75, 2026-10-16):** `alerts.riskLevel` fica legível — todo alerta gravado é vermelho (a coluna não diz nada além da existência da linha) e ela sustenta os predicados SQL da INV-03; ver `spec/lgpd_data_audit.md` §2.3 | | **INV-05** | O paciente **nunca** pode acessar dados de outro paciente | Autenticação com segregação por `patient_id` | ### 1.3 Métricas de Sucesso (North Star) diff --git a/spec/lgpd_data_audit.md b/spec/lgpd_data_audit.md index 7107753..ed9ce83 100644 --- a/spec/lgpd_data_audit.md +++ b/spec/lgpd_data_audit.md @@ -47,8 +47,7 @@ A tabela abaixo consolida o mapeamento exaustivo de dados persistidos pelo backe | **triage_sessions** | `id` | `uuid` | Pseudonimizado | UUID v4 (`gen_random_uuid()`) | Identificador sintético da sessão de triagem. | | | `patientId` | `uuid` | Pseudonimizado | Chave estrangeira (`patients.id`) | Liga o histórico clínico diretamente ao perfil do paciente no território. | | | `answers` | `text` (ciphertext) + `answersKeyVersion` (`bigint`) | Sensível (Saúde - Art. 5º, II) | **Cifrado em repouso (AES-256-GCM, aplicação)** — ver §2.3 | Contém sintomas declarados e relatos de dor, cifrados na borda do repositório ORM antes do INSERT; atende INV-04. | -| | `resultRisk` | `text` | Sensível (Saúde - Art. 5º, II) | Enum textual (`VERMELHO`, `AMARELO`, etc.) | Classificação determinística calculada no frontend/backend; uso restrito à priorização da fila de atendimento do ACS. | -| | `resultDisplay` | `text` | Sensível (Saúde - Art. 5º, II) | Texto descritivo | Descrição textual com diretivas clínicas associadas aos sintomas avaliados. | +| | `resultRiskEncrypted` + `resultRiskKeyVersion` | `text` (ciphertext) + `bigint` | Sensível (Saúde - Art. 5º, II) | **Cifrado em repouso (AES-256-GCM, aplicação)** — ver §2.3 | **Atualizado (2026-10-16, #75):** nome do `RiskLevel` com largura fixa (o tamanho do ciphertext não revela a classe), cifrado em `OrmTriageSessionStore` e decifrado só no histórico do "Meus Dados". `resultDisplay` deixou de existir: era o rótulo derivado do risco (minimização). | | | `createdAt` | `timestamp without time zone` | Metadado Técnico | Timestamp | Marco temporal para aplicação da política de retenção legal de 5 anos (LGPD-RF07). | | | `deviceId` | `text` | Identificador de Dispositivo | String de identificador de hardware | Se atrelado a identificadores fixos do sistema (IMEI, MAC, Android ID), caracteriza dado pessoal rastreável; requer pseudonimização com salt efêmero. | | **triage_answer** *(DTO)* | `questionId`, `value`, `details` | `String` / Estrutura serializada | Sensível (Saúde - Art. 5º, II) | Payload unitário de resposta | Estrutura unitária persistida dentro do campo `triage_sessions.answers`. | @@ -61,8 +60,7 @@ A tabela abaixo consolida o mapeamento exaustivo de dados persistidos pelo backe | | `startedAt` | `timestamp without time zone` | Metadado Operacional | Timestamp de início | Registro do início da intervenção no domicílio. | | | `completedAt` | `timestamp without time zone` | Metadado Operacional | Timestamp de conclusão | Registro da finalização da visita pelo ACS. | | | `status` | `text` | Metadado Operacional | Enum textual (`SCHEDULED`, `IN_PROGRESS`, etc.) | Estado operacional do ciclo de vida da visita. | -| | `riskLevelBefore` | `text` | Sensível (Saúde - Art. 5º, II) | Enum textual | Avaliação clínica de gravidade anterior à intervenção. | -| | `riskLevelAfter` | `text` | Sensível (Saúde - Art. 5º, II) | Enum textual | Reavaliação clínica executada pelo ACS após o atendimento presencial. | +| | `riskLevelBeforeEncrypted`/`riskLevelAfterEncrypted` + `*KeyVersion` | `text` (ciphertext) + `bigint` | Sensível (Saúde - Art. 5º, II) | **Cifrado em repouso (AES-256-GCM, aplicação)** — ver §2.3 | **Atualizado (2026-10-16, #75):** mesmo codec de largura fixa; o "depois" ausente também é cifrado, com o mesmo tamanho. Linha sem risco cifrado faz o `pull` falhar alto em vez de inventar risco. | | | `notes` | `text` (ciphertext) + `notesKeyVersion` (`bigint`) | Sensível (Saúde - Art. 5º, II) | **Cifrado em repouso (AES-256-GCM, aplicação)** — ver §2.3 | Campo de anotações não estruturadas do ACS, cifrado na borda do repositório ORM antes do INSERT; risco de conter diagnósticos/relatos sobre terceiros permanece (é conteúdo livre), mas não mais como texto claro em repouso. | | | `syncStatus` | `text` | Metadado Técnico | Enum textual (`PENDING`, `SYNCED`, `CONFLICT`) | Controle da máquina de estados (FSM) de sincronização offline-first. | | | `localId` | `uuid` | Pseudonimizado | UUID gerado no SQLite do cliente | Chave de controle de concorrência e idempotência offline. | @@ -79,7 +77,7 @@ A tabela abaixo consolida o mapeamento exaustivo de dados persistidos pelo backe | | `escalatedAt` | `timestamp without time zone` (anulável) | Metadado Operacional | Timestamp do escalonamento (relógio do servidor) | Quando a coordenação escalonou o vermelho vencido pelo backoffice (issue #44); nulo enquanto não escalonado. Escalonar não muda o `status` (o alerta segue `pending`). | | | `escalatedBy` | `uuid` (anulável) | Pseudonimizado (dado pessoal do profissional) | Chave para `users.id` do staff (coordenador/administrador) que escalonou | Identifica o **profissional**, não o titular, como `decidedBy` de `data_subject_requests`; nunca vai para o payload MQTT do reforço nem para a resposta do RPC. Nulo enquanto não escalonado. | | | `escalationReason` | `text` (anulável) | Metadado Operacional | Enum fechado `EscalationReason` (`noAckTimeout`, `acsUnreachable`, `patientRecontact`) | Motivo escolhido numa lista fechada, **sem texto livre**: não há campo em que o operador possa digitar dado do paciente. Nulo enquanto não escalonado. | -| | `riskLevel` | `text` | Sensível (Saúde - Art. 5º, II) | Enum textual (`VERMELHO`, `AMARELO`) | Grau de urgência do acionamento. | +| | `riskLevel` | `text` | Sensível (Saúde - Art. 5º, II) | Enum textual (`VERMELHO`, `AMARELO`) | Grau de urgência do acionamento. **Decisão (2026-10-16, #75): permanece em texto plano** — só `red` é gravado (teste `INV-04 (#75)` em `test/unit/red_alert_discard_paths_test.dart`), e a coluna é usada nos predicados SQL de escalonamento, guarda do expurgo e indicadores (INV-03). Se alerta não vermelho passar a existir, cifrar. | | | `locationHash` | `text` | Pseudonimizado | Geohash (ex: `'6gyf4bf'`) | Delimitação territorial codificada; requer validação de precisão para não fixar o endereço domiciliar exato. | | | `status` | `text` | Metadado Operacional | Enum textual (`PENDING`, `ACKNOWLEDGED`, etc.) | Estado da máquina de alerta de urgência. | | | `mqttTopic` | `text` | Metadado Técnico / Territorial | String (`/alerts/{micro_area_id}`) | Canal de mensageria; não deve conter identificadores diretos do paciente no caminho do tópico. | @@ -274,6 +272,8 @@ A invariante de negócio **INV-04** estabelece categoricamente: *"Dados de saúd Uma leitura indevida decorrente de dump de banco ou backup comprometido já não expõe texto claro diretamente: exige também a chave `HEALTH_DATA_ENCRYPTION_KEY`, que fica fora do Postgres. Isso reduz, mas não elimina, o risco — a chave é um segredo único por ambiente (sem KMS/HSM/rotação automática nesta fase, ver `backend/CLAUDE.md`), e comprometer o processo do servidor (não só o banco) ainda expõe o texto claro em memória durante o uso normal. +**Classificações de risco (2026-10-16, issue #75).** `triage_sessions.resultRisk` e `visits.riskLevelBefore`/`riskLevelAfter` passaram ao mesmo padrão (`*Encrypted`/`*KeyVersion`, codec `encrypted_risk_level.dart`). Como o AES-GCM preserva o tamanho do texto claro, o nome do nível é preenchido até a largura do maior nome (a ausência também), e o teste `test/unit/encrypted_risk_level_test.dart` prova que os quatro casos dão ciphertexts do mesmo comprimento. `triage_sessions.resultDisplay` foi removida. `alerts.riskLevel` fica legível por decisão registrada no PRD (INV-04): é constante (`red`) e é chave dos filtros SQL que impedem o descarte de alerta vermelho. O `payload` de `alert_outbox` carrega o mesmo `risk_level` constante; o risco dele é o `locationHash`, tratado em §2.2. Migrações sem backfill (`20261016135918697` e `20261016140159775`): um `pg_data/` anterior perde o risco das triagens (somem do histórico do "Meus Dados") e das visitas (o `pull` do ACS falha até recriar o volume). + --- ### 2.4. Identificadores de Dispositivo (`deviceId`) From 726f10eb40a007d6e70ac125c2769e604193363b Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Fri, 16 Oct 2026 10:19:35 -0400 Subject: [PATCH 7/7] =?UTF-8?q?fix(backend):=20visita=20anterior=20=C3=A0?= =?UTF-8?q?=20cifra=20do=20risco=20n=C3=A3o=20derruba=20o=20lote=20nem=20o?= =?UTF-8?q?=20pull=20(#75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 2 +- PROGRESS.md | 2 +- .../visits/visit_sync_service.dart | 29 +++++- .../database/orm_visit_store.dart | 18 ++-- .../health_data_encryption_test.dart | 89 ++++++++++++++++--- spec/lgpd_data_audit.md | 4 +- 6 files changed, 119 insertions(+), 25 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 86e6018..b6ce624 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,7 +72,7 @@ existing content with no warning. Either `docker compose down && rm -rf pg_data/` before starting on the new schema, or accept the loss of synthetic dev data in those three fields. -Issue #75 repeats it: migrations `20261016135918697` and `20261016140159775` `DROP COLUMN` `triage_sessions.resultRisk`/`resultDisplay` and `visits.riskLevelBefore`/`riskLevelAfter` with no backfill. On an old volume, old triages drop out of the patient's "Meus Dados" risk history and the ACS `visits.pull` fails with `StateError` on any pre-migration visit until `docker compose down && rm -rf pg_data/`. +Issue #75 repeats it: migrations `20261016135918697` and `20261016140159775` `DROP COLUMN` `triage_sessions.resultRisk`/`resultDisplay` and `visits.riskLevelBefore`/`riskLevelAfter` with no backfill. On an old volume, old triages drop out of the patient's "Meus Dados" risk history and a pre-migration visit is left out of the ACS `visits.pull` and answered `SyncStatus.rejected` (`visita anterior à cifra do risco (#75)`, only that visit; the rest of the batch uploads) by `visits.sync`/`syncLegacy`/`syncDeferred`, until `docker compose down && rm -rf pg_data/`. Rotating the MQTT passwords is enough on its own: `infra/docker/mosquitto/init.sh` rewrites the broker's `passwordfile` on every boot. It used to only create it diff --git a/PROGRESS.md b/PROGRESS.md index 1d82c43..283d11b 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -1660,5 +1660,5 @@ Backlog priorizado na seção final do relatório; conferência por `python3 scr - Entregue: `triage_sessions.resultRisk` e `visits.riskLevelBefore`/`riskLevelAfter` cifrados em AES-256-GCM na borda ORM, com codec de largura fixa (o tamanho do ciphertext não revela a classe); `triage_sessions.resultDisplay` removida (derivada). Migrações `20261016135918697` (triagem) e `20261016140159775` (visitas). - Decisão: `alerts.riskLevel` permanece legível (só `red` é gravado; chave dos filtros SQL da INV-03). Registrada no PRD (INV-04) e em `spec/lgpd_data_audit.md` §2.3; amarrada pelo teste `INV-04 (#75)` de `test/unit/red_alert_discard_paths_test.dart`. -- Provas: `test/unit/encrypted_risk_level_test.dart`; testes `#75` em `test/integration/health_data_encryption_test.dart` (linha bruta por `row_to_json` sem risco legível, ida e volta, `After` nulo e conclusão, linha antiga falha alto) e em `test/integration/patient_data_overview_endpoint_test.dart`. +- Provas: `test/unit/encrypted_risk_level_test.dart`; testes `#75` em `test/integration/health_data_encryption_test.dart` (linha bruta por `row_to_json` sem risco legível, ida e volta, `After` nulo e conclusão, linha antiga fora do `pull` e `rejected` só para ela no `sync`) e em `test/integration/patient_data_overview_endpoint_test.dart`. - Aberto: migrações sem backfill (volume antigo precisa ser recriado); rotação de chave continua inexistente (`keyVersion` só marcador), como nas demais colunas cifradas. diff --git a/backend/sinalacs_server/lib/src/application/visits/visit_sync_service.dart b/backend/sinalacs_server/lib/src/application/visits/visit_sync_service.dart index 1bc5ca9..f922f62 100644 --- a/backend/sinalacs_server/lib/src/application/visits/visit_sync_service.dart +++ b/backend/sinalacs_server/lib/src/application/visits/visit_sync_service.dart @@ -112,12 +112,26 @@ class VisitRecord { /// Serverpod gera para os seus próprios modelos. const Object _keep = Object(); +/// A visita existe no banco, mas sem o risco cifrado: foi gravada antes da +/// migração da #75, que descartou as colunas em claro sem backfill. Não há +/// risco legível para devolver, e inventar um violaria a INV-02; quem chama +/// recusa só esta visita. Leva só o id sintético (VAZ-01). +class UnreadableVisitException implements Exception { + const UnreadableVisitException(this.visitId); + + final UuidValue? visitId; + + @override + String toString() => 'Visita $visitId sem riskLevelBefore cifrado (#75).'; +} + /// Persistência das visitas sincronizadas. /// /// Interface no mesmo padrão de `AlertStore`: mantém o serviço testável sem /// Postgres real. abstract interface class VisitStore { - /// Visita já gravada com este `localId`, se houver. + /// Visita já gravada com este `localId`, se houver. Lança + /// [UnreadableVisitException] se ela existe mas não tem o risco cifrado. Future findByLocalId(String localId); /// Grava uma visita nova, já com `version` inicial. @@ -386,7 +400,18 @@ class VisitSyncService { } final acsId = UuidValue.fromString(user.id); - final existing = await _store.findByLocalId(entry.localId); + final VisitRecord? existing; + try { + existing = await _store.findByLocalId(entry.localId); + } on UnreadableVisitException { + // Terminal e só desta visita: o resto do lote segue, e o aparelho a + // mantém na fila para revisão em vez de achar que a sessão caiu. + return VisitSyncResult( + localId: entry.localId, + syncStatus: SyncStatus.rejected, + message: 'visita anterior à cifra do risco (#75)', + ); + } if (existing == null) { final inserted = await _store.insert(VisitRecord( diff --git a/backend/sinalacs_server/lib/src/infrastructure/database/orm_visit_store.dart b/backend/sinalacs_server/lib/src/infrastructure/database/orm_visit_store.dart index b6796c2..972df10 100644 --- a/backend/sinalacs_server/lib/src/infrastructure/database/orm_visit_store.dart +++ b/backend/sinalacs_server/lib/src/infrastructure/database/orm_visit_store.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:serverpod/serverpod.dart'; import 'package:sinalacs_server/src/application/visits/visit_sync_service.dart'; import 'package:sinalacs_server/src/generated/protocol.dart'; @@ -106,7 +108,13 @@ class OrmVisitStore implements VisitStore { // Laço, e não list literal: decifrar é assíncrono. final records = []; for (final visit in visits) { - records.add(await _toRecord(visit)); + try { + records.add(await _toRecord(visit)); + } on UnreadableVisitException catch (error) { + // Uma linha anterior à #75 não pode derrubar o pull do território + // inteiro: fica de fora, e o log leva só o id (VAZ-01). + stderr.writeln('$error Omitida do pull.'); + } } return records; } @@ -157,11 +165,9 @@ class OrmVisitStore implements VisitStore { row.riskLevelBeforeKeyVersion, ); // Ciphertext vazio = linha anterior à migração da #75 ou gravada fora - // deste store. Falha alto: a fila do ACS não pode receber um risco - // inventado (INV-02). Só o id sintético vai na mensagem (VAZ-01). - if (before == null) { - throw StateError('Visita ${row.id} sem riskLevelBefore cifrado (#75).'); - } + // deste store. A fila do ACS não pode receber um risco inventado + // (INV-02): quem chama decide o que fazer com a visita ilegível. + if (before == null) throw UnreadableVisitException(row.id); final after = plain != null ? plain.riskLevelAfter : await _cipher.decryptRiskLevel( diff --git a/backend/sinalacs_server/test/integration/health_data_encryption_test.dart b/backend/sinalacs_server/test/integration/health_data_encryption_test.dart index 49792e7..7d13286 100644 --- a/backend/sinalacs_server/test/integration/health_data_encryption_test.dart +++ b/backend/sinalacs_server/test/integration/health_data_encryption_test.dart @@ -472,10 +472,9 @@ void main() { expect(puxadas.single.riskLevelAfter, RiskLevel.yellow); }); - test('#75: visita sem risco cifrado (anterior à migração) faz o pull ' - 'falhar alto, sem inventar risco', () async { - final session = sessionBuilder.build(); - await _seed(session); + // Visita "antiga": gravada antes da migração da #75, sem o risco cifrado + // (o default '' das colunas novas). + Future visitaAntiga(Session session) async { final notas = await encryptedVisitNotes(const {}); await Visit.db.insertRow( session, @@ -483,7 +482,7 @@ void main() { patientId: UuidValue.fromString(_patientId), acsId: UuidValue.fromString(_acsId), scheduledAt: DateTime.utc(2026, 9, 17, 9), - status: 'realizada', + status: 'agendada', notesEncrypted: notas.ciphertextBase64, notesKeyVersion: notas.keyVersion, syncStatus: SyncStatus.synced, @@ -492,16 +491,80 @@ void main() { version: 1, ), ); - final service = VisitSyncService( - store: OrmVisitStore(session: () => session, cipher: testHealthDataCipher()), - audit: _SilentAuditTrail(), - clock: () => DateTime.utc(2026, 9, 17, 12), - ); + } - expect( - () => service.pull(user: _acs, since: DateTime.utc(2026, 9, 16)), - throwsA(isA()), + VisitSyncService servicoDeVisitas(Session session) => VisitSyncService( + store: OrmVisitStore(session: () => session, cipher: testHealthDataCipher()), + audit: _SilentAuditTrail(), + clock: () => DateTime.utc(2026, 9, 17, 12), + ); + + test('#75: o pull omite a visita sem risco cifrado (anterior à migração) ' + 'e devolve as demais, sem inventar risco', () async { + final session = sessionBuilder.build(); + await _seed(session); + await visitaAntiga(session); + final service = servicoDeVisitas(session); + await service.sync(user: _acs, entries: [ + VisitSyncEntry( + localId: '00000000-0000-4000-8000-0000000000b2', + patientId: _patientId, + scheduledAt: DateTime.utc(2026, 9, 17, 9), + status: 'agendada', + riskLevelBefore: RiskLevel.yellow, + notes: const {}, + version: 0, + arrivalMethod: ArrivalMethod.manual, + ), + ]); + + final puxadas = await service.pull(user: _acs, since: DateTime.utc(2026, 9, 16)); + + expect(puxadas.map((v) => v.localId), ['00000000-0000-4000-8000-0000000000b2']); + expect(puxadas.single.riskLevelBefore, RiskLevel.yellow); + }); + + test('#75: reenviar a visita sem risco cifrado devolve rejected só para ela; ' + 'o resto do lote sobe', () async { + final session = sessionBuilder.build(); + await _seed(session); + await visitaAntiga(session); + final service = servicoDeVisitas(session); + + final resultados = await service.sync(user: _acs, entries: [ + VisitSyncEntry( + localId: _localId, + patientId: _patientId, + scheduledAt: DateTime.utc(2026, 9, 17, 9), + completedAt: DateTime.utc(2026, 9, 17, 10), + status: 'realizada', + riskLevelBefore: RiskLevel.red, + riskLevelAfter: RiskLevel.green, + notes: const {}, + version: 0, + arrivalMethod: ArrivalMethod.manual, + ), + VisitSyncEntry( + localId: '00000000-0000-4000-8000-0000000000b2', + patientId: _patientId, + scheduledAt: DateTime.utc(2026, 9, 17, 9), + status: 'agendada', + riskLevelBefore: RiskLevel.yellow, + notes: const {}, + version: 0, + arrivalMethod: ArrivalMethod.manual, + ), + ]); + + expect(resultados.map((r) => r.syncStatus), [SyncStatus.rejected, SyncStatus.synced]); + // A recusa não diz o risco nem o conteúdo da visita (VAZ-01). + expect(resultados.first.message, 'visita anterior à cifra do risco (#75)'); + final antiga = await Visit.db.findFirstRow( + session, + where: (t) => t.localId.equals(UuidValue.fromString(_localId)), ); + expect(antiga!.riskLevelBeforeEncrypted, isEmpty, reason: 'nada é regravado'); + expect(antiga.version, 1); }); }); } diff --git a/spec/lgpd_data_audit.md b/spec/lgpd_data_audit.md index ed9ce83..1a544c0 100644 --- a/spec/lgpd_data_audit.md +++ b/spec/lgpd_data_audit.md @@ -60,7 +60,7 @@ A tabela abaixo consolida o mapeamento exaustivo de dados persistidos pelo backe | | `startedAt` | `timestamp without time zone` | Metadado Operacional | Timestamp de início | Registro do início da intervenção no domicílio. | | | `completedAt` | `timestamp without time zone` | Metadado Operacional | Timestamp de conclusão | Registro da finalização da visita pelo ACS. | | | `status` | `text` | Metadado Operacional | Enum textual (`SCHEDULED`, `IN_PROGRESS`, etc.) | Estado operacional do ciclo de vida da visita. | -| | `riskLevelBeforeEncrypted`/`riskLevelAfterEncrypted` + `*KeyVersion` | `text` (ciphertext) + `bigint` | Sensível (Saúde - Art. 5º, II) | **Cifrado em repouso (AES-256-GCM, aplicação)** — ver §2.3 | **Atualizado (2026-10-16, #75):** mesmo codec de largura fixa; o "depois" ausente também é cifrado, com o mesmo tamanho. Linha sem risco cifrado faz o `pull` falhar alto em vez de inventar risco. | +| | `riskLevelBeforeEncrypted`/`riskLevelAfterEncrypted` + `*KeyVersion` | `text` (ciphertext) + `bigint` | Sensível (Saúde - Art. 5º, II) | **Cifrado em repouso (AES-256-GCM, aplicação)** — ver §2.3 | **Atualizado (2026-10-16, #75):** mesmo codec de largura fixa; o "depois" ausente também é cifrado, com o mesmo tamanho. Linha sem risco cifrado fica fora do `pull` e o reenvio dela recebe `rejected` — nunca um risco inventado. | | | `notes` | `text` (ciphertext) + `notesKeyVersion` (`bigint`) | Sensível (Saúde - Art. 5º, II) | **Cifrado em repouso (AES-256-GCM, aplicação)** — ver §2.3 | Campo de anotações não estruturadas do ACS, cifrado na borda do repositório ORM antes do INSERT; risco de conter diagnósticos/relatos sobre terceiros permanece (é conteúdo livre), mas não mais como texto claro em repouso. | | | `syncStatus` | `text` | Metadado Técnico | Enum textual (`PENDING`, `SYNCED`, `CONFLICT`) | Controle da máquina de estados (FSM) de sincronização offline-first. | | | `localId` | `uuid` | Pseudonimizado | UUID gerado no SQLite do cliente | Chave de controle de concorrência e idempotência offline. | @@ -272,7 +272,7 @@ A invariante de negócio **INV-04** estabelece categoricamente: *"Dados de saúd Uma leitura indevida decorrente de dump de banco ou backup comprometido já não expõe texto claro diretamente: exige também a chave `HEALTH_DATA_ENCRYPTION_KEY`, que fica fora do Postgres. Isso reduz, mas não elimina, o risco — a chave é um segredo único por ambiente (sem KMS/HSM/rotação automática nesta fase, ver `backend/CLAUDE.md`), e comprometer o processo do servidor (não só o banco) ainda expõe o texto claro em memória durante o uso normal. -**Classificações de risco (2026-10-16, issue #75).** `triage_sessions.resultRisk` e `visits.riskLevelBefore`/`riskLevelAfter` passaram ao mesmo padrão (`*Encrypted`/`*KeyVersion`, codec `encrypted_risk_level.dart`). Como o AES-GCM preserva o tamanho do texto claro, o nome do nível é preenchido até a largura do maior nome (a ausência também), e o teste `test/unit/encrypted_risk_level_test.dart` prova que os quatro casos dão ciphertexts do mesmo comprimento. `triage_sessions.resultDisplay` foi removida. `alerts.riskLevel` fica legível por decisão registrada no PRD (INV-04): é constante (`red`) e é chave dos filtros SQL que impedem o descarte de alerta vermelho. O `payload` de `alert_outbox` carrega o mesmo `risk_level` constante; o risco dele é o `locationHash`, tratado em §2.2. Migrações sem backfill (`20261016135918697` e `20261016140159775`): um `pg_data/` anterior perde o risco das triagens (somem do histórico do "Meus Dados") e das visitas (o `pull` do ACS falha até recriar o volume). +**Classificações de risco (2026-10-16, issue #75).** `triage_sessions.resultRisk` e `visits.riskLevelBefore`/`riskLevelAfter` passaram ao mesmo padrão (`*Encrypted`/`*KeyVersion`, codec `encrypted_risk_level.dart`). Como o AES-GCM preserva o tamanho do texto claro, o nome do nível é preenchido até a largura do maior nome (a ausência também), e o teste `test/unit/encrypted_risk_level_test.dart` prova que os quatro casos dão ciphertexts do mesmo comprimento. `triage_sessions.resultDisplay` foi removida. `alerts.riskLevel` fica legível por decisão registrada no PRD (INV-04): é constante (`red`) e é chave dos filtros SQL que impedem o descarte de alerta vermelho. O `payload` de `alert_outbox` carrega o mesmo `risk_level` constante; o risco dele é o `locationHash`, tratado em §2.2. Migrações sem backfill (`20261016135918697` e `20261016140159775`): um `pg_data/` anterior perde o risco das triagens (somem do histórico do "Meus Dados") e das visitas (a visita antiga fica fora do `pull` do ACS e o reenvio dela recebe `rejected`, sem derrubar o resto do lote, até recriar o volume). ---