Move navigation from _menu.json files in each navigation-parent directory to a naviga...
authorNick Downing <nick@ndcode.org>
Thu, 27 Jan 2022 10:40:06 +0000 (21:40 +1100)
committerNick Downing <nick@ndcode.org>
Thu, 27 Jan 2022 10:40:06 +0000 (21:40 +1100)
16 files changed:
_config/n.sh
_config/navigation.json [new file with mode: 0644]
_lib/blog_post.jst [new file with mode: 0644]
_lib/breadcrumbs.jst
_lib/get_navigation.jst [new file with mode: 0644]
_lib/navbar.jst
_lib/post.jst [deleted file]
_menu.json [deleted file]
api/globals/set.json.jst
api/navigation/get.json.jst [new file with mode: 0644]
api/navigation/set.json.jst [new file with mode: 0644]
blog/20220114/index.html.jst
blog/_menu.json [deleted file]
blog/index.html.jst
my_account/_menu.json [deleted file]
my_account/index.html.jst

index 419f2e7..b89228d 100755 (executable)
@@ -1,3 +1,4 @@
 #!/bin/sh
 ./set.mjs /api/globals/set.json <globals.json
 ./set.mjs /api/nodemailers/set.json <nodemailers.json
+./set.mjs /api/navigation/set.json <navigation.json
diff --git a/_config/navigation.json b/_config/navigation.json
new file mode 100644 (file)
index 0000000..cca630b
--- /dev/null
@@ -0,0 +1,85 @@
+{
+  "title": "Home",
+  "children": {
+    "blog": {
+      "title": "Blog",
+      "children": {
+        "20220114": {
+          "title": "14/01/2022",
+          "children": {},
+          "menu": [],
+          "description": "Log-structured JSON database",
+          "author": "Nick Downing",
+          "image": "image.svg",
+          "thumbnail": "thumbnail.svg"
+        }
+      },
+      "menu": [
+        "20220114"
+      ]
+    },
+    "projects": {
+      "title": "Projects",
+      "children": {},
+      "menu": []
+    },
+    "sphinx": {
+      "title": "Documentation",
+      "children": {},
+      "menu": []
+    },
+    "contact": {
+      "title": "Contact",
+      "children": {},
+      "menu": []
+    },
+    "my_account": {
+      "title": "My account",
+      "children": {
+        "sign_up": {
+          "title": "Sign up",
+          "children": {},
+          "menu": [],
+          "icon": "/_svg/icon_sign_up.svg"
+        },
+        "send_verification_email": {
+          "title": "Send verification email",
+          "children": {},
+          "menu": []
+        },
+        "verify_email": {
+          "title": "Verify email",
+          "children": {},
+          "menu": []
+        },
+        "password_reset": {
+          "title": "Password reset",
+          "children": {},
+          "menu": [],
+          "icon": "/_svg/icon_password_reset.svg"
+        },
+        "verify_password": {
+          "title": "Verify password",
+          "children": {},
+          "menu": []
+        }
+      },
+      "menu": [
+        "sign_up",
+        "password_reset"
+      ]
+    },
+    "search": {
+      "title": "Search",
+      "children": {},
+      "menu": []
+    }
+  },
+  "menu": [
+    "blog",
+    "projects",
+    "sphinx",
+    "contact",
+    "my_account"
+  ]
+}
diff --git a/_lib/blog_post.jst b/_lib/blog_post.jst
new file mode 100644 (file)
index 0000000..401761b
--- /dev/null
@@ -0,0 +1,40 @@
+let assert = require('assert')
+
+return async (env, head, body, scripts) => {
+  let breadcrumbs = await _require('/_lib/breadcrumbs.jst')
+  let get_navigation = await _require('/_lib/get_navigation.jst')
+  let navbar = await _require('/_lib/navbar.jst')
+
+  await navbar(
+    env,
+    head,
+    // body
+    async _out => {
+      await breadcrumbs(env, _out)
+
+      let transaction = await env.site.database.Transaction()
+      try {
+        let root = await transaction.get()
+        let p = await get_navigation(root, env.component_names)
+
+        div.row.mb-3 {
+          div.col-sm-12 {
+            img.img-responsive(
+              src=await p.get_json('image') || 'image.jpg'
+            ) {}
+          }
+        }
+
+        h3 {
+          `${await p.get_json('description')}—by ${await p.get_json('author')}`
+        }
+      }
+      finally {
+        transaction.rollback()
+      }
+
+      await body(_out)
+    },
+    scripts
+  )
+}
index a9d9ae1..754a211 100644 (file)
@@ -1,27 +1,24 @@
 let assert = require('assert')
 
 return async (env, _out) => {
-  let pathname = env.parsed_url.pathname
-  assert(pathname.slice(0, 1) === '/')
+  // the breadcrumbs have already been determined by navbar.jst, as
+  // the HTML title is similar to the breadcrumbs (but without links)
+  let component_names = env.component_names
+  let component_titles = env.component_titles
 
-  // find number of path components, their positions, and names
-  let components = [{index: 0, name: 'Home'}]
-  for (let i = 1, j; (j = pathname.indexOf('/', i)) !== -1; i = j + 1) {
-    let menu = await env.site.get_menu(`${pathname.slice(0, i)}_menu.json`)
-    let dir = pathname.slice(i, j)
-    components.push({index: j, name: menu.entries[menu.index[dir]].name})
-  }
-
-  // present components as breadcrumbs, except last one as text
+  // present component_titles as breadcrumbs, except last one as text
   h2.mt-3 {
-    for (let i = 0; i < components.length - 1; ++i) {
+    for (let i = 0; i < component_names.length; ++i) {
       a.h4(
-        href=`${pathname.slice(0, components[i].index)}/index.html`
-      ) {`${components[i].name}`}
+        href=
+          `${
+            component_names.slice(0, i).map(name => '/' + name).join('')
+          }/index.html`
+      ) {`${component_titles[i]}`}
       ' '
       span.h5 {'>'}
       ' '
     }
-    `${components[components.length - 1].name}`
+    `${component_titles[component_names.length]}`
   }
 }
