mirror of
https://github.com/acaloiaro/frm
synced 2026-07-21 18:29:12 +00:00
support form submission receiver
This commit is contained in:
parent
d1d5804d11
commit
871ef0ef1b
10 changed files with 82 additions and 46 deletions
|
|
@ -3,6 +3,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -72,6 +73,10 @@ func main() {
|
||||||
WorkspaceIDUrlParam: workspaceParamName, // name of the chi URL parameter name
|
WorkspaceIDUrlParam: workspaceParamName, // name of the chi URL parameter name
|
||||||
BuilderMountPoint: fmt.Sprintf("/frm/{%s}/build", workspaceParamName),
|
BuilderMountPoint: fmt.Sprintf("/frm/{%s}/build", workspaceParamName),
|
||||||
CollectorMountPoint: fmt.Sprintf("/frm/{%s}/collect", workspaceParamName),
|
CollectorMountPoint: fmt.Sprintf("/frm/{%s}/collect", workspaceParamName),
|
||||||
|
Reciever: func(ctx context.Context, submission frm.FormSubmission) (err error) {
|
||||||
|
log.Println("GOT SUBMISSION!", submission)
|
||||||
|
return
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ CREATE TYPE submission_status AS ENUM (
|
||||||
-- submissions of forms/fields to the collector
|
-- submissions of forms/fields to the collector
|
||||||
CREATE TABLE IF NOT EXISTS form_submissions (
|
CREATE TABLE IF NOT EXISTS form_submissions (
|
||||||
id BIGINT PRIMARY KEY DEFAULT nextval('submission_ids'),
|
id BIGINT PRIMARY KEY DEFAULT nextval('submission_ids'),
|
||||||
form_id BIGINT REFERENCES forms(id) ON DELETE CASCADE,
|
form_id BIGINT REFERENCES forms(id) ON DELETE CASCADE NOT NULL ,
|
||||||
workspace_id TEXT NOT NULL,
|
workspace_id TEXT NOT NULL,
|
||||||
subject_id TEXT DEFAULT NULL,
|
subject_id TEXT DEFAULT NULL,
|
||||||
fields jsonb default '{}' NOT NULL,
|
fields jsonb default '{}' NOT NULL,
|
||||||
|
|
|
||||||
39
frm.go
39
frm.go
|
|
@ -19,24 +19,29 @@ var ErrNoInstanceAvailable = errors.New("no frm instance is available on the con
|
||||||
|
|
||||||
// Frm is the primary API into frm
|
// Frm is the primary API into frm
|
||||||
type Frm struct {
|
type Frm struct {
|
||||||
BuilderMountPoint string // relative URL path where frm mounts the builder to your app's router
|
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
|
CollectorMountPoint string // relative URL path where frm mounts the collector to your app's router
|
||||||
DBArgs internal.DBArgs // database arguments
|
DBArgs internal.DBArgs // database arguments
|
||||||
WorkspaceID string // ID of the workspace that frm acts on behalf of
|
Receiver FormSubmissionReceiver // function that processes incoming form submissions
|
||||||
WorkspaceIDUrlParam string // name of the URL parameter that provides your workspace ID
|
WorkspaceID string // ID of the workspace that frm acts on behalf of
|
||||||
|
WorkspaceIDUrlParam string // name of the URL parameter that provides your workspace ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Args are arguments passed to Frm
|
// Args are arguments passed to Frm
|
||||||
type Args struct {
|
type Args struct {
|
||||||
BuilderMountPoint string // path on the router to mount frm's builder
|
BuilderMountPoint string // path on the router to mount frm's builder
|
||||||
CollectorMountPoint string // path on the router to mount frm's collector
|
CollectorMountPoint string // path on the router to mount frm's collector
|
||||||
PostgresDisableSSL bool // disable ssl when connecting to postgres
|
Reciever FormSubmissionReceiver // function that processes incoming form submissions
|
||||||
PostgresURL string // postgres database URL
|
PostgresDisableSSL bool // disable ssl when connecting to postgres
|
||||||
PostgresSchema string // postgres schema where frm stores data
|
PostgresURL string // postgres database URL
|
||||||
WorkspaceID string // ID of the workspace for which frm is being initialized
|
PostgresSchema string // postgres schema where frm stores data
|
||||||
WorkspaceIDUrlParam string // named URL parameter that identifies the workspace, e.g. for route /{workspace_id}, the value would be "workspace_id"
|
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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FormSubmissionReceiver processes form submissions
|
||||||
|
type FormSubmissionReceiver = func(ctx context.Context, submission FormSubmission) (err error)
|
||||||
|
|
||||||
// FormStatus is the status of a Form
|
// FormStatus is the status of a Form
|
||||||
//
|
//
|
||||||
// - Published forms are available to be used
|
// - Published forms are available to be used
|
||||||
|
|
@ -54,17 +59,17 @@ func New(args Args) (f *Frm, err error) {
|
||||||
if args.WorkspaceID == "" && args.WorkspaceIDUrlParam == "" {
|
if args.WorkspaceID == "" && args.WorkspaceIDUrlParam == "" {
|
||||||
return nil, ErrCannotDetermineWorkspace
|
return nil, ErrCannotDetermineWorkspace
|
||||||
}
|
}
|
||||||
|
|
||||||
f = &Frm{
|
f = &Frm{
|
||||||
BuilderMountPoint: strings.TrimSuffix(args.BuilderMountPoint, "/"),
|
BuilderMountPoint: strings.TrimSuffix(args.BuilderMountPoint, "/"),
|
||||||
CollectorMountPoint: strings.TrimSuffix(args.CollectorMountPoint, "/"),
|
CollectorMountPoint: strings.TrimSuffix(args.CollectorMountPoint, "/"),
|
||||||
WorkspaceID: args.WorkspaceID,
|
|
||||||
WorkspaceIDUrlParam: args.WorkspaceIDUrlParam,
|
|
||||||
DBArgs: internal.DBArgs{
|
DBArgs: internal.DBArgs{
|
||||||
URL: args.PostgresURL,
|
URL: args.PostgresURL,
|
||||||
DisableSSL: args.PostgresDisableSSL,
|
DisableSSL: args.PostgresDisableSSL,
|
||||||
Schema: args.PostgresSchema,
|
Schema: args.PostgresSchema,
|
||||||
},
|
},
|
||||||
|
Receiver: args.Reciever,
|
||||||
|
WorkspaceID: args.WorkspaceID,
|
||||||
|
WorkspaceIDUrlParam: args.WorkspaceIDUrlParam,
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -116,7 +121,7 @@ func (f *Frm) ListForms(ctx context.Context, args ListFormsArgs) (forms []Form,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instance returns the frm instance from the request context
|
// Instance returns the frm instance from the request context (if available)
|
||||||
func Instance(ctx context.Context) (i *Frm, err error) {
|
func Instance(ctx context.Context) (i *Frm, err error) {
|
||||||
var ok bool
|
var ok bool
|
||||||
i, ok = ctx.Value(internal.FrmContextKey).(*Frm)
|
i, ok = ctx.Value(internal.FrmContextKey).(*Frm)
|
||||||
|
|
@ -137,7 +142,7 @@ func (f *Frm) CreateShortCode(ctx context.Context, args CreateShortCodeArgs) (sc
|
||||||
s, err = internal.Q(ctx, f.DBArgs).SaveShortCode(ctx, internal.SaveShortCodeParams{
|
s, err = internal.Q(ctx, f.DBArgs).SaveShortCode(ctx, internal.SaveShortCodeParams{
|
||||||
WorkspaceID: f.WorkspaceID,
|
WorkspaceID: f.WorkspaceID,
|
||||||
FormID: &args.FormID,
|
FormID: &args.FormID,
|
||||||
ShortCode: internal.GenCode(),
|
ShortCode: internal.GenShortCode(),
|
||||||
SubjectID: args.SubjectID,
|
SubjectID: args.SubjectID,
|
||||||
})
|
})
|
||||||
return (ShortCode)(s), err
|
return (ShortCode)(s), err
|
||||||
|
|
|
||||||
10
go.mod
10
go.mod
|
|
@ -18,8 +18,10 @@ require (
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/lib/pq v1.10.9 // indirect
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
go.uber.org/atomic v1.7.0 // indirect
|
go.uber.org/atomic v1.10.0 // indirect
|
||||||
golang.org/x/crypto v0.27.0 // indirect
|
golang.org/x/crypto v0.32.0 // indirect
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
golang.org/x/net v0.34.0 // indirect
|
||||||
golang.org/x/text v0.18.0 // indirect
|
golang.org/x/sync v0.10.0 // indirect
|
||||||
|
golang.org/x/text v0.21.0 // indirect
|
||||||
|
golang.org/x/tools v0.29.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
32
go.sum
32
go.sum
|
|
@ -77,22 +77,22 @@ go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2
|
||||||
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
|
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
|
||||||
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
|
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
|
||||||
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
|
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
|
||||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ func Collect(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
formID, err := formID(ctx, i)
|
formID, err := formID(ctx, i)
|
||||||
if err != nil {
|
if err != nil || formID == nil {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -157,9 +157,10 @@ func Collect(w http.ResponseWriter, r *http.Request) {
|
||||||
Value: fieldValue,
|
Value: fieldValue,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, err = internal.Q(ctx, i.DBArgs).SaveSubmission(ctx, internal.SaveSubmissionParams{
|
var s internal.FormSubmission
|
||||||
|
s, err = internal.Q(ctx, i.DBArgs).SaveSubmission(ctx, internal.SaveSubmissionParams{
|
||||||
// ID: submissionID, TODO save submission id
|
// ID: submissionID, TODO save submission id
|
||||||
FormID: formID,
|
FormID: *formID,
|
||||||
WorkspaceID: i.WorkspaceID,
|
WorkspaceID: i.WorkspaceID,
|
||||||
SubjectID: &shortCode.SubjectID,
|
SubjectID: &shortCode.SubjectID,
|
||||||
Status: internal.SubmissionStatusPartial,
|
Status: internal.SubmissionStatusPartial,
|
||||||
|
|
@ -170,7 +171,12 @@ func Collect(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if i.Receiver != nil {
|
||||||
|
err = i.Receiver(ctx, (frm.FormSubmission)(s))
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("[collector] unable to execute submission receiver", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
err = ui.ThankYou().Render(ctx, w)
|
err = ui.ThankYou().Render(ctx, w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("[collector] unable to render thank you page", "error", err)
|
slog.Error("[collector] unable to render thank you page", "error", err)
|
||||||
|
|
|
||||||
|
|
@ -239,8 +239,8 @@ func (f Form) JSON() string {
|
||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenCode generates new shortcodes
|
// GenShortCode generates new shortcodes
|
||||||
func GenCode() string {
|
func GenShortCode() string {
|
||||||
b := make([]rune, DefaultShortcodeLen)
|
b := make([]rune, DefaultShortcodeLen)
|
||||||
chsLen := len(shortcodeCharset)
|
chsLen := len(shortcodeCharset)
|
||||||
for i := range b {
|
for i := range b {
|
||||||
|
|
@ -248,3 +248,18 @@ func GenCode() string {
|
||||||
}
|
}
|
||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FormSubmissionMap converts a FormSubmission to its map[string]any representation (for queueing webhooks)
|
||||||
|
func FormSubmissionMap(s FormSubmission) (m map[string]any) {
|
||||||
|
m = map[string]any{}
|
||||||
|
m["id"] = s.ID
|
||||||
|
m["form_id"] = s.FormID
|
||||||
|
m["workspace_id"] = s.WorkspaceID
|
||||||
|
if s.SubjectID != nil {
|
||||||
|
m["subject_id"] = *s.SubjectID
|
||||||
|
}
|
||||||
|
m["fields"] = s.Fields
|
||||||
|
m["created_at"] = s.CreatedAt
|
||||||
|
m["updated_at"] = s.UpdatedAt
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ type Form struct {
|
||||||
// Respondants submit forms/fields to the collector as form_submissions
|
// Respondants submit forms/fields to the collector as form_submissions
|
||||||
type FormSubmission struct {
|
type FormSubmission struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
FormID *int64 `json:"form_id"`
|
FormID int64 `json:"form_id"`
|
||||||
WorkspaceID string `json:"workspace_id"`
|
WorkspaceID string `json:"workspace_id"`
|
||||||
// identifies the subject/respondent who filled out the form
|
// identifies the subject/respondent who filled out the form
|
||||||
SubjectID *string `json:"subject_id"`
|
SubjectID *string `json:"subject_id"`
|
||||||
|
|
|
||||||
|
|
@ -417,7 +417,7 @@ SET updated_at = timezone('utc', now()),
|
||||||
|
|
||||||
type SaveSubmissionParams struct {
|
type SaveSubmissionParams struct {
|
||||||
ID interface{} `json:"id"`
|
ID interface{} `json:"id"`
|
||||||
FormID *int64 `json:"form_id"`
|
FormID int64 `json:"form_id"`
|
||||||
WorkspaceID string `json:"workspace_id"`
|
WorkspaceID string `json:"workspace_id"`
|
||||||
SubjectID *string `json:"subject_id"`
|
SubjectID *string `json:"subject_id"`
|
||||||
Fields types.FormFieldValues `json:"fields"`
|
Fields types.FormFieldValues `json:"fields"`
|
||||||
|
|
|
||||||
3
types.go
3
types.go
|
|
@ -10,5 +10,8 @@ type Form internal.Form
|
||||||
// Forms is a slice of forms
|
// Forms is a slice of forms
|
||||||
type Forms []internal.Form
|
type Forms []internal.Form
|
||||||
|
|
||||||
|
// FormSubmission is a form submission
|
||||||
|
type FormSubmission internal.FormSubmission
|
||||||
|
|
||||||
// ShortCode is a short code
|
// ShortCode is a short code
|
||||||
type ShortCode internal.ShortCode
|
type ShortCode internal.ShortCode
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue