From 6a23e485ba80413b4dea5d0a18436b68fd2348b9 Mon Sep 17 00:00:00 2001 From: pdelange Date: Wed, 29 Mar 2023 23:12:29 +0200 Subject: [PATCH] WIP: ingest data for user tables --- .dockerignore | 3 + .gitignore | 7 + api/api.go | 1 + api/handle_ingestion.go | 46 +++++++ api/routes.go | 8 ++ app/app.go | 3 + app/data_model.go | 3 + app/handle_ingest_object.go | 15 +++ app/parse_ingestion_json.go | 148 ++++++++++++++++++++++ go.mod | 6 + go.sum | 20 +++ migrations/20230313103809_init_marble.sql | 11 ++ pg_repository/fill_repository.go | 23 +++- pg_repository/handle_ingest_object.go | 73 +++++++++++ 14 files changed, 362 insertions(+), 5 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 api/handle_ingestion.go create mode 100644 app/handle_ingest_object.go create mode 100644 app/parse_ingestion_json.go create mode 100644 pg_repository/handle_ingest_object.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..1dbe12002 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +# ignore already-built apps +output +output/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..b62013d6e --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# ignore .last_id file, used to store the last id of the decision stored when calling curl +.last_id + +# output folder, used to build the app outside of docker if needed +/output + +.DS_Store diff --git a/api/api.go b/api/api.go index 4e4d83e6d..96b330ee6 100644 --- a/api/api.go +++ b/api/api.go @@ -24,6 +24,7 @@ type AppInterface interface { PayloadFromTriggerObject(organizationID string, triggerObject map[string]any) (app.Payload, error) CreateDecision(organizationID string, scenarioID string, payload app.Payload) (app.Decision, error) GetDecision(organizationID string, requestedDecisionID string) (app.Decision, error) + IngestObject(orgID string, ingestPayload app.IngestPayload) (err error) } func New(port string, a AppInterface) (*http.Server, error) { diff --git a/api/handle_ingestion.go b/api/handle_ingestion.go new file mode 100644 index 000000000..a53627ae1 --- /dev/null +++ b/api/handle_ingestion.go @@ -0,0 +1,46 @@ +package api + +import ( + "fmt" + "io/ioutil" + "marble/marble-backend/app" + "net/http" + + "github.com/go-chi/chi/v5" +) + +func (a *API) handleIngestion() http.HandlerFunc { + + /////////////////////////////// + // Request and Response types defined in scope + /////////////////////////////// + + // return is a decision + + return func(w http.ResponseWriter, r *http.Request) { + + /////////////////////////////// + // Authorize request + /////////////////////////////// + orgID, err := orgIDFromCtx(r.Context()) + if err != nil { + http.Error(w, "", http.StatusUnauthorized) + return + } + + object_type := chi.URLParam(r, "object_type") + fmt.Printf("Received object type: %s\n", object_type) + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + panic(err) + } + + object_body := body + fmt.Printf("Received object body: %s\n", object_body) + + a.app.IngestObject(orgID, app.IngestPayload{ObjectType: object_type, ObjectBody: object_body}) + + } + +} diff --git a/api/routes.go b/api/routes.go index d1c46e03b..aaa9b8224 100644 --- a/api/routes.go +++ b/api/routes.go @@ -24,6 +24,14 @@ func (a *API) routes() { }) + a.router.Route("/ingestion", func(r chi.Router) { + + // use authentication middleware + r.Use(a.authCtx) + r.Post("/{object_type}", a.handleIngestion()) + + }) + } func (a *API) displayRoutes() { diff --git a/app/app.go b/app/app.go index 7d8d47eb4..192ed939d 100644 --- a/app/app.go +++ b/app/app.go @@ -18,6 +18,9 @@ type RepositoryInterface interface { // Decisions StoreDecision(orgID string, decision Decision) (id string, err error) GetDecision(orgID string, decisionID string) (Decision, error) + + // Ingestion + IngestObject(orgID string, ingestPayload IngestPayload) (err error) } func New(r RepositoryInterface) (*App, error) { diff --git a/app/data_model.go b/app/data_model.go index 4435273a5..350bf4b03 100644 --- a/app/data_model.go +++ b/app/data_model.go @@ -10,6 +10,7 @@ const ( Int Float String + Timestamp ) func (d DataType) String() string { @@ -22,6 +23,8 @@ func (d DataType) String() string { return "Float" case String: return "String" + case Timestamp: + return "Timestamp" } return "unknown" } diff --git a/app/handle_ingest_object.go b/app/handle_ingest_object.go new file mode 100644 index 000000000..2d70e30a8 --- /dev/null +++ b/app/handle_ingest_object.go @@ -0,0 +1,15 @@ +package app + +import ( + "fmt" +) + +type IngestPayload struct { + ObjectType string + ObjectBody []byte +} + +func (a *App) IngestObject(organizationID string, ingestPayload IngestPayload) (err error) { + fmt.Println(ingestPayload) + return a.repository.IngestObject(organizationID, ingestPayload) +} diff --git a/app/parse_ingestion_json.go b/app/parse_ingestion_json.go new file mode 100644 index 000000000..d00c95a2f --- /dev/null +++ b/app/parse_ingestion_json.go @@ -0,0 +1,148 @@ +package app + +import ( + "encoding/json" + "fmt" + "log" + "strings" + "time" + "unicode" + + "github.com/go-playground/validator" + dynamicstruct "github.com/ompluscator/dynamic-struct" +) + +func capitalize(str string) string { + runes := []rune(str) + runes[0] = unicode.ToUpper(runes[0]) + return string(runes) +} + +type DynamicStructWithReader struct { + Instance interface{} + Reader dynamicstruct.Reader + Table Table +} + +var validate *validator.Validate + +func makeDynamicStructBuilder(fields map[string]Field) dynamicstruct.DynamicStruct { + custom_type := dynamicstruct.NewStruct() + + var stringPointerType *string + var intPointerType *int + var floatPointerType *float32 // or 64 ? I don't see a good reason to use 64 + var boolPointerType *bool + var timePointerType *time.Time + + // those fields are mandatory for all tables + custom_type.AddField("Object_id", stringPointerType, `validate:"required"`) + custom_type.AddField("Updated_at", timePointerType, `validate:"required"`) + + for fieldName, field := range fields { + switch strings.ToLower(fieldName) { + case "object_id", "updated_at": + // already added above, with a different validation tag + break + default: + switch field.DataType { + case Bool: + custom_type.AddField(capitalize(fieldName), boolPointerType, "") + case Int: + custom_type.AddField(capitalize(fieldName), intPointerType, "") + case Float: + custom_type.AddField(capitalize(fieldName), floatPointerType, "") + case String: + custom_type.AddField(capitalize(fieldName), stringPointerType, "") + case Timestamp: + custom_type.AddField(capitalize(fieldName), timePointerType, "") + } + } + } + return custom_type.Build() +} + +func validateParsedJson(instance interface{}) error { + validate = validator.New() + err := validate.Struct(instance) + if err != nil { + + // This error should happen in the dynamic struct is badly formatted, or if the tags + // contain bad values. If this returns an error, it's a 500 error. + if _, ok := err.(*validator.InvalidValidationError); ok { + log.Println(err) + return err + } + + // Otherwise it's a 400 error and we can access the reasons from here + count := 0 + for _, err := range err.(validator.ValidationErrors) { + fmt.Printf("The input object is not valid: key %v, validation tag: '%v', receive value %v", err.Field(), err.Tag(), err.Param()) + count++ + } + if count > 0 { + return err + } + } + return nil +} + +func ParseToDataModelObject(dataModel DataModel, jsonBody []byte, tableName string) (DynamicStructWithReader, error) { + table := dataModel.Tables[tableName] + fields := table.Fields + + custom_type := makeDynamicStructBuilder(fields) + + dynamicStructInstance := custom_type.New() + dynamicStructReader := dynamicstruct.NewReader(dynamicStructInstance) + + // This is where errors can happen while parson the json. We could for instance have badly formatted + // json, or timestamps. + // We could also have more serious errors, like a non-capitalized field in the dynamic struct that + // causes a panic. We should manage the errors accordingly. + err := json.Unmarshal(jsonBody, &dynamicStructInstance) + if err != nil { + // add code here to distinguish between badly formatted json and other errors + return DynamicStructWithReader{Instance: dynamicStructInstance, Reader: dynamicStructReader, Table: table}, err + } + + // If the data has been successfully parsed, we can validate it + // This is done using the validate tags on the dynamic struct + // There are two possible cases of error + err = validateParsedJson(dynamicStructInstance) + if err != nil { + return DynamicStructWithReader{Instance: dynamicStructInstance, Reader: dynamicStructReader, Table: table}, err + } + + return DynamicStructWithReader{Instance: dynamicStructInstance, Reader: dynamicStructReader, Table: table}, nil +} + +func ReadFieldFromDynamicStruct(dynamicStruct DynamicStructWithReader, fieldName string) interface{} { + check := dynamicStruct.Reader.HasField((capitalize(fieldName))) + if !check { + log.Fatalf("Field %v not found in dynamic struct", fieldName) + } + field := dynamicStruct.Reader.GetField(capitalize(fieldName)) + table := dynamicStruct.Table + fields := table.Fields + fieldFromModel, ok := fields[fieldName] + if !ok { + log.Fatalf("Field %v not found in table when reading from dynamic struct", fieldName) + } + + switch fieldFromModel.DataType { + case Bool: + return field.PointerBool() + case Int: + return field.PointerInt() + case Float: + return field.PointerFloat32() + case String: + return field.PointerString() + case Timestamp: + return field.PointerTime() + default: + log.Fatal("Unknown data type") + return nil + } +} diff --git a/go.mod b/go.mod index 7402d4ff3..3daa0a82b 100644 --- a/go.mod +++ b/go.mod @@ -9,10 +9,16 @@ require ( ) require ( + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator v9.31.0+incompatible // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/puddle/v2 v2.2.0 // indirect + github.com/leodido/go-urn v1.2.2 // indirect + github.com/ompluscator/dynamic-struct v1.4.0 // indirect golang.org/x/crypto v0.7.0 // indirect + golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/text v0.8.0 // indirect ) diff --git a/go.sum b/go.sum index 6cf786658..3643fe0bc 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,12 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA= +github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= @@ -14,18 +20,31 @@ github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHo github.com/jackc/puddle/v2 v2.2.0 h1:RdcDk92EJBuBS55nQMMYFXTxwstHug4jkhT5pq8VxPk= github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4= +github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/ompluscator/dynamic-struct v1.4.0 h1:I/Si9LZtItSwiTMe7vosEuIu2TKdOvWbE3R/lokpN4Q= +github.com/ompluscator/dynamic-struct v1.4.0/go.mod h1:ADQ1+6Ox1D+ntuNwTHyl1NvpAqY2lBXPSPbcO4CJdeA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pressly/goose/v3 v3.10.0 h1:Gn5E9CkPqTtWvfaDVqtJqMjYtsrZ9K5mU/8wzTsvg04= github.com/pressly/goose/v3 v3.10.0/go.mod h1:c5D3a7j66cT0fhRPj7KsXolfduVrhLlxKZjmCVSey5w= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= +golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -36,6 +55,7 @@ golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= diff --git a/migrations/20230313103809_init_marble.sql b/migrations/20230313103809_init_marble.sql index 5f04e2076..fc2a6a434 100644 --- a/migrations/20230313103809_init_marble.sql +++ b/migrations/20230313103809_init_marble.sql @@ -110,6 +110,17 @@ CREATE TABLE decision_rules( REFERENCES decisions(id) ); +CREATE TABLE transactions( + id uuid DEFAULT uuid_generate_v4(), + object_id VARCHAR NOT NULL, + updated_at TIMESTAMP NOT NULL, + value double precision, + title VARCHAR, + description VARCHAR, + + PRIMARY KEY(id) +) + -- +goose StatementEnd -- +goose Down diff --git a/pg_repository/fill_repository.go b/pg_repository/fill_repository.go index b8be65a68..4c60fe80e 100644 --- a/pg_repository/fill_repository.go +++ b/pg_repository/fill_repository.go @@ -34,6 +34,19 @@ func (r *PGRepository) FillOrgWithTestData(orgID string) { }, }, }, + "transactions": { + Fields: map[string]app.Field{ + "id": {DataType: app.String}, + "object_id": { + DataType: app.String, + }, + "updated_at": {DataType: app.Timestamp}, + "value": {DataType: app.Float}, + "title": {DataType: app.String}, + "description": {DataType: app.String}, + }, + LinksToSingle: map[string]app.LinkToSingle{}, + }, "user": { Fields: map[string]app.Field{ "id": { @@ -57,31 +70,31 @@ func (r *PGRepository) FillOrgWithTestData(orgID string) { // Basic logical rules := []app.Rule{ { - RootNode: app.And{app.True{}, app.True{}}, + RootNode: app.And{Left: app.True{}, Right: app.True{}}, ScoreModifier: 2, Name: "Rule 1 Name", Description: "Rule 1 Desc", }, { - RootNode: app.And{app.True{}, app.False{}}, + RootNode: app.And{Left: app.True{}, Right: app.False{}}, ScoreModifier: 2, Name: "Rule 2 Name", Description: "Rule 2 Desc", }, { - RootNode: app.And{app.True{}, app.And{app.True{}, app.Eq{app.IntValue{5}, app.IntValue{5}}}}, + RootNode: app.And{Left: app.True{}, Right: app.And{Left: app.True{}, Right: app.Eq{Left: app.IntValue{Value: 5}, Right: app.IntValue{Value: 5}}}}, ScoreModifier: 2, Name: "Rule 3 Name", Description: "Rule 3 Desc", }, { - RootNode: app.And{app.True{}, app.And{app.True{}, app.Eq{app.IntValue{6}, app.IntValue{5}}}}, + RootNode: app.And{Left: app.True{}, Right: app.And{Left: app.True{}, Right: app.Eq{Left: app.IntValue{Value: 6}, Right: app.IntValue{Value: 5}}}}, ScoreModifier: 2, Name: "Rule 4 Name", Description: "Rule 4 Desc", }, { - RootNode: app.And{app.True{}, app.And{app.True{}, app.Eq{app.FloatValue{5}, app.IntValue{5}}}}, + RootNode: app.And{Left: app.True{}, Right: app.And{Left: app.True{}, Right: app.Eq{Left: app.FloatValue{Value: 5}, Right: app.IntValue{Value: 5}}}}, ScoreModifier: 2, Name: "Rule 5 Name", Description: "Rule 5 Desc", diff --git a/pg_repository/handle_ingest_object.go b/pg_repository/handle_ingest_object.go new file mode 100644 index 000000000..be572e90d --- /dev/null +++ b/pg_repository/handle_ingest_object.go @@ -0,0 +1,73 @@ +package pg_repository + +import ( + "context" + "fmt" + "log" + "marble/marble-backend/app" + "strings" +) + +func (r *PGRepository) PayloadToDynamicStruct(payload app.IngestPayload, dataModel app.DataModel) (err error) { + return nil +} + +func (r *PGRepository) IngestObject(orgID string, ingestPayload app.IngestPayload) (err error) { + dataModel, err := r.GetDataModel(orgID) + if err != nil { + log.Printf("Unable to find datamodel by orgId for ingestion: %v", err) + return err + } + + _ = r.PayloadToDynamicStruct(ingestPayload, dataModel) + payloadStructWithReader, err := app.ParseToDataModelObject(dataModel, ingestPayload.ObjectBody, ingestPayload.ObjectType) + if err != nil { + log.Printf("Error while parsing struct in repository IngestObject: %v", err) + return err + } + + tx, err := r.db.Begin(context.Background()) + if err != nil { + return err + } + // Rollback is safe to call even if the tx is already closed, so if + // the tx commits successfully, this is a no-op + defer tx.Rollback(context.Background()) + + tables := dataModel.Tables + table, ok := tables[ingestPayload.ObjectType] + if !ok { + return fmt.Errorf("table %s not found in data model", ingestPayload.ObjectType) + } + + columnNamesSlice := make([]string, len(table.Fields)) + valuesNumberSlice := make([]string, len(table.Fields)) + values := make([]interface{}, len(table.Fields)) + i := 0 + for k := range table.Fields { + columnNamesSlice[i] = k + valuesNumberSlice[i] = fmt.Sprintf("$%d", i+1) + values[i] = app.ReadFieldFromDynamicStruct(payloadStructWithReader, k) + i++ + } + + columnNames := strings.Join(columnNamesSlice, ", ") + valuesNumbers := strings.Join(valuesNumberSlice, ", ") + // insert the decision + insertDecisionQueryString := fmt.Sprintf(` + INSERT INTO %s + (%s) + VALUES (%s) + RETURNING "id"; + `, ingestPayload.ObjectType, columnNames, valuesNumbers) + + var createdObjectId string + err = tx.QueryRow(context.TODO(), insertDecisionQueryString, values..., + ).Scan(&createdObjectId) + + fmt.Printf("Created object in db: type %s, id %s", ingestPayload.ObjectType, createdObjectId) + if err != nil { + return err + } + return nil +}