/ database.lisp
database.lisp
 1  (in-package :asteroid)
 2  
 3  ;; Database connection parameters for direct postmodern queries
 4  (defun get-db-connection-params ()
 5    "Get database connection parameters for postmodern"
 6    (list (or (uiop:getenv "ASTEROID_DB_NAME") "asteroid")
 7          (or (uiop:getenv "ASTEROID_DB_USER") "asteroid")
 8          (or (uiop:getenv "ASTEROID_DB_PASSWORD") "asteroid_db_2025")
 9          (or (uiop:getenv "ASTEROID_DB_HOST") "localhost")
10          :port (parse-integer (or (uiop:getenv "ASTEROID_DB_PORT") "5432"))))
11  
12  (defmacro with-db (&body body)
13    "Execute body with database connection"
14    `(postmodern:with-connection (get-db-connection-params)
15       ,@body))
16  
17  ;; Database initialization - must be in db:connected trigger because
18  ;; the system could load before the database is ready.
19  
20  (define-trigger db:connected ()
21    "Initialize database collections when database connects"
22    (unless (db:collection-exists-p "tracks")
23      (db:create "tracks" '((title :text)
24                            (artist :text)
25                            (album :text)
26                            (duration :integer)
27                            (file-path :text)
28                            (format :text)
29                            (bitrate :integer)
30                            (added-date :integer)
31                            (play-count :integer))))
32    
33    (unless (db:collection-exists-p "playlists")
34      (db:create "playlists" '((name :text)
35                               (description :text)
36                               (created-date :integer)
37                               (user-id :integer)
38                               (track-ids :text))))
39    
40    (unless (db:collection-exists-p "USERS")
41      (db:create "USERS" '((username :text)
42                           (email :text)
43                           (password-hash :text)
44                           (role :text)
45                           (active :integer)
46                           (created-date :integer)
47                           (last-login :integer))))
48  
49    (unless (db:collection-exists-p "playlist_tracks")
50      (db:create "playlist_tracks" '((track_id :integer)
51                                     (position :integer)
52                                     (added_date :integer))))
53  
54    ;; TODO: the radiance db interface is too basic to contain anything
55    ;; but strings, integers, booleans, and maybe timestamps... we will
56    ;; need to rethink this. currently track/playlist relationships are
57    ;; defined in the SQL file 'init-db.sql' referenced in the docker
58    ;; config for postgresql, but our lisp code doesn't leverage it.
59    
60    ;; (unless (db:collection-exists-p "sessions")
61    ;;   (db:create "sessions" '(())))
62    
63    (format t "~2&Database collections initialized~%"))
64  
65  (defun data-model-as-alist (model)
66    "Converts a radiance data-model instance into a alist"
67    (unless (dm:hull-p model)
68      (loop for field in (dm:fields model)
69            collect (cons field (dm:field model field)))))
70  
71  (defun lambdalite-db-p ()
72    "Checks if application is using lambdalite as database backend"
73    (string= (string-upcase (package-name (db:implementation)))
74             "I-LAMBDALITE"))
75  
76  (defun data-model-save (data-model)
77    "Wrapper on data-model save method to bypass error using dm:save on lambdalite.
78  It uses the same approach as dm:save under the hood through db:save."
79    (if (lambdalite-db-p)
80        (progn
81          (format t "Updating lambdalite collection '~a'~%" (dm:collection data-model))
82          (db:update (dm:collection data-model)
83                     (db:query (:= '_id (dm:id data-model)))
84                     (dm:field-table data-model)))
85        (progn
86          (format t "Updating database table '~a'~%" (dm:collection data-model))
87          (dm:save data-model))))