frm/frm.go

289 lines
9.1 KiB
Go
Raw Normal View History

2024-11-05 16:18:04 +00:00
package frm
import (
"context"
"errors"
2025-01-04 15:12:17 +00:00
"fmt"
"path/filepath"
2025-01-21 18:26:23 +00:00
"strings"
2025-02-20 21:51:59 +00:00
"time"
2024-11-05 16:18:04 +00:00
"github.com/acaloiaro/frm/internal"
)
const (
2025-02-11 21:54:17 +00:00
EventDraftCreated = "frmDraftCreated" // htmx event sent when new drafts are created
2025-02-20 21:51:59 +00:00
EventCloneCreated = "frmCloneCreated" // htmx event sent when new clones are created
2025-02-11 21:54:17 +00:00
DefaultCopyNameSuffix = "(COPY)" // the default suffix added to forms when they're copied
)
var ErrCannotDetermineWorkspace = errors.New("workspace cannot be determine without WorkspaceID or WorkspaceIDUrlParam")
2025-01-09 19:11:53 +00:00
var ErrNoInstanceAvailable = errors.New("no frm instance is available on the context")
// Frm is the primary API into frm
2024-11-05 16:18:04 +00:00
type Frm struct {
2025-01-23 14:31:12 +00:00
BuilderMountPoint string // relative URL path where frm mounts the builder to your app's router
CollectorMountPoint string // relative URL path where frm mounts the collector to your app's router
2025-02-06 21:24:38 +00:00
CollectorFooter string // footer shown at the bottom of the collector page
2025-02-20 21:51:59 +00:00
DraftMaxAge time.Duration // the duration that form drafts may remain in the draft stage before removal
2025-01-23 14:31:12 +00:00
DBArgs internal.DBArgs // database arguments
Receiver FormSubmissionReceiver // function that processes incoming form submissions
WorkspaceID string // ID of the workspace that frm acts on behalf of
WorkspaceIDUrlParam string // name of the URL parameter that provides your workspace ID
2024-11-05 16:18:04 +00:00
}
// Args are arguments passed to Frm
2024-11-05 16:18:04 +00:00
type Args struct {
2025-01-23 14:31:12 +00:00
BuilderMountPoint string // path on the router to mount frm's builder
CollectorMountPoint string // path on the router to mount frm's collector
2025-02-06 21:24:38 +00:00
CollectorFooter string // footer shown at the bottom of the collector page
2025-02-20 21:51:59 +00:00
DraftMaxAge time.Duration // the duration that form drafts may remain in the draft state before removal
2025-01-23 14:31:12 +00:00
PostgresDisableSSL bool // disable ssl when connecting to postgres
PostgresSchema string // postgres schema where frm stores data
2025-02-06 21:24:38 +00:00
PostgresURL string // postgres database URL
Reciever FormSubmissionReceiver // function that processes incoming form submissions
2025-01-23 14:31:12 +00:00
WorkspaceID string // ID of the workspace for which frm is being initialized
WorkspaceIDUrlParam string // named URL parameter that identifies the workspace, e.g. for route /{workspace_id}, the value would be "workspace_id"
2024-11-05 16:18:04 +00:00
}
2025-01-23 14:31:12 +00:00
// FormSubmissionReceiver processes form submissions
type FormSubmissionReceiver = func(ctx context.Context, submission FormSubmission) (err error)
2025-01-18 16:09:16 +00:00
// FormStatus is the status of a Form
//
// - Published forms are available to be used
//
// - Draft forms are in a draft state, yet to be published
2025-02-11 21:54:17 +00:00
//
// - Archived forms are not intended to be used
2025-01-18 16:09:16 +00:00
type FormStatus = internal.FormStatus
const FormStatusPublished = internal.FormStatusPublished
const FormStatusDraft = internal.FormStatusDraft
2025-02-11 21:54:17 +00:00
const FormStatusArchived = internal.FormStatusArchived
2024-11-05 16:18:04 +00:00
// New initializes a new frm instance
//
2025-01-04 15:12:17 +00:00
// If the frm database has not yet been initialized, Init() should be called before mounting to a router
func New(args Args) (f *Frm, err error) {
2025-01-14 16:46:22 +00:00
if args.WorkspaceID == "" && args.WorkspaceIDUrlParam == "" {
return nil, ErrCannotDetermineWorkspace
}
f = &Frm{
2025-01-21 18:26:23 +00:00
BuilderMountPoint: strings.TrimSuffix(args.BuilderMountPoint, "/"),
CollectorMountPoint: strings.TrimSuffix(args.CollectorMountPoint, "/"),
2025-02-06 21:24:38 +00:00
CollectorFooter: args.CollectorFooter,
2025-02-20 21:51:59 +00:00
DraftMaxAge: args.DraftMaxAge,
2025-01-09 19:11:53 +00:00
DBArgs: internal.DBArgs{
URL: args.PostgresURL,
DisableSSL: args.PostgresDisableSSL,
Schema: args.PostgresSchema,
},
2025-01-23 14:31:12 +00:00
Receiver: args.Reciever,
WorkspaceID: args.WorkspaceID,
WorkspaceIDUrlParam: args.WorkspaceIDUrlParam,
2024-11-05 16:18:04 +00:00
}
return
2024-11-05 16:18:04 +00:00
}
// Init initializes the frm database if it hasn't been initialized
2024-11-05 16:18:04 +00:00
func (f *Frm) Init(ctx context.Context) (err error) {
2025-01-09 19:11:53 +00:00
err = internal.InitializeDB(ctx, f.DBArgs)
2025-02-20 21:51:59 +00:00
if err != nil {
return
}
go func() {
err = internal.DraftMonitor(ctx, f.DBArgs, f.DraftMaxAge)
if err != nil {
return
}
}()
2024-11-05 16:18:04 +00:00
return
}
// GetForm retrieves forms by ID
2024-11-05 16:18:04 +00:00
func (f *Frm) GetForm(ctx context.Context, id int64) (form Form, err error) {
var frm internal.Form
2025-01-09 19:11:53 +00:00
frm, err = internal.Q(ctx, f.DBArgs).GetForm(ctx, internal.GetFormParams{
2024-12-17 17:25:33 +00:00
WorkspaceID: f.WorkspaceID,
ID: id,
})
2024-11-05 16:18:04 +00:00
if err != nil {
return
}
form = (Form)(frm)
return
}
2025-02-11 21:54:17 +00:00
// CopyFormArgs are passed to frm.CopyForm()
type CopyFormArgs struct {
ForgetParentForm bool // forget the parent form from which the copied form is copied
ID int64 // the id of the form to copy
NameSuffix string // suffix to be added to the original form's name, e.g. "(COPY)"
}
// CopyForm copies existing Forms
//
// Returns the copied form
func (f *Frm) CopyForm(ctx context.Context, args CopyFormArgs) (form Form, err error) {
var of internal.Form
of, err = internal.Q(ctx, f.DBArgs).GetForm(ctx, internal.GetFormParams{
WorkspaceID: f.WorkspaceID,
ID: args.ID,
})
if err != nil {
return
}
copiedFormName := of.Name
if args.NameSuffix != "" {
copiedFormName = fmt.Sprintf("%s %s", of.Name, args.NameSuffix)
}
p := &internal.SaveFormParams{
WorkspaceID: of.WorkspaceID,
FormID: &of.ID,
Name: copiedFormName,
Fields: of.Fields,
2025-02-20 21:51:59 +00:00
Status: FormStatusDraft,
2025-02-11 21:54:17 +00:00
}
if args.ForgetParentForm {
p.FormID = nil
}
nf, err := internal.Q(ctx, f.DBArgs).SaveForm(ctx, *p)
if err != nil {
return
}
form = (Form)(nf)
return
}
2025-01-23 19:28:24 +00:00
// GetFormSubmission retrieves form submissions by ID
func (f *Frm) GetFormSubmission(ctx context.Context, submissionID int64) (sub FormSubmission, err error) {
var s internal.FormSubmission
s, err = internal.Q(ctx, f.DBArgs).GetFormSubmission(ctx, internal.GetFormSubmissionParams{
WorkspaceID: f.WorkspaceID,
SubmissionID: submissionID,
})
if err != nil {
return
}
sub = (FormSubmission)(s)
return
}
type ListFormsArgs struct {
Statuses []FormStatus
}
// ListForms lists all forms for the current workspace
func (f *Frm) ListForms(ctx context.Context, args ListFormsArgs) (forms []Form, err error) {
var fs Forms
statuses := []internal.FormStatus{}
for _, s := range args.Statuses {
statuses = append(statuses, (internal.FormStatus)(s))
}
2025-01-09 19:11:53 +00:00
fs, err = internal.Q(ctx, f.DBArgs).ListForms(ctx, internal.ListFormsParams{
WorkspaceID: f.WorkspaceID,
Statuses: statuses,
})
if err != nil {
return
}
for _, f := range fs {
forms = append(forms, (Form)(f))
}
return
}
2025-01-04 15:12:17 +00:00
2025-01-23 14:31:12 +00:00
// Instance returns the frm instance from the request context (if available)
2025-01-09 19:11:53 +00:00
func Instance(ctx context.Context) (i *Frm, err error) {
var ok bool
i, ok = ctx.Value(internal.FrmContextKey).(*Frm)
if !ok {
return nil, ErrNoInstanceAvailable
}
return
}
2025-01-18 16:09:16 +00:00
type CreateShortCodeArgs struct {
FormID int64
SubjectID string
}
// CreateShortCode creates short code for a given form and subject
func (f *Frm) CreateShortCode(ctx context.Context, args CreateShortCodeArgs) (sc ShortCode, err error) {
var s internal.ShortCode
s, err = internal.Q(ctx, f.DBArgs).SaveShortCode(ctx, internal.SaveShortCodeParams{
WorkspaceID: f.WorkspaceID,
FormID: &args.FormID,
2025-01-23 14:31:12 +00:00
ShortCode: internal.GenShortCode(),
2025-01-18 16:09:16 +00:00
SubjectID: args.SubjectID,
})
return (ShortCode)(s), err
}
2025-01-28 15:43:28 +00:00
// BuilderPath returns paths to frm builder endpoints
//
// It uses the builer's mount point on the router to generate builder paths
func BuilderPath(ctx context.Context, path string) string {
base, ok := ctx.Value(internal.BuilderMountPointContextKey).(string)
if !ok {
return "/"
}
return fmt.Sprintf("%s/%s", base, path)
}
// CollectorPath returns paths to frm collector endpoints
2025-01-14 15:51:29 +00:00
//
// It uses the collector's mount point on the router to generate collector paths
func CollectorPath(ctx context.Context, path string) string {
base, ok := ctx.Value(internal.CollectorMountPointContextKey).(string)
2025-01-09 19:11:53 +00:00
if !ok {
return "/"
}
2025-01-14 15:51:29 +00:00
urlPath := filepath.Clean(fmt.Sprintf("%s/%s", base, path))
return urlPath
2025-01-09 19:11:53 +00:00
}
// BuilderPathForm returns the builder URL path for the provided form ID
func BuilderPathForm(ctx context.Context, formID int64) string {
base, ok := ctx.Value(internal.BuilderMountPointContextKey).(string)
if !ok {
return "/"
}
return fmt.Sprintf("%s/%d", base, formID)
}
2025-01-14 16:46:22 +00:00
// BuilderPathFormField returns the builder URL path for the provided form ID and field ID
func BuilderPathFormField(ctx context.Context, formID int64, fieldID string, args ...string) string {
base, ok := ctx.Value(internal.BuilderMountPointContextKey).(string)
if !ok {
return "/"
}
additionalPath := ""
if len(args) > 0 {
additionalPath = args[0]
}
path := filepath.Clean(fmt.Sprintf("%s/%d/fields/%s/%s", base, formID, fieldID, additionalPath))
return path
}
2025-02-13 22:15:36 +00:00
// CollectorPathShortCode returns the collector's URL path for the provided shortcode
func CollectorPathShortCode(ctx context.Context, shortCode string) string {
2025-01-09 19:11:53 +00:00
base, ok := ctx.Value(internal.CollectorMountPointContextKey).(string)
if !ok {
return "/"
}
2025-01-10 18:52:12 +00:00
base = filepath.Clean(base)
2025-02-13 22:15:36 +00:00
return fmt.Sprintf("%s/s/%s", base, shortCode)
2025-01-09 19:11:53 +00:00
}