gitoriaLog in with ident

ident

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Branchmain5fdbb6b2ident mission 009: report — scratch folder notemremain/lib/identities.hl

5.6 KB

  1. // lib/identities.hl — THE IDENTITIES of an account (piece 1, ticket #24; short ids ident#23; avatar ticket #15).
  2. // Every write to the identities table is here. Field checks, the label and the short id's alphabet:
  3. // lib/identities-helpers.hl. Statics only, the server realm.
  4. //
  5. // identitiesTable pk @id index @account, !shortId { account (account @id), shortId (5 chars, the PUBLIC id, ident#23), identityName, nickname,
  6. // firstname, lastname, avatar (data URL of the uploaded picture, ticket #15), created, updated }
  7. // every field but `account` optional
  8. //
  9. // An account ALWAYS has at least one identity (the first login makes its DEFAULT one, lib/accounts.hl
  10. // createAccount): the last one cannot be deleted. The DEFAULT identity is the account's oldest remaining
  11. // one (by `created`). Public identity ids are the records' UUIDs.
  12. import { MPackDB } from 'hl:mpackdb'
  13. import { now } from 'hl:time'
  14. import { dir, countOf, first, merged, isId, str, oldestFirst } from './util.hl'
  15. import { checkFields, shownAvatar, labelOf, newShortId, normShortId } from './identities-helpers.hl'
  16. static identitiesTable = new MPackDB(file = dir + '/identities.db', primaryKey = '@id', indexes = ['@account' '!shortId'])
  17. // ---- THE SHORT ID (ident#23) ------------------------------------------------------------
  18. // ONE PUBLIC ID per identity, kept forever, the same in every app: 5 characters (e.g. a68sz)
  19. // (lib/identities-helpers.hl newShortId). Case does not matter (stored lowercase, looked up
  20. // lowercased). Unique: checked against the `!shortId` index when it is made. Random (not
  21. // counted), so it says nothing about how many identities exist.
  22. // a fresh short id nobody has
  23. static freshShortId = () => {
  24. let id = newShortId()
  25. while (countOf(identitiesTable.find('shortId', id)) > 0) { id = newShortId() }
  26. return id
  27. }
  28. static identityByShortId = (v) => {
  29. t = normShortId(v)
  30. return t == null ? null : first(identitiesTable.find('shortId', t))
  31. }
  32. // every identity made before ident#23 gets its short id now, once (at load)
  33. static backfillShortIds = () => {
  34. let n = 0
  35. rows = identitiesTable.find(null, null)
  36. if (countOf(rows) > 0) {
  37. for (r of rows) {
  38. if (r.shortId == null || r.shortId == '') {
  39. identitiesTable.update(r.id, merged(r, { shortId = freshShortId() }))
  40. n = n + 1
  41. }
  42. }
  43. }
  44. return n
  45. }
  46. static shortIdsBackfilled = backfillShortIds()
  47. // ---- reading ------------------------------------------------------------------------------
  48. // the account's identities, oldest first (stored `created`); the first is the DEFAULT one
  49. static identityRecords = (accountId) => {
  50. if (!isId(accountId)) { return [] }
  51. return oldestFirst(identitiesTable.find('account', accountId))
  52. }
  53. static identityRows = (accountId) => {
  54. recs = identityRecords(accountId)
  55. out = []
  56. let n = 1
  57. for (r of recs) {
  58. let full = (str(r.firstname) + ' ' + str(r.lastname)).trim()
  59. let parts = []
  60. if (str(r.nickname) != '') { parts.push('“' + r.nickname + '”') }
  61. if (full != '') { parts.push(full) }
  62. out.push({
  63. id = r.id
  64. shortId = str(r.shortId)
  65. label = labelOf(r, n)
  66. identityName = str(r.identityName)
  67. nickname = str(r.nickname)
  68. firstname = str(r.firstname)
  69. lastname = str(r.lastname)
  70. avatar = shownAvatar(r)
  71. hasAvatar = shownAvatar(r) != ''
  72. details = parts.length > 0 ? parts.join(' · ') : 'no names'
  73. isDefault = n == 1
  74. canDelete = recs.length > 1
  75. })
  76. n = n + 1
  77. }
  78. return out
  79. }
  80. // the identity `id` if it belongs to the account, else null
  81. static ownIdentity = (accountId, id) => {
  82. if (!isId(id)) { return null }
  83. r = identitiesTable.fetch(id)
  84. if (r == null || r.account != accountId) { return null }
  85. return r
  86. }
  87. // any identity record by its id (null when it is gone), and the account it belongs to
  88. static identityRecord = (identityId) => { return isId(identityId) ? identitiesTable.fetch(identityId) : null }
  89. static accountOfIdentity = (identityId) => {
  90. r = identityRecord(identityId)
  91. return r != null ? r.account : null
  92. }
  93. // ---- writing ------------------------------------------------------------------------------
  94. // THE DEFAULT IDENTITY of a new account (lib/accounts.hl createAccount); its names are optional (asked next)
  95. static addDefaultIdentity = (accountId) => {
  96. return identitiesTable.put({ account = accountId shortId = freshShortId() identityName = 'Default' nickname = '' firstname = '' lastname = '' avatar = '' created = now() updated = now() })
  97. }
  98. // answers { error, field } or { identity (public id) }
  99. static createIdentity = (accountId, fields) => {
  100. c = checkFields(fields)
  101. if (c.error != null) { return c }
  102. rec = { account = accountId shortId = freshShortId() identityName = '' nickname = '' firstname = '' lastname = '' avatar = '' created = now() updated = now() }
  103. for (k of c.fields.keys()) { rec[k] = c.fields[k] }
  104. id = identitiesTable.put(rec)
  105. return { identity = id }
  106. }
  107. // only the fields named change; answers { error, field } or { identity }
  108. static updateIdentity = (accountId, id, fields) => {
  109. r = ownIdentity(accountId, id)
  110. if (r == null) { return { error = 'no such identity' field = 'id' } }
  111. c = checkFields(fields)
  112. if (c.error != null) { return c }
  113. changes = { updated = now() }
  114. for (k of c.fields.keys()) { changes[k] = c.fields[k] }
  115. identitiesTable.update(r.id, merged(r, changes))
  116. return { identity = id }
  117. }
  118. // THE LAST IDENTITY STAYS: answers { error } or { deleted }
  119. static deleteIdentity = (accountId, id) => {
  120. r = ownIdentity(accountId, id)
  121. if (r == null) { return { error = 'no such identity' } }
  122. if (identityRecords(accountId).length <= 1) { return { error = 'this is your only identity — an account always keeps one' } }
  123. identitiesTable.delete(r.id)
  124. return { deleted = id }
  125. }

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