2026-10-23 12:24:54 +00:00
|
|
|
// Package database provides the persistence layer for kereru-forms.
|
|
|
|
|
//
|
|
|
|
|
// The Store interface deliberately hides SQL details so additional providers
|
|
|
|
|
// (PostgreSQL, MariaDB) can be added later without changing the API layer.
|
|
|
|
|
package database
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Common persistence errors.
|
|
|
|
|
var (
|
|
|
|
|
ErrNotFound = errors.New("not found")
|
|
|
|
|
ErrConflict = errors.New("conflict")
|
|
|
|
|
ErrAccessCode = errors.New("access code is not valid")
|
|
|
|
|
ErrCodeAlreadyIn = errors.New("access code has already been used")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// QuestionOption is a selectable value for a select question.
|
|
|
|
|
type QuestionOption struct {
|
|
|
|
|
Value string `json:"value"`
|
|
|
|
|
Label string `json:"label"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-25 10:53:17 +00:00
|
|
|
// Section groups questions within a form.
|
|
|
|
|
type Section struct {
|
|
|
|
|
ID string
|
|
|
|
|
FormID string
|
|
|
|
|
Position int
|
|
|
|
|
Identifier string
|
|
|
|
|
Title *string
|
|
|
|
|
Description *string
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-23 12:24:54 +00:00
|
|
|
// Question is a single question belonging to a form version.
|
|
|
|
|
type Question struct {
|
|
|
|
|
ID string
|
|
|
|
|
FormID string
|
2026-10-25 10:53:17 +00:00
|
|
|
SectionID *string
|
2026-10-23 12:24:54 +00:00
|
|
|
Position int
|
|
|
|
|
Key string
|
|
|
|
|
Label string
|
|
|
|
|
Description *string
|
|
|
|
|
Type string
|
|
|
|
|
Required bool
|
|
|
|
|
Options []QuestionOption
|
|
|
|
|
MinValue *float64
|
|
|
|
|
MaxValue *float64
|
|
|
|
|
Placeholder *string
|
|
|
|
|
Multiline bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Form is a concrete version of a logical form.
|
|
|
|
|
type Form struct {
|
|
|
|
|
ID string
|
|
|
|
|
FormGroupID string
|
|
|
|
|
Version int
|
|
|
|
|
Title string
|
|
|
|
|
Description *string
|
|
|
|
|
Introduction *string
|
|
|
|
|
IsActive bool
|
|
|
|
|
RequiresAccessCode bool
|
|
|
|
|
SignatureRequired bool
|
|
|
|
|
ExternalReferenceRequired bool
|
|
|
|
|
ExternalReferenceLabel *string
|
|
|
|
|
LogoURL *string
|
|
|
|
|
PDFURL *string
|
|
|
|
|
CompanyName *string
|
|
|
|
|
ContactEmail *string
|
|
|
|
|
ContactPhone *string
|
|
|
|
|
ContactURL *string
|
|
|
|
|
CreatedAt time.Time
|
|
|
|
|
UpdatedAt time.Time
|
|
|
|
|
Questions []Question
|
2026-10-25 10:53:17 +00:00
|
|
|
Sections []Section
|
2026-10-23 12:24:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FormSummary is a lightweight form listing entry.
|
|
|
|
|
type FormSummary struct {
|
|
|
|
|
ID string
|
|
|
|
|
FormGroupID string
|
|
|
|
|
Version int
|
|
|
|
|
Title string
|
|
|
|
|
Description *string
|
|
|
|
|
LogoURL *string
|
|
|
|
|
PDFURL *string
|
|
|
|
|
ExternalReferenceRequired bool
|
|
|
|
|
ExternalReferenceLabel *string
|
|
|
|
|
IsActive bool
|
|
|
|
|
QuestionCount int
|
|
|
|
|
CreatedAt time.Time
|
|
|
|
|
UpdatedAt time.Time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Answer is a captured answer inside a submission.
|
|
|
|
|
type Answer struct {
|
|
|
|
|
QuestionID string
|
|
|
|
|
QuestionKey string
|
|
|
|
|
Value json.RawMessage
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Signature is a captured digital signature.
|
|
|
|
|
type Signature struct {
|
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
DataURL string `json:"dataUrl"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Title *string `json:"title,omitempty"`
|
|
|
|
|
SignedAt time.Time `json:"signedAt"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Submission is a completed form response.
|
|
|
|
|
type Submission struct {
|
|
|
|
|
ID string
|
|
|
|
|
FormID string
|
|
|
|
|
FormVersion int
|
|
|
|
|
UserID string
|
|
|
|
|
UserIDType string
|
|
|
|
|
SubmittedAt time.Time
|
|
|
|
|
IPAddress *string
|
|
|
|
|
UserAgent *string
|
|
|
|
|
Locale *string
|
|
|
|
|
Timezone *string
|
|
|
|
|
Place *string
|
|
|
|
|
Referer *string
|
|
|
|
|
ExternalReference *string
|
|
|
|
|
Signature *Signature
|
|
|
|
|
OfflinePDFName *string
|
|
|
|
|
OfflinePDF []byte
|
|
|
|
|
Answers []Answer
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SubmissionSummary is a lightweight submission listing entry.
|
|
|
|
|
type SubmissionSummary struct {
|
|
|
|
|
ID string
|
|
|
|
|
FormID string
|
|
|
|
|
FormVersion int
|
|
|
|
|
UserID string
|
|
|
|
|
UserIDType string
|
|
|
|
|
ExternalReference *string
|
|
|
|
|
SubmittedAt time.Time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AccessCode is a single-use code granting access to a form.
|
|
|
|
|
type AccessCode struct {
|
|
|
|
|
ID string
|
|
|
|
|
FormID string
|
|
|
|
|
Code string
|
|
|
|
|
Used bool
|
|
|
|
|
UsedAt *time.Time
|
|
|
|
|
CreatedAt time.Time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Store is the persistence contract used by the API layer.
|
|
|
|
|
type Store interface {
|
|
|
|
|
Close() error
|
|
|
|
|
|
|
|
|
|
CreateForm(ctx context.Context, form *Form) error
|
|
|
|
|
UpdateForm(ctx context.Context, form *Form) error
|
|
|
|
|
GetForm(ctx context.Context, id string) (*Form, error)
|
|
|
|
|
ListForms(ctx context.Context, includeInactive bool) ([]FormSummary, error)
|
|
|
|
|
SetFormActive(ctx context.Context, id string, active bool) error
|
|
|
|
|
DeleteForm(ctx context.Context, id string) error
|
|
|
|
|
|
|
|
|
|
CreateSubmission(ctx context.Context, submission *Submission, accessCode string) error
|
|
|
|
|
GetSubmission(ctx context.Context, id string) (*Submission, error)
|
|
|
|
|
GetSubmissionOfflinePDF(ctx context.Context, id string) ([]byte, string, error)
|
|
|
|
|
ListSubmissions(ctx context.Context, formID string) ([]SubmissionSummary, error)
|
|
|
|
|
|
|
|
|
|
CreateAccessCodes(ctx context.Context, codes []AccessCode) error
|
|
|
|
|
ListAccessCodes(ctx context.Context, formID string) ([]AccessCode, error)
|
|
|
|
|
|
|
|
|
|
Ping(ctx context.Context) error
|
|
|
|
|
}
|