miniblog

Miniblog: A command-line static blog system in Common Lisp
Log | Files | Refs | README | LICENSE

commit 378c05b0d7b7dd1c9c9bf3ea296db9bb3019070a
parent 4b210b7b5beccddf110bd2890f633852cfddb51b
Author: Decay <decay@todayiwilllaunchmyinfantsonintoorbit.com>
Date:   Sat,  2 Nov 2024 14:06:57 -0700

Initial support for genericizing posts

We're adding a tree structure to posts similar to pages (and with a more
efficient traversal method!) that will allow simplifying the schema and
supporting multiple different taxonomies other than just posts and
pages. Next steps will involve automatic migrations; for now you will
want to run:

(let ((mito:*connection* (apply #'dbi:connect-cached miniblog:*db-config*)))
  (mito:migrate-table 'miniblog.db:blog-entries))

Will be adding automigration in a future commit.

Diffstat:
Msrc/data.lisp | 31+++++++++++++++++++++++++++++--
Msrc/db.lisp | 50++++++++++++++++++++++++++++++++++++--------------
Msrc/packages.lisp | 7++++---
3 files changed, 69 insertions(+), 19 deletions(-)

diff --git a/src/data.lisp b/src/data.lisp @@ -26,6 +26,7 @@ (list :id (mito.dao.mixin:object-id entry) :created-at (mito.dao.mixin:object-created-at entry) :last-updated-at (mito.dao.mixin:object-updated-at entry) + :parent (entry-parent entry) :title (entry-title entry) :content (entry-content entry) :rendered-content (entry-rendered-content entry) @@ -52,9 +53,35 @@ (defun add-entry (title content &key rendered-content (username "nobody") draftp) (miniblog.db:add-entry title content :rendered-content rendered-content :draftp draftp :username username :transform #'xform)) +(defun make-entry-tree (id subtree) + (let ((dict (make-hash-table))) + (flet ((attach-node (node) + (let* ((node-id (getf node :id)) + (parent-id (or (getf node :parent) 0)) + (curr-node (gethash node-id dict)) + (parent-node (gethash parent-id dict)) + (siblings (getf parent-node :children))) + (if (and curr-node (getf curr-node :children) (getf node :children)) + (progn + (format t "womp womp ~A~%" (getf node :children)) + (rplaca (getf node :children) (getf curr-node :children))) + (setf (getf node :children) (list))) + (setf (getf parent-node :children) (cons node siblings)) + (setf (gethash parent-id dict) parent-node) + (setf (gethash node-id dict) node)))) + (loop for node in subtree + do (attach-node node)) + (let ((root (gethash id dict))) + (if (getf root :id) + ; Real node, return as root + root + ; Nonexistent node (probably 0), return the list of first-level children instead + (getf root :children)))))) + (defun get-entry (id) "Get entry by id, or nil if the requested id isn't found" - (miniblog.db:get-entry id :transform #'xform)) + (let ((subtree (miniblog.db:get-subtree-for-entry id :transform #'xform))) + (make-entry-tree id subtree))) (defmacro with-entry-id (entry id &rest body) "Takes a varname to hold the entry list and a post id @@ -74,7 +101,7 @@ (defun get-entries (&key year month max-entries include-drafts-p) "Get entries from the database, optionally limited to a date range or count" - (miniblog.db:get-entries :year year :month month :drafts include-drafts-p :max-entries max-entries :transform #'xform)) + (make-entry-tree 0 (miniblog.db:get-entries :year year :month month :drafts include-drafts-p :max-entries max-entries :transform #'xform))) (defun update-entry (id title content &key rendered-content (username "nobody") draftp reset-timestamp-p) "Update entry by id. Returns the updated entry or nil if the id doesn't exist." diff --git a/src/db.lisp b/src/db.lisp @@ -5,6 +5,8 @@ :accessor entry-username) (last-updated-by :col-type (:varchar 64) :accessor entry-last-updated-by) + (parent :col-type (or :bigint :null) + :accessor entry-parent) (title :col-type (:varchar 200) :accessor entry-title) (content :col-type (:varchar 16384) @@ -105,29 +107,49 @@ ((eql :drafts-only) '((:= :draftp "true"))) (null '((:= :draftp "false")))))) (if (or date-clauses drafts-clause) - (where `(:and ,@date-clauses ,@drafts-clause)))))) - - (mapcar transform + (where `(:and ,@date-clauses ,@drafts-clause (:or (:is-null :parent) (:= :parent 0)))))))) + (let ((entries (select-dao 'blog-entries (where-clause year month) (order-by (:desc :created_at)) (if max-entries - (limit max-entries)))))) + (limit max-entries))))) + (get-subtree-for-entry + (loop for entry in entries + collect (mito.dao.mixin:object-id entry)) + :transform transform)))) + +(defun get-subtree-for-entry (ids &key transform) + "Fetches all posts dependent on post(s) IDS via recursive CTE. Since we use handhacked SQL here there may be compatibility issues for databases with weak support for recursive CTEs; this has been verified as functional at least in mysql, sqlite and postgres" + (labels ((convert-to-dao (record) + (apply #'mito.dao.mixin:make-dao-instance (cons 'blog-entries record)))) + (let* ((ids (etypecase ids + (number (list ids)) + (cons ids))) + (subentries + (mapcar #'convert-to-dao + (retrieve-by-sql + ; Construct the query by adding (LENGTH IDS) placeholders in the where clause + (format nil "with recursive posttree (id,username,last_updated_by,title,content,created_at,updated_at,rendered_content,draftp,parent) as (select e.id,e.username,e.last_updated_by,e.title,e.content,e.created_at,e.updated_at,e.rendered_content,e.draftp,e.parent from blog_entries as e where e.id in (~{?~*~^,~}) union all select e.id,e.username,e.last_updated_by,e.title,e.content,e.created_at,e.updated_at,e.rendered_content,e.draftp,e.parent from blog_entries as e inner join posttree as d on e.parent = d.id) select id,username,last_updated_by,title,content,created_at,updated_at,rendered_content,draftp,parent from posttree order by created_at" ids) + :binds ids)))) + (if transform + (mapcar transform subentries) + subentries)))) (defun update-entry (id title content &key rendered-content (username "nobody") (transform #'identity) draftp reset-timestamp-p) "Update entry by id. Returns the updated entry or nil if the id doesn't exist." (let ((entry (get-raw-entry id))) (if entry - (funcall transform - (progn (setf (entry-title entry) title) - (setf (entry-content entry) content) - (setf (entry-rendered-content entry) rendered-content) - (setf (entry-last-updated-by entry) username) - (setf (entry-draftp entry) (as-sql-bool draftp)) - (when reset-timestamp-p - (setf (mito.dao.mixin:object-created-at entry) (local-time:now))) ; FIXME: This is a horrible abuse of Mito - (save-dao entry) - entry))))) + (funcall transform + (progn (setf (entry-title entry) title) + (setf (entry-content entry) content) + (setf (entry-rendered-content entry) rendered-content) + (setf (entry-last-updated-by entry) username) + (setf (entry-draftp entry) (as-sql-bool draftp)) + (when reset-timestamp-p + (setf (mito.dao.mixin:object-created-at entry) (local-time:now))) ; FIXME: This is a horrible abuse of Mito + (save-dao entry) + entry))))) (defun update-entry-rendered-content (id rendered-content &key (username "nobody") (transform #'identity)) "Updated just the rendered content for an entry by ID. Returns the updated entry or NIL if ID doesn't exist." diff --git a/src/packages.lisp b/src/packages.lisp @@ -21,12 +21,13 @@ (defpackage :miniblog.db (:use :cl :mito :sxql) (:export #:blog-entries #:blog-pages - #:entry-username #:entry-last-updated-by #:entry-title #:entry-content - #:entry-rendered-content #:entry-draftp + #:entry-username #:entry-last-updated-by #:entry-parent #:entry-title + #:entry-content #:entry-rendered-content #:entry-draftp #:page-username #:page-last-updated-by #:page-name #:page-parent #:page-title #:page-content #:page-rendered-content #:page-draftp #:init #:init-tables #:add-entry #:get-entry #:get-entries #:update-entry #:update-entry-rendered-content #:delete-entry + #:get-subtree-for-entry #:add-page #:get-page #:get-pages #:update-page #:update-page-rendered-content #:delete-page #:move-page)) @@ -34,7 +35,7 @@ (defpackage :miniblog.data (:use :cl) (:import-from :miniblog.db :blog-entries :blog-pages - :entry-username :entry-last-updated-by :entry-title + :entry-username :entry-last-updated-by :entry-parent :entry-title :entry-content :entry-rendered-content :entry-draftp :page-username :page-last-updated-by :page-name :page-parent :page-title :page-content :page-rendered-content :page-draftp)