feat(L07): add addresses and memory card
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
|
||||
const STATE_SCHEMA = 'stem-card-backend-state.v1';
|
||||
|
||||
export class CardStateDatabase {
|
||||
constructor(file) {
|
||||
this.file = path.resolve(file);
|
||||
mkdirSync(path.dirname(this.file), { recursive: true });
|
||||
this.database = new DatabaseSync(this.file);
|
||||
this.database.exec(`
|
||||
PRAGMA journal_mode = WAL;
|
||||
PRAGMA synchronous = NORMAL;
|
||||
CREATE TABLE IF NOT EXISTS card_state (
|
||||
key TEXT PRIMARY KEY,
|
||||
schema_name TEXT NOT NULL,
|
||||
revision INTEGER NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
value_json TEXT NOT NULL
|
||||
) STRICT;
|
||||
CREATE TABLE IF NOT EXISTS card_event (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
occurred_at TEXT NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
actor TEXT NOT NULL,
|
||||
task_id TEXT,
|
||||
block_id TEXT,
|
||||
phase_id TEXT,
|
||||
step_id TEXT,
|
||||
snapshot_ref TEXT,
|
||||
evidence_ref TEXT,
|
||||
payload_json TEXT NOT NULL
|
||||
) STRICT;
|
||||
CREATE INDEX IF NOT EXISTS card_event_occurred_at
|
||||
ON card_event(occurred_at, id);
|
||||
`);
|
||||
this.readStatement = this.database.prepare(`
|
||||
SELECT value_json
|
||||
FROM card_state
|
||||
WHERE key = ?
|
||||
`);
|
||||
this.writeStatement = this.database.prepare(`
|
||||
INSERT INTO card_state (key, schema_name, revision, updated_at, value_json)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET
|
||||
schema_name = excluded.schema_name,
|
||||
revision = excluded.revision,
|
||||
updated_at = excluded.updated_at,
|
||||
value_json = excluded.value_json
|
||||
`);
|
||||
this.appendEventStatement = this.database.prepare(`
|
||||
INSERT INTO card_event (
|
||||
occurred_at, event_type, actor,
|
||||
task_id, block_id, phase_id, step_id, snapshot_ref,
|
||||
evidence_ref, payload_json
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING id
|
||||
`);
|
||||
this.listEventsStatement = this.database.prepare(`
|
||||
SELECT
|
||||
id, occurred_at, event_type, actor,
|
||||
task_id, block_id, phase_id, step_id, snapshot_ref,
|
||||
evidence_ref, payload_json
|
||||
FROM card_event
|
||||
WHERE id > ?
|
||||
ORDER BY id ASC
|
||||
LIMIT ?
|
||||
`);
|
||||
}
|
||||
|
||||
read(key) {
|
||||
const row = this.readStatement.get(key);
|
||||
if (!row) return null;
|
||||
try {
|
||||
return JSON.parse(row.value_json);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
write(key, value) {
|
||||
const revision = Number.isSafeInteger(Number(value?.revision))
|
||||
? Number(value.revision)
|
||||
: 0;
|
||||
const updatedAt = String(value?.updated_at ?? value?.selected_at ?? new Date().toISOString());
|
||||
this.writeStatement.run(
|
||||
key,
|
||||
STATE_SCHEMA,
|
||||
revision,
|
||||
updatedAt,
|
||||
JSON.stringify(value)
|
||||
);
|
||||
return value;
|
||||
}
|
||||
|
||||
appendEvent({
|
||||
eventType,
|
||||
actor = 'system',
|
||||
navigation = null,
|
||||
evidenceRef = null,
|
||||
payload = {},
|
||||
occurredAt = new Date().toISOString()
|
||||
}) {
|
||||
const row = this.appendEventStatement.get(
|
||||
String(occurredAt),
|
||||
String(eventType),
|
||||
String(actor),
|
||||
navigation?.task?.id ?? null,
|
||||
navigation?.block?.id ?? null,
|
||||
navigation?.phase?.id ?? null,
|
||||
navigation?.step?.id ?? null,
|
||||
navigation?.snapshot_ref ?? null,
|
||||
evidenceRef == null ? null : String(evidenceRef),
|
||||
JSON.stringify(payload ?? {})
|
||||
);
|
||||
return {
|
||||
id: Number(row.id),
|
||||
occurred_at: String(occurredAt),
|
||||
event_type: String(eventType),
|
||||
actor: String(actor),
|
||||
task_id: navigation?.task?.id ?? null,
|
||||
block_id: navigation?.block?.id ?? null,
|
||||
phase_id: navigation?.phase?.id ?? null,
|
||||
step_id: navigation?.step?.id ?? null,
|
||||
snapshot_ref: navigation?.snapshot_ref ?? null,
|
||||
evidence_ref: evidenceRef == null ? null : String(evidenceRef),
|
||||
payload: payload ?? {}
|
||||
};
|
||||
}
|
||||
|
||||
listEvents({ afterId = 0, limit = 200 } = {}) {
|
||||
return this.listEventsStatement.all(Number(afterId), Number(limit)).map(row => {
|
||||
let payload = {};
|
||||
try {
|
||||
payload = JSON.parse(row.payload_json);
|
||||
} catch {
|
||||
// Preserve the audit row even if an old payload cannot be decoded.
|
||||
}
|
||||
return {
|
||||
id: Number(row.id),
|
||||
occurred_at: row.occurred_at,
|
||||
event_type: row.event_type,
|
||||
actor: row.actor,
|
||||
task_id: row.task_id,
|
||||
block_id: row.block_id,
|
||||
phase_id: row.phase_id,
|
||||
step_id: row.step_id,
|
||||
snapshot_ref: row.snapshot_ref,
|
||||
evidence_ref: row.evidence_ref,
|
||||
payload
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.database.close();
|
||||
}
|
||||
}
|
||||
|
||||
export { STATE_SCHEMA };
|
||||
Reference in New Issue
Block a user