diff --git a/.gitignore b/.gitignore index 76add87..0aa8944 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ +.env +db +screenmark node_modules -dist \ No newline at end of file +dist diff --git a/backend/.gen/haystack/haystack/model/image_links.go b/backend/.gen/haystack/haystack/model/image_links.go new file mode 100644 index 0000000..ae8f816 --- /dev/null +++ b/backend/.gen/haystack/haystack/model/image_links.go @@ -0,0 +1,18 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package model + +import ( + "github.com/google/uuid" +) + +type ImageLinks struct { + ID uuid.UUID `sql:"primary_key"` + Link string + ImageID uuid.UUID +} diff --git a/backend/.gen/haystack/haystack/model/image_tags.go b/backend/.gen/haystack/haystack/model/image_tags.go new file mode 100644 index 0000000..a3c666c --- /dev/null +++ b/backend/.gen/haystack/haystack/model/image_tags.go @@ -0,0 +1,17 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package model + +import ( + "github.com/google/uuid" +) + +type ImageTags struct { + TagID uuid.UUID + ImageID uuid.UUID +} diff --git a/backend/.gen/haystack/haystack/model/image_text.go b/backend/.gen/haystack/haystack/model/image_text.go new file mode 100644 index 0000000..5abddee --- /dev/null +++ b/backend/.gen/haystack/haystack/model/image_text.go @@ -0,0 +1,18 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package model + +import ( + "github.com/google/uuid" +) + +type ImageText struct { + ID uuid.UUID `sql:"primary_key"` + ImageText string + ImageID uuid.UUID +} diff --git a/backend/.gen/haystack/haystack/model/user_images.go b/backend/.gen/haystack/haystack/model/user_images.go new file mode 100644 index 0000000..18c33ca --- /dev/null +++ b/backend/.gen/haystack/haystack/model/user_images.go @@ -0,0 +1,19 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package model + +import ( + "github.com/google/uuid" +) + +type UserImages struct { + ID uuid.UUID `sql:"primary_key"` + ImageName string + Image []byte + UserID uuid.UUID +} diff --git a/backend/.gen/haystack/haystack/model/user_tags.go b/backend/.gen/haystack/haystack/model/user_tags.go new file mode 100644 index 0000000..e2d12d7 --- /dev/null +++ b/backend/.gen/haystack/haystack/model/user_tags.go @@ -0,0 +1,18 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package model + +import ( + "github.com/google/uuid" +) + +type UserTags struct { + ID uuid.UUID `sql:"primary_key"` + Tag string + UserID uuid.UUID +} diff --git a/backend/.gen/haystack/haystack/model/users.go b/backend/.gen/haystack/haystack/model/users.go new file mode 100644 index 0000000..a35be7f --- /dev/null +++ b/backend/.gen/haystack/haystack/model/users.go @@ -0,0 +1,16 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package model + +import ( + "github.com/google/uuid" +) + +type Users struct { + ID uuid.UUID `sql:"primary_key"` +} diff --git a/backend/.gen/haystack/haystack/table/image_links.go b/backend/.gen/haystack/haystack/table/image_links.go new file mode 100644 index 0000000..2b9ad5c --- /dev/null +++ b/backend/.gen/haystack/haystack/table/image_links.go @@ -0,0 +1,81 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package table + +import ( + "github.com/go-jet/jet/v2/postgres" +) + +var ImageLinks = newImageLinksTable("haystack", "image_links", "") + +type imageLinksTable struct { + postgres.Table + + // Columns + ID postgres.ColumnString + Link postgres.ColumnString + ImageID postgres.ColumnString + + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList +} + +type ImageLinksTable struct { + imageLinksTable + + EXCLUDED imageLinksTable +} + +// AS creates new ImageLinksTable with assigned alias +func (a ImageLinksTable) AS(alias string) *ImageLinksTable { + return newImageLinksTable(a.SchemaName(), a.TableName(), alias) +} + +// Schema creates new ImageLinksTable with assigned schema name +func (a ImageLinksTable) FromSchema(schemaName string) *ImageLinksTable { + return newImageLinksTable(schemaName, a.TableName(), a.Alias()) +} + +// WithPrefix creates new ImageLinksTable with assigned table prefix +func (a ImageLinksTable) WithPrefix(prefix string) *ImageLinksTable { + return newImageLinksTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} + +// WithSuffix creates new ImageLinksTable with assigned table suffix +func (a ImageLinksTable) WithSuffix(suffix string) *ImageLinksTable { + return newImageLinksTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} + +func newImageLinksTable(schemaName, tableName, alias string) *ImageLinksTable { + return &ImageLinksTable{ + imageLinksTable: newImageLinksTableImpl(schemaName, tableName, alias), + EXCLUDED: newImageLinksTableImpl("", "excluded", ""), + } +} + +func newImageLinksTableImpl(schemaName, tableName, alias string) imageLinksTable { + var ( + IDColumn = postgres.StringColumn("id") + LinkColumn = postgres.StringColumn("link") + ImageIDColumn = postgres.StringColumn("image_id") + allColumns = postgres.ColumnList{IDColumn, LinkColumn, ImageIDColumn} + mutableColumns = postgres.ColumnList{LinkColumn, ImageIDColumn} + ) + + return imageLinksTable{ + Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), + + //Columns + ID: IDColumn, + Link: LinkColumn, + ImageID: ImageIDColumn, + + AllColumns: allColumns, + MutableColumns: mutableColumns, + } +} diff --git a/backend/.gen/haystack/haystack/table/image_tags.go b/backend/.gen/haystack/haystack/table/image_tags.go new file mode 100644 index 0000000..9aaa4f8 --- /dev/null +++ b/backend/.gen/haystack/haystack/table/image_tags.go @@ -0,0 +1,78 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package table + +import ( + "github.com/go-jet/jet/v2/postgres" +) + +var ImageTags = newImageTagsTable("haystack", "image_tags", "") + +type imageTagsTable struct { + postgres.Table + + // Columns + TagID postgres.ColumnString + ImageID postgres.ColumnString + + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList +} + +type ImageTagsTable struct { + imageTagsTable + + EXCLUDED imageTagsTable +} + +// AS creates new ImageTagsTable with assigned alias +func (a ImageTagsTable) AS(alias string) *ImageTagsTable { + return newImageTagsTable(a.SchemaName(), a.TableName(), alias) +} + +// Schema creates new ImageTagsTable with assigned schema name +func (a ImageTagsTable) FromSchema(schemaName string) *ImageTagsTable { + return newImageTagsTable(schemaName, a.TableName(), a.Alias()) +} + +// WithPrefix creates new ImageTagsTable with assigned table prefix +func (a ImageTagsTable) WithPrefix(prefix string) *ImageTagsTable { + return newImageTagsTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} + +// WithSuffix creates new ImageTagsTable with assigned table suffix +func (a ImageTagsTable) WithSuffix(suffix string) *ImageTagsTable { + return newImageTagsTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} + +func newImageTagsTable(schemaName, tableName, alias string) *ImageTagsTable { + return &ImageTagsTable{ + imageTagsTable: newImageTagsTableImpl(schemaName, tableName, alias), + EXCLUDED: newImageTagsTableImpl("", "excluded", ""), + } +} + +func newImageTagsTableImpl(schemaName, tableName, alias string) imageTagsTable { + var ( + TagIDColumn = postgres.StringColumn("tag_id") + ImageIDColumn = postgres.StringColumn("image_id") + allColumns = postgres.ColumnList{TagIDColumn, ImageIDColumn} + mutableColumns = postgres.ColumnList{TagIDColumn, ImageIDColumn} + ) + + return imageTagsTable{ + Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), + + //Columns + TagID: TagIDColumn, + ImageID: ImageIDColumn, + + AllColumns: allColumns, + MutableColumns: mutableColumns, + } +} diff --git a/backend/.gen/haystack/haystack/table/image_text.go b/backend/.gen/haystack/haystack/table/image_text.go new file mode 100644 index 0000000..090a735 --- /dev/null +++ b/backend/.gen/haystack/haystack/table/image_text.go @@ -0,0 +1,81 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package table + +import ( + "github.com/go-jet/jet/v2/postgres" +) + +var ImageText = newImageTextTable("haystack", "image_text", "") + +type imageTextTable struct { + postgres.Table + + // Columns + ID postgres.ColumnString + ImageText postgres.ColumnString + ImageID postgres.ColumnString + + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList +} + +type ImageTextTable struct { + imageTextTable + + EXCLUDED imageTextTable +} + +// AS creates new ImageTextTable with assigned alias +func (a ImageTextTable) AS(alias string) *ImageTextTable { + return newImageTextTable(a.SchemaName(), a.TableName(), alias) +} + +// Schema creates new ImageTextTable with assigned schema name +func (a ImageTextTable) FromSchema(schemaName string) *ImageTextTable { + return newImageTextTable(schemaName, a.TableName(), a.Alias()) +} + +// WithPrefix creates new ImageTextTable with assigned table prefix +func (a ImageTextTable) WithPrefix(prefix string) *ImageTextTable { + return newImageTextTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} + +// WithSuffix creates new ImageTextTable with assigned table suffix +func (a ImageTextTable) WithSuffix(suffix string) *ImageTextTable { + return newImageTextTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} + +func newImageTextTable(schemaName, tableName, alias string) *ImageTextTable { + return &ImageTextTable{ + imageTextTable: newImageTextTableImpl(schemaName, tableName, alias), + EXCLUDED: newImageTextTableImpl("", "excluded", ""), + } +} + +func newImageTextTableImpl(schemaName, tableName, alias string) imageTextTable { + var ( + IDColumn = postgres.StringColumn("id") + ImageTextColumn = postgres.StringColumn("image_text") + ImageIDColumn = postgres.StringColumn("image_id") + allColumns = postgres.ColumnList{IDColumn, ImageTextColumn, ImageIDColumn} + mutableColumns = postgres.ColumnList{ImageTextColumn, ImageIDColumn} + ) + + return imageTextTable{ + Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), + + //Columns + ID: IDColumn, + ImageText: ImageTextColumn, + ImageID: ImageIDColumn, + + AllColumns: allColumns, + MutableColumns: mutableColumns, + } +} diff --git a/backend/.gen/haystack/haystack/table/table_use_schema.go b/backend/.gen/haystack/haystack/table/table_use_schema.go new file mode 100644 index 0000000..f7f65cd --- /dev/null +++ b/backend/.gen/haystack/haystack/table/table_use_schema.go @@ -0,0 +1,19 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package table + +// UseSchema sets a new schema name for all generated table SQL builder types. It is recommended to invoke +// this method only once at the beginning of the program. +func UseSchema(schema string) { + ImageLinks = ImageLinks.FromSchema(schema) + ImageTags = ImageTags.FromSchema(schema) + ImageText = ImageText.FromSchema(schema) + UserImages = UserImages.FromSchema(schema) + UserTags = UserTags.FromSchema(schema) + Users = Users.FromSchema(schema) +} diff --git a/backend/.gen/haystack/haystack/table/user_images.go b/backend/.gen/haystack/haystack/table/user_images.go new file mode 100644 index 0000000..98bbd64 --- /dev/null +++ b/backend/.gen/haystack/haystack/table/user_images.go @@ -0,0 +1,84 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package table + +import ( + "github.com/go-jet/jet/v2/postgres" +) + +var UserImages = newUserImagesTable("haystack", "user_images", "") + +type userImagesTable struct { + postgres.Table + + // Columns + ID postgres.ColumnString + ImageName postgres.ColumnString + Image postgres.ColumnString + UserID postgres.ColumnString + + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList +} + +type UserImagesTable struct { + userImagesTable + + EXCLUDED userImagesTable +} + +// AS creates new UserImagesTable with assigned alias +func (a UserImagesTable) AS(alias string) *UserImagesTable { + return newUserImagesTable(a.SchemaName(), a.TableName(), alias) +} + +// Schema creates new UserImagesTable with assigned schema name +func (a UserImagesTable) FromSchema(schemaName string) *UserImagesTable { + return newUserImagesTable(schemaName, a.TableName(), a.Alias()) +} + +// WithPrefix creates new UserImagesTable with assigned table prefix +func (a UserImagesTable) WithPrefix(prefix string) *UserImagesTable { + return newUserImagesTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} + +// WithSuffix creates new UserImagesTable with assigned table suffix +func (a UserImagesTable) WithSuffix(suffix string) *UserImagesTable { + return newUserImagesTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} + +func newUserImagesTable(schemaName, tableName, alias string) *UserImagesTable { + return &UserImagesTable{ + userImagesTable: newUserImagesTableImpl(schemaName, tableName, alias), + EXCLUDED: newUserImagesTableImpl("", "excluded", ""), + } +} + +func newUserImagesTableImpl(schemaName, tableName, alias string) userImagesTable { + var ( + IDColumn = postgres.StringColumn("id") + ImageNameColumn = postgres.StringColumn("image_name") + ImageColumn = postgres.StringColumn("image") + UserIDColumn = postgres.StringColumn("user_id") + allColumns = postgres.ColumnList{IDColumn, ImageNameColumn, ImageColumn, UserIDColumn} + mutableColumns = postgres.ColumnList{ImageNameColumn, ImageColumn, UserIDColumn} + ) + + return userImagesTable{ + Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), + + //Columns + ID: IDColumn, + ImageName: ImageNameColumn, + Image: ImageColumn, + UserID: UserIDColumn, + + AllColumns: allColumns, + MutableColumns: mutableColumns, + } +} diff --git a/backend/.gen/haystack/haystack/table/user_tags.go b/backend/.gen/haystack/haystack/table/user_tags.go new file mode 100644 index 0000000..99dfc9e --- /dev/null +++ b/backend/.gen/haystack/haystack/table/user_tags.go @@ -0,0 +1,81 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package table + +import ( + "github.com/go-jet/jet/v2/postgres" +) + +var UserTags = newUserTagsTable("haystack", "user_tags", "") + +type userTagsTable struct { + postgres.Table + + // Columns + ID postgres.ColumnString + Tag postgres.ColumnString + UserID postgres.ColumnString + + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList +} + +type UserTagsTable struct { + userTagsTable + + EXCLUDED userTagsTable +} + +// AS creates new UserTagsTable with assigned alias +func (a UserTagsTable) AS(alias string) *UserTagsTable { + return newUserTagsTable(a.SchemaName(), a.TableName(), alias) +} + +// Schema creates new UserTagsTable with assigned schema name +func (a UserTagsTable) FromSchema(schemaName string) *UserTagsTable { + return newUserTagsTable(schemaName, a.TableName(), a.Alias()) +} + +// WithPrefix creates new UserTagsTable with assigned table prefix +func (a UserTagsTable) WithPrefix(prefix string) *UserTagsTable { + return newUserTagsTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} + +// WithSuffix creates new UserTagsTable with assigned table suffix +func (a UserTagsTable) WithSuffix(suffix string) *UserTagsTable { + return newUserTagsTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} + +func newUserTagsTable(schemaName, tableName, alias string) *UserTagsTable { + return &UserTagsTable{ + userTagsTable: newUserTagsTableImpl(schemaName, tableName, alias), + EXCLUDED: newUserTagsTableImpl("", "excluded", ""), + } +} + +func newUserTagsTableImpl(schemaName, tableName, alias string) userTagsTable { + var ( + IDColumn = postgres.StringColumn("id") + TagColumn = postgres.StringColumn("tag") + UserIDColumn = postgres.StringColumn("user_id") + allColumns = postgres.ColumnList{IDColumn, TagColumn, UserIDColumn} + mutableColumns = postgres.ColumnList{TagColumn, UserIDColumn} + ) + + return userTagsTable{ + Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), + + //Columns + ID: IDColumn, + Tag: TagColumn, + UserID: UserIDColumn, + + AllColumns: allColumns, + MutableColumns: mutableColumns, + } +} diff --git a/backend/.gen/haystack/haystack/table/users.go b/backend/.gen/haystack/haystack/table/users.go new file mode 100644 index 0000000..73d5fd7 --- /dev/null +++ b/backend/.gen/haystack/haystack/table/users.go @@ -0,0 +1,75 @@ +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package table + +import ( + "github.com/go-jet/jet/v2/postgres" +) + +var Users = newUsersTable("haystack", "users", "") + +type usersTable struct { + postgres.Table + + // Columns + ID postgres.ColumnString + + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList +} + +type UsersTable struct { + usersTable + + EXCLUDED usersTable +} + +// AS creates new UsersTable with assigned alias +func (a UsersTable) AS(alias string) *UsersTable { + return newUsersTable(a.SchemaName(), a.TableName(), alias) +} + +// Schema creates new UsersTable with assigned schema name +func (a UsersTable) FromSchema(schemaName string) *UsersTable { + return newUsersTable(schemaName, a.TableName(), a.Alias()) +} + +// WithPrefix creates new UsersTable with assigned table prefix +func (a UsersTable) WithPrefix(prefix string) *UsersTable { + return newUsersTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} + +// WithSuffix creates new UsersTable with assigned table suffix +func (a UsersTable) WithSuffix(suffix string) *UsersTable { + return newUsersTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} + +func newUsersTable(schemaName, tableName, alias string) *UsersTable { + return &UsersTable{ + usersTable: newUsersTableImpl(schemaName, tableName, alias), + EXCLUDED: newUsersTableImpl("", "excluded", ""), + } +} + +func newUsersTableImpl(schemaName, tableName, alias string) usersTable { + var ( + IDColumn = postgres.StringColumn("id") + allColumns = postgres.ColumnList{IDColumn} + mutableColumns = postgres.ColumnList{} + ) + + return usersTable{ + Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), + + //Columns + ID: IDColumn, + + AllColumns: allColumns, + MutableColumns: mutableColumns, + } +} diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..c5dca0b --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,14 @@ +module screenmark/screenmark + +go 1.24.0 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-jet/jet/v2 v2.12.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..7ccdb24 --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,17 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-jet/jet/v2 v2.12.0 h1:z2JfvBAZgsfxlQz6NXBYdZTXc7ep3jhbszTLtETv1JE= +github.com/go-jet/jet/v2 v2.12.0/go.mod h1:ufQVRQeI1mbcO5R8uCEVcVf3Foej9kReBdwDx7YMWUM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/main.go b/backend/main.go new file mode 100644 index 0000000..6d472b1 --- /dev/null +++ b/backend/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "fmt" + "io" + "log" + "net/http" + "screenmark/screenmark/models" + "time" + + "github.com/joho/godotenv" + "github.com/lib/pq" +) + +func main() { + err := godotenv.Load() + if err != nil { + panic(err) + } + + err = models.InitDatabase() + if err != nil { + panic(err) + } + + listener := pq.NewListener(models.CONNECTION, time.Second, time.Second, func(event pq.ListenerEventType, err error) { + if err != nil { + panic(err) + } + }) + defer listener.Close() + + go func() { + err := listener.Listen("new_image") + if err != nil { + panic(err) + } + + select { + case parameters := <-listener.Notify: + log.Println("received notification, new image available: " + parameters.Extra) + + go func() { + openAiClient, err := CreateOpenAiClient() + if err != nil { + panic(err) + } + + image, err := models.GetImage(parameters.Extra) + if err != nil { + log.Println(err) + return + } + + imageInfo, err := openAiClient.GetImageInfo(image.ImageName, image.Image) + if err != nil { + log.Println(err) + return + } + + log.Printf("Info: %+v\n", imageInfo) + }() + } + }() + + mux := http.NewServeMux() + + mux.HandleFunc("OPTIONS /image/{name}", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Access-Control-Allow-Origin", "*") + w.Header().Add("Access-Control-Allow-Credentials", "*") + w.Header().Add("Access-Control-Allow-Headers", "*") + }) + + mux.HandleFunc("POST /image/{name}", func(w http.ResponseWriter, r *http.Request) { + imageName := r.PathValue("name") + + userId := r.Header.Get("userId") + + w.Header().Add("Access-Control-Allow-Origin", "*") + w.Header().Add("Access-Control-Allow-Credentials", "*") + w.Header().Add("Access-Control-Allow-Headers", "*") + + if len(imageName) == 0 { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "You need to provide a name in the path") + return + } + + image, err := io.ReadAll(r.Body) + + if err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Couldnt read the image from the request body") + return + } + + err = models.SaveImage(userId, imageName, image) + if err != nil { + log.Println(err) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Could not save image to DB") + return + } + }) + + log.Println("Listening and serving.") + + http.ListenAndServe(":3040", mux) +} diff --git a/backend/models/database.go b/backend/models/database.go new file mode 100644 index 0000000..87edbb8 --- /dev/null +++ b/backend/models/database.go @@ -0,0 +1,19 @@ +package models + +import ( + "database/sql" + + _ "github.com/lib/pq" +) + +const CONNECTION = "postgresql://localhost:5432/haystack?sslmode=disable" + +var db *sql.DB + +func InitDatabase() error { + database, err := sql.Open("postgres", CONNECTION) + + db = database + + return err +} diff --git a/backend/models/image.go b/backend/models/image.go new file mode 100644 index 0000000..39d91e9 --- /dev/null +++ b/backend/models/image.go @@ -0,0 +1,33 @@ +package models + +import ( + "errors" + "fmt" + + . "github.com/go-jet/jet/v2/postgres" + "screenmark/screenmark/.gen/haystack/haystack/model" + . "screenmark/screenmark/.gen/haystack/haystack/table" + + "github.com/google/uuid" +) + +func SaveImage(userId string, imageName string, imageData []byte) error { + stmt := UserImages.INSERT(UserImages.UserID, UserImages.ImageName, UserImages.Image).VALUES(userId, imageName, imageData) + _, err := stmt.Exec(db) + + return err +} + +func GetImage(imageId string) (model.UserImages, error) { + id := uuid.MustParse(imageId) + stmt := UserImages.SELECT(UserImages.ImageName, UserImages.Image).WHERE(UserImages.ID.EQ(UUID(id))) + + images := []model.UserImages{} + err := stmt.Query(db, &images) + + if len(images) != 1 { + return model.UserImages{}, errors.New(fmt.Sprintf("Expected 1, got %d\n", len(images))) + } + + return images[0], err +} diff --git a/backend/openai.go b/backend/openai.go new file mode 100644 index 0000000..1bcd3aa --- /dev/null +++ b/backend/openai.go @@ -0,0 +1,280 @@ +package main + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" +) + +type ImageInfo struct { + Tags []string `json:"tags"` + Text []string `json:"text"` + Links []string `json:"links"` +} + +type ResponseFormat struct { + Type string `json:"type"` + JsonSchema any `json:"json_schema"` +} + +type OpenAiRequestBody struct { + Model string `json:"model"` + Temperature float64 `json:"temperature"` + ResponseFormat ResponseFormat `json:"response_format"` + + OpenAiMessages +} + +type OpenAiMessages struct { + Messages []OpenAiMessage `json:"messages"` +} + +type OpenAiMessage interface { + MessageToJson() ([]byte, error) +} + +type OpenAiTextMessage struct { + Role string `json:"role"` + Content string `json:"content"` +} + +func (textContent OpenAiTextMessage) MessageToJson() ([]byte, error) { + // TODO: Validate the `Role`. + return json.Marshal(textContent) +} + +type OpenAiArrayMessage struct { + Role string `json:"role"` + Content []OpenAiContent `json:"content"` +} + +func (arrayContent OpenAiArrayMessage) MessageToJson() ([]byte, error) { + return json.Marshal(arrayContent) +} + +func (content *OpenAiMessages) AddImage(imageName string, image []byte) error { + extension := filepath.Ext(imageName) + if len(extension) == 0 { + // TODO: could also validate for image types we support. + return errors.New("Image does not have extension") + } + + extension = extension[1:] + + encodedString := base64.StdEncoding.EncodeToString(image) + + arrayMessage := OpenAiArrayMessage{Role: ROLE_USER, Content: make([]OpenAiContent, 1)} + arrayMessage.Content[0] = OpenAiImage{ + ImageType: IMAGE_TYPE, + ImageUrl: ImageUrl{ + Url: fmt.Sprintf("data:image/%s;base64,%s", extension, encodedString), + }, + } + + content.Messages = append(content.Messages, arrayMessage) + + return nil +} + +func (content *OpenAiMessages) AddSystem(prompt string) error { + if len(content.Messages) != 0 { + return errors.New("You can only add a system prompt at the beginning") + } + + content.Messages = append(content.Messages, OpenAiTextMessage{ + Role: ROLE_SYSTEM, + Content: prompt, + }) + + return nil +} + +type OpenAiContent interface { + ToJson() ([]byte, error) +} + +type ImageUrl struct { + Url string `json:"url"` +} + +type OpenAiImage struct { + ImageType string `json:"type"` + ImageUrl ImageUrl `json:"image_url"` +} + +func (imageMessage OpenAiImage) ToJson() ([]byte, error) { + imageMessage.ImageType = IMAGE_TYPE + return json.Marshal(imageMessage) +} + +type OpenAiClient struct { + url string + apiKey string + systemPrompt string + responseFormat string + + Do func(req *http.Request) (*http.Response, error) +} + +// func (client OpenAiClient) Do(req *http.Request) () { +// httpClient := http.Client{} +// return httpClient.Do(req) +// } + +const OPENAI_API_KEY = "OPENAI_API_KEY" +const ROLE_USER = "user" +const ROLE_SYSTEM = "system" +const IMAGE_TYPE = "image_url" + +// TODO: extract to text file probably +const PROMPT = ` +You are an image information extractor. The user will provide you with screenshots and your job is to extract any relevant links and text +that the image might contain. You will also try your best to assign some tags to this image, avoid too many tags. + +This system is part of a bookmark manager, who's main goal is to allow the user to search through various screenshots. +` + +const RESPONSE_FORMAT = ` +{ + "name": "schema_description", + "schema": { + "type": "object", + "properties": { + "tags": { + "type": "array", + "description": "A list of tags you think the image is relevant to.", + "items": { + "type": "string" + } + }, + "text": { + "type": "array", + "description": "A list of sentences the image contains.", + "items": { + "type": "string" + } + }, + "links": { + "type": "array", + "description": "A list of all the links you can find in the image.", + "items": { + "type": "string" + } + } + }, + "required": [ + "tags", + "text", + "links" + ], + "additionalProperties": false + }, + "strict": true +} +` + +func CreateOpenAiClient() (OpenAiClient, error) { + apiKey := os.Getenv(OPENAI_API_KEY) + + if len(apiKey) == 0 { + return OpenAiClient{}, errors.New(OPENAI_API_KEY + " was not found.") + } + + return OpenAiClient{ + apiKey: apiKey, + url: "https://api.openai.com/v1/chat/completions", + systemPrompt: PROMPT, + Do: func(req *http.Request) (*http.Response, error) { + client := &http.Client{} + return client.Do(req) + }, + }, nil +} + +func (client OpenAiClient) getRequest(body []byte) (*http.Request, error) { + req, err := http.NewRequest("POST", client.url, bytes.NewBuffer(body)) + if err != nil { + return req, err + } + + req.Header.Add("Authorization", "Bearer "+client.apiKey) + req.Header.Add("Content-Type", "application/json") + + return req, nil +} + +func getCompletionsForImage(model string, temperature float64, prompt, imageName string, imageData []byte) (OpenAiRequestBody, error) { + request := OpenAiRequestBody{ + Model: model, + Temperature: temperature, + } + + // TODO: Add build pattern here that deals with errors in some internal state? + // I want a monad!!! + err := request.AddSystem(prompt) + if err != nil { + return request, err + } + + err = request.AddImage(imageName, imageData) + if err != nil { + return request, err + } + + return request, nil +} + +func (client OpenAiClient) GetImageInfo(imageName string, imageData []byte) (ImageInfo, error) { + aiRequest, err := getCompletionsForImage("gpt-4o-mini", 1.0, client.systemPrompt, imageName, imageData) + if err != nil { + return ImageInfo{}, err + } + + var jsonSchema any + err = json.Unmarshal([]byte(RESPONSE_FORMAT), &jsonSchema) + if err != nil { + return ImageInfo{}, err + } + + aiRequest.ResponseFormat = ResponseFormat{ + Type: "json_schema", + JsonSchema: jsonSchema, + } + + jsonAiRequest, err := json.Marshal(aiRequest) + if err != nil { + return ImageInfo{}, err + } + + request, err := client.getRequest(jsonAiRequest) + if err != nil { + return ImageInfo{}, err + } + + resp, err := client.Do(request) + if err != nil { + return ImageInfo{}, err + } + + response, err := io.ReadAll(resp.Body) + if err != nil { + return ImageInfo{}, err + } + + info := ImageInfo{} + err = json.Unmarshal(response, &info) + if err != nil { + return ImageInfo{}, err + } + + log.Println(string(response)) + + return info, nil +} diff --git a/backend/openai_test.go b/backend/openai_test.go new file mode 100644 index 0000000..3a2f663 --- /dev/null +++ b/backend/openai_test.go @@ -0,0 +1,151 @@ +package main + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "testing" +) + +func TestMessageBuilder(t *testing.T) { + content := OpenAiMessages{} + + err := content.AddSystem("Some prompt") + + if err != nil { + t.Log(err) + t.FailNow() + } + + if len(content.Messages) != 1 { + t.Logf("Expected length 1, got %d.\n", len(content.Messages)) + t.FailNow() + } +} + +func TestMessageBuilderImage(t *testing.T) { + content := OpenAiMessages{} + + prompt := "some prompt" + imageTitle := "image.png" + data := []byte("some data") + + content.AddSystem(prompt) + content.AddImage(imageTitle, data) + + if len(content.Messages) != 2 { + t.Logf("Expected length 2, got %d.\n", len(content.Messages)) + t.FailNow() + } + + promptMessage, ok := content.Messages[0].(OpenAiTextMessage) + if !ok { + t.Logf("Expected text content message, got %T\n", content.Messages[0]) + t.FailNow() + } + + if promptMessage.Role != ROLE_SYSTEM { + t.Log("Prompt message role is incorrect.") + t.FailNow() + } + + if promptMessage.Content != prompt { + t.Log("Prompt message content is incorrect.") + t.FailNow() + } + + arrayContentMessage, ok := content.Messages[1].(OpenAiArrayMessage) + if !ok { + t.Logf("Expected text content message, got %T\n", content.Messages[1]) + t.FailNow() + } + + if arrayContentMessage.Role != ROLE_USER { + t.Log("Array content message role is incorrect.") + t.FailNow() + } + + if len(arrayContentMessage.Content) != 1 { + t.Logf("Expected length 1, got %d.\n", len(arrayContentMessage.Content)) + t.FailNow() + } + + imageContent, ok := arrayContentMessage.Content[0].(OpenAiImage) + if !ok { + t.Logf("Expected text content message, got %T\n", arrayContentMessage.Content[0]) + t.FailNow() + } + + base64data := base64.StdEncoding.EncodeToString(data) + url := fmt.Sprintf("data:image/%s;base64,%s", "png", base64data) + + if imageContent.ImageUrl.Url != url { + t.Logf("Expected %s, but got %s.\n", url, imageContent.ImageUrl.Url) + t.FailNow() + } +} + +func TestFullImageRequest(t *testing.T) { + request, err := getCompletionsForImage("model", 0.1, "You are an assistant", "image.png", []byte("some data")) + if err != nil { + t.Log(request) + t.FailNow() + } + + jsonData, err := json.Marshal(request) + if err != nil { + t.Log(err) + t.FailNow() + } + + expectedJson := `{"model":"model","temperature":0.1,"response_format":{"type":"","json_schema":""},"messages":[{"role":"system","content":"You are an assistant"},{"role":"user","content":[{"type":"image_url","image_url":{"url":"data:image/png;base64,c29tZSBkYXRh"}}]}]}` + + if string(jsonData) != expectedJson { + t.Logf("Expected:\n%s\n Got:\n%s\n", expectedJson, string(jsonData)) + t.FailNow() + } +} + +func TestResponse(t *testing.T) { + testResponse := `{"tags": ["tag1", "tag2"], "text": ["text1"], "links": []}` + buffer := bytes.NewReader([]byte(testResponse)) + + body := io.NopCloser(buffer) + + client := OpenAiClient{ + url: "http://localhost:1234", + apiKey: "some-key", + Do: func(_req *http.Request) (*http.Response, error) { + return &http.Response{Body: body}, nil + }, + } + + info, err := client.GetImageInfo("image.png", []byte("some data")) + if err != nil { + t.Log(err) + t.FailNow() + } + + if len(info.Tags) != 2 || len(info.Text) != 1 || len(info.Links) != 0 { + t.Logf("Some lengths are wrong.\nTags: %d\nText: %d\nLinks: %d\n", len(info.Tags), len(info.Text), len(info.Links)) + t.FailNow() + } + + if info.Tags[0] != "tag1" { + t.Log("0th tag is wrong.") + t.FailNow() + } + + if info.Tags[1] != "tag2" { + t.Log("1th tag is wrong.") + t.FailNow() + } + + if info.Text[0] != "text1" { + t.Log("0th text is wrong.") + t.FailNow() + } +} diff --git a/backend/schema.sql b/backend/schema.sql new file mode 100644 index 0000000..edbd83d --- /dev/null +++ b/backend/schema.sql @@ -0,0 +1,56 @@ +DROP SCHEMA IF EXISTS haystack CASCADE; + +CREATE SCHEMA haystack; + +/* -----| Schema tables |----- */ + +CREATE TABLE haystack.users ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid() +); + +CREATE TABLE haystack.user_images ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + image_name TEXT NOT NULL, + image BYTEA NOT NULL, + user_id uuid NOT NULL REFERENCES haystack.users (id) +); + +CREATE TABLE haystack.user_tags ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + tag TEXT NOT NULL, + user_id uuid NOT NULL REFERENCES haystack.users (id) +); + +CREATE TABLE haystack.image_tags ( + tag_id UUID NOT NULL REFERENCES haystack.user_tags (id), + image_id UUID NOT NULL REFERENCES haystack.user_images (id) +); + +CREATE TABLE haystack.image_text ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + image_text TEXT NOT NULL, + image_id UUID NOT NULL REFERENCES haystack.user_images (id) +); + +CREATE TABLE haystack.image_links ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + link TEXT NOT NULL, + image_id UUID NOT NULL REFERENCES haystack.user_images (id) +); + +/* -----| Stored Procedures |----- */ + +CREATE OR REPLACE FUNCTION notify_new_image() +RETURNS TRIGGER AS $$ +BEGIN + PERFORM pg_notify('new_image', NEW.id::texT); + RETURN NEW; +END +$$ LANGUAGE plpgsql; + +/* -----| Triggers |----- */ + +CREATE OR REPLACE TRIGGER on_new_image AFTER INSERT +ON haystack.user_images +FOR EACH ROW +EXECUTE PROCEDURE notify_new_image()