/ 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))))