|
|
|
@@ -1,5 +1,9 @@
|
|
|
|
|
#lang racket/base
|
|
|
|
|
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
;; PostgreSQL-backed page, history, search, bookmark and upload storage.
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
|
|
|
|
|
(require db
|
|
|
|
|
json
|
|
|
|
|
racket/file
|
|
|
|
@@ -13,6 +17,9 @@
|
|
|
|
|
|
|
|
|
|
(provide ensure-wiki-data!
|
|
|
|
|
valid-slug?
|
|
|
|
|
valid-page-reference?
|
|
|
|
|
page-reference
|
|
|
|
|
split-page-reference
|
|
|
|
|
title->slug
|
|
|
|
|
list-pages
|
|
|
|
|
read-page
|
|
|
|
@@ -38,6 +45,10 @@
|
|
|
|
|
; post : The data directory and writable static directory exist.
|
|
|
|
|
; result : void.
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
;; Supporting functions
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
|
|
|
|
|
(define (ensure-wiki-data! config)
|
|
|
|
|
(for ((directory (in-list (list (wiki-config-data-dir config)
|
|
|
|
|
(data-static-directory config)))))
|
|
|
|
@@ -66,6 +77,42 @@
|
|
|
|
|
(valid-slug-character? char))
|
|
|
|
|
(not (member slug '("." "..")))))
|
|
|
|
|
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
; goal : Build the external wiki reference for a namespace and slug.
|
|
|
|
|
; pre : namespace and slug are strings; slug is a valid page slug.
|
|
|
|
|
; post : No state is changed.
|
|
|
|
|
; result : slug for the root namespace, otherwise namespace:slug.
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
(define (page-reference namespace slug)
|
|
|
|
|
(if (string=? (string-trim namespace) "")
|
|
|
|
|
slug
|
|
|
|
|
(string-append (string-trim namespace) ":" slug)))
|
|
|
|
|
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
; goal : Split an external page reference into namespace and slug.
|
|
|
|
|
; pre : reference is a string.
|
|
|
|
|
; post : No state is changed.
|
|
|
|
|
; result : Two values: namespace and slug. The namespace is empty for root pages.
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
(define (split-page-reference reference)
|
|
|
|
|
(define match (regexp-match #px"^([^:]+):(.*)$" reference))
|
|
|
|
|
(if match
|
|
|
|
|
(values (list-ref match 1) (list-ref match 2))
|
|
|
|
|
(values "" reference)))
|
|
|
|
|
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
; goal : Check whether a namespace-qualified page reference is valid.
|
|
|
|
|
; pre : reference is a string.
|
|
|
|
|
; post : No state is changed.
|
|
|
|
|
; result : #t for root slugs or namespace:slug references with letter/number namespaces.
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
(define (valid-page-reference? reference)
|
|
|
|
|
(define-values (namespace slug) (split-page-reference reference))
|
|
|
|
|
(and (valid-slug? slug)
|
|
|
|
|
(or (string=? namespace "")
|
|
|
|
|
(and (valid-slug? namespace)
|
|
|
|
|
(<= (string-length namespace) 80)))))
|
|
|
|
|
|
|
|
|
|
(define (title->slug title)
|
|
|
|
|
(define normalized
|
|
|
|
|
(string-downcase
|
|
|
|
@@ -101,8 +148,12 @@
|
|
|
|
|
(if (list? value) value '())))
|
|
|
|
|
|
|
|
|
|
(define (row->page row [include-markdown? #t])
|
|
|
|
|
(define namespace (vector-ref row 9))
|
|
|
|
|
(define slug (vector-ref row 0))
|
|
|
|
|
(define result
|
|
|
|
|
(hash 'slug (vector-ref row 0)
|
|
|
|
|
(hash 'slug (page-reference namespace slug)
|
|
|
|
|
'pageSlug slug
|
|
|
|
|
'namespace namespace
|
|
|
|
|
'title (vector-ref row 1)
|
|
|
|
|
'createdAt (vector-ref row 3)
|
|
|
|
|
'updatedAt (vector-ref row 4)
|
|
|
|
@@ -115,7 +166,7 @@
|
|
|
|
|
result))
|
|
|
|
|
|
|
|
|
|
(define page-columns
|
|
|
|
|
"slug, title, markdown, created_at, updated_at, created_by, updated_by, tags, current_version")
|
|
|
|
|
"slug, title, markdown, created_at, updated_at, created_by, updated_by, tags, current_version, namespace")
|
|
|
|
|
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
; goal : List current wiki page metadata.
|
|
|
|
@@ -130,7 +181,7 @@
|
|
|
|
|
(for/list ((row (in-list
|
|
|
|
|
(query-rows db
|
|
|
|
|
(string-append "SELECT " page-columns
|
|
|
|
|
" FROM pages WHERE archived = FALSE ORDER BY lower(title), title")))))
|
|
|
|
|
" FROM pages WHERE archived = FALSE ORDER BY lower(namespace), namespace, lower(title), title")))))
|
|
|
|
|
(row->page row #f)))))
|
|
|
|
|
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
@@ -139,18 +190,19 @@
|
|
|
|
|
; post : The pages table has only been read.
|
|
|
|
|
; result : Page metadata with Markdown, or #f when the page does not exist.
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
(define (read-page config slug)
|
|
|
|
|
(if (not (valid-slug? slug))
|
|
|
|
|
(define (read-page config reference)
|
|
|
|
|
(if (not (valid-page-reference? reference))
|
|
|
|
|
#f
|
|
|
|
|
(call-with-wiki-database
|
|
|
|
|
config
|
|
|
|
|
(λ (db)
|
|
|
|
|
(define row
|
|
|
|
|
(query-maybe-row db
|
|
|
|
|
(string-append "SELECT " page-columns
|
|
|
|
|
" FROM pages WHERE slug = $1 AND archived = FALSE")
|
|
|
|
|
slug))
|
|
|
|
|
(if row (row->page row) #f)))))
|
|
|
|
|
(let-values (((namespace slug) (split-page-reference reference)))
|
|
|
|
|
(call-with-wiki-database
|
|
|
|
|
config
|
|
|
|
|
(λ (db)
|
|
|
|
|
(define row
|
|
|
|
|
(query-maybe-row db
|
|
|
|
|
(string-append "SELECT " page-columns
|
|
|
|
|
" FROM pages WHERE namespace = $1 AND slug = $2 AND archived = FALSE")
|
|
|
|
|
namespace slug))
|
|
|
|
|
(if row (row->page row) #f))))))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(define (replace-todos! db page-id markdown)
|
|
|
|
@@ -178,7 +230,8 @@ SQL
|
|
|
|
|
; post : Current page state and version 1 are committed atomically.
|
|
|
|
|
; result : The new page metadata with Markdown.
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
(define (create-page! config slug title markdown author [summary "Created page"] [tags '()])
|
|
|
|
|
(define (create-page! config reference title markdown author [summary "Created page"] [tags '()])
|
|
|
|
|
(define-values (namespace slug) (split-page-reference reference))
|
|
|
|
|
(call-with-wiki-database
|
|
|
|
|
config
|
|
|
|
|
(λ (db)
|
|
|
|
@@ -189,20 +242,20 @@ SQL
|
|
|
|
|
(define page-id
|
|
|
|
|
(query-value db
|
|
|
|
|
#<<SQL
|
|
|
|
|
INSERT INTO pages(slug, title, markdown, tags, current_version,
|
|
|
|
|
INSERT INTO pages(namespace, slug, title, markdown, tags, current_version,
|
|
|
|
|
created_at, updated_at, created_by, updated_by, search_document)
|
|
|
|
|
VALUES ($1, $2, $3, $4, 1, $5, $5, $6, $6,
|
|
|
|
|
setweight(to_tsvector('simple', coalesce($2, '')), 'A') ||
|
|
|
|
|
setweight(to_tsvector('simple', coalesce($3, '')), 'B'))
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, 1, $6, $6, $7, $7,
|
|
|
|
|
setweight(to_tsvector('simple', coalesce($3, '')), 'A') ||
|
|
|
|
|
setweight(to_tsvector('simple', coalesce($4, '')), 'B'))
|
|
|
|
|
RETURNING id
|
|
|
|
|
SQL
|
|
|
|
|
slug title markdown (tags->text tags) now author))
|
|
|
|
|
namespace slug title markdown (tags->text tags) now author))
|
|
|
|
|
(define page-version-id
|
|
|
|
|
(insert-version! db page-id 1 title markdown author "create" summary now tags))
|
|
|
|
|
(replace-todos! db page-id markdown)
|
|
|
|
|
(replace-current-attachment-references! db page-id markdown now)
|
|
|
|
|
(record-version-attachment-references! db page-id page-version-id markdown now)))))
|
|
|
|
|
(read-page config slug))
|
|
|
|
|
(read-page config reference))
|
|
|
|
|
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
; goal : Save a new version of an existing wiki page.
|
|
|
|
@@ -210,7 +263,8 @@ SQL
|
|
|
|
|
; post : Current page and version history are committed atomically.
|
|
|
|
|
; result : The updated page metadata with Markdown.
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
(define (update-page! config slug title markdown author base-version [summary "Edited page"] [tags #f])
|
|
|
|
|
(define (update-page! config reference title markdown author base-version [summary "Edited page"] [tags #f] [new-namespace #f])
|
|
|
|
|
(define-values (namespace slug) (split-page-reference reference))
|
|
|
|
|
(call-with-wiki-database
|
|
|
|
|
config
|
|
|
|
|
(λ (db)
|
|
|
|
@@ -219,8 +273,8 @@ SQL
|
|
|
|
|
(λ ()
|
|
|
|
|
(define row
|
|
|
|
|
(query-maybe-row db
|
|
|
|
|
"SELECT id, current_version, tags FROM pages WHERE slug = $1 AND archived = FALSE FOR UPDATE"
|
|
|
|
|
slug))
|
|
|
|
|
"SELECT id, current_version, tags, namespace FROM pages WHERE namespace = $1 AND slug = $2 AND archived = FALSE FOR UPDATE"
|
|
|
|
|
namespace slug))
|
|
|
|
|
(unless row
|
|
|
|
|
(error 'update-page! "unknown page: ~a" slug))
|
|
|
|
|
(define current-version (vector-ref row 1))
|
|
|
|
@@ -232,25 +286,33 @@ SQL
|
|
|
|
|
(error 'update-page! "version-conflict"))
|
|
|
|
|
(define page-tags
|
|
|
|
|
(if tags tags (text->tags (vector-ref row 2))))
|
|
|
|
|
(define target-namespace
|
|
|
|
|
(if (eq? new-namespace #f)
|
|
|
|
|
(vector-ref row 3)
|
|
|
|
|
(string-trim new-namespace)))
|
|
|
|
|
(unless (or (string=? target-namespace "")
|
|
|
|
|
(and (valid-slug? target-namespace)
|
|
|
|
|
(<= (string-length target-namespace) 80)))
|
|
|
|
|
(error 'update-page! "invalid namespace: ~a" target-namespace))
|
|
|
|
|
(define next-version (+ current-version 1))
|
|
|
|
|
(define now (current-seconds))
|
|
|
|
|
(query-exec db
|
|
|
|
|
#<<SQL
|
|
|
|
|
UPDATE pages
|
|
|
|
|
SET title = $1, markdown = $2, tags = $3, current_version = $4,
|
|
|
|
|
updated_at = $5, updated_by = $6,
|
|
|
|
|
updated_at = $5, updated_by = $6, namespace = $7,
|
|
|
|
|
search_document = setweight(to_tsvector('simple', coalesce($1, '')), 'A') ||
|
|
|
|
|
setweight(to_tsvector('simple', coalesce($2, '')), 'B')
|
|
|
|
|
WHERE id = $7
|
|
|
|
|
WHERE id = $8
|
|
|
|
|
SQL
|
|
|
|
|
title markdown (tags->text page-tags) next-version now author (vector-ref row 0))
|
|
|
|
|
title markdown (tags->text page-tags) next-version now author target-namespace (vector-ref row 0))
|
|
|
|
|
(define page-id (vector-ref row 0))
|
|
|
|
|
(define page-version-id
|
|
|
|
|
(insert-version! db page-id next-version title markdown author "edit" summary now page-tags))
|
|
|
|
|
(replace-todos! db page-id markdown)
|
|
|
|
|
(replace-current-attachment-references! db page-id markdown now)
|
|
|
|
|
(record-version-attachment-references! db page-id page-version-id markdown now)))))
|
|
|
|
|
(read-page config slug))
|
|
|
|
|
(read-page config (page-reference (if (eq? new-namespace #f) namespace (string-trim new-namespace)) slug)))
|
|
|
|
|
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
; goal : Archive an existing wiki page.
|
|
|
|
@@ -258,7 +320,8 @@ SQL
|
|
|
|
|
; post : The page is marked archived while its versions and attachments remain stored.
|
|
|
|
|
; result : void.
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
(define (archive-page! config slug author)
|
|
|
|
|
(define (archive-page! config reference author)
|
|
|
|
|
(define-values (namespace slug) (split-page-reference reference))
|
|
|
|
|
(call-with-wiki-database
|
|
|
|
|
config
|
|
|
|
|
(λ (db)
|
|
|
|
@@ -267,10 +330,10 @@ SQL
|
|
|
|
|
#<<SQL
|
|
|
|
|
UPDATE pages
|
|
|
|
|
SET archived = TRUE, archived_at = $1, archived_by = $2
|
|
|
|
|
WHERE slug = $3 AND archived = FALSE
|
|
|
|
|
WHERE namespace = $3 AND slug = $4 AND archived = FALSE
|
|
|
|
|
RETURNING id
|
|
|
|
|
SQL
|
|
|
|
|
(current-seconds) author slug))
|
|
|
|
|
(current-seconds) author namespace slug))
|
|
|
|
|
(unless id
|
|
|
|
|
(error 'archive-page! "unknown page: ~a" slug))
|
|
|
|
|
(query-exec db
|
|
|
|
@@ -278,13 +341,14 @@ SQL
|
|
|
|
|
id)))
|
|
|
|
|
(void))
|
|
|
|
|
|
|
|
|
|
(define (page-id config slug)
|
|
|
|
|
(define (page-id config reference)
|
|
|
|
|
(define-values (namespace slug) (split-page-reference reference))
|
|
|
|
|
(call-with-wiki-database
|
|
|
|
|
config
|
|
|
|
|
(λ (db)
|
|
|
|
|
(query-maybe-value db
|
|
|
|
|
"SELECT id FROM pages WHERE slug = $1 AND archived = FALSE"
|
|
|
|
|
slug))))
|
|
|
|
|
"SELECT id FROM pages WHERE namespace = $1 AND slug = $2 AND archived = FALSE"
|
|
|
|
|
namespace slug))))
|
|
|
|
|
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
; goal : Read the version history for a wiki page.
|
|
|
|
@@ -292,12 +356,13 @@ SQL
|
|
|
|
|
; post : Page version rows have only been read.
|
|
|
|
|
; result : A newest-first list of version metadata hashes.
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
(define (page-history config slug)
|
|
|
|
|
(define (page-history config reference)
|
|
|
|
|
(define-values (namespace slug) (split-page-reference reference))
|
|
|
|
|
(call-with-wiki-database
|
|
|
|
|
config
|
|
|
|
|
(λ (db)
|
|
|
|
|
(define id
|
|
|
|
|
(query-maybe-value db "SELECT id FROM pages WHERE slug = $1 AND archived = FALSE" slug))
|
|
|
|
|
(query-maybe-value db "SELECT id FROM pages WHERE namespace = $1 AND slug = $2 AND archived = FALSE" namespace slug))
|
|
|
|
|
(unless id
|
|
|
|
|
(error 'page-history "unknown page: ~a" slug))
|
|
|
|
|
(for/list ((row (in-list
|
|
|
|
@@ -323,7 +388,8 @@ SQL
|
|
|
|
|
; post : Version rows have only been read.
|
|
|
|
|
; result : Version metadata with Markdown, or #f when the version is absent.
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
(define (read-version config slug version)
|
|
|
|
|
(define (read-version config reference version)
|
|
|
|
|
(define-values (namespace slug) (split-page-reference reference))
|
|
|
|
|
(define version-number
|
|
|
|
|
(if (number? version) version (string->number version)))
|
|
|
|
|
(and version-number
|
|
|
|
@@ -336,9 +402,9 @@ SQL
|
|
|
|
|
SELECT v.version, v.title, v.markdown, v.author, v.action, v.summary, v.tags, v.created_at
|
|
|
|
|
FROM page_versions v
|
|
|
|
|
JOIN pages p ON p.id = v.page_id
|
|
|
|
|
WHERE p.slug = $1 AND p.archived = FALSE AND v.version = $2
|
|
|
|
|
WHERE p.namespace = $1 AND p.slug = $2 AND p.archived = FALSE AND v.version = $3
|
|
|
|
|
SQL
|
|
|
|
|
slug version-number))
|
|
|
|
|
namespace slug version-number))
|
|
|
|
|
(and row
|
|
|
|
|
(hash 'version (vector-ref row 0)
|
|
|
|
|
'title (vector-ref row 1)
|
|
|
|
@@ -369,7 +435,8 @@ SELECT p.slug,
|
|
|
|
|
p.title,
|
|
|
|
|
ts_rank(p.search_document, q.query) AS rank,
|
|
|
|
|
ts_headline('simple', p.markdown, q.query,
|
|
|
|
|
'StartSel=[[[, StopSel=]]], MaxWords=28, MinWords=8, ShortWord=2') AS snippet
|
|
|
|
|
'StartSel=[[[, StopSel=]]], MaxWords=28, MinWords=8, ShortWord=2') AS snippet,
|
|
|
|
|
p.namespace
|
|
|
|
|
FROM pages p, q
|
|
|
|
|
WHERE p.archived = FALSE
|
|
|
|
|
AND p.search_document @@ q.query
|
|
|
|
@@ -377,7 +444,8 @@ ORDER BY rank DESC, lower(p.title), p.title
|
|
|
|
|
LIMIT 50
|
|
|
|
|
SQL
|
|
|
|
|
query-text))))
|
|
|
|
|
(hash 'slug (vector-ref row 0)
|
|
|
|
|
(hash 'slug (page-reference (vector-ref row 4) (vector-ref row 0))
|
|
|
|
|
'namespace (vector-ref row 4)
|
|
|
|
|
'title (vector-ref row 1)
|
|
|
|
|
'rank (vector-ref row 2)
|
|
|
|
|
'snippet (vector-ref row 3)))))))
|
|
|
|
@@ -395,14 +463,15 @@ SQL
|
|
|
|
|
(for/list ((row (in-list
|
|
|
|
|
(query-rows db
|
|
|
|
|
#<<SQL
|
|
|
|
|
SELECT p.slug, p.title, t.item_number, t.line_number, t.text
|
|
|
|
|
SELECT p.slug, p.title, t.item_number, t.line_number, t.text, p.namespace
|
|
|
|
|
FROM todo_items t
|
|
|
|
|
JOIN pages p ON p.id = t.page_id
|
|
|
|
|
WHERE p.archived = FALSE
|
|
|
|
|
ORDER BY lower(p.title), p.title, t.item_number
|
|
|
|
|
ORDER BY lower(p.namespace), p.namespace, lower(p.title), p.title, t.item_number
|
|
|
|
|
SQL
|
|
|
|
|
))))
|
|
|
|
|
(hash 'slug (vector-ref row 0)
|
|
|
|
|
(hash 'slug (page-reference (vector-ref row 5) (vector-ref row 0))
|
|
|
|
|
'namespace (vector-ref row 5)
|
|
|
|
|
'title (vector-ref row 1)
|
|
|
|
|
'number (vector-ref row 2)
|
|
|
|
|
'line (vector-ref row 3)
|
|
|
|
@@ -441,15 +510,16 @@ SQL
|
|
|
|
|
(for/list ((row (in-list
|
|
|
|
|
(query-rows db
|
|
|
|
|
#<<SQL
|
|
|
|
|
SELECT p.slug, p.title, b.section, b.position, b.created_at, p.updated_at
|
|
|
|
|
SELECT p.slug, p.title, b.section, b.position, b.created_at, p.updated_at, p.namespace
|
|
|
|
|
FROM bookmarks b
|
|
|
|
|
JOIN pages p ON p.id = b.page_id
|
|
|
|
|
WHERE b.user_id = $1
|
|
|
|
|
AND p.archived = FALSE
|
|
|
|
|
ORDER BY lower(b.section), b.section, b.position, b.created_at, lower(p.title), p.title
|
|
|
|
|
ORDER BY lower(p.namespace), p.namespace, lower(b.section), b.section, b.position, b.created_at, lower(p.title), p.title
|
|
|
|
|
SQL
|
|
|
|
|
user-id))))
|
|
|
|
|
(hash 'slug (vector-ref row 0)
|
|
|
|
|
(hash 'slug (page-reference (vector-ref row 6) (vector-ref row 0))
|
|
|
|
|
'namespace (vector-ref row 6)
|
|
|
|
|
'title (vector-ref row 1)
|
|
|
|
|
'section (vector-ref row 2)
|
|
|
|
|
'position (vector-ref row 3)
|
|
|
|
@@ -462,7 +532,8 @@ SQL
|
|
|
|
|
; post : Exactly one bookmark exists for user-id and the page.
|
|
|
|
|
; result : void.
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
(define (set-bookmark! config user-id slug section)
|
|
|
|
|
(define (set-bookmark! config user-id reference section)
|
|
|
|
|
(define-values (namespace slug) (split-page-reference reference))
|
|
|
|
|
(call-with-wiki-database
|
|
|
|
|
config
|
|
|
|
|
(λ (db)
|
|
|
|
@@ -471,8 +542,8 @@ SQL
|
|
|
|
|
(λ ()
|
|
|
|
|
(define page-id
|
|
|
|
|
(query-maybe-value db
|
|
|
|
|
"SELECT id FROM pages WHERE slug = $1 AND archived = FALSE"
|
|
|
|
|
slug))
|
|
|
|
|
"SELECT id FROM pages WHERE namespace = $1 AND slug = $2 AND archived = FALSE"
|
|
|
|
|
namespace slug))
|
|
|
|
|
(unless page-id
|
|
|
|
|
(error 'set-bookmark! "unknown page: ~a" slug))
|
|
|
|
|
(define clean-section (string-trim section))
|
|
|
|
@@ -501,7 +572,8 @@ SQL
|
|
|
|
|
; post : The bookmark no longer exists; other bookmarks are unchanged.
|
|
|
|
|
; result : void.
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
(define (delete-bookmark! config user-id slug)
|
|
|
|
|
(define (delete-bookmark! config user-id reference)
|
|
|
|
|
(define-values (namespace slug) (split-page-reference reference))
|
|
|
|
|
(call-with-wiki-database
|
|
|
|
|
config
|
|
|
|
|
(λ (db)
|
|
|
|
@@ -509,9 +581,9 @@ SQL
|
|
|
|
|
#<<SQL
|
|
|
|
|
DELETE FROM bookmarks
|
|
|
|
|
WHERE user_id = $1
|
|
|
|
|
AND page_id = (SELECT id FROM pages WHERE slug = $2)
|
|
|
|
|
AND page_id = (SELECT id FROM pages WHERE namespace = $2 AND slug = $3)
|
|
|
|
|
SQL
|
|
|
|
|
user-id slug)))
|
|
|
|
|
user-id namespace slug)))
|
|
|
|
|
(void))
|
|
|
|
|
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
@@ -535,7 +607,8 @@ SELECT a.id,
|
|
|
|
|
a.uploaded_at,
|
|
|
|
|
a.uploaded_by,
|
|
|
|
|
owner.slug,
|
|
|
|
|
owner.title
|
|
|
|
|
owner.title,
|
|
|
|
|
owner.namespace
|
|
|
|
|
FROM attachments a
|
|
|
|
|
JOIN pages owner ON owner.id = a.page_id
|
|
|
|
|
WHERE NOT EXISTS (
|
|
|
|
@@ -552,7 +625,7 @@ SQL
|
|
|
|
|
(for/list ((use-row (in-list
|
|
|
|
|
(query-rows db
|
|
|
|
|
#<<SQL
|
|
|
|
|
SELECT p.slug, p.title, pv.version, ar.referenced_at
|
|
|
|
|
SELECT p.slug, p.title, pv.version, ar.referenced_at, p.namespace
|
|
|
|
|
FROM attachment_references ar
|
|
|
|
|
JOIN pages p ON p.id = ar.page_id
|
|
|
|
|
LEFT JOIN page_versions pv ON pv.id = ar.page_version_id
|
|
|
|
@@ -562,7 +635,8 @@ ORDER BY ar.referenced_at DESC, ar.id DESC
|
|
|
|
|
LIMIT 5
|
|
|
|
|
SQL
|
|
|
|
|
attachment-id))))
|
|
|
|
|
(hash 'slug (vector-ref use-row 0)
|
|
|
|
|
(hash 'slug (page-reference (vector-ref use-row 4) (vector-ref use-row 0))
|
|
|
|
|
'namespace (vector-ref use-row 4)
|
|
|
|
|
'title (vector-ref use-row 1)
|
|
|
|
|
'version (if (sql-null? (vector-ref use-row 2)) #f (vector-ref use-row 2))
|
|
|
|
|
'referencedAt (vector-ref use-row 3))))
|
|
|
|
@@ -573,7 +647,8 @@ SQL
|
|
|
|
|
'size (vector-ref row 4)
|
|
|
|
|
'uploadedAt (vector-ref row 5)
|
|
|
|
|
'uploadedBy (vector-ref row 6)
|
|
|
|
|
'ownerSlug (vector-ref row 7)
|
|
|
|
|
'ownerSlug (page-reference (vector-ref row 9) (vector-ref row 7))
|
|
|
|
|
'ownerNamespace (vector-ref row 9)
|
|
|
|
|
'ownerTitle (vector-ref row 8)
|
|
|
|
|
'lastUses last-uses)))))
|
|
|
|
|
|
|
|
|
@@ -619,10 +694,10 @@ SQL
|
|
|
|
|
; post : Attachment metadata and bytes are stored in one PostgreSQL row.
|
|
|
|
|
; result : A hash containing original name, stored name and page-local URL.
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
(define (save-upload! config slug original-name content author)
|
|
|
|
|
(define id (page-id config slug))
|
|
|
|
|
(define (save-upload! config reference original-name content author)
|
|
|
|
|
(define id (page-id config reference))
|
|
|
|
|
(unless id
|
|
|
|
|
(error 'save-upload! "unknown page: ~a" slug))
|
|
|
|
|
(error 'save-upload! "unknown page: ~a" reference))
|
|
|
|
|
(define stored-name
|
|
|
|
|
(format "~a-~a-~a" (current-seconds) (random 1000000) (safe-file-name original-name)))
|
|
|
|
|
(define mime-type
|
|
|
|
@@ -653,7 +728,7 @@ SQL
|
|
|
|
|
author)))
|
|
|
|
|
(hash 'name original-name
|
|
|
|
|
'storedName stored-name
|
|
|
|
|
'url (format "/uploads/~a/~a" slug stored-name)))
|
|
|
|
|
'url (format "/uploads/~a/~a" reference stored-name)))
|
|
|
|
|
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
; goal : Read one stored attachment from PostgreSQL.
|
|
|
|
@@ -661,8 +736,9 @@ SQL
|
|
|
|
|
; post : PostgreSQL has only been read.
|
|
|
|
|
; result : A hash containing bytes, MIME type and names, or #f when absent.
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
|
(define (uploaded-file config slug stored-name)
|
|
|
|
|
(and (valid-slug? slug)
|
|
|
|
|
(define (uploaded-file config reference stored-name)
|
|
|
|
|
(define-values (namespace slug) (split-page-reference reference))
|
|
|
|
|
(and (valid-page-reference? reference)
|
|
|
|
|
(not (regexp-match? #px"[/\\\\]" stored-name))
|
|
|
|
|
(call-with-wiki-database
|
|
|
|
|
config
|
|
|
|
@@ -674,8 +750,10 @@ SELECT a.original_name, a.stored_name, a.mime_type, a.content, a.size
|
|
|
|
|
FROM attachments a
|
|
|
|
|
JOIN pages p ON p.id = a.page_id
|
|
|
|
|
WHERE p.slug = $1 AND p.archived = FALSE AND a.stored_name = $2
|
|
|
|
|
ORDER BY CASE WHEN p.namespace = $3 THEN 0 ELSE 1 END, a.id
|
|
|
|
|
LIMIT 1
|
|
|
|
|
SQL
|
|
|
|
|
slug stored-name))
|
|
|
|
|
slug stored-name namespace))
|
|
|
|
|
(if row
|
|
|
|
|
(hash 'originalName (vector-ref row 0)
|
|
|
|
|
'storedName (vector-ref row 1)
|
|
|
|
|