/** * Generate Hyperschema + HyperDB definitions for PearData. * * Usage: node scripts/build-db.js * * SCHEMA SAFETY (Holepunch rules): * 1. Fields are append-only — deprecate instead of removing * 2. Do not delete/reset committed files under spec/ * 3. After merge to main, schema changes are permanent */ import path from 'path' import { fileURLToPath } from 'url' import Hyperschema from 'hyperschema' import HyperDB from 'hyperdb/builder' const __dirname = path.dirname(fileURLToPath(import.meta.url)) const ROOT = path.join(__dirname, '..') const SCHEMA_DIR = path.join(ROOT, 'spec', 'hyperschema') const DB_DIR = path.join(ROOT, 'spec', 'hyperdb') setupSchema() setupDb() console.log('Wrote', SCHEMA_DIR) console.log('Wrote', DB_DIR) function setupSchema() { const schema = Hyperschema.from(SCHEMA_DIR) const ns = schema.namespace('peardata') ns.register({ name: 'node', fields: [ { name: 'nodeId', type: 'string', required: true }, { name: 'hostname', type: 'string', required: true }, { name: 'publicKeyHex', type: 'string', required: false }, { name: 'platform', type: 'string', required: false }, { name: 'arch', type: 'string', required: false }, { name: 'cpus', type: 'uint', required: false }, { name: 'totalMemMiB', type: 'uint', required: false }, { name: 'agentVersion', type: 'string', required: false }, { name: 'labelsJson', type: 'string', required: false }, { name: 'updatedAt', type: 'uint', required: true }, ], }) ns.register({ name: 'peer-link', fields: [ { name: 'localNodeId', type: 'string', required: true }, { name: 'remotePublicKey', type: 'string', required: true }, { name: 'role', type: 'string', required: false }, { name: 'alias', type: 'string', required: false }, { name: 'dbKeyHex', type: 'string', required: false }, { name: 'syncMode', type: 'string', required: false }, // push | pull | both { name: 'linkedAt', type: 'uint', required: true }, { name: 'lastSeen', type: 'uint', required: false }, // Append-only v2: swarm topic to rejoin after restart { name: 'discoveryKeyHex', type: 'string', required: false }, ], }) ns.register({ name: 'alert-config', fields: [ { name: 'id', type: 'string', required: true }, { name: 'chart', type: 'string', required: true }, { name: 'dimension', type: 'string', required: true }, { name: 'warn', type: 'float64', required: false }, { name: 'crit', type: 'float64', required: false }, { name: 'comparator', type: 'string', required: false }, { name: 'enabled', type: 'bool', required: false }, { name: 'info', type: 'string', required: false }, { name: 'updatedAt', type: 'uint', required: true }, ], }) ns.register({ name: 'alert-event', fields: [ { name: 'id', type: 'string', required: true }, { name: 'ts', type: 'uint', required: true }, { name: 'chart', type: 'string', required: true }, { name: 'dimension', type: 'string', required: false }, { name: 'severity', type: 'string', required: true }, { name: 'value', type: 'float64', required: false }, { name: 'threshold', type: 'float64', required: false }, { name: 'message', type: 'string', required: false }, { name: 'cleared', type: 'bool', required: false }, ], }) // Warm / downsampled metric buckets (NOT 1s hot path) ns.register({ name: 'metric-point', fields: [ { name: 'chart', type: 'string', required: true }, { name: 'ts', type: 'uint', required: true }, // unix ms { name: 'context', type: 'string', required: true }, { name: 'tier', type: 'uint', required: true }, // 1 = ~1m, 2 = future coarser { name: 'valuesJson', type: 'string', required: true }, ], }) ns.register({ name: 'job', fields: [ { name: 'id', type: 'string', required: true }, { name: 'name', type: 'string', required: true }, { name: 'status', type: 'string', required: true }, { name: 'startedAt', type: 'uint', required: false }, { name: 'finishedAt', type: 'uint', required: false }, { name: 'resultJson', type: 'string', required: false }, { name: 'error', type: 'string', required: false }, ], }) Hyperschema.toDisk(schema, { esm: true }) } function setupDb() { const db = HyperDB.from(SCHEMA_DIR, DB_DIR) const ns = db.namespace('peardata') ns.collections.register({ name: 'node', schema: '@peardata/node', key: ['nodeId'], }) ns.collections.register({ name: 'peer-link', schema: '@peardata/peer-link', key: ['localNodeId', 'remotePublicKey'], }) ns.collections.register({ name: 'alert-config', schema: '@peardata/alert-config', key: ['id'], }) ns.collections.register({ name: 'alert-event', schema: '@peardata/alert-event', key: ['id', 'ts'], }) ns.collections.register({ name: 'metric-point', schema: '@peardata/metric-point', key: ['chart', 'ts'], }) ns.collections.register({ name: 'job', schema: '@peardata/job', key: ['id'], }) ns.indexes.register({ name: 'node-by-hostname', collection: '@peardata/node', key: ['hostname'], }) ns.indexes.register({ name: 'peer-link-by-remote', collection: '@peardata/peer-link', key: ['remotePublicKey'], }) ns.indexes.register({ name: 'alert-event-by-chart', collection: '@peardata/alert-event', key: ['chart', 'ts'], }) // metric-point is only queried by primary key [chart, ts]. Secondary indexes // tripled Hyperbee write amplification and were unused. HyperDB schema rules // require deprecating (not deleting) committed indexes so IDs stay stable. ns.indexes.register({ name: 'metric-point-by-context', collection: '@peardata/metric-point', key: ['context', 'ts'], deprecated: true, }) ns.indexes.register({ name: 'metric-point-by-tier', collection: '@peardata/metric-point', key: ['tier', 'chart', 'ts'], deprecated: true, }) HyperDB.toDisk(db) }