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