Skip to content

Commit

Permalink
WIP: ingest data for user tables
Browse files Browse the repository at this point in the history
  • Loading branch information
pascaldelange committed Mar 29, 2023
1 parent 1292df3 commit 6a23e48
Show file tree
Hide file tree
Showing 14 changed files with 362 additions and 5 deletions.
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ignore already-built apps
output
output/*
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
46 changes: 46 additions & 0 deletions api/handle_ingestion.go
Original file line number Diff line number Diff line change
@@ -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})

}

}
8 changes: 8 additions & 0 deletions api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
3 changes: 3 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions app/data_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const (
Int
Float
String
Timestamp
)

func (d DataType) String() string {
Expand All @@ -22,6 +23,8 @@ func (d DataType) String() string {
return "Float"
case String:
return "String"
case Timestamp:
return "Timestamp"
}
return "unknown"
}
Expand Down
15 changes: 15 additions & 0 deletions app/handle_ingest_object.go
Original file line number Diff line number Diff line change
@@ -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)
}
148 changes: 148 additions & 0 deletions app/parse_ingestion_json.go
Original file line number Diff line number Diff line change
@@ -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
}
}
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
20 changes: 20 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand All @@ -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=
Expand Down
11 changes: 11 additions & 0 deletions migrations/20230313103809_init_marble.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 6a23e48

Please sign in to comment.