mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
chore(release): v1.28.16 — documents-only shares stay documents-only
A share link created to carry only documents now records an explicit, frozen document_only flag, and the shared view gates on that flag directly instead of inferring documents-only from "are all report sections off?". The inference would have re-opened an existing documents-only link the day a new report section was added — the link's frozen settings never mention the new section, so it would default on and begin serving that data. The flag closes that path; the derived check stays only as the fallback for links created before the column. Migration 0237 adds the column and freezes it on for every existing link whose sections are entirely off (checked at any nesting depth), leaving full-record shares untouched.
This commit is contained in:
1 parent
fcff168112
commit
6d02711e33
11 files changed
+121
-8
No files matched your search
@@ -294,6 +294,10 @@ describe("POST /api/share-links — create", () => {
|
||||
// Sections persist as an explicit all-OFF blob (distinct from `{}` = full
|
||||
// record defaults). The clinician view reads this as "no report".
|
||||
expect(createArg.data.sectionsJson).toEqual(EMPTY_DOCTOR_REPORT_PREFS);
|
||||
// v1.28.16 — the frozen documents-only flag is persisted so the view path
|
||||
// gates on the column, not on inferring "all sections off". This is what a
|
||||
// future report section can never re-open.
|
||||
expect(createArg.data.documentOnly).toBe(true);
|
||||
// The picked document still rides along.
|
||||
expect(createArg.data.documents).toEqual({
|
||||
create: [{ documentId: "doc-a" }],
|
||||
|
||||
@@ -67,6 +67,7 @@ const shareLinkSummarySelect = {
|
||||
rangeEnd: true,
|
||||
resourceTypes: true,
|
||||
allowFhirApi: true,
|
||||
documentOnly: true,
|
||||
passphraseHash: true,
|
||||
expiresAt: true,
|
||||
createdAt: true,
|
||||
@@ -84,6 +85,7 @@ function toSummary(row: {
|
||||
rangeEnd: Date | null;
|
||||
resourceTypes: string[];
|
||||
allowFhirApi: boolean;
|
||||
documentOnly: boolean;
|
||||
passphraseHash: string | null;
|
||||
expiresAt: Date;
|
||||
createdAt: Date;
|
||||
@@ -99,6 +101,9 @@ function toSummary(row: {
|
||||
rangeEnd: row.rangeEnd ? row.rangeEnd.toISOString() : null,
|
||||
resourceTypes: row.resourceTypes,
|
||||
allowFhirApi: row.allowFhirApi,
|
||||
// v1.28.16 — the frozen documents-only flag (owner-facing; the serve path
|
||||
// gates on the column, not on "are all sections off?").
|
||||
documentOnly: row.documentOnly,
|
||||
// v1.18.7 — surface only WHETHER a passphrase guards the link, never the
|
||||
// hash itself. Legacy (null-hash) links read `false` and stay ungated.
|
||||
protected: row.passphraseHash !== null,
|
||||
@@ -193,6 +198,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
sectionsJson: sectionsJson as Prisma.InputJsonValue,
|
||||
resourceTypes,
|
||||
allowFhirApi,
|
||||
documentOnly,
|
||||
expiresAt: new Date(input.expiresAt),
|
||||
documents:
|
||||
documentIds.length > 0
|
||||
|
||||
@@ -213,10 +213,50 @@ describe("loadShareViewData — documents-only share exposes no health data", ()
|
||||
// share), so the aggregator DOES run — the empty-scope short-circuit must
|
||||
// not swallow a normal record share.
|
||||
const { report, documentOnly } = await loadShareViewData(
|
||||
ctx({ sectionsJson: {} }),
|
||||
ctx({ sectionsJson: {}, documentOnly: false }),
|
||||
);
|
||||
expect(collect).toHaveBeenCalledTimes(1);
|
||||
expect(documentOnly).toBe(false);
|
||||
expect(report).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* v1.28.16 — the frozen `documentOnly` COLUMN is authoritative. Once a link is
|
||||
* created documents-only, it stays documents-only regardless of what the report
|
||||
* sections resolve to — so a report section added to the prefs shape LATER can
|
||||
* never re-open an existing documents-only link. The legacy fallback (derive
|
||||
* from "all sections off") still holds for pre-column links where the flag is
|
||||
* false.
|
||||
*/
|
||||
describe("loadShareViewData — documentOnly column is authoritative", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
collect.mockResolvedValue({ patient: { displayName: "Shared record" } });
|
||||
findDocs.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
it("serves no report when the column is set, even if sections would aggregate", async () => {
|
||||
// Sections that resolve to an ENABLED scope (defaults) — the derived check
|
||||
// alone would run the aggregator. The frozen column must veto it. This is
|
||||
// the exact future-leak the column closes: a new section defaulting on can
|
||||
// no longer widen an old documents-only link.
|
||||
const { report, documentOnly } = await loadShareViewData(
|
||||
ctx({ sectionsJson: { bp: true }, documentOnly: true }),
|
||||
);
|
||||
expect(collect).not.toHaveBeenCalled();
|
||||
expect(report).toBeNull();
|
||||
expect(documentOnly).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to the derived all-off check for a legacy link (column false)", async () => {
|
||||
// A pre-column documents-only link reads `documentOnly:false` from the row
|
||||
// but still has every section off — the derived fallback keeps it closed.
|
||||
const { report, documentOnly } = await loadShareViewData(
|
||||
ctx({ sectionsJson: EMPTY_DOCTOR_REPORT_PREFS, documentOnly: false }),
|
||||
);
|
||||
expect(collect).not.toHaveBeenCalled();
|
||||
expect(report).toBeNull();
|
||||
expect(documentOnly).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -52,6 +52,13 @@ export interface ShareContext {
|
||||
resourceTypes: string[];
|
||||
/** Whether the scoped FHIR API is reachable via this link at all. */
|
||||
allowFhirApi: boolean;
|
||||
/**
|
||||
* v1.28.16 — authoritative documents-only flag, frozen at create. When true
|
||||
* the view loader serves ONLY documents and never aggregates a health report,
|
||||
* regardless of `sectionsJson`. Legacy links minted before the column read
|
||||
* `false` here and fall back to the derived all-sections-off check.
|
||||
*/
|
||||
documentOnly: boolean;
|
||||
/** Absolute expiry instant. */
|
||||
expiresAt: Date;
|
||||
}
|
||||
@@ -84,6 +91,7 @@ export async function resolveShareToken(
|
||||
sectionsJson: true,
|
||||
resourceTypes: true,
|
||||
allowFhirApi: true,
|
||||
documentOnly: true,
|
||||
expiresAt: true,
|
||||
revokedAt: true,
|
||||
},
|
||||
@@ -114,6 +122,7 @@ export async function resolveShareToken(
|
||||
sectionsJson: row.sectionsJson,
|
||||
resourceTypes: row.resourceTypes,
|
||||
allowFhirApi: row.allowFhirApi,
|
||||
documentOnly: row.documentOnly,
|
||||
expiresAt: row.expiresAt,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -95,10 +95,15 @@ export async function loadShareViewData(
|
||||
const sections = parseDoctorReportPrefs(context.sectionsJson);
|
||||
const range = frozenRange(context);
|
||||
|
||||
// A share with NO report section enabled is a documents-only share: never
|
||||
// aggregate — no health metric is read from the DB, let alone served. This is
|
||||
// the load-bearing guarantee behind "share this document, not the record".
|
||||
const documentOnly = !hasAnyReportSection(sections);
|
||||
// A documents-only share never aggregates — no health metric is read from the
|
||||
// DB, let alone served. This is the load-bearing guarantee behind "share this
|
||||
// document, not the record". v1.28.16: the frozen `documentOnly` COLUMN is
|
||||
// authoritative; the derived all-sections-off check remains as the fallback
|
||||
// for legacy links minted before the column (where it reads `false`). Because
|
||||
// the column can only pin — never widen — this is strictly safer than the
|
||||
// derived check alone: a future report section can never re-open a link the
|
||||
// owner froze as documents-only.
|
||||
const documentOnly = context.documentOnly || !hasAnyReportSection(sections);
|
||||
|
||||
const [report, documents] = await Promise.all([
|
||||
documentOnly
|
||||
|
||||
Reference in new issue
Block a user