Data and structures converted

This commit is contained in:
Daniel J. Summers 2018-01-17 23:00:26 -06:00
parent 8d8d112fff
commit 0807aa300a
3 changed files with 372 additions and 0 deletions

326
src/api/data/data.go Normal file
View 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(&note.AsOf, &note.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
View 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
View File

@ -0,0 +1,11 @@
// myPrayerJournal API Server
package main
import (
"fmt"
"time"
)
func main() {
fmt.Print(time.Now().UnixNano() / int64(1000000))
}