643 lines
19 KiB
Go
643 lines
19 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
// Open creates a Store for the requested driver. SQLite is fully supported;
|
|
// the driver switch is the extension point for PostgreSQL and MariaDB.
|
|
func Open(driver, dsn string) (Store, error) {
|
|
switch strings.ToLower(strings.TrimSpace(driver)) {
|
|
case "", "sqlite", "sqlite3":
|
|
return openSQLite(dsn)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported database driver %q (supported: sqlite)", driver)
|
|
}
|
|
}
|
|
|
|
type sqliteStore struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func openSQLite(dsn string) (*sqliteStore, error) {
|
|
if err := ensureSQLiteDir(dsn); err != nil {
|
|
return nil, err
|
|
}
|
|
db, err := sql.Open("sqlite", dsn)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open sqlite: %w", err)
|
|
}
|
|
// SQLite allows a single writer; a small pool keeps behaviour predictable.
|
|
db.SetMaxOpenConns(1)
|
|
db.SetMaxIdleConns(1)
|
|
db.SetConnMaxLifetime(0)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
if err := db.PingContext(ctx); err != nil {
|
|
_ = db.Close()
|
|
return nil, fmt.Errorf("ping sqlite: %w", err)
|
|
}
|
|
|
|
store := &sqliteStore{db: db}
|
|
if err := store.migrate(ctx); err != nil {
|
|
_ = db.Close()
|
|
return nil, fmt.Errorf("migrate sqlite: %w", err)
|
|
}
|
|
return store, nil
|
|
}
|
|
|
|
func ensureSQLiteDir(dsn string) error {
|
|
path := dsn
|
|
if idx := strings.Index(path, "?"); idx >= 0 {
|
|
path = path[:idx]
|
|
}
|
|
path = strings.TrimPrefix(path, "file:")
|
|
if path == "" || path == ":memory:" || strings.HasPrefix(path, ":memory:") {
|
|
return nil
|
|
}
|
|
dir := filepath.Dir(path)
|
|
if dir == "." || dir == "" {
|
|
return nil
|
|
}
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return fmt.Errorf("create database directory %s: %w", dir, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *sqliteStore) Close() error { return s.db.Close() }
|
|
|
|
func (s *sqliteStore) Ping(ctx context.Context) error { return s.db.PingContext(ctx) }
|
|
|
|
func formatTime(t time.Time) string { return t.UTC().Format(time.RFC3339Nano) }
|
|
|
|
func parseTime(v string) time.Time {
|
|
t, err := time.Parse(time.RFC3339Nano, v)
|
|
if err != nil {
|
|
return time.Time{}
|
|
}
|
|
return t.UTC()
|
|
}
|
|
|
|
func nullString(v *string) sql.NullString {
|
|
if v == nil || *v == "" {
|
|
return sql.NullString{}
|
|
}
|
|
return sql.NullString{String: *v, Valid: true}
|
|
}
|
|
|
|
func fromNullString(v sql.NullString) *string {
|
|
if !v.Valid {
|
|
return nil
|
|
}
|
|
s := v.String
|
|
return &s
|
|
}
|
|
|
|
func boolToInt(b bool) int {
|
|
if b {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (s *sqliteStore) CreateForm(ctx context.Context, form *Form) error {
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
if err := insertForm(ctx, tx, form); err != nil {
|
|
return err
|
|
}
|
|
for i := range form.Questions {
|
|
if err := insertQuestion(ctx, tx, &form.Questions[i]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (s *sqliteStore) UpdateForm(ctx context.Context, form *Form) error {
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
res, err := tx.ExecContext(ctx, `UPDATE forms SET
|
|
title = ?, description = ?, introduction = ?, is_active = ?,
|
|
requires_access_code = ?, signature_required = ?,
|
|
external_reference_required = ?, external_reference_label = ?,
|
|
logo_url = ?, pdf_url = ?, company_name = ?, contact_email = ?,
|
|
contact_phone = ?, contact_url = ?, updated_at = ?
|
|
WHERE id = ?`,
|
|
form.Title, nullString(form.Description), nullString(form.Introduction),
|
|
boolToInt(form.IsActive), boolToInt(form.RequiresAccessCode),
|
|
boolToInt(form.SignatureRequired), boolToInt(form.ExternalReferenceRequired),
|
|
nullString(form.ExternalReferenceLabel), nullString(form.LogoURL),
|
|
nullString(form.PDFURL), nullString(form.CompanyName),
|
|
nullString(form.ContactEmail), nullString(form.ContactPhone),
|
|
nullString(form.ContactURL), formatTime(form.UpdatedAt), form.ID,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n, _ := res.RowsAffected(); n == 0 {
|
|
return ErrNotFound
|
|
}
|
|
|
|
if _, err := tx.ExecContext(ctx, `DELETE FROM questions WHERE form_id = ?`, form.ID); err != nil {
|
|
return err
|
|
}
|
|
for i := range form.Questions {
|
|
form.Questions[i].FormID = form.ID
|
|
if err := insertQuestion(ctx, tx, &form.Questions[i]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func insertForm(ctx context.Context, tx *sql.Tx, form *Form) error {
|
|
_, err := tx.ExecContext(ctx, `INSERT INTO forms (
|
|
id, form_group_id, version, title, description, introduction, is_active,
|
|
requires_access_code, signature_required, external_reference_required,
|
|
external_reference_label, logo_url, pdf_url, company_name, contact_email,
|
|
contact_phone, contact_url, created_at, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
form.ID, form.FormGroupID, form.Version, form.Title,
|
|
nullString(form.Description), nullString(form.Introduction),
|
|
boolToInt(form.IsActive), boolToInt(form.RequiresAccessCode),
|
|
boolToInt(form.SignatureRequired), boolToInt(form.ExternalReferenceRequired),
|
|
nullString(form.ExternalReferenceLabel), nullString(form.LogoURL),
|
|
nullString(form.PDFURL), nullString(form.CompanyName),
|
|
nullString(form.ContactEmail), nullString(form.ContactPhone),
|
|
nullString(form.ContactURL), formatTime(form.CreatedAt), formatTime(form.UpdatedAt),
|
|
)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "UNIQUE") {
|
|
return ErrConflict
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func insertQuestion(ctx context.Context, tx *sql.Tx, q *Question) error {
|
|
options, err := json.Marshal(q.Options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var minV, maxV sql.NullFloat64
|
|
if q.MinValue != nil {
|
|
minV = sql.NullFloat64{Float64: *q.MinValue, Valid: true}
|
|
}
|
|
if q.MaxValue != nil {
|
|
maxV = sql.NullFloat64{Float64: *q.MaxValue, Valid: true}
|
|
}
|
|
_, err = tx.ExecContext(ctx, `INSERT INTO questions (
|
|
id, form_id, position, question_key, label, description, type, required,
|
|
options_json, min_value, max_value, placeholder, multiline
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
q.ID, q.FormID, q.Position, q.Key, q.Label, nullString(q.Description),
|
|
q.Type, boolToInt(q.Required), string(options), minV, maxV,
|
|
nullString(q.Placeholder), boolToInt(q.Multiline),
|
|
)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "UNIQUE") {
|
|
return ErrConflict
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
const formColumns = `id, form_group_id, version, title, description, introduction,
|
|
is_active, requires_access_code, signature_required, external_reference_required,
|
|
external_reference_label, logo_url, pdf_url, company_name, contact_email,
|
|
contact_phone, contact_url, created_at, updated_at`
|
|
|
|
func (s *sqliteStore) GetForm(ctx context.Context, id string) (*Form, error) {
|
|
row := s.db.QueryRowContext(ctx, `SELECT `+formColumns+` FROM forms WHERE id = ?`, id)
|
|
form, err := scanForm(row)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
questions, err := s.listQuestions(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
form.Questions = questions
|
|
return form, nil
|
|
}
|
|
|
|
func scanForm(row interface{ Scan(...any) error }) (*Form, error) {
|
|
var (
|
|
form Form
|
|
description, introduction, logoURL sql.NullString
|
|
pdfURL, externalReferenceLabel sql.NullString
|
|
companyName, email, phone, contactURL sql.NullString
|
|
createdAt, updatedAt string
|
|
isActive, requiresAccessCode int
|
|
signatureRequired, externalRefReq int
|
|
)
|
|
err := row.Scan(
|
|
&form.ID, &form.FormGroupID, &form.Version, &form.Title,
|
|
&description, &introduction, &isActive, &requiresAccessCode,
|
|
&signatureRequired, &externalRefReq, &externalReferenceLabel,
|
|
&logoURL, &pdfURL, &companyName, &email, &phone, &contactURL,
|
|
&createdAt, &updatedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
form.Description = fromNullString(description)
|
|
form.Introduction = fromNullString(introduction)
|
|
form.LogoURL = fromNullString(logoURL)
|
|
form.PDFURL = fromNullString(pdfURL)
|
|
form.ExternalReferenceLabel = fromNullString(externalReferenceLabel)
|
|
form.CompanyName = fromNullString(companyName)
|
|
form.ContactEmail = fromNullString(email)
|
|
form.ContactPhone = fromNullString(phone)
|
|
form.ContactURL = fromNullString(contactURL)
|
|
form.IsActive = isActive == 1
|
|
form.RequiresAccessCode = requiresAccessCode == 1
|
|
form.SignatureRequired = signatureRequired == 1
|
|
form.ExternalReferenceRequired = externalRefReq == 1
|
|
form.CreatedAt = parseTime(createdAt)
|
|
form.UpdatedAt = parseTime(updatedAt)
|
|
return &form, nil
|
|
}
|
|
|
|
func (s *sqliteStore) listQuestions(ctx context.Context, formID string) ([]Question, error) {
|
|
rows, err := s.db.QueryContext(ctx, `SELECT
|
|
id, form_id, position, question_key, label, description, type, required,
|
|
options_json, min_value, max_value, placeholder, multiline
|
|
FROM questions WHERE form_id = ? ORDER BY position`, formID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var questions []Question
|
|
for rows.Next() {
|
|
var (
|
|
q Question
|
|
description, placeholder sql.NullString
|
|
optionsJSON string
|
|
minV, maxV sql.NullFloat64
|
|
required, multiline int
|
|
)
|
|
if err := rows.Scan(&q.ID, &q.FormID, &q.Position, &q.Key, &q.Label,
|
|
&description, &q.Type, &required, &optionsJSON, &minV, &maxV,
|
|
&placeholder, &multiline); err != nil {
|
|
return nil, err
|
|
}
|
|
q.Description = fromNullString(description)
|
|
q.Placeholder = fromNullString(placeholder)
|
|
q.Required = required == 1
|
|
q.Multiline = multiline == 1
|
|
if minV.Valid {
|
|
v := minV.Float64
|
|
q.MinValue = &v
|
|
}
|
|
if maxV.Valid {
|
|
v := maxV.Float64
|
|
q.MaxValue = &v
|
|
}
|
|
if optionsJSON != "" && optionsJSON != "null" {
|
|
if err := json.Unmarshal([]byte(optionsJSON), &q.Options); err != nil {
|
|
return nil, fmt.Errorf("decode options for question %s: %w", q.ID, err)
|
|
}
|
|
}
|
|
questions = append(questions, q)
|
|
}
|
|
return questions, rows.Err()
|
|
}
|
|
|
|
func (s *sqliteStore) ListForms(ctx context.Context, includeInactive bool) ([]FormSummary, error) {
|
|
query := `SELECT f.id, f.form_group_id, f.version, f.title, f.description, f.logo_url,
|
|
f.pdf_url, f.external_reference_required, f.external_reference_label,
|
|
f.is_active, f.created_at, f.updated_at,
|
|
(SELECT COUNT(*) FROM questions q WHERE q.form_id = f.id) AS question_count
|
|
FROM forms f`
|
|
if !includeInactive {
|
|
query += ` WHERE f.is_active = 1`
|
|
}
|
|
query += ` ORDER BY f.updated_at DESC`
|
|
|
|
rows, err := s.db.QueryContext(ctx, query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
summaries := make([]FormSummary, 0)
|
|
for rows.Next() {
|
|
var (
|
|
summary FormSummary
|
|
description sql.NullString
|
|
logoURL sql.NullString
|
|
pdfURL sql.NullString
|
|
externalRefLabel sql.NullString
|
|
createdAt string
|
|
updatedAt string
|
|
isActive int
|
|
externalRefRequired int
|
|
)
|
|
if err := rows.Scan(&summary.ID, &summary.FormGroupID, &summary.Version,
|
|
&summary.Title, &description, &logoURL, &pdfURL,
|
|
&externalRefRequired, &externalRefLabel, &isActive, &createdAt, &updatedAt,
|
|
&summary.QuestionCount); err != nil {
|
|
return nil, err
|
|
}
|
|
summary.Description = fromNullString(description)
|
|
summary.LogoURL = fromNullString(logoURL)
|
|
summary.PDFURL = fromNullString(pdfURL)
|
|
summary.ExternalReferenceLabel = fromNullString(externalRefLabel)
|
|
summary.ExternalReferenceRequired = externalRefRequired == 1
|
|
summary.IsActive = isActive == 1
|
|
summary.CreatedAt = parseTime(createdAt)
|
|
summary.UpdatedAt = parseTime(updatedAt)
|
|
summaries = append(summaries, summary)
|
|
}
|
|
return summaries, rows.Err()
|
|
}
|
|
|
|
func (s *sqliteStore) SetFormActive(ctx context.Context, id string, active bool) error {
|
|
res, err := s.db.ExecContext(ctx,
|
|
`UPDATE forms SET is_active = ?, updated_at = ? WHERE id = ?`,
|
|
boolToInt(active), formatTime(time.Now()), id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n, _ := res.RowsAffected(); n == 0 {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *sqliteStore) DeleteForm(ctx context.Context, id string) error {
|
|
res, err := s.db.ExecContext(ctx, `DELETE FROM forms WHERE id = ?`, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n, _ := res.RowsAffected(); n == 0 {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *sqliteStore) CreateSubmission(ctx context.Context, sub *Submission, accessCode string) error {
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
var signatureJSON sql.NullString
|
|
if sub.Signature != nil {
|
|
encoded, err := json.Marshal(sub.Signature)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
signatureJSON = sql.NullString{String: string(encoded), Valid: true}
|
|
}
|
|
var offlinePDF any
|
|
if len(sub.OfflinePDF) > 0 {
|
|
offlinePDF = sub.OfflinePDF
|
|
}
|
|
|
|
_, err = tx.ExecContext(ctx, `INSERT INTO submissions (
|
|
id, form_id, form_version, user_id, user_id_type, submitted_at,
|
|
ip_address, user_agent, locale, timezone, place, referer,
|
|
external_reference, signature_json, offline_pdf_name, offline_pdf
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
sub.ID, sub.FormID, sub.FormVersion, sub.UserID, sub.UserIDType,
|
|
formatTime(sub.SubmittedAt), nullString(sub.IPAddress),
|
|
nullString(sub.UserAgent), nullString(sub.Locale), nullString(sub.Timezone),
|
|
nullString(sub.Place), nullString(sub.Referer),
|
|
nullString(sub.ExternalReference), signatureJSON,
|
|
nullString(sub.OfflinePDFName), offlinePDF,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for i := range sub.Answers {
|
|
if _, err := tx.ExecContext(ctx, `INSERT INTO answers (
|
|
id, submission_id, question_id, question_key, value_json
|
|
) VALUES (?, ?, ?, ?, ?)`,
|
|
newID(), sub.ID, sub.Answers[i].QuestionID,
|
|
sub.Answers[i].QuestionKey, string(sub.Answers[i].Value),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if accessCode != "" {
|
|
res, err := tx.ExecContext(ctx, `UPDATE access_codes
|
|
SET used = 1, used_at = ?, submission_id = ?
|
|
WHERE form_id = ? AND code = ? AND used = 0`,
|
|
formatTime(time.Now()), sub.ID, sub.FormID, accessCode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n, _ := res.RowsAffected(); n == 0 {
|
|
var used int
|
|
err := tx.QueryRowContext(ctx,
|
|
`SELECT used FROM access_codes WHERE form_id = ? AND code = ?`,
|
|
sub.FormID, accessCode).Scan(&used)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return ErrAccessCode
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ErrCodeAlreadyIn
|
|
}
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (s *sqliteStore) GetSubmission(ctx context.Context, id string) (*Submission, error) {
|
|
var (
|
|
sub Submission
|
|
submittedAt string
|
|
ipAddress, userAgent, locale, timezone sql.NullString
|
|
place, referer sql.NullString
|
|
signatureJSON sql.NullString
|
|
offlinePDFName sql.NullString
|
|
externalReference sql.NullString
|
|
)
|
|
err := s.db.QueryRowContext(ctx, `SELECT id, form_id, form_version, user_id,
|
|
user_id_type, submitted_at, ip_address, user_agent, locale, timezone,
|
|
place, referer, external_reference, signature_json, offline_pdf_name
|
|
FROM submissions WHERE id = ?`, id).Scan(
|
|
&sub.ID, &sub.FormID, &sub.FormVersion, &sub.UserID, &sub.UserIDType,
|
|
&submittedAt, &ipAddress, &userAgent, &locale, &timezone, &place, &referer,
|
|
&externalReference, &signatureJSON, &offlinePDFName,
|
|
)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sub.SubmittedAt = parseTime(submittedAt)
|
|
sub.IPAddress = fromNullString(ipAddress)
|
|
sub.UserAgent = fromNullString(userAgent)
|
|
sub.Locale = fromNullString(locale)
|
|
sub.Timezone = fromNullString(timezone)
|
|
sub.Place = fromNullString(place)
|
|
sub.Referer = fromNullString(referer)
|
|
sub.ExternalReference = fromNullString(externalReference)
|
|
sub.OfflinePDFName = fromNullString(offlinePDFName)
|
|
if signatureJSON.Valid && signatureJSON.String != "" {
|
|
var signature Signature
|
|
if err := json.Unmarshal([]byte(signatureJSON.String), &signature); err != nil {
|
|
return nil, fmt.Errorf("decode signature for submission %s: %w", id, err)
|
|
}
|
|
sub.Signature = &signature
|
|
}
|
|
|
|
rows, err := s.db.QueryContext(ctx, `SELECT question_id, question_key, value_json
|
|
FROM answers WHERE submission_id = ?`, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var (
|
|
a Answer
|
|
valueJSON string
|
|
)
|
|
if err := rows.Scan(&a.QuestionID, &a.QuestionKey, &valueJSON); err != nil {
|
|
return nil, err
|
|
}
|
|
a.Value = json.RawMessage(valueJSON)
|
|
sub.Answers = append(sub.Answers, a)
|
|
}
|
|
return &sub, rows.Err()
|
|
}
|
|
|
|
func (s *sqliteStore) GetSubmissionOfflinePDF(ctx context.Context, id string) ([]byte, string, error) {
|
|
var (
|
|
data []byte
|
|
filename sql.NullString
|
|
)
|
|
err := s.db.QueryRowContext(ctx,
|
|
`SELECT offline_pdf, offline_pdf_name FROM submissions WHERE id = ?`, id).
|
|
Scan(&data, &filename)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, "", ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if len(data) == 0 {
|
|
return nil, "", ErrNotFound
|
|
}
|
|
name := "submission.pdf"
|
|
if filename.Valid && strings.TrimSpace(filename.String) != "" {
|
|
name = filepath.Base(filename.String)
|
|
}
|
|
return data, name, nil
|
|
}
|
|
|
|
func (s *sqliteStore) ListSubmissions(ctx context.Context, formID string) ([]SubmissionSummary, error) {
|
|
rows, err := s.db.QueryContext(ctx, `SELECT id, form_id, form_version,
|
|
user_id, user_id_type, external_reference, submitted_at FROM submissions
|
|
WHERE form_id = ? ORDER BY submitted_at DESC`, formID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
summaries := make([]SubmissionSummary, 0)
|
|
for rows.Next() {
|
|
var (
|
|
summary SubmissionSummary
|
|
submittedAt string
|
|
externalReference sql.NullString
|
|
)
|
|
if err := rows.Scan(&summary.ID, &summary.FormID, &summary.FormVersion,
|
|
&summary.UserID, &summary.UserIDType, &externalReference, &submittedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
summary.ExternalReference = fromNullString(externalReference)
|
|
summary.SubmittedAt = parseTime(submittedAt)
|
|
summaries = append(summaries, summary)
|
|
}
|
|
return summaries, rows.Err()
|
|
}
|
|
|
|
func (s *sqliteStore) CreateAccessCodes(ctx context.Context, codes []AccessCode) error {
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
for _, code := range codes {
|
|
_, err := tx.ExecContext(ctx, `INSERT INTO access_codes
|
|
(id, form_id, code, used, created_at) VALUES (?, ?, ?, ?, ?)`,
|
|
code.ID, code.FormID, code.Code, boolToInt(code.Used), formatTime(code.CreatedAt))
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "UNIQUE") {
|
|
return ErrConflict
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (s *sqliteStore) ListAccessCodes(ctx context.Context, formID string) ([]AccessCode, error) {
|
|
rows, err := s.db.QueryContext(ctx, `SELECT id, form_id, code, used,
|
|
used_at, created_at FROM access_codes WHERE form_id = ? ORDER BY created_at`, formID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
codes := make([]AccessCode, 0)
|
|
for rows.Next() {
|
|
var (
|
|
code AccessCode
|
|
usedAt sql.NullString
|
|
createdAt string
|
|
used int
|
|
)
|
|
if err := rows.Scan(&code.ID, &code.FormID, &code.Code, &used,
|
|
&usedAt, &createdAt); err != nil {
|
|
return nil, err
|
|
}
|
|
code.Used = used == 1
|
|
code.CreatedAt = parseTime(createdAt)
|
|
if usedAt.Valid {
|
|
t := parseTime(usedAt.String)
|
|
code.UsedAt = &t
|
|
}
|
|
codes = append(codes, code)
|
|
}
|
|
return codes, rows.Err()
|
|
}
|