mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
Merge branch 'v143/dawarich' into release/v1.43.0
# Conflicts: # src/__tests__/contract-stub-inventory.test.ts
This commit is contained in:
commit
da860aeb3e
101 files changed
+5354
-216
No files matched your search
@@ -33,23 +33,10 @@ const CONTRACT_STUBS: Readonly<Record<string, readonly string[]>> = {
|
||||
"anamnesis.life_circumstances.confirm",
|
||||
],
|
||||
"anamnesis/life-circumstances/route.ts": ["anamnesis.life_circumstances.get"],
|
||||
"dawarich/connect/route.ts": ["dawarich.connect"],
|
||||
"dawarich/disconnect/route.ts": ["dawarich.disconnect"],
|
||||
"dawarich/places/route.ts": ["dawarich.places.get"],
|
||||
"dawarich/settings/route.ts": ["dawarich.settings.patch"],
|
||||
"dawarich/status/route.ts": ["dawarich.status.get"],
|
||||
"dawarich/sync/route.ts": ["dawarich.sync"],
|
||||
"dawarich/test/route.ts": ["dawarich.test"],
|
||||
"environment/travel/suggestions/[id]/dismiss/route.ts": [
|
||||
"environment.travel_suggestion.dismiss",
|
||||
],
|
||||
"environment/travel/suggestions/route.ts": [
|
||||
"environment.travel_suggestions.get",
|
||||
],
|
||||
};
|
||||
|
||||
/** The size of the register when it was written; see the header. */
|
||||
const REGISTER_SIZE = 14;
|
||||
const REGISTER_SIZE = 5;
|
||||
|
||||
function routeFiles(): string[] {
|
||||
return walkSourceFiles(API_ROOT, { floor: 400, extensions: [".ts"] })
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* v1.43 — pair guard binding `DawarichDay` in `prisma/schema.prisma` to the
|
||||
* backup that carries it (`src/lib/export/environment-backup.ts`, the
|
||||
* `dawarichDays` arm of the environment section), on the pattern of
|
||||
* `environment-backup-completeness.test.ts`.
|
||||
*
|
||||
* A column missing from the select comes back NULL on every restored day
|
||||
* without a word, because the section schema is `.passthrough()`. So both
|
||||
* ends are compared as literals:
|
||||
*
|
||||
* - every scalar column of `DawarichDay` is in `DAWARICH_DAY_BACKUP_SELECT`
|
||||
* (or excluded here with a reason);
|
||||
* - every selected column is written by the export (`row.<column>`);
|
||||
* - every selected column is written back by the restore (`<column>:`).
|
||||
*
|
||||
* Mutation check: delete `trainDistanceM: true,` from the select and the
|
||||
* first case names it; delete `trainDistanceM:` from the restore and the
|
||||
* restore case names it.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const schema = readFileSync(join(ROOT, "prisma", "schema.prisma"), "utf8");
|
||||
const source = readFileSync(
|
||||
join(ROOT, "src", "lib", "export", "environment-backup.ts"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const EXCLUDED: Record<string, string> = {
|
||||
id: "nothing addresses a day by its id; a day is identified by its date",
|
||||
userId:
|
||||
"user-scoped backup; the owner is re-derived from the restoring account",
|
||||
};
|
||||
|
||||
function block(text: string, start: string, end = "\n}"): string {
|
||||
const from = text.indexOf(start);
|
||||
expect(from, `${start} not found`).toBeGreaterThan(-1);
|
||||
const to = text.indexOf(end, from);
|
||||
expect(to).toBeGreaterThan(from);
|
||||
return text.slice(from, to);
|
||||
}
|
||||
|
||||
function scalarColumns(model: string): string[] {
|
||||
const models = new Set(
|
||||
[...schema.matchAll(/^model\s+([A-Za-z_]\w*)\s*\{/gm)].map((m) => m[1]),
|
||||
);
|
||||
const out: string[] = [];
|
||||
for (const raw of block(schema, `model ${model} {`).split("\n").slice(1)) {
|
||||
const line = raw.trim();
|
||||
if (!line || line.startsWith("//") || line.startsWith("@@")) continue;
|
||||
const m = /^([a-zA-Z_]\w*)\s+([A-Za-z_]\w*)(\[\])?/.exec(line);
|
||||
if (!m || m[3] || models.has(m[2]) || line.includes("@relation")) continue;
|
||||
out.push(m[1]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const columns = scalarColumns("DawarichDay");
|
||||
const selected = [
|
||||
...block(source, "const DAWARICH_DAY_BACKUP_SELECT = {").matchAll(
|
||||
/^ {2}(\w+): true,$/gm,
|
||||
),
|
||||
].map((m) => m[1]);
|
||||
const exportBody = block(source, "dawarichDays: dawarichRows.map(", "})),");
|
||||
const restoreBody = block(
|
||||
source,
|
||||
"export async function restoreEnvironmentData(",
|
||||
"\n}\n",
|
||||
);
|
||||
const restoreDawarich = block(restoreBody, "tx.dawarichDay.createMany(", "});");
|
||||
|
||||
describe("DawarichDay backup completeness", () => {
|
||||
it("parses both ends into a plausible shape", () => {
|
||||
expect(columns).toContain("countryCodeEncrypted");
|
||||
expect(columns).toContain("awayFromHomeMinutes");
|
||||
expect(selected.length).toBeGreaterThan(8);
|
||||
});
|
||||
|
||||
it("the backup select covers every column", () => {
|
||||
const missing = columns.filter(
|
||||
(c) => !selected.includes(c) && !(c in EXCLUDED),
|
||||
);
|
||||
expect(
|
||||
missing,
|
||||
"DAWARICH_DAY_BACKUP_SELECT omits these columns; a restore brings them back NULL",
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("the export writes every selected column into the entry", () => {
|
||||
const unwritten = selected.filter(
|
||||
(c) => !new RegExp(`row\\.${c}\\b`).test(exportBody),
|
||||
);
|
||||
expect(unwritten, "selected but never exported").toEqual([]);
|
||||
});
|
||||
|
||||
it("the restore writes every selected column back", () => {
|
||||
const unwritten = selected.filter(
|
||||
(c) => !new RegExp(`\\b${c}:`).test(restoreDawarich),
|
||||
);
|
||||
expect(unwritten, "selected but never restored").toEqual([]);
|
||||
});
|
||||
|
||||
it("the restore clears the days it replaces", () => {
|
||||
expect(restoreBody).toMatch(/tx\.dawarichDay\.deleteMany\(/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* v1.43 — Dawarich is a movement context, never a location store. The schema
|
||||
* holds daily values only: no column of `DawarichDay`, and no Dawarich column
|
||||
* on `User`, may hold a coordinate, a place name, an address or a visit.
|
||||
*
|
||||
* The check reads the column names and their doc comments in
|
||||
* `prisma/schema.prisma`. A new column whose name or description says
|
||||
* latitude, longitude, coordinates, geometry, address, label, a place name or
|
||||
* a visit list fails here and has to argue its case in review.
|
||||
*
|
||||
* Mutation check: add `placeName String?` to `DawarichDay` and the first case
|
||||
* names it.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const schema = readFileSync(
|
||||
join(process.cwd(), "prisma", "schema.prisma"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const FORBIDDEN =
|
||||
/\b(lat|lon|lng|latitude|longitude|coord\w*|geo\w*|geometry|address|label|street|city|placeName|place_name|visits?Json|track|path|point)\b/i;
|
||||
|
||||
/** Column lines of a model with the `///` doc comment above each. */
|
||||
function columns(model: string): Array<{ name: string; text: string }> {
|
||||
const start = schema.indexOf(`model ${model} {`);
|
||||
expect(start, `model ${model} not found`).toBeGreaterThan(-1);
|
||||
const body = schema.slice(start, schema.indexOf("\n}", start));
|
||||
const out: Array<{ name: string; text: string }> = [];
|
||||
let doc = "";
|
||||
for (const raw of body.split("\n").slice(1)) {
|
||||
const line = raw.trim();
|
||||
if (line.startsWith("///")) {
|
||||
doc += ` ${line.slice(3)}`;
|
||||
continue;
|
||||
}
|
||||
const m = /^([a-zA-Z_]\w*)\s+\w/.exec(line);
|
||||
if (m && !line.startsWith("@@"))
|
||||
out.push({ name: m[1], text: `${m[1]} ${doc}` });
|
||||
doc = "";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
describe("Dawarich stores no location", () => {
|
||||
it("DawarichDay holds daily values only", () => {
|
||||
const cols = columns("DawarichDay");
|
||||
expect(cols.map((c) => c.name)).toContain("awayFromHomeMinutes");
|
||||
const offending = cols
|
||||
.filter((c) => c.name !== "user")
|
||||
.filter((c) => FORBIDDEN.test(c.name) || FORBIDDEN.test(c.text))
|
||||
.map((c) => c.name);
|
||||
expect(offending).toEqual([]);
|
||||
});
|
||||
|
||||
it("the User row holds the connection, never a place", () => {
|
||||
const cols = columns("User").filter((c) => /^dawarich/.test(c.name));
|
||||
expect(cols.map((c) => c.name).sort()).toEqual([
|
||||
"dawarichAllowPrivateHost",
|
||||
"dawarichApiKeyEncrypted",
|
||||
"dawarichDays",
|
||||
"dawarichPrefsEncrypted",
|
||||
"dawarichUrlEncrypted",
|
||||
]);
|
||||
const offending = cols
|
||||
.filter((c) => FORBIDDEN.test(c.name))
|
||||
.map((c) => c.name);
|
||||
expect(offending).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -649,6 +649,14 @@ const REGISTRY: DestructiveEntry[] = [
|
||||
recovery: "permanent",
|
||||
confirm: ["ConfirmDialog"],
|
||||
},
|
||||
{
|
||||
file: "components/settings/integrations/dawarich-card.tsx",
|
||||
destroys:
|
||||
"the stored Dawarich address, key and home settings, and with the second entry every stored movement day — reconnecting means typing them again; deleted days come back only as far as the instance still holds them",
|
||||
triggers: ["components/settings/integrations/integration-card.tsx"],
|
||||
recovery: "permanent",
|
||||
confirm: ["ConfirmDialog"],
|
||||
},
|
||||
{
|
||||
file: "components/settings/integrations/document-sources-card.tsx",
|
||||
destroys:
|
||||
|
||||
@@ -157,6 +157,7 @@ describe("record settings contract", () => {
|
||||
oura: false,
|
||||
"google-health": false,
|
||||
strava: false,
|
||||
dawarich: false,
|
||||
} as const;
|
||||
|
||||
expect(
|
||||
@@ -187,6 +188,7 @@ describe("record settings contract", () => {
|
||||
oura: false,
|
||||
"google-health": false,
|
||||
strava: false,
|
||||
dawarich: false,
|
||||
} as const;
|
||||
|
||||
expect(
|
||||
|
||||
@@ -377,6 +377,11 @@ const PINNED_AFFORDANCES: Record<
|
||||
"toast.success": 2,
|
||||
},
|
||||
// The saved-credentials confirmation line.
|
||||
// Connected, a fetch queued, the home saved: confirmations of a request
|
||||
// that wrote no record (the fetch itself reports through `health`).
|
||||
"src/components/settings/integrations/dawarich-card.tsx": {
|
||||
"toast.success": 3,
|
||||
},
|
||||
"src/components/settings/integrations/credentials-form.tsx": {
|
||||
"text-success": 1,
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user