Files
peardata/scripts/build-db.js
T
Raven Scott c541c27ad8
CI / test (push) Successful in 1m17s
Release rolling / release (push) Successful in 8m15s
Prune work
2026-07-30 12:38:17 -04:00

197 lines
6.0 KiB
JavaScript

/**
* 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)
}