Data and structures converted
This commit is contained in:
parent
8d8d112fff
commit
0807aa300a
326
src/api/data/data.go
Normal file
326
src/api/data/data.go
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
// Package data contains data access functions for myPrayerJournal.
|
||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lucsky/cuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
currentRequestSQL = `
|
||||||
|
SELECT "requestId", "text", "asOf", "lastStatus"
|
||||||
|
FROM mpj.journal`
|
||||||
|
journalSQL = `
|
||||||
|
SELECT "requestId", "text", "asOf", "lastStatus"
|
||||||
|
FROM mpj.journal
|
||||||
|
WHERE "userId" = $1
|
||||||
|
AND "lastStatus" <> 'Answered'`
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Data Access */
|
||||||
|
|
||||||
|
// Retrieve a basic request
|
||||||
|
func retrieveRequest(db *sql.DB, reqID, userID string) (*Request, bool) {
|
||||||
|
req := Request{}
|
||||||
|
err := db.QueryRow(`
|
||||||
|
SELECT "requestId", "enteredOn"
|
||||||
|
FROM mpj.request
|
||||||
|
WHERE "requestId" = $1
|
||||||
|
AND "userId" = $2`, reqID, userID).Scan(
|
||||||
|
&req.ID, &req.EnteredOn,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if err != sql.ErrNoRows {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
req.UserID = userID
|
||||||
|
return &req, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unix time in JavaScript Date.now() precision.
|
||||||
|
func jsNow() int64 {
|
||||||
|
return time.Now().UnixNano() / int64(1000000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through rows and create journal requests from them.
|
||||||
|
func makeJournal(rows *sql.Rows, userID string) []JournalRequest {
|
||||||
|
var out []JournalRequest
|
||||||
|
for rows.Next() {
|
||||||
|
req := JournalRequest{}
|
||||||
|
err := rows.Scan(&req.RequestID, &req.Text, &req.AsOf, &req.LastStatus)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, req)
|
||||||
|
}
|
||||||
|
if rows.Err() != nil {
|
||||||
|
log.Print(rows.Err())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddHistory creates a history entry for a prayer request, given the status and updated text.
|
||||||
|
func AddHistory(db *sql.DB, userID, reqID, status, text string) int {
|
||||||
|
if _, ok := retrieveRequest(db, reqID, userID); !ok {
|
||||||
|
return 404
|
||||||
|
}
|
||||||
|
_, err := db.Exec(`
|
||||||
|
INSERT INTO mpj.history
|
||||||
|
("requestId", "asOf", "status", "text")
|
||||||
|
VALUES
|
||||||
|
($1, $2, $3, NULLIF($4, ''))`,
|
||||||
|
reqID, jsNow(), status, text)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
return 500
|
||||||
|
}
|
||||||
|
return 204
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddNew stores a new prayer request and its initial history record.
|
||||||
|
func AddNew(db *sql.DB, userID, text string) (*JournalRequest, bool) {
|
||||||
|
id := cuid.New()
|
||||||
|
now := jsNow()
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
tx.Rollback()
|
||||||
|
} else {
|
||||||
|
tx.Commit()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_, err = tx.Exec(
|
||||||
|
`INSERT INTO mpj.request ("requestId", "enteredOn", "userId") VALUES ($1, $2, $3)`,
|
||||||
|
id, now, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
_, err = tx.Exec(
|
||||||
|
`INSERT INTO mpj.history ("requestId", "asOf", "status", "text") VALUES ($1, $2, 'Created', $3)`,
|
||||||
|
id, now, text)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return &JournalRequest{RequestID: id, Text: text, AsOf: now, LastStatus: `Created`}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddNote adds a note to a prayer request.
|
||||||
|
func AddNote(db *sql.DB, userID, reqID, note string) int {
|
||||||
|
if _, ok := retrieveRequest(db, reqID, userID); !ok {
|
||||||
|
return 404
|
||||||
|
}
|
||||||
|
_, err := db.Exec(`
|
||||||
|
INSERT INTO mpj.note
|
||||||
|
("requestId", "asOf", "notes")
|
||||||
|
VALUES
|
||||||
|
($1, $2, $3)`,
|
||||||
|
reqID, jsNow(), note)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
return 500
|
||||||
|
}
|
||||||
|
return 204
|
||||||
|
}
|
||||||
|
|
||||||
|
// Answered retrieves all answered requests for the given user.
|
||||||
|
func Answered(db *sql.DB, userID string) []JournalRequest {
|
||||||
|
rows, err := db.Query(currentRequestSQL+
|
||||||
|
`WHERE "userId" = $1
|
||||||
|
AND "lastStatus" = 'Answered'
|
||||||
|
ORDER BY "asOf" DESC`,
|
||||||
|
userID)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
return makeJournal(rows, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByID retrieves a journal request by its ID.
|
||||||
|
func ByID(db *sql.DB, userID, reqID string) (*JournalRequest, bool) {
|
||||||
|
req := JournalRequest{}
|
||||||
|
err := db.QueryRow(currentRequestSQL+
|
||||||
|
`WHERE "requestId" = $1
|
||||||
|
AND "userId = $2`,
|
||||||
|
userID, reqID).Scan(
|
||||||
|
&req.RequestID, &req.Text, &req.AsOf, &req.LastStatus,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return &req, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// FullByID retrieves a journal request, including its full history and notes.
|
||||||
|
func FullByID(db *sql.DB, userID, reqID string) (*JournalRequest, bool) {
|
||||||
|
req, ok := ByID(db, userID, reqID)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
hRows, err := db.Query(`
|
||||||
|
SELECT "asOf", "status", COALESCE("text", '') AS "text"
|
||||||
|
FROM mpj.history
|
||||||
|
WHERE "requestId" = $1
|
||||||
|
ORDER BY "asOf"`,
|
||||||
|
reqID)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
defer hRows.Close()
|
||||||
|
for hRows.Next() {
|
||||||
|
hist := History{}
|
||||||
|
err = hRows.Scan(&hist.AsOf, &hist.Status, &hist.Text)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
req.History = append(req.History, hist)
|
||||||
|
}
|
||||||
|
if hRows.Err() != nil {
|
||||||
|
log.Print(hRows.Err())
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
req.Notes = NotesByID(db, userID, reqID)
|
||||||
|
return req, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Journal retrieves the current user's active prayer journal.
|
||||||
|
func Journal(db *sql.DB, userID string) []JournalRequest {
|
||||||
|
rows, err := db.Query(journalSQL, userID)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
return makeJournal(rows, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotesByID retrieves the notes for a given prayer request
|
||||||
|
func NotesByID(db *sql.DB, userID, reqID string) []Note {
|
||||||
|
if _, ok := retrieveRequest(db, reqID, userID); !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rows, err := db.Query(`
|
||||||
|
SELECT "asOf", "notes"
|
||||||
|
FROM mpj.note
|
||||||
|
WHERE "requestId" = $1
|
||||||
|
ORDER BY "asOf" DESC`,
|
||||||
|
reqID)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var notes []Note
|
||||||
|
for rows.Next() {
|
||||||
|
note := Note{}
|
||||||
|
err = rows.Scan(¬e.AsOf, ¬e.Notes)
|
||||||
|
if err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
notes = append(notes, note)
|
||||||
|
}
|
||||||
|
if rows.Err() != nil {
|
||||||
|
log.Print(rows.Err())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return notes
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DDL */
|
||||||
|
|
||||||
|
// EnsureDB makes sure we have a known state of data structures.
|
||||||
|
func EnsureDB(db *sql.DB) {
|
||||||
|
tableSQL := func(table string) string {
|
||||||
|
return fmt.Sprintf(`SELECT 1 FROM pg_tables WHERE schemaname='mpj' AND tablename='%s'`, table)
|
||||||
|
}
|
||||||
|
indexSQL := func(table, index string) string {
|
||||||
|
return fmt.Sprintf(`SELECT 1 FROM pg_indexes WHERE schemaname='mpj' AND tablename='%s' AND indexname='%s'`,
|
||||||
|
table, index)
|
||||||
|
}
|
||||||
|
check := func(name, test, fix string) {
|
||||||
|
count := 0
|
||||||
|
err := db.QueryRow(test).Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
log.Printf("Fixing up %s...\n", name)
|
||||||
|
_, err = db.Exec(fix)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check(`myPrayerJournal Schema`, `SELECT 1 FROM pg_namespace WHERE nspname='mpj'`,
|
||||||
|
`CREATE SCHEMA mpj;
|
||||||
|
COMMENT ON SCHEMA mpj IS 'myPrayerJournal data'`)
|
||||||
|
if _, err := db.Exec(`SET search_path TO mpj`); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
check(`request Table`, tableSQL(`request`),
|
||||||
|
`CREATE TABLE mpj.request (
|
||||||
|
"requestId" varchar(25) PRIMARY KEY,
|
||||||
|
"enteredOn" bigint NOT NULL,
|
||||||
|
"userId" varchar(100) NOT NULL);
|
||||||
|
COMMENT ON TABLE mpj.request IS 'Requests'`)
|
||||||
|
check(`history Table`, tableSQL(`history`),
|
||||||
|
`CREATE TABLE mpj.history (
|
||||||
|
"requestId" varchar(25) NOT NULL REFERENCES mpj.request,
|
||||||
|
"asOf" bigint NOT NULL,
|
||||||
|
"status" varchar(25),
|
||||||
|
"text" text,
|
||||||
|
PRIMARY KEY ("requestId", "asOf"));
|
||||||
|
COMMENT ON TABLE mpj.history IS 'Request update history'`)
|
||||||
|
check(`note Table`, tableSQL(`note`),
|
||||||
|
`CREATE TABLE mpj.note (
|
||||||
|
"requestId" varchar(25) NOT NULL REFERENCES mpj.request,
|
||||||
|
"asOf" bigint NOT NULL,
|
||||||
|
"notes" text NOT NULL,
|
||||||
|
PRIMARY KEY ("requestId", "asOf"));
|
||||||
|
COMMENT ON TABLE mpj.note IS 'Notes regarding a request'`)
|
||||||
|
check(`request.userId Index`, indexSQL(`request`, `idx_request_userId`),
|
||||||
|
`CREATE INDEX "idx_request_userId" ON mpj.request ("userId");
|
||||||
|
COMMENT ON INDEX "idx_request_userId" IS 'Requests are retrieved by user'`)
|
||||||
|
check(`journal View`, `SELECT 1 FROM pg_views WHERE schemaname='mpj' AND viewname='journal'`,
|
||||||
|
`CREATE VIEW mpj.journal AS
|
||||||
|
SELECT
|
||||||
|
request."requestId",
|
||||||
|
request."userId",
|
||||||
|
(SELECT "text"
|
||||||
|
FROM mpj.history
|
||||||
|
WHERE history."requestId" = request."requestId"
|
||||||
|
AND "text" IS NOT NULL
|
||||||
|
ORDER BY "asOf" DESC
|
||||||
|
LIMIT 1) AS "text",
|
||||||
|
(SELECT "asOf"
|
||||||
|
FROM mpj.history
|
||||||
|
WHERE history."requestId" = request."requestId"
|
||||||
|
ORDER BY "asOf" DESC
|
||||||
|
LIMIT 1) AS "asOf",
|
||||||
|
(SELECT "status"
|
||||||
|
FROM mpj.history
|
||||||
|
WHERE history."requestId" = request."requestId"
|
||||||
|
ORDER BY "asOf" DESC
|
||||||
|
LIMIT 1) AS "lastStatus"
|
||||||
|
FROM mpj.request;
|
||||||
|
COMMENT ON VIEW mpj.journal IS 'Requests with latest text'`)
|
||||||
|
}
|
35
src/api/data/entities.go
Normal file
35
src/api/data/entities.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
// History is a record of action taken on a prayer request, including updates to its text.
|
||||||
|
type History struct {
|
||||||
|
RequestID string `json:"requestId"`
|
||||||
|
AsOf int64 `json:"asOf"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note is a note regarding a prayer request that does not result in an update to its text.
|
||||||
|
type Note struct {
|
||||||
|
RequestID string `json:"requestId"`
|
||||||
|
AsOf int64 `json:"asOf"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request is the identifying record for a prayer request.
|
||||||
|
type Request struct {
|
||||||
|
ID string `json:"requestId"`
|
||||||
|
EnteredOn int64 `json:"enteredOn"`
|
||||||
|
UserID string `json:"userId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JournalRequest is the form of a prayer request returned for the request journal display. It also contains
|
||||||
|
// properties that may be filled for history and notes.
|
||||||
|
type JournalRequest struct {
|
||||||
|
RequestID string `json:"requestId"`
|
||||||
|
UserID string `json:"userId"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
AsOf int64 `json:"asOf"`
|
||||||
|
LastStatus string `json:"lastStatus"`
|
||||||
|
History []History `json:"history"`
|
||||||
|
Notes []Note `json:"notes"`
|
||||||
|
}
|
11
src/my-prayer-journal.go
Normal file
11
src/my-prayer-journal.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// myPrayerJournal API Server
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Print(time.Now().UnixNano() / int64(1000000))
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user