Files
memind/health-page-data-draft-store.mjs
T
john 2baf29b3ae feat(health): complete P0 health channel — baseline engine, page-data, MindSpace UI
Deliver encrypted health zone, observation API, baseline maturity pipeline,
page-data bindings, and H5/WeChat channel integration for health P0.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 18:04:57 +08:00

115 lines
4.4 KiB
JavaScript

import { createUserDataSpaceService } from './user-data-space-service.mjs';
import { ensureHealthPageDataForUser } from './health-page-data-bootstrap.mjs';
import { HEALTH_OBSERVATION_DRAFTS_DATASET } from './health-page-data-schema.mjs';
const DATASET_NAME = HEALTH_OBSERVATION_DRAFTS_DATASET.name;
function parseJsonField(value, fallback = {}) {
if (value == null) return fallback;
if (typeof value === 'object') return value;
try {
return JSON.parse(String(value));
} catch {
return fallback;
}
}
function mapPgDraftRow(row, userId, sourceRef = null) {
return {
id: Number(row.id),
userId: String(userId),
draftKey: row.draft_key,
metricSet: row.metric_set,
extracted: parseJsonField(row.extracted),
validation: parseJsonField(row.validation),
status: row.status,
channel: row.channel,
sourceRef: sourceRef ?? parseJsonField(row.extracted)?.sourceRef ?? null,
createdAt: row.created_at ? new Date(row.created_at).getTime() : Date.now(),
expiresAt: row.expires_at ? new Date(row.expires_at).getTime() : Date.now(),
committedAt: row.committed_at ? new Date(row.committed_at).getTime() : null,
};
}
export function createPageDataHealthObservationDraftStore({ resolveWorkspaceRoot, logger = console }) {
if (typeof resolveWorkspaceRoot !== 'function') {
throw new Error('createPageDataHealthObservationDraftStore requires resolveWorkspaceRoot');
}
async function getUserDataSpace(userId) {
const workspaceRoot = await resolveWorkspaceRoot(userId);
if (!workspaceRoot) return null;
const service = createUserDataSpaceService({ workspaceRoot, userId: String(userId) });
await ensureHealthPageDataForUser(service, userId);
return service;
}
async function listPending(userId, { limit = 100 } = {}) {
const service = await getUserDataSpace(userId);
if (!service) return [];
try {
const result = await service.readDatasetRows(DATASET_NAME, {
limit,
orderBy: 'created_at',
orderDir: 'desc',
});
const now = Date.now();
return (result.rows ?? [])
.map((row) => mapPgDraftRow(row, userId))
.filter((row) => row.status === 'pending' && row.expiresAt > now);
} catch (error) {
if (error?.code === 'dataset_not_found') return [];
throw error;
}
}
return {
backend: 'page_data',
async findByKey(userId, draftKey) {
const pending = await listPending(userId);
return pending.find((row) => row.draftKey === draftKey) ?? null;
},
async findPendingBySourceRef(userId, sourceRef) {
if (!sourceRef) return null;
const pending = await listPending(userId);
return pending.find((row) => row.sourceRef === sourceRef) ?? null;
},
async insert(userId, draft) {
const service = await getUserDataSpace(userId);
if (!service) {
const error = Object.assign(new Error('用户健康草稿空间不可用'), { code: 'health_storage_unavailable' });
throw error;
}
const extracted = { ...draft.extracted, sourceRef: draft.sourceRef ?? null };
try {
const { row } = await service.insertDatasetRow(DATASET_NAME, {
draft_key: draft.draftKey,
metric_set: draft.metricSet,
extracted: JSON.stringify(extracted),
validation: JSON.stringify(draft.validation ?? {}),
status: 'pending',
channel: draft.channel ?? 'h5',
expires_at: new Date(draft.expiresAt ?? Date.now() + 86400000).toISOString(),
});
return mapPgDraftRow(row, userId, draft.sourceRef ?? null);
} catch (error) {
logger.warn?.('Page Data health draft insert failed:', error);
throw error;
}
},
async update(userId, draftKey, patch) {
const service = await getUserDataSpace(userId);
if (!service) return null;
const existing = await this.findByKey(userId, draftKey);
if (!existing?.id) return null;
const payload = {};
if (patch.extracted != null) payload.extracted = JSON.stringify(patch.extracted);
if (patch.validation != null) payload.validation = JSON.stringify(patch.validation);
if (patch.status != null) payload.status = patch.status;
if (patch.committedAt != null) payload.committed_at = new Date(patch.committedAt).toISOString();
const { row } = await service.updateDatasetRow(DATASET_NAME, existing.id, payload);
return mapPgDraftRow(row, userId, existing.sourceRef);
},
};
}