ident
All repositories: gitoria
8.5 KB
// tools/migrate-009.hl — ONE-OFF MIGRATION (ticket ident #10 / old #39, mission 009,// 2026-09-24): the old `*id` tables (`storage/ident-<table>.*`) into the new ones// (`storage/mpackdb/<table>.*`, pk `@id` UUID), plus the session files.//// * every row of every table (accounts, identities, otp, sends, apps, connections,// requests, grants) is copied field by field; `id` becomes a new UUID and the old// PUBLIC id (old mpackdb id + 1 — what the pages, faces and sessions used) is kept// as `oldId`.// * references are rewritten to the new UUIDs: identities.account, apps.owner,// connections.app / .identity / .pair ('<app>:<identity>'), requests.app,// grants.app / .identity.// * WHAT APPS HOLD STAYS BYTE-IDENTICAL: apps.apiKey, apps.secretHash,// connections.appIdentity, grants.appIdentity/hash, requests.rid are copied as is.// * SESSIONS (optional, IDENT_SESSIONS_MIGRATE = the .sessions dir): a file whose// `"user":{"id":<old account number>}` names a migrated account gets that account's// UUID instead (only that substring changes). Anonymous sessions and ones already// migrated are left alone.//// IDEMPOTENT: a row whose oldId is already in its new table is not written again, a// session whose user id is already a string is skipped — a second run prints "0 new".// The OLD FILES are opened with compact = false and never written by this tool — but// hl:mpackdb rewrites a table's files on open (hybriel #40): RUN IT ON A COPY of them.//// STOP THE SERVER FIRST (it holds the tables and the sessions in memory), back up, then://// IDENT_OLD_STORAGE=/abs/copy/ident IDENT_STORAGE=$PWD/storage/mpackdb \// IDENT_SESSIONS_MIGRATE=$PWD/.sessions bin/hybriel tools/migrate-009.hl//// All paths ABSOLUTE (Hybriel resolves relative paths against tools/).import { MPackDB } from 'hl:mpackdb'import { env } from 'hl:proc'import { exists, listDir, readFile, writeFile } from 'hl:fs'import { dir, countOf } from '../lib/util.hl'import { accountsTable } from '../lib/accounts.hl'import { identitiesTable } from '../lib/identities.hl'import { otpTable, sendsTable } from '../lib/login.hl'import { appsTable, connectionsTable, requestsTable, grantsTable } from '../lib/apps.hl'oldPrefix = env('IDENT_OLD_STORAGE')if (oldPrefix == null || !oldPrefix.startsWith('/') || env('IDENT_STORAGE') == null || !env('IDENT_STORAGE').startsWith('/')) {hlError('set IDENT_OLD_STORAGE (old table prefix, e.g. /abs/copy/ident) and IDENT_STORAGE (new table directory, e.g. $PWD/storage/mpackdb), both ABSOLUTE')}tableNames = ['accounts' 'identities' 'otp' 'sends' 'apps' 'connections' 'requests' 'grants']for (t of tableNames) {if (!exists(oldPrefix + '-' + t + '.mpack')) { hlError('no old table ' + oldPrefix + '-' + t + '.mpack') }}// the old tables, exactly as the old store.hl / apps.hl declared them; compact = falseoldAccounts = new MPackDB(file = oldPrefix + '-accounts.db', primaryKey = '*id', indexes = ['!email'], compact = false)oldIdentities = new MPackDB(file = oldPrefix + '-identities.db', primaryKey = '*id', indexes = ['account'], compact = false)oldOtp = new MPackDB(file = oldPrefix + '-otp.db', primaryKey = '*id', indexes = ['email'], compact = false)oldSends = new MPackDB(file = oldPrefix + '-sends.db', primaryKey = '*id', indexes = ['email'], compact = false)oldApps = new MPackDB(file = oldPrefix + '-apps.db', primaryKey = '*id', indexes = ['owner' '!apiKey'], compact = false)oldConnections = new MPackDB(file = oldPrefix + '-connections.db', primaryKey = '*id', indexes = ['!pair' 'app' '!appIdentity'], compact = false)oldRequests = new MPackDB(file = oldPrefix + '-requests.db', primaryKey = '*id', indexes = ['!rid'], compact = false)oldGrants = new MPackDB(file = oldPrefix + '-grants.db', primaryKey = '*id', indexes = ['!hash'], compact = false)static all = (table) => {out = []found = table.find(null, null)if (countOf(found) > 0) { for (r of found) { out.push(r) } }return out}// old public id (as a string key) → new UUID, from the rows already migratedstatic mapOf = (table) => {m = {}for (r of all(table)) { if (r.oldId != null) { m['' + r.oldId] = r.id } }return m}// every field of `rec` but `id`, then `extra` on topstatic copied = (rec, extra) => {out = {}for (k of rec.keys()) { if (k != 'id') { out[k] = rec[k] } }for (k of extra.keys()) { out[k] = extra[k] }return out}failed = 0report = []// copy one old table: `refs(rec)` answers the extra fields (the rewritten references),// or { skip = reason } when a reference cannot be resolvedmigrateTable = (name, oldTable, newTable, refs) => {have = mapOf(newTable)let fresh = 0let known = 0for (r of all(oldTable)) {let oldId = r.id + 1if (have['' + oldId] != null) {known = known + 1} else {let extra = refs(r)if (extra.skip != null) {console.log('SKIP ' + name + ' old ' + oldId + ': ' + extra.skip)failed = failed + 1} else {extra.oldId = oldIdlet id = newTable.put(copied(r, extra))if (id == null) {console.log('FAIL ' + name + ' old ' + oldId + ': ' + newTable.lastError())failed = failed + 1} else {have['' + oldId] = idfresh = fresh + 1}}}}report.push(name + ' ' + fresh + ' new, ' + known + ' already there (old ' + oldTable.count() + ' → new ' + newTable.count() + ')')return have}none = (r) => { return {} }acc = migrateTable('accounts', oldAccounts, accountsTable, none)ids = migrateTable('identities', oldIdentities, identitiesTable, (r) => {a = acc['' + r.account]return a == null ? { skip = 'its account ' + r.account + ' is gone' } : { account = a }})migrateTable('otp', oldOtp, otpTable, none)migrateTable('sends', oldSends, sendsTable, none)apps = migrateTable('apps', oldApps, appsTable, (r) => {o = acc['' + r.owner]return o == null ? { skip = 'its owner ' + r.owner + ' is gone' } : { owner = o }})// a connection of a DELETED identity stays (as it did before: its id is still the app's),// its identity reads 'gone:<old id>' so the pair stays unique and matches no identitymigrateTable('connections', oldConnections, connectionsTable, (r) => {a = apps['' + r.app]if (a == null) { return { skip = 'its app ' + r.app + ' is gone' } }let i = ids['' + r.identity]if (i == null) { i = 'gone:' + r.identity }return { app = a identity = i pair = a + ':' + i }})migrateTable('requests', oldRequests, requestsTable, (r) => {a = apps['' + r.app]return a == null ? { skip = 'its app ' + r.app + ' is gone' } : { app = a }})migrateTable('grants', oldGrants, grantsTable, (r) => {a = apps['' + r.app]i = ids['' + r.identity]if (a == null) { return { skip = 'its app ' + r.app + ' is gone' } }return { app = a identity = i == null ? 'gone:' + r.identity : i }})// ---- the session files --------------------------------------------------------------sessDir = env('IDENT_SESSIONS_MIGRATE')static digitsAt = (s, p0) => {let i = p0while (i < s.length && s.charCodeAt(i) >= 48 && s.charCodeAt(i) <= 57) { i = i + 1 }return i}if (sessDir != null && sessDir != '') {if (!sessDir.startsWith('/') || !exists(sessDir)) { hlError('IDENT_SESSIONS_MIGRATE must be an existing ABSOLUTE directory') }let moved = 0let already = 0let anonymous = 0let unknown = 0pre = '"user":{"id":'for (e of listDir(sessDir)) {let raw = readFile(e.path)let at = raw == null ? -1 : raw.indexOf(pre)if (at < 0) {anonymous = anonymous + 1} else {let p0 = at + pre.lengthlet end = digitsAt(raw, p0)if (end == p0) {already = already + 1 // a string id: migrated before} else if (raw[end] != '}') {console.log('SESSION ' + e.name + ': unexpected user shape, left alone')unknown = unknown + 1} else {let n = raw.slice(p0, end)let to = acc[n]if (to == null) {console.log('SESSION ' + e.name + ': account ' + n + ' is not migrated, left alone (it reads as signed out)')unknown = unknown + 1} else {let next = raw.slice(0, p0) + '"' + to + '"' + raw.slice(end, raw.length)let check = JSON.parse(next)if (check == null || check.user == null || check.user.id != to) {console.log('SESSION ' + e.name + ': rewrite did not read back, left alone')failed = failed + 1} else {writeFile(e.path, next, 384)moved = moved + 1}}}}}report.push('sessions ' + moved + ' new, ' + already + ' already there, ' + anonymous + ' anonymous, ' + unknown + ' left alone (' + sessDir + ')')}for (line of report) { console.log('migrate: ' + line) }console.log('migrate: ' + failed + ' failed (' + oldPrefix + ' → ' + dir + ')')
Branches
- mainmain branch
Latest commits
- 5fdbb6b2ident mission 009: report — scratch folder notemre
- 74235815ident mission 009 (4/4): docs (README files map + same-output test, STATUS, LOG), report, tests/letcount.py + realdata-baseline/comparemre
- fe183516ident mission 009 (3/4): let only where reassigned — 293 never-reassigned lets are plain declarations; kept: reassigned, loop bodies, names of a file member, a name declared twice in one function; same outputmre
- d2e7f91bident mission 009 (2/4): one lib file per topic (login, accounts, identities, apps, invites, selector, notify + helpers, util), function routes as thin wrappers in lib/api.hl, project.hl = the map; same outputmre
- 91017164ident mission 009 (1/4): file moves — the root .hl files into lib/ (api.hl → lib/api-helpers.hl), styles.hl → components/styles.hl; imports adjusted, no other changemre
- f8bdcbc2ident: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); all gates greenmre
- ff78726cident: Hybriel master 190aa11d (fc838894 GC correctness, #127, #126 closure scopes); gates all greenmre
- a3a7d21aident: Hybriel master 8efba065 (#126 GC by bytes, #48 lambda params copy); session-writing lambdas take &sessionmre
- 98226b41antcolony#40: mission references point to the moved missionsmre
- ff805b9aantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
- 51a7bcdfident: Hybriel master 73267707 (#122); /code uses the new page() signature; pending address passed as parameter; once-checksmre
- 836f644fident#24: installable app (manifest, service worker, data-free offline /start), own iconmre
- 8bebbbf2deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- cc063ea2deploy.sh: never send .git or .gitignore to Byrodinmre
- 81b15b7bState of 2026-09-27, before the move to gitoriamre