diff --git a/db/bobgen.yaml b/db/bobgen.yaml index 14240cab..007bb1d0 100644 --- a/db/bobgen.yaml +++ b/db/bobgen.yaml @@ -22,6 +22,7 @@ psql: - "comms" - "fieldseeker" - "fileupload" + - "lob" - "public" - "publicreport" - "stadia" diff --git a/db/dberrors/lob.event.bob.go b/db/dberrors/lob.event.bob.go new file mode 100644 index 00000000..24bc1626 --- /dev/null +++ b/db/dberrors/lob.event.bob.go @@ -0,0 +1,17 @@ +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package dberrors + +var LobEventErrors = &lobEventErrors{ + ErrUniqueEventPkey: &UniqueConstraintError{ + schema: "lob", + table: "event", + columns: []string{"id"}, + s: "event_pkey", + }, +} + +type lobEventErrors struct { + ErrUniqueEventPkey *UniqueConstraintError +} diff --git a/db/dbinfo/lob.event.bob.go b/db/dbinfo/lob.event.bob.go new file mode 100644 index 00000000..91a6f8ee --- /dev/null +++ b/db/dbinfo/lob.event.bob.go @@ -0,0 +1,122 @@ +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package dbinfo + +import "github.com/aarondl/opt/null" + +var LobEvents = Table[ + lobEventColumns, + lobEventIndexes, + lobEventForeignKeys, + lobEventUniques, + lobEventChecks, +]{ + Schema: "lob", + Name: "event", + Columns: lobEventColumns{ + Created: column{ + Name: "created", + DBType: "timestamp without time zone", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + Body: column{ + Name: "body", + DBType: "jsonb", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + ID: column{ + Name: "id", + DBType: "text", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + Type: column{ + Name: "type_", + DBType: "text", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + }, + Indexes: lobEventIndexes{ + EventPkey: index{ + Type: "btree", + Name: "event_pkey", + Columns: []indexColumn{ + { + Name: "id", + Desc: null.FromCond(false, true), + IsExpression: false, + }, + }, + Unique: true, + Comment: "", + NullsFirst: []bool{false}, + NullsDistinct: false, + Where: "", + Include: []string{}, + }, + }, + PrimaryKey: &constraint{ + Name: "event_pkey", + Columns: []string{"id"}, + Comment: "", + }, + + Comment: "", +} + +type lobEventColumns struct { + Created column + Body column + ID column + Type column +} + +func (c lobEventColumns) AsSlice() []column { + return []column{ + c.Created, c.Body, c.ID, c.Type, + } +} + +type lobEventIndexes struct { + EventPkey index +} + +func (i lobEventIndexes) AsSlice() []index { + return []index{ + i.EventPkey, + } +} + +type lobEventForeignKeys struct{} + +func (f lobEventForeignKeys) AsSlice() []foreignKey { + return []foreignKey{} +} + +type lobEventUniques struct{} + +func (u lobEventUniques) AsSlice() []constraint { + return []constraint{} +} + +type lobEventChecks struct{} + +func (c lobEventChecks) AsSlice() []check { + return []check{} +} diff --git a/db/migrations/00144_lob_events.sql b/db/migrations/00144_lob_events.sql new file mode 100644 index 00000000..7ae162a8 --- /dev/null +++ b/db/migrations/00144_lob_events.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE SCHEMA lob; +CREATE TABLE lob.event ( + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + body JSONB NOT NULL, + id TEXT NOT NULL, + type_ TEXT NOT NULL, + PRIMARY KEY(id) +); +-- +goose Down +DROP TABLE lob.event; +DROP SCHEMA lob; diff --git a/db/models/bob_where.bob.go b/db/models/bob_where.bob.go index cf0a5e2a..8ce55cbb 100644 --- a/db/models/bob_where.bob.go +++ b/db/models/bob_where.bob.go @@ -80,6 +80,7 @@ func Where[Q psql.Filterable]() struct { H3Aggregations h3AggregationWhere[Q] Jobs jobWhere[Q] Leads leadWhere[Q] + LobEvents lobEventWhere[Q] LogImpersonations logImpersonationWhere[Q] NoteAudios noteAudioWhere[Q] NoteAudioBreadcrumbs noteAudioBreadcrumbWhere[Q] @@ -191,6 +192,7 @@ func Where[Q psql.Filterable]() struct { H3Aggregations h3AggregationWhere[Q] Jobs jobWhere[Q] Leads leadWhere[Q] + LobEvents lobEventWhere[Q] LogImpersonations logImpersonationWhere[Q] NoteAudios noteAudioWhere[Q] NoteAudioBreadcrumbs noteAudioBreadcrumbWhere[Q] @@ -301,6 +303,7 @@ func Where[Q psql.Filterable]() struct { H3Aggregations: buildH3AggregationWhere[Q](H3Aggregations.Columns), Jobs: buildJobWhere[Q](Jobs.Columns), Leads: buildLeadWhere[Q](Leads.Columns), + LobEvents: buildLobEventWhere[Q](LobEvents.Columns), LogImpersonations: buildLogImpersonationWhere[Q](LogImpersonations.Columns), NoteAudios: buildNoteAudioWhere[Q](NoteAudios.Columns), NoteAudioBreadcrumbs: buildNoteAudioBreadcrumbWhere[Q](NoteAudioBreadcrumbs.Columns), diff --git a/db/models/lob.event.bob.go b/db/models/lob.event.bob.go new file mode 100644 index 00000000..20882a10 --- /dev/null +++ b/db/models/lob.event.bob.go @@ -0,0 +1,426 @@ +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package models + +import ( + "context" + "encoding/json" + "io" + "time" + + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/types" + "github.com/aarondl/opt/omit" +) + +// LobEvent is an object representing the database table. +type LobEvent struct { + Created time.Time `db:"created" ` + Body types.JSON[json.RawMessage] `db:"body" ` + ID string `db:"id,pk" ` + Type string `db:"type_" ` +} + +// LobEventSlice is an alias for a slice of pointers to LobEvent. +// This should almost always be used instead of []*LobEvent. +type LobEventSlice []*LobEvent + +// LobEvents contains methods to work with the event table +var LobEvents = psql.NewTablex[*LobEvent, LobEventSlice, *LobEventSetter]("lob", "event", buildLobEventColumns("lob.event")) + +// LobEventsQuery is a query on the event table +type LobEventsQuery = *psql.ViewQuery[*LobEvent, LobEventSlice] + +func buildLobEventColumns(alias string) lobEventColumns { + return lobEventColumns{ + ColumnsExpr: expr.NewColumnsExpr( + "created", "body", "id", "type_", + ).WithParent("lob.event"), + tableAlias: alias, + Created: psql.Quote(alias, "created"), + Body: psql.Quote(alias, "body"), + ID: psql.Quote(alias, "id"), + Type: psql.Quote(alias, "type_"), + } +} + +type lobEventColumns struct { + expr.ColumnsExpr + tableAlias string + Created psql.Expression + Body psql.Expression + ID psql.Expression + Type psql.Expression +} + +func (c lobEventColumns) Alias() string { + return c.tableAlias +} + +func (lobEventColumns) AliasedAs(alias string) lobEventColumns { + return buildLobEventColumns(alias) +} + +// LobEventSetter is used for insert/upsert/update operations +// All values are optional, and do not have to be set +// Generated columns are not included +type LobEventSetter struct { + Created omit.Val[time.Time] `db:"created" ` + Body omit.Val[types.JSON[json.RawMessage]] `db:"body" ` + ID omit.Val[string] `db:"id,pk" ` + Type omit.Val[string] `db:"type_" ` +} + +func (s LobEventSetter) SetColumns() []string { + vals := make([]string, 0, 4) + if s.Created.IsValue() { + vals = append(vals, "created") + } + if s.Body.IsValue() { + vals = append(vals, "body") + } + if s.ID.IsValue() { + vals = append(vals, "id") + } + if s.Type.IsValue() { + vals = append(vals, "type_") + } + return vals +} + +func (s LobEventSetter) Overwrite(t *LobEvent) { + if s.Created.IsValue() { + t.Created = s.Created.MustGet() + } + if s.Body.IsValue() { + t.Body = s.Body.MustGet() + } + if s.ID.IsValue() { + t.ID = s.ID.MustGet() + } + if s.Type.IsValue() { + t.Type = s.Type.MustGet() + } +} + +func (s *LobEventSetter) Apply(q *dialect.InsertQuery) { + q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) { + return LobEvents.BeforeInsertHooks.RunHooks(ctx, exec, s) + }) + + q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { + vals := make([]bob.Expression, 4) + if s.Created.IsValue() { + vals[0] = psql.Arg(s.Created.MustGet()) + } else { + vals[0] = psql.Raw("DEFAULT") + } + + if s.Body.IsValue() { + vals[1] = psql.Arg(s.Body.MustGet()) + } else { + vals[1] = psql.Raw("DEFAULT") + } + + if s.ID.IsValue() { + vals[2] = psql.Arg(s.ID.MustGet()) + } else { + vals[2] = psql.Raw("DEFAULT") + } + + if s.Type.IsValue() { + vals[3] = psql.Arg(s.Type.MustGet()) + } else { + vals[3] = psql.Raw("DEFAULT") + } + + return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "") + })) +} + +func (s LobEventSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] { + return um.Set(s.Expressions()...) +} + +func (s LobEventSetter) Expressions(prefix ...string) []bob.Expression { + exprs := make([]bob.Expression, 0, 4) + + if s.Created.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "created")...), + psql.Arg(s.Created), + }}) + } + + if s.Body.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "body")...), + psql.Arg(s.Body), + }}) + } + + if s.ID.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "id")...), + psql.Arg(s.ID), + }}) + } + + if s.Type.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "type_")...), + psql.Arg(s.Type), + }}) + } + + return exprs +} + +// FindLobEvent retrieves a single record by primary key +// If cols is empty Find will return all columns. +func FindLobEvent(ctx context.Context, exec bob.Executor, IDPK string, cols ...string) (*LobEvent, error) { + if len(cols) == 0 { + return LobEvents.Query( + sm.Where(LobEvents.Columns.ID.EQ(psql.Arg(IDPK))), + ).One(ctx, exec) + } + + return LobEvents.Query( + sm.Where(LobEvents.Columns.ID.EQ(psql.Arg(IDPK))), + sm.Columns(LobEvents.Columns.Only(cols...)), + ).One(ctx, exec) +} + +// LobEventExists checks the presence of a single record by primary key +func LobEventExists(ctx context.Context, exec bob.Executor, IDPK string) (bool, error) { + return LobEvents.Query( + sm.Where(LobEvents.Columns.ID.EQ(psql.Arg(IDPK))), + ).Exists(ctx, exec) +} + +// AfterQueryHook is called after LobEvent is retrieved from the database +func (o *LobEvent) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error { + var err error + + switch queryType { + case bob.QueryTypeSelect: + ctx, err = LobEvents.AfterSelectHooks.RunHooks(ctx, exec, LobEventSlice{o}) + case bob.QueryTypeInsert: + ctx, err = LobEvents.AfterInsertHooks.RunHooks(ctx, exec, LobEventSlice{o}) + case bob.QueryTypeUpdate: + ctx, err = LobEvents.AfterUpdateHooks.RunHooks(ctx, exec, LobEventSlice{o}) + case bob.QueryTypeDelete: + ctx, err = LobEvents.AfterDeleteHooks.RunHooks(ctx, exec, LobEventSlice{o}) + } + + return err +} + +// primaryKeyVals returns the primary key values of the LobEvent +func (o *LobEvent) primaryKeyVals() bob.Expression { + return psql.Arg(o.ID) +} + +func (o *LobEvent) pkEQ() dialect.Expression { + return psql.Quote("lob.event", "id").EQ(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { + return o.primaryKeyVals().WriteSQL(ctx, w, d, start) + })) +} + +// Update uses an executor to update the LobEvent +func (o *LobEvent) Update(ctx context.Context, exec bob.Executor, s *LobEventSetter) error { + v, err := LobEvents.Update(s.UpdateMod(), um.Where(o.pkEQ())).One(ctx, exec) + if err != nil { + return err + } + + *o = *v + + return nil +} + +// Delete deletes a single LobEvent record with an executor +func (o *LobEvent) Delete(ctx context.Context, exec bob.Executor) error { + _, err := LobEvents.Delete(dm.Where(o.pkEQ())).Exec(ctx, exec) + return err +} + +// Reload refreshes the LobEvent using the executor +func (o *LobEvent) Reload(ctx context.Context, exec bob.Executor) error { + o2, err := LobEvents.Query( + sm.Where(LobEvents.Columns.ID.EQ(psql.Arg(o.ID))), + ).One(ctx, exec) + if err != nil { + return err + } + + *o = *o2 + + return nil +} + +// AfterQueryHook is called after LobEventSlice is retrieved from the database +func (o LobEventSlice) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error { + var err error + + switch queryType { + case bob.QueryTypeSelect: + ctx, err = LobEvents.AfterSelectHooks.RunHooks(ctx, exec, o) + case bob.QueryTypeInsert: + ctx, err = LobEvents.AfterInsertHooks.RunHooks(ctx, exec, o) + case bob.QueryTypeUpdate: + ctx, err = LobEvents.AfterUpdateHooks.RunHooks(ctx, exec, o) + case bob.QueryTypeDelete: + ctx, err = LobEvents.AfterDeleteHooks.RunHooks(ctx, exec, o) + } + + return err +} + +func (o LobEventSlice) pkIN() dialect.Expression { + if len(o) == 0 { + return psql.Raw("NULL") + } + + return psql.Quote("lob.event", "id").In(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { + pkPairs := make([]bob.Expression, len(o)) + for i, row := range o { + pkPairs[i] = row.primaryKeyVals() + } + return bob.ExpressSlice(ctx, w, d, start, pkPairs, "", ", ", "") + })) +} + +// copyMatchingRows finds models in the given slice that have the same primary key +// then it first copies the existing relationships from the old model to the new model +// and then replaces the old model in the slice with the new model +func (o LobEventSlice) copyMatchingRows(from ...*LobEvent) { + for i, old := range o { + for _, new := range from { + if new.ID != old.ID { + continue + } + + o[i] = new + break + } + } +} + +// UpdateMod modifies an update query with "WHERE primary_key IN (o...)" +func (o LobEventSlice) UpdateMod() bob.Mod[*dialect.UpdateQuery] { + return bob.ModFunc[*dialect.UpdateQuery](func(q *dialect.UpdateQuery) { + q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) { + return LobEvents.BeforeUpdateHooks.RunHooks(ctx, exec, o) + }) + + q.AppendLoader(bob.LoaderFunc(func(ctx context.Context, exec bob.Executor, retrieved any) error { + var err error + switch retrieved := retrieved.(type) { + case *LobEvent: + o.copyMatchingRows(retrieved) + case []*LobEvent: + o.copyMatchingRows(retrieved...) + case LobEventSlice: + o.copyMatchingRows(retrieved...) + default: + // If the retrieved value is not a LobEvent or a slice of LobEvent + // then run the AfterUpdateHooks on the slice + _, err = LobEvents.AfterUpdateHooks.RunHooks(ctx, exec, o) + } + + return err + })) + + q.AppendWhere(o.pkIN()) + }) +} + +// DeleteMod modifies an delete query with "WHERE primary_key IN (o...)" +func (o LobEventSlice) DeleteMod() bob.Mod[*dialect.DeleteQuery] { + return bob.ModFunc[*dialect.DeleteQuery](func(q *dialect.DeleteQuery) { + q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) { + return LobEvents.BeforeDeleteHooks.RunHooks(ctx, exec, o) + }) + + q.AppendLoader(bob.LoaderFunc(func(ctx context.Context, exec bob.Executor, retrieved any) error { + var err error + switch retrieved := retrieved.(type) { + case *LobEvent: + o.copyMatchingRows(retrieved) + case []*LobEvent: + o.copyMatchingRows(retrieved...) + case LobEventSlice: + o.copyMatchingRows(retrieved...) + default: + // If the retrieved value is not a LobEvent or a slice of LobEvent + // then run the AfterDeleteHooks on the slice + _, err = LobEvents.AfterDeleteHooks.RunHooks(ctx, exec, o) + } + + return err + })) + + q.AppendWhere(o.pkIN()) + }) +} + +func (o LobEventSlice) UpdateAll(ctx context.Context, exec bob.Executor, vals LobEventSetter) error { + if len(o) == 0 { + return nil + } + + _, err := LobEvents.Update(vals.UpdateMod(), o.UpdateMod()).All(ctx, exec) + return err +} + +func (o LobEventSlice) DeleteAll(ctx context.Context, exec bob.Executor) error { + if len(o) == 0 { + return nil + } + + _, err := LobEvents.Delete(o.DeleteMod()).Exec(ctx, exec) + return err +} + +func (o LobEventSlice) ReloadAll(ctx context.Context, exec bob.Executor) error { + if len(o) == 0 { + return nil + } + + o2, err := LobEvents.Query(sm.Where(o.pkIN())).All(ctx, exec) + if err != nil { + return err + } + + o.copyMatchingRows(o2...) + + return nil +} + +type lobEventWhere[Q psql.Filterable] struct { + Created psql.WhereMod[Q, time.Time] + Body psql.WhereMod[Q, types.JSON[json.RawMessage]] + ID psql.WhereMod[Q, string] + Type psql.WhereMod[Q, string] +} + +func (lobEventWhere[Q]) AliasedAs(alias string) lobEventWhere[Q] { + return buildLobEventWhere[Q](buildLobEventColumns(alias)) +} + +func buildLobEventWhere[Q psql.Filterable](cols lobEventColumns) lobEventWhere[Q] { + return lobEventWhere[Q]{ + Created: psql.Where[Q, time.Time](cols.Created), + Body: psql.Where[Q, types.JSON[json.RawMessage]](cols.Body), + ID: psql.Where[Q, string](cols.ID), + Type: psql.Where[Q, string](cols.Type), + } +} diff --git a/resource/lob_hook.go b/resource/lob_hook.go index 291be117..0068f019 100644 --- a/resource/lob_hook.go +++ b/resource/lob_hook.go @@ -9,9 +9,11 @@ import ( /* "github.com/Gleipnir-Technology/nidus-sync/db/enums" - "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/html" */ + bobtypes "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/db/models" nhttp "github.com/Gleipnir-Technology/nidus-sync/http" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" @@ -92,12 +94,13 @@ type LobEventType struct { Object string `json:"object"` } type LobEvent struct { - Body LobEventBody `json:"body"` - DateCreated time.Time `json:"date_created"` - ID string `json:"id"` - Object string `json:"object"` - ReferenceID string `json:"reference_id"` - EventType LobEventType `json:"event_type"` + //Body LobEventBody `json:"body"` + Body json.RawMessage `json:"body"` + DateCreated time.Time `json:"date_created"` + ID string `json:"id"` + Object string `json:"object"` + ReferenceID string `json:"reference_id"` + EventType LobEventType `json:"event_type"` } func (res *lobHookR) Event(ctx context.Context, w http.ResponseWriter, r *http.Request) *nhttp.ErrorWithStatus { @@ -110,7 +113,22 @@ func (res *lobHookR) Event(ctx context.Context, w http.ResponseWriter, r *http.R if err != nil { return nhttp.NewBadRequest("unmarshal json: %w", err) } - log.Info().Str("method", r.Method).Str("content", string(body)).Str("id", event.ID).Msg("got lob event") + + var inner_body bobtypes.JSON[json.RawMessage] + err = inner_body.UnmarshalJSON(event.Body) + if err != nil { + return nhttp.NewError("unmarshal inner body: %w", err) + } + _, err = models.LobEvents.Insert(&models.LobEventSetter{ + Created: omit.From(event.DateCreated), + Body: omit.From(inner_body), + ID: omit.From(event.ID), + Type: omit.From(event.EventType.ID), + }).One(ctx, db.PGInstance.BobDB) + if err != nil { + return nhttp.NewError("save event: %w", err) + } + log.Info().Str("event.id", event.ID).Msg("saved lob event") http.Error(w, "", http.StatusNoContent) return nil }