ident
All repositories: gitoria
5.6 KB
// lib/identities.hl — THE IDENTITIES of an account (piece 1, ticket #24; short ids ident#23; avatar ticket #15).// Every write to the identities table is here. Field checks, the label and the short id's alphabet:// lib/identities-helpers.hl. Statics only, the server realm.//// identitiesTable pk @id index @account, !shortId { account (account @id), shortId (5 chars, the PUBLIC id, ident#23), identityName, nickname,// firstname, lastname, avatar (data URL of the uploaded picture, ticket #15), created, updated }// every field but `account` optional//// An account ALWAYS has at least one identity (the first login makes its DEFAULT one, lib/accounts.hl// createAccount): the last one cannot be deleted. The DEFAULT identity is the account's oldest remaining// one (by `created`). Public identity ids are the records' UUIDs.import { MPackDB } from 'hl:mpackdb'import { now } from 'hl:time'import { dir, countOf, first, merged, isId, str, oldestFirst } from './util.hl'import { checkFields, shownAvatar, labelOf, newShortId, normShortId } from './identities-helpers.hl'static identitiesTable = new MPackDB(file = dir + '/identities.db', primaryKey = '@id', indexes = ['@account' '!shortId'])// ---- THE SHORT ID (ident#23) ------------------------------------------------------------// ONE PUBLIC ID per identity, kept forever, the same in every app: 5 characters (e.g. a68sz)// (lib/identities-helpers.hl newShortId). Case does not matter (stored lowercase, looked up// lowercased). Unique: checked against the `!shortId` index when it is made. Random (not// counted), so it says nothing about how many identities exist.// a fresh short id nobody hasstatic freshShortId = () => {let id = newShortId()while (countOf(identitiesTable.find('shortId', id)) > 0) { id = newShortId() }return id}static identityByShortId = (v) => {t = normShortId(v)return t == null ? null : first(identitiesTable.find('shortId', t))}// every identity made before ident#23 gets its short id now, once (at load)static backfillShortIds = () => {let n = 0rows = identitiesTable.find(null, null)if (countOf(rows) > 0) {for (r of rows) {if (r.shortId == null || r.shortId == '') {identitiesTable.update(r.id, merged(r, { shortId = freshShortId() }))n = n + 1}}}return n}static shortIdsBackfilled = backfillShortIds()// ---- reading ------------------------------------------------------------------------------// the account's identities, oldest first (stored `created`); the first is the DEFAULT onestatic identityRecords = (accountId) => {if (!isId(accountId)) { return [] }return oldestFirst(identitiesTable.find('account', accountId))}static identityRows = (accountId) => {recs = identityRecords(accountId)out = []let n = 1for (r of recs) {let full = (str(r.firstname) + ' ' + str(r.lastname)).trim()let parts = []if (str(r.nickname) != '') { parts.push('“' + r.nickname + '”') }if (full != '') { parts.push(full) }out.push({id = r.idshortId = str(r.shortId)label = labelOf(r, n)identityName = str(r.identityName)nickname = str(r.nickname)firstname = str(r.firstname)lastname = str(r.lastname)avatar = shownAvatar(r)hasAvatar = shownAvatar(r) != ''details = parts.length > 0 ? parts.join(' · ') : 'no names'isDefault = n == 1canDelete = recs.length > 1})n = n + 1}return out}// the identity `id` if it belongs to the account, else nullstatic ownIdentity = (accountId, id) => {if (!isId(id)) { return null }r = identitiesTable.fetch(id)if (r == null || r.account != accountId) { return null }return r}// any identity record by its id (null when it is gone), and the account it belongs tostatic identityRecord = (identityId) => { return isId(identityId) ? identitiesTable.fetch(identityId) : null }static accountOfIdentity = (identityId) => {r = identityRecord(identityId)return r != null ? r.account : null}// ---- writing ------------------------------------------------------------------------------// THE DEFAULT IDENTITY of a new account (lib/accounts.hl createAccount); its names are optional (asked next)static addDefaultIdentity = (accountId) => {return identitiesTable.put({ account = accountId shortId = freshShortId() identityName = 'Default' nickname = '' firstname = '' lastname = '' avatar = '' created = now() updated = now() })}// answers { error, field } or { identity (public id) }static createIdentity = (accountId, fields) => {c = checkFields(fields)if (c.error != null) { return c }rec = { account = accountId shortId = freshShortId() identityName = '' nickname = '' firstname = '' lastname = '' avatar = '' created = now() updated = now() }for (k of c.fields.keys()) { rec[k] = c.fields[k] }id = identitiesTable.put(rec)return { identity = id }}// only the fields named change; answers { error, field } or { identity }static updateIdentity = (accountId, id, fields) => {r = ownIdentity(accountId, id)if (r == null) { return { error = 'no such identity' field = 'id' } }c = checkFields(fields)if (c.error != null) { return c }changes = { updated = now() }for (k of c.fields.keys()) { changes[k] = c.fields[k] }identitiesTable.update(r.id, merged(r, changes))return { identity = id }}// THE LAST IDENTITY STAYS: answers { error } or { deleted }static deleteIdentity = (accountId, id) => {r = ownIdentity(accountId, id)if (r == null) { return { error = 'no such identity' } }if (identityRecords(accountId).length <= 1) { return { error = 'this is your only identity — an account always keeps one' } }identitiesTable.delete(r.id)return { deleted = id }}
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