diff --git a/_lib/get_navigation.jst b/_lib/get_navigation.jst
new file mode 100644 (file)
index 0000000..17ec41b
--- /dev/null
@@ -0,0 +1,8 @@
+return async (root, component_names) => {
+  let p = await root.get('navigation')
+  for (let i = 0; i < component_names.length; ++i) {
+    let children = await p.get('children')
+    p = await children.get(component_names[i])
+  }
+  return p
+}
index 02bbab5..318c6f1 100644 (file)
@@ -1,6 +1,10 @@
 let assert = require('assert')
 let XDate = require('xdate')
 
+let arrays_equal =
+  (a, b) =>
+    a.length === b.length && a.every((value, index) => value === b[index])
 return async (env, head, body, scripts) => {
   //let cart = await _require('/online_store/cart.jst')
   let fa_arrow_circle_left = await env.site.get_min_svg('/_svg/fa_arrow-circle-left.svg')
@@ -13,26 +17,52 @@ return async (env, head, body, scripts) => {
   let icon_cross = await env.site.get_min_svg('/_svg/icon_cross.svg')
   //let icon_tick = await env.site.get_min_svg('/_svg/icon_tick.svg')
   let logo_large = await env.site.get_min_svg('/_svg/logo_large.svg')
-  let menu = await env.site.get_menu('/_menu.json')
   let page = await _require('/_lib/page.jst')
 
   // initialize env.cart
   //await cart(env)
 
+  // compute breadcrumbs from directories of the path
+  let component_names = env.parsed_url.pathname.split('/')
+  assert(component_names.length >= 2)
+  assert(component_names[0].length === 0)
+  assert(component_names[component_names.length - 1].length)
+  component_names = component_names.slice(1, -1)
+
   let transaction = await env.site.database.Transaction()
   let signed_in_as
   let site_title, copyright
+  let component_titles // collects breadcrumb titles for current page
+  let menu_names, menu_titles // collects top level of menu for the navbar
   let feedback_draft
   try {
-    let root = await transaction.get({})
+    let root = await transaction.get()
 
     let session = await get_session(env, root)
     signed_in_as = await session.get_json('signed_in_as')
 
-    let globals = await root.get('globals', {})
+    let globals = await root.get('globals')
     site_title = await globals.get_json('site_title')
     copyright = await globals.get_json('copyright')
 
+    let navigation = await root.get('navigation')
+
+    let p = navigation
+    component_titles = [await p.get_json('title')] // Home
+    for (let i = 0; i < component_names.length; ++i) {
+      let children = await p.get('children')
+      p = await children.get(component_names[i])
+      component_titles.push(await p.get_json('title'))
+    }
+
+    menu_names = await navigation.get_json('menu')
+    let children = await navigation.get('children')
+    menu_titles = [await navigation.get_json('title')] // Home
+    for (let i = 0; i < menu_names.length; ++i) {
+      let child = await children.get(menu_names[i])
+      menu_titles.push(await child.get('title'))
+    }
+
     feedback_draft = await session.get_json('feedback_draft')
     if (feedback_draft === undefined || env.now >= feedback_draft.expires)
       feedback_draft = null
@@ -41,21 +71,32 @@ return async (env, head, body, scripts) => {
     transaction.rollback()
   }
 
+  // save breadcrumbs and their titles for breadcrumbs.jst
+  // note: component_titles.length === component_names.length + 1
+  // component_titles[0] corresponds to /, is 'Home' or similar
+  // component_titles[i] corresponds to component_names[i - 1], i >= 1
+  env.component_names = component_names
+  env.component_titles = component_titles
+  console.log('cn', component_names)
+  console.log('ct', component_titles)
+
+  // note: menu_titles.length === menu_names.length + 1
+  // menu_titles[0] corresponds to /, is 'Home' or similar
+  // menu_titles[i] corresponds to menu_names[i - 1], i >= 1a
+  // (navbar has Home appearing at same level as its immediate children)
+  console.log('mn', menu_names)
+  console.log('mt', menu_titles)
+
   await page(
     env,
     // head
     async _out => {
-      // extract top-level directory name
-      assert(env.parsed_url.pathname.slice(0, 1) === '/')
-      let index = env.parsed_url.pathname.indexOf('/', 1)
-      let dir = index === -1 ? '' : env.parsed_url.pathname.slice(1, index)
-
       title {
         `${site_title}: ${
-          dir.length === 0 ?
-            'Home' :
-            menu.entries[menu.index[dir]].name
-          }`
+          component_titles[
+            component_names.length >= 2 ? 1 : component_names.length
+          ]
+        }`
       }
 
       await head(_out)
@@ -139,33 +180,34 @@ return async (env, head, body, scripts) => {
           }
           div.collapse.navbar-collapse#navbarSupportedContent {
             ul.navbar-nav.mr-auto {
-              if (dir.length === 0)
-                li.nav-item.active {
-                  a.nav-link(href="/index.html") {
-                    'Home'
-                    span.sr-only {' (current)'}
-                  }
-                }
-              else
-                li.nav-item {
-                  a.nav-link(href="/index.html") {'Home'}
-                }
-              let entries = menu.entries
-              for (let i = 0; i < entries.length; ++i)
-                if (entries[i].navbar)
-                  if (entries[i].dir === dir)
-                    li.nav-item.active {
-                      a.nav-link(href=`/${entries[i].dir}/index.html`) {
-                        `${entries[i].name}`
-                        span.sr-only {' (current)'}
-                      }
+              // the active entry in the navbar bar is based on which top-level
+              // page we are under, even if we are not directly on that page
+              // but one of its children, this may be unexpected as the active
+              // entry does not highlight on hover, but you can still click it;
+              // we determine here the path to the corresponding top-level page
+              let component_prefix = component_names.slice(0, 1)
+
+              for (let i = 0; i < menu_titles.length; ++i) {
+                // construct path to the top-level page about to be described
+                let menu_prefix =
+                  i == 0 ? [] : [menu_names[i - 1]]
+                let menu_prefix_path =
+                  menu_prefix.map(name => '/' + name).join('') + '/index.html'
+
+                if (arrays_equal(menu_prefix, component_prefix))
+                  li.nav-item.active {
+                    a.nav-link(href=menu_prefix_path) {
+                      `${menu_titles[i]}`
+                      span.sr-only {' (current)'}
                     }
-                  else
-                    li.nav-item {
-                      a.nav-link(href=`/${entries[i].dir}/index.html`) {
-                        `${entries[i].name}`
-                      }
+                  }
+                else
+                  li.nav-item {
+                    a.nav-link(href=menu_prefix_path) {
+                      `${menu_titles[i]}`
                     }
+                  }
+              }
               //li.nav-item.dropdown {
               //  a.nav-link.dropdown-toggle#navbarDropdown(href="#" role="button" data-toggle="dropdown" aria-expanded="false") {
               //    'Dropdown'
diff --git a/_lib/post.jst b/_lib/post.jst
deleted file mode 100644 (file)
index 75d261a..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-let assert = require('assert')
-
-return async (env, head, body, scripts) => {
-  let breadcrumbs = await _require('/_lib/breadcrumbs.jst')
-  let navbar = await _require('/_lib/navbar.jst')
-
-  await navbar(
-    env,
-    head,
-    // body
-    async _out => {
-      await breadcrumbs(env, _out)
-
-      // extract second-level directory name
-      assert(env.parsed_url.pathname.slice(0, 6) === '/blog/')
-      let index = env.parsed_url.pathname.indexOf('/', 6)
-      let dir = index === -1 ? '' : env.parsed_url.pathname.slice(6, index)
-
-      // load article data
-      let menu = await env.site.get_menu('/blog/_menu.json')
-      let article = menu.entries[menu.index[dir]]
-
-      div.row.mb-3 {
-        div.col-sm-12 {
-          img.img-responsive(src=article.image || 'image.jpg') {}
-        }
-      }
-
-      h3 {`${article.description}—by ${article.author}`}
-
-      await body(_out)
-    },
-    scripts
-  )
-}
diff --git a/_menu.json b/_menu.json
deleted file mode 100644 (file)
index be3779f..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-  "entries": [
-    {"dir": "blog", "name": "Blog", "navbar": true},
-    {"dir": "projects", "name": "Projects", "navbar": true},
-    {"dir": "sphinx", "name": "Documentation", "navbar": true},
-    {"dir": "contact", "name": "Contact", "navbar": true},
-    {"dir": "my_account", "name": "My account", "navbar": true},
-    {"dir": "search", "name": "Search"}
-  ]
-}
index e1bea58..64d385d 100644 (file)
@@ -3,6 +3,7 @@ let XDate = require('xdate')
 
 return async env => {
   let post_request = await _require('/_lib/post_request.jst')
+  let get_account = await _require('/_lib/get_account.jst')
   let get_session = await _require('/_lib/get_session.jst')
 
   await post_request(
diff --git a/api/navigation/get.json.jst b/api/navigation/get.json.jst
new file mode 100644 (file)
index 0000000..ac842d8
--- /dev/null
@@ -0,0 +1,40 @@
+let jst_server = (await import('@ndcode/jst_server')).default
+let XDate = require('xdate')
+
+return async env => {
+  let get_account = await _require('/_lib/get_account.jst')
+  let get_session = await _require('/_lib/get_session.jst')
+  let post_request = await _require('/_lib/post_request.jst')
+
+  await post_request(
+    // env
+    env,
+    // handler
+    async () => {
+      let transaction = await env.site.database.Transaction()
+      try {
+        let root = await transaction.get({})
+        let session = await get_session(env, root)
+
+        let account = await get_account(root, session)
+        if (account === undefined)
+          throw new jst_server.Problem(
+            'Unauthorized',
+            'Please sign in first.',
+            401
+          )
+        if (!await account.get_json('administrator'))
+          throw new jst_server.Problem(
+            'Unauthorized',
+            'Not administrator.',
+            401
+          )
+
+        return /*await*/ root.get_json('navigation', {})
+      }
+      finally {
+        transaction.rollback()
+      }
+    }
+  )
+}
diff --git a/api/navigation/set.json.jst b/api/navigation/set.json.jst
new file mode 100644 (file)
index 0000000..696c90f
--- /dev/null
@@ -0,0 +1,45 @@
+let jst_server = (await import('@ndcode/jst_server')).default
+let XDate = require('xdate')
+
+return async env => {
+  let get_account = await _require('/_lib/get_account.jst')
+  let get_session = await _require('/_lib/get_session.jst')
+  let post_request = await _require('/_lib/post_request.jst')
+
+  await post_request(
+    // env
+    env,
+    // handler
+    async navigation => {
+      // do not bother trying to coerce and/or validate
+      // too complex and nested (do it when we have an automated routine)
+
+      let transaction = await env.site.database.Transaction()
+      try {
+        let root = await transaction.get({})
+        let session = await get_session(env, root)
+
+        let account = await get_account(root, session)
+        if (account === undefined)
+          throw new jst_server.Problem(
+            'Unauthorized',
+            'Please sign in first.',
+            401
+          )
+        if (!await account.get_json('administrator'))
+          throw new jst_server.Problem(
+            'Unauthorized',
+            'Not administrator.',
+            401
+          )
+
+        root.set_json('navigation', navigation)
+        await transaction.commit()
+      }
+      catch (error) {
+        transaction.rollback()
+        throw error
+      }
+    }
+  )
+}
index d1cead1..9f9712d 100644 (file)
@@ -1,17 +1,21 @@
 return async env => {
-  let post = await _require('/_lib/post.jst')
+  let blog_post = await _require('/_lib/blog_post.jst')
 
-  await post(
+  await blog_post(
     env,
     // head
     async _out => {},
     // body
     async _out => {
-      p {'Normally I would write a blog post coincident with the release of a new version of the public website you are reading; in this case there is some heavy refactoring going on and so I have decided to just document the process of the work and let it be viewed and interacted with later when ready for release.'}
+      p {
+        'Normally I would write a blog post coincident with the release of a new version of the public website you are reading; in this case there is some heavy refactoring going on and so I have decided to just document the process of the work and let it be viewed and interacted with later when ready for release.'
+      }
 
       h4 {'NoSQL discussion'}
 
-      p {'One of the early decisions I made when teaching myself web development was that I would not be using SQL for the backend. I simply believe the disadvantages outweigh the advantages for what I am trying to do. And I’m probably not the only person with this view, because the NoSQL movement has been gaining traction.'}
+      p {
+        'One of the early decisions I made when teaching myself web development was that I would not be using SQL for the backend. I simply believe the disadvantages outweigh the advantages for what I am trying to do. And I’m probably not the only person with this view, because the NoSQL movement has been gaining traction.'
+      }
 
       p {
         'SQL pros:'
@@ -31,7 +35,9 @@ return async env => {
         }
       }
 
-      p {'I want my application to be as self-contained and self-configuring as possible, so basically I would just check out the website’s repository on the intended Virtual Private Server (VPS) and launch it. In a traditional configuration you have a long list of other services to configure first, such as PHP, PHP-FPM, MySQL, etc, etc, and setting up or migrating to a new server could be days of work with tedious troubleshooting. There are of course cases where these activities have a payoff (such as if you require a highly scalable website), but my case is not one of them.'}
+      p {
+        'I want my application to be as self-contained and self-configuring as possible, so basically I would just check out the website’s repository on the intended Virtual Private Server (VPS) and launch it. In a traditional configuration you have a long list of other services to configure first, such as PHP, PHP-FPM, MySQL, etc, etc, and setting up or migrating to a new server could be days of work with tedious troubleshooting. There are of course cases where these activities have a payoff (such as if you require a highly scalable website), but my case is not one of them.'
+      }
 
       h4 {'First try—all JSON approach'}
 
@@ -50,11 +56,15 @@ return async env => {
         ', and I would then have access to complex queries which I do not with JSON, but I felt the added complexity was not worthwhile. If I need to do complex queries, I will simply write them in Javascript, including the maintenance of any indices needed to carry out the queries efficiently. This will give me complete control, rather than simply hoping the SQL server optimizes the indices and queries well.'
       }
 
-      p {'My JSON database was sufficient for building the prototype blog and online store that you can interact with now, but it did suffer from a serious problem which is that upon successful launch, there could be thousands of customers and/or orders and the database could potentially grow to a huge size. Then it would not be feasible to hold the entire file in memory or write it every 5 seconds.'}
+      p {
+        'My JSON database was sufficient for building the prototype blog and online store that you can interact with now, but it did suffer from a serious problem which is that upon successful launch, there could be thousands of customers and/or orders and the database could potentially grow to a huge size. Then it would not be feasible to hold the entire file in memory or write it every 5 seconds.'
+      }
 
       h4 {'Second try—log-structured JSON approach'}
 
-      p {'To solve the scalability issue, my plan has always been to allow the JSON tree to be read into memory lazily as required (and cached, with some kind of cache policy implemented), and for modifications to be written to the end of the file. Thus the file would be log-structured and simply keep growing forever, except for the intervention of a daily cleaning routine which would copy all the live (still reachable) parts of the log into a new log to reduce the size.'}
+      p {
+        'To solve the scalability issue, my plan has always been to allow the JSON tree to be read into memory lazily as required (and cached, with some kind of cache policy implemented), and for modifications to be written to the end of the file. Thus the file would be log-structured and simply keep growing forever, except for the intervention of a daily cleaning routine which would copy all the live (still reachable) parts of the log into a new log to reduce the size.'
+      }
 
       p {
         'The database I created with this approach is called '
@@ -82,7 +92,9 @@ return async env => {
         ', is to allocate new objects contiguously from a region of spare memory instead of scanning for a free spot every time, and perform a periodic garbage collection to reclaim free space. It was controversial at the time because of garbage collection pauses, but if you can perform garbage collection concurrently with normal processing, there should be no significant impact.'
       }
 
-      p {'For my application, it seemed simpler to implement a daily copy from old to new log than to deal with free-space management. And there are certain other benefits, such as having a series of snapshots of the database to aid in trouble-shoooting. I also liked the idea of the log being human-readable (traditional for web developers, who like ASCII formats such as JSON rather than binary formats), and this would have complicated the free-space management considerably.'}
+      p {
+        'For my application, it seemed simpler to implement a daily copy from old to new log than to deal with free-space management. And there are certain other benefits, such as having a series of snapshots of the database to aid in trouble-shoooting. I also liked the idea of the log being human-readable (traditional for web developers, who like ASCII formats such as JSON rather than binary formats), and this would have complicated the free-space management considerably.'
+      }
 
       h4 {
         tt {'LogJSON'}
@@ -171,7 +183,9 @@ return async env => {
         ', containing the file pointer and length of the previously written JSON.'
       }
 
-      p {'So in the above example, it’s possible to open the file, synchronize to the root element using the angle brackets, and then read the root element as a reference to an unread portion of the file, without actually having to read the contents of the root element until they are required.'}
+      p {
+        'So in the above example, it’s possible to open the file, synchronize to the root element using the angle brackets, and then read the root element as a reference to an unread portion of the file, without actually having to read the contents of the root element until they are required.'
+      }
 
       h5 {'Nested objects'}
 
@@ -199,7 +213,9 @@ return async env => {
 `
       }
 
-      p {'And the encoded version in LogJSON is:'}
+      p {
+        'And the encoded version in LogJSON is:'
+      }
 
       pre {
         `{
@@ -284,7 +300,9 @@ return async env => {
 `
       }
 
-      p {'Finally, we need to rewrite the root object to point to the newly written subtree:'}
+      p {
+        'Finally, we need to rewrite the root object to point to the newly written subtree:'
+      }
 
       pre {
         `<[
@@ -294,7 +312,9 @@ return async env => {
 `
       }
 
-      p {'And of course the file will not be considered modified (the old root will be active) until the new root has been fully written.'}
+      p {
+        'And of course the file will not be considered modified (the old root will be active) until the new root has been fully written.'
+      }
 
       h5 {'Modified tree‒full example'}
 
@@ -433,11 +453,17 @@ return async env => {
 
       h4 {'Garbage collection'}
 
-      p {'I have implemented the garbage collection in a rather sophisticated way, taking advantage of the immutability of the log file once written in order that garbage collection can proceed in the background without disturbing the operation of the website.'}
+      p {
+        'I have implemented the garbage collection in a rather sophisticated way, taking advantage of the immutability of the log file once written in order that garbage collection can proceed in the background without disturbing the operation of the website.'
+      }
 
-      p {'A garbage collection is triggered every midnight in Universal Time Coordinated (UTC), that is, when the date rolls over. A new log file is constructed by copying all the live data from the old log file into it. And then transaction processing is briefly paused whilst we atomically rename the old log file out of the way to a dated name (contains the year, month and day in UTC) and rename the new log file over the old one. If the system crashes during renaming it will recover at the next restart.'}
+      p {
+        'A garbage collection is triggered every midnight in Universal Time Coordinated (UTC), that is, when the date rolls over. A new log file is constructed by copying all the live data from the old log file into it. And then transaction processing is briefly paused whilst we atomically rename the old log file out of the way to a dated name (contains the year, month and day in UTC) and rename the new log file over the old one. If the system crashes during renaming it will recover at the next restart.'
+      }
 
-      p {'The synchronization magic happens by atomically grabbing the current root object and copying everything reachable from it into the new log file, but allowing more transactions to be written into the old log file simultaneously. It then atomically grabs the updated root and copies that as well, and continues to do so until both files are up-to-date and it can perform the atomic replacement.'}
+      p {
+        'The synchronization magic happens by atomically grabbing the current root object and copying everything reachable from it into the new log file, but allowing more transactions to be written into the old log file simultaneously. It then atomically grabs the updated root and copies that as well, and continues to do so until both files are up-to-date and it can perform the atomic replacement.'
+      }
 
       h4 {'Concurrency'}
 
@@ -449,11 +475,15 @@ return async env => {
         ' as not conflicting.'
       }
 
-      p {'Currently there is a bit of a risk that if I do not implement good try/catch handling in my code, then a webserver request will crash, leaving the webserver running but the mutex in ‘held’ state. This would hang the webserver, requiring a manual restart. I could implement some sort of disaster recovery, but there is probably no point at this time, I will simply use try/catch and make sure transactions are rolled back if not completed.'}
+      p {
+        'Currently there is a bit of a risk that if I do not implement good try/catch handling in my code, then a webserver request will crash, leaving the webserver running but the mutex in ‘held’ state. This would hang the webserver, requiring a manual restart. I could implement some sort of disaster recovery, but there is probably no point at this time, I will simply use try/catch and make sure transactions are rolled back if not completed.'
+      }
 
       h4 {'Conclusion'}
 
-      p {'The log-structured format provides a useful way of maintaining huge JSON files. It remains to complete the documentation and release the source code and NPM package. It also remains to implement a stress test of my website, once up and running, to simulate thousands of user accounts and multiple logins to the same account all performing transactions simultaneously. Therefore, things are still experimental at this stage. I enjoy writing this blog to update on those experiments.'}
+      p {
+        'The log-structured format provides a useful way of maintaining huge JSON files. It remains to complete the documentation and release the source code and NPM package. It also remains to implement a stress test of my website, once up and running, to simulate thousands of user accounts and multiple logins to the same account all performing transactions simultaneously. Therefore, things are still experimental at this stage. I enjoy writing this blog to update on those experiments.'
+      }
     },
     // scripts
     async _out => {}
diff --git a/blog/_menu.json b/blog/_menu.json
deleted file mode 100644 (file)
index 56603d8..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-  "entries": [
-    {
-      "dir": "20220114",
-      "name": "14/01/2022",
-      "description": "Log-structured JSON database",
-      "author": "Nick Downing",
-      "image": "image.svg",
-      "thumbnail": "thumbnail.svg"
-    }
-  ]
-}
index 3345d9f..cacd4a1 100644 (file)
@@ -1,6 +1,6 @@
 return async env => {
   let breadcrumbs = await _require('/_lib/breadcrumbs.jst')
-  let menu = await env.site.get_menu('/blog/_menu.json')
+  let get_navigation = await _require('/_lib/get_navigation.jst')
   let navbar = await _require('/_lib/navbar.jst')
 
   await navbar(
@@ -9,21 +9,36 @@ return async env => {
     async _out => {
       await breadcrumbs(env, _out)
 
-      ul.nav.flex-column {
-        let entries = menu.entries
-        for (let i = 0; i < entries.length; ++i) {
-          let article = entries[i]
-          li.nav-item {
-            a.nav-link(href=`${article.dir}/index.html`) {
-              table.icon-and-text {
-                tr {
-                  td {
-                    img(src=`${article.dir}/${article.thumbnail || 'thumbnail.jpg'}`) {}
-                  }
-                  td {
-                    span.text-h2{`${article.name}`}
-                    br {}
-                    span.text-h4{`${article.description}—by ${article.author}`}
+      let transaction = await env.site.database.Transaction()
+      try {
+        let root = await transaction.get()
+        let p = await get_navigation(root, env.component_names)
+        let children = await p.get('children')
+        let menu = await p.get_json('menu')
+
+        ul.nav.flex-column {
+          for (let i = 0; i < menu.length; ++i) {
+            let name = menu[i]
+            let q = await children.get(name)
+
+            li.nav-item {
+              a.nav-link(href=`${name}/index.html`) {
+                table.icon-and-text {
+                  tr {
+                    td {
+                      img(
+                        src=`${name}/${await q.get_json('thumbnail') || 'thumbnail.jpg'}`
+                      ) {}
+                    }
+                    td {
+                      span.h2{
+                        `${await q.get_json('title')}`
+                      }
+                      br {}
+                      span.h4{
+                        `${await q.get_json('description')}—by ${await q.get_json('author')}`
+                      }
+                    }
                   }
                 }
               }
@@ -31,6 +46,9 @@ return async env => {
           }
         }
       }
+      finally {
+        transaction.rollback()
+      }
     },
     async _out => {}
   )
diff --git a/my_account/_menu.json b/my_account/_menu.json
deleted file mode 100644 (file)
index 30cfa1a..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-{
-  "entries": [
-    {
-      "dir": "sign_up",
-      "name": "Sign up",
-      "icon": "/_svg/icon_sign_up.svg"
-    },
-    {"dir": "send_verification_email", "name": "Send verification email"},
-    {"dir": "verify_email", "name": "Verify email"},
-    {
-      "dir": "password_reset",
-      "name": "Password reset",
-      "icon": "/_svg/icon_password_reset.svg"
-    },
-    {"dir": "verify_password", "name": "Verify password"}
-  ]
-}
index d7deb7e..452516d 100644 (file)
@@ -7,10 +7,10 @@ return async env => {
   let fa_trash = await env.site.get_min_svg('/_svg/fa_trash.svg')
   let get_placeholder = await _require('/_lib/get_placeholder.jst')
   let get_account = await _require('/_lib/get_account.jst')
+  let get_navigation = await _require('/_lib/get_navigation.jst')
   let get_session = await _require('/_lib/get_session.jst')
   let icon_cross = await env.site.get_min_svg('/_svg/icon_cross.svg')
   let icon_tick = await env.site.get_min_svg('/_svg/icon_tick.svg')
-  let menu = await env.site.get_menu('/my_account/_menu.json')
   let navbar = await _require('/_lib/navbar.jst')
 
   // preload draft details if any
@@ -227,27 +227,42 @@ return async env => {
         // signed out
         p {'For account maintenance, please click on one of the options below.'}
 
-        ul.nav.flex-column {
-          let entries = menu.entries
-          for (let i = 0; i < entries.length; ++i) {
-            let entry = entries[i]
-            if (Object.prototype.hasOwnProperty.call(entry, 'icon'))
+        let transaction = await env.site.database.Transaction()
+        try {
+          let root = await transaction.get()
+          let p = await get_navigation(root, env.component_names)
+          let children = await p.get('children')
+          let menu = await p.get_json('menu')
+  
+          ul.nav.flex-column {
+            for (let i = 0; i < menu.length; ++i) {
+              let name = menu[i]
+              let q = await children.get(name)
+  
               li.nav-item {
-                a.nav-link(href=`${entry.dir}/index.html`) {
+                a.nav-link(href=`${name}/index.html`) {
                   table.icon-and-text {
                     tr {
                       td {
-                        _out.push(await env.site.get_min_svg(entry.icon))
+                        _out.push(
+                          await env.site.get_min_svg(await q.get_json('icon'))
+                        )
                       }
                       td {
-                        span.h2{`${entry.name}`}
+                        span.h2{
+                          `${await q.get_json('title')}`
+                        }
                       }
                     }
                   }
                 }
               }
+            }
           }
         }
+        finally {
+          transaction.rollback()
+        }
       }
     },
     // scripts