Compare commits
126 Commits
00e530df4b
...
main
Author | SHA1 | Date | |
---|---|---|---|
106d3b1fa1 | |||
b9f6b77286 | |||
3c8fd843e6 | |||
e61af3007f | |||
3594baceb5 | |||
d534779fad | |||
a776c88cab | |||
72de7c7648 | |||
a8b150857c | |||
dd4f508346 | |||
f21ee57632 | |||
0e42c9002b | |||
9e60a41f0a | |||
eaff553dc9 | |||
6880811236 | |||
38bda46dcf | |||
bd86ad499b | |||
838ab37fc1 | |||
9948d2521b | |||
64abf79f9c | |||
0d41a65435 | |||
ecd1529130 | |||
015a7cb5cd | |||
980b42aa44 | |||
649cfe0b02 | |||
1fb9616aa7 | |||
013447fa90 | |||
221afb599b | |||
f8619d3ef7 | |||
f6393c9a59 | |||
561064a194 | |||
3015d7bac2 | |||
a3345afbfa | |||
f078ac7d0b | |||
e28d9e5d16 | |||
29c56bee1c | |||
3ebc0810e7 | |||
0c595f76a3 | |||
176d2b0bd4 | |||
115d08a245 | |||
b4b600bd7c | |||
ce2cd977ac | |||
8b6b9453a8 | |||
2dd9f33303 | |||
94ee8bdb7e | |||
5d1c758451 | |||
00359e2e8d | |||
95330c163b | |||
84a0996be9 | |||
48579267b5 | |||
8b54d502f2 | |||
e45688d57e | |||
f7c9c97f0a | |||
76924a0332 | |||
d97593d487 | |||
de96f12b55 | |||
70161da3ed | |||
3a182fc49b | |||
ec7bd469f9 | |||
6523b10699 | |||
61d2b81e8c | |||
fe0968716d | |||
769f3981cd | |||
a78f766122 | |||
10cea769bf | |||
f5e65524aa | |||
390a216260 | |||
3e57d10360 | |||
28a4b37dde | |||
4de4431390 | |||
5ff7788a7b | |||
13170a33e8 | |||
5024933852 | |||
706d562e3e | |||
fda09ae07a | |||
5de5e0b56e | |||
a0bf27dd16 | |||
3d05ff708e | |||
ee109f05a0 | |||
f4d8c9f083 | |||
a1af3feb1d | |||
8597584cf0 | |||
88d033314e | |||
9cae780431 | |||
fa71f68de5 | |||
0058cdce40 | |||
37f966e508 | |||
59bf884f5d | |||
2ac996db73 | |||
e19e6562bb | |||
a283bc1bcd | |||
1b816e512a | |||
4d0dcccf94 | |||
bc4e4ab36c | |||
a663f27fb2 | |||
744b300d00 | |||
818a163235 | |||
2b1eb2b948 | |||
018f0e96d4 | |||
d5594c6e32 | |||
7d9845737e | |||
68010503ab | |||
251e2bc553 | |||
5d0fa51e01 | |||
ad4967a97d | |||
eb0914c9ca | |||
5c8e0094f4 | |||
4b85cae22c | |||
75b2cc53a4 | |||
bb3ae507ea | |||
0d5e6146f2 | |||
ec18cb0ee0 | |||
510cb3012b | |||
6e2c6acd9d | |||
5130691ab9 | |||
1a9731c4bb | |||
300a4925df | |||
4870a8b1b1 | |||
275ae4598f | |||
eeb6d2bb3b | |||
a89c6dc658 | |||
c6ad67345e | |||
459c8e1c4e | |||
ec4e8b7e2a | |||
5a1f3bb75b | |||
d4b14605c1 |
@ -1,24 +0,0 @@
|
||||
//
|
||||
// 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"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Events struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
Name string
|
||||
Description *string
|
||||
StartDateTime *time.Time
|
||||
EndDateTime *time.Time
|
||||
LocationID *uuid.UUID
|
||||
OrganizerID *uuid.UUID
|
||||
CreatedAt *time.Time
|
||||
}
|
@ -9,10 +9,15 @@ package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Image struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
ImageName string
|
||||
Image []byte
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
UserID uuid.UUID
|
||||
ImageName string
|
||||
Description string
|
||||
Status Progress
|
||||
Image []byte
|
||||
CreatedAt *time.Time
|
||||
}
|
||||
|
@ -1,20 +0,0 @@
|
||||
//
|
||||
// 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"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ImageContacts struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
ImageID uuid.UUID
|
||||
ContactID uuid.UUID
|
||||
CreatedAt *time.Time
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
//
|
||||
// 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"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ImageEvents struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
EventID uuid.UUID
|
||||
ImageID uuid.UUID
|
||||
CreatedAt *time.Time
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
//
|
||||
// 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"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ImageLocations struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
LocationID uuid.UUID
|
||||
ImageID uuid.UUID
|
||||
CreatedAt *time.Time
|
||||
}
|
@ -9,12 +9,11 @@ package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ImageNotes struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
ImageID uuid.UUID
|
||||
NoteID uuid.UUID
|
||||
CreatedAt *time.Time
|
||||
type ImageSchemaItems struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
Value *string
|
||||
SchemaItemID uuid.UUID
|
||||
ImageID uuid.UUID
|
||||
}
|
@ -11,8 +11,8 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ImageLinks struct {
|
||||
type ImageStacks struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
Link string
|
||||
ImageID uuid.UUID
|
||||
StackID uuid.UUID
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
//
|
||||
// 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 {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
TagID uuid.UUID
|
||||
ImageID uuid.UUID
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
//
|
||||
// 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"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Locations struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
Name string
|
||||
Address *string
|
||||
Description *string
|
||||
CreatedAt *time.Time
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
//
|
||||
// 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"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Logs struct {
|
||||
Log string
|
||||
ImageID uuid.UUID
|
||||
CreatedAt *time.Time
|
||||
}
|
@ -9,13 +9,12 @@ package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Notes struct {
|
||||
type SchemaItems struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
Name string
|
||||
Description *string
|
||||
Content string
|
||||
CreatedAt *time.Time
|
||||
Item string
|
||||
Value string
|
||||
Description string
|
||||
StackID uuid.UUID
|
||||
}
|
@ -12,11 +12,11 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Contacts struct {
|
||||
type Stacks struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
UserID uuid.UUID
|
||||
Status Progress
|
||||
Name string
|
||||
Description *string
|
||||
PhoneNumber *string
|
||||
Email *string
|
||||
Description string
|
||||
CreatedAt *time.Time
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
//
|
||||
// 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"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserContacts struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
UserID uuid.UUID
|
||||
ContactID uuid.UUID
|
||||
CreatedAt *time.Time
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
//
|
||||
// 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"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserEvents struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
EventID uuid.UUID
|
||||
UserID uuid.UUID
|
||||
CreatedAt *time.Time
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
//
|
||||
// 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"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserImages struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
ImageID uuid.UUID
|
||||
UserID uuid.UUID
|
||||
CreatedAt *time.Time
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
//
|
||||
// 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 UserImagesToProcess struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
Status Progress
|
||||
ImageID uuid.UUID
|
||||
UserID uuid.UUID
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
//
|
||||
// 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"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserLocations struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
LocationID uuid.UUID
|
||||
UserID uuid.UUID
|
||||
CreatedAt *time.Time
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
//
|
||||
// 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"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserNotes struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
UserID uuid.UUID
|
||||
NoteID uuid.UUID
|
||||
CreatedAt *time.Time
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
}
|
@ -9,9 +9,11 @@ package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Users struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
Email string
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
Email string
|
||||
CreatedAt *time.Time
|
||||
}
|
||||
|
@ -1,90 +0,0 @@
|
||||
//
|
||||
// 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 Contacts = newContactsTable("haystack", "contacts", "")
|
||||
|
||||
type contactsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
Name postgres.ColumnString
|
||||
Description postgres.ColumnString
|
||||
PhoneNumber postgres.ColumnString
|
||||
Email postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type ContactsTable struct {
|
||||
contactsTable
|
||||
|
||||
EXCLUDED contactsTable
|
||||
}
|
||||
|
||||
// AS creates new ContactsTable with assigned alias
|
||||
func (a ContactsTable) AS(alias string) *ContactsTable {
|
||||
return newContactsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new ContactsTable with assigned schema name
|
||||
func (a ContactsTable) FromSchema(schemaName string) *ContactsTable {
|
||||
return newContactsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new ContactsTable with assigned table prefix
|
||||
func (a ContactsTable) WithPrefix(prefix string) *ContactsTable {
|
||||
return newContactsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new ContactsTable with assigned table suffix
|
||||
func (a ContactsTable) WithSuffix(suffix string) *ContactsTable {
|
||||
return newContactsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newContactsTable(schemaName, tableName, alias string) *ContactsTable {
|
||||
return &ContactsTable{
|
||||
contactsTable: newContactsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newContactsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newContactsTableImpl(schemaName, tableName, alias string) contactsTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
NameColumn = postgres.StringColumn("name")
|
||||
DescriptionColumn = postgres.StringColumn("description")
|
||||
PhoneNumberColumn = postgres.StringColumn("phone_number")
|
||||
EmailColumn = postgres.StringColumn("email")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, NameColumn, DescriptionColumn, PhoneNumberColumn, EmailColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{NameColumn, DescriptionColumn, PhoneNumberColumn, EmailColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return contactsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
Name: NameColumn,
|
||||
Description: DescriptionColumn,
|
||||
PhoneNumber: PhoneNumberColumn,
|
||||
Email: EmailColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
//
|
||||
// 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 Events = newEventsTable("haystack", "events", "")
|
||||
|
||||
type eventsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
Name postgres.ColumnString
|
||||
Description postgres.ColumnString
|
||||
StartDateTime postgres.ColumnTimestamp
|
||||
EndDateTime postgres.ColumnTimestamp
|
||||
LocationID postgres.ColumnString
|
||||
OrganizerID postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type EventsTable struct {
|
||||
eventsTable
|
||||
|
||||
EXCLUDED eventsTable
|
||||
}
|
||||
|
||||
// AS creates new EventsTable with assigned alias
|
||||
func (a EventsTable) AS(alias string) *EventsTable {
|
||||
return newEventsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new EventsTable with assigned schema name
|
||||
func (a EventsTable) FromSchema(schemaName string) *EventsTable {
|
||||
return newEventsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new EventsTable with assigned table prefix
|
||||
func (a EventsTable) WithPrefix(prefix string) *EventsTable {
|
||||
return newEventsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new EventsTable with assigned table suffix
|
||||
func (a EventsTable) WithSuffix(suffix string) *EventsTable {
|
||||
return newEventsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newEventsTable(schemaName, tableName, alias string) *EventsTable {
|
||||
return &EventsTable{
|
||||
eventsTable: newEventsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newEventsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newEventsTableImpl(schemaName, tableName, alias string) eventsTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
NameColumn = postgres.StringColumn("name")
|
||||
DescriptionColumn = postgres.StringColumn("description")
|
||||
StartDateTimeColumn = postgres.TimestampColumn("start_date_time")
|
||||
EndDateTimeColumn = postgres.TimestampColumn("end_date_time")
|
||||
LocationIDColumn = postgres.StringColumn("location_id")
|
||||
OrganizerIDColumn = postgres.StringColumn("organizer_id")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, NameColumn, DescriptionColumn, StartDateTimeColumn, EndDateTimeColumn, LocationIDColumn, OrganizerIDColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{NameColumn, DescriptionColumn, StartDateTimeColumn, EndDateTimeColumn, LocationIDColumn, OrganizerIDColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return eventsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
Name: NameColumn,
|
||||
Description: DescriptionColumn,
|
||||
StartDateTime: StartDateTimeColumn,
|
||||
EndDateTime: EndDateTimeColumn,
|
||||
LocationID: LocationIDColumn,
|
||||
OrganizerID: OrganizerIDColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
@ -17,9 +17,13 @@ type imageTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
ImageName postgres.ColumnString
|
||||
Image postgres.ColumnString
|
||||
ID postgres.ColumnString
|
||||
UserID postgres.ColumnString
|
||||
ImageName postgres.ColumnString
|
||||
Description postgres.ColumnString
|
||||
Status postgres.ColumnString
|
||||
Image postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
@ -60,20 +64,28 @@ func newImageTable(schemaName, tableName, alias string) *ImageTable {
|
||||
|
||||
func newImageTableImpl(schemaName, tableName, alias string) imageTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
ImageNameColumn = postgres.StringColumn("image_name")
|
||||
ImageColumn = postgres.StringColumn("image")
|
||||
allColumns = postgres.ColumnList{IDColumn, ImageNameColumn, ImageColumn}
|
||||
mutableColumns = postgres.ColumnList{ImageNameColumn, ImageColumn}
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
UserIDColumn = postgres.StringColumn("user_id")
|
||||
ImageNameColumn = postgres.StringColumn("image_name")
|
||||
DescriptionColumn = postgres.StringColumn("description")
|
||||
StatusColumn = postgres.StringColumn("status")
|
||||
ImageColumn = postgres.StringColumn("image")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, UserIDColumn, ImageNameColumn, DescriptionColumn, StatusColumn, ImageColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{UserIDColumn, ImageNameColumn, DescriptionColumn, StatusColumn, ImageColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return imageTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
ImageName: ImageNameColumn,
|
||||
Image: ImageColumn,
|
||||
ID: IDColumn,
|
||||
UserID: UserIDColumn,
|
||||
ImageName: ImageNameColumn,
|
||||
Description: DescriptionColumn,
|
||||
Status: StatusColumn,
|
||||
Image: ImageColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
|
@ -1,84 +0,0 @@
|
||||
//
|
||||
// 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 ImageContacts = newImageContactsTable("haystack", "image_contacts", "")
|
||||
|
||||
type imageContactsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
ImageID postgres.ColumnString
|
||||
ContactID postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type ImageContactsTable struct {
|
||||
imageContactsTable
|
||||
|
||||
EXCLUDED imageContactsTable
|
||||
}
|
||||
|
||||
// AS creates new ImageContactsTable with assigned alias
|
||||
func (a ImageContactsTable) AS(alias string) *ImageContactsTable {
|
||||
return newImageContactsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new ImageContactsTable with assigned schema name
|
||||
func (a ImageContactsTable) FromSchema(schemaName string) *ImageContactsTable {
|
||||
return newImageContactsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new ImageContactsTable with assigned table prefix
|
||||
func (a ImageContactsTable) WithPrefix(prefix string) *ImageContactsTable {
|
||||
return newImageContactsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new ImageContactsTable with assigned table suffix
|
||||
func (a ImageContactsTable) WithSuffix(suffix string) *ImageContactsTable {
|
||||
return newImageContactsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newImageContactsTable(schemaName, tableName, alias string) *ImageContactsTable {
|
||||
return &ImageContactsTable{
|
||||
imageContactsTable: newImageContactsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newImageContactsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newImageContactsTableImpl(schemaName, tableName, alias string) imageContactsTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
ImageIDColumn = postgres.StringColumn("image_id")
|
||||
ContactIDColumn = postgres.StringColumn("contact_id")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, ContactIDColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{ImageIDColumn, ContactIDColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return imageContactsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
ContactID: ContactIDColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
//
|
||||
// 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 ImageEvents = newImageEventsTable("haystack", "image_events", "")
|
||||
|
||||
type imageEventsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
EventID postgres.ColumnString
|
||||
ImageID postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type ImageEventsTable struct {
|
||||
imageEventsTable
|
||||
|
||||
EXCLUDED imageEventsTable
|
||||
}
|
||||
|
||||
// AS creates new ImageEventsTable with assigned alias
|
||||
func (a ImageEventsTable) AS(alias string) *ImageEventsTable {
|
||||
return newImageEventsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new ImageEventsTable with assigned schema name
|
||||
func (a ImageEventsTable) FromSchema(schemaName string) *ImageEventsTable {
|
||||
return newImageEventsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new ImageEventsTable with assigned table prefix
|
||||
func (a ImageEventsTable) WithPrefix(prefix string) *ImageEventsTable {
|
||||
return newImageEventsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new ImageEventsTable with assigned table suffix
|
||||
func (a ImageEventsTable) WithSuffix(suffix string) *ImageEventsTable {
|
||||
return newImageEventsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newImageEventsTable(schemaName, tableName, alias string) *ImageEventsTable {
|
||||
return &ImageEventsTable{
|
||||
imageEventsTable: newImageEventsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newImageEventsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newImageEventsTableImpl(schemaName, tableName, alias string) imageEventsTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
EventIDColumn = postgres.StringColumn("event_id")
|
||||
ImageIDColumn = postgres.StringColumn("image_id")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, EventIDColumn, ImageIDColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{EventIDColumn, ImageIDColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return imageEventsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
EventID: EventIDColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
//
|
||||
// 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,
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
//
|
||||
// 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 ImageLocations = newImageLocationsTable("haystack", "image_locations", "")
|
||||
|
||||
type imageLocationsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
LocationID postgres.ColumnString
|
||||
ImageID postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type ImageLocationsTable struct {
|
||||
imageLocationsTable
|
||||
|
||||
EXCLUDED imageLocationsTable
|
||||
}
|
||||
|
||||
// AS creates new ImageLocationsTable with assigned alias
|
||||
func (a ImageLocationsTable) AS(alias string) *ImageLocationsTable {
|
||||
return newImageLocationsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new ImageLocationsTable with assigned schema name
|
||||
func (a ImageLocationsTable) FromSchema(schemaName string) *ImageLocationsTable {
|
||||
return newImageLocationsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new ImageLocationsTable with assigned table prefix
|
||||
func (a ImageLocationsTable) WithPrefix(prefix string) *ImageLocationsTable {
|
||||
return newImageLocationsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new ImageLocationsTable with assigned table suffix
|
||||
func (a ImageLocationsTable) WithSuffix(suffix string) *ImageLocationsTable {
|
||||
return newImageLocationsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newImageLocationsTable(schemaName, tableName, alias string) *ImageLocationsTable {
|
||||
return &ImageLocationsTable{
|
||||
imageLocationsTable: newImageLocationsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newImageLocationsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newImageLocationsTableImpl(schemaName, tableName, alias string) imageLocationsTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
LocationIDColumn = postgres.StringColumn("location_id")
|
||||
ImageIDColumn = postgres.StringColumn("image_id")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, LocationIDColumn, ImageIDColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{LocationIDColumn, ImageIDColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return imageLocationsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
LocationID: LocationIDColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
//
|
||||
// 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 ImageNotes = newImageNotesTable("haystack", "image_notes", "")
|
||||
|
||||
type imageNotesTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
ImageID postgres.ColumnString
|
||||
NoteID postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type ImageNotesTable struct {
|
||||
imageNotesTable
|
||||
|
||||
EXCLUDED imageNotesTable
|
||||
}
|
||||
|
||||
// AS creates new ImageNotesTable with assigned alias
|
||||
func (a ImageNotesTable) AS(alias string) *ImageNotesTable {
|
||||
return newImageNotesTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new ImageNotesTable with assigned schema name
|
||||
func (a ImageNotesTable) FromSchema(schemaName string) *ImageNotesTable {
|
||||
return newImageNotesTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new ImageNotesTable with assigned table prefix
|
||||
func (a ImageNotesTable) WithPrefix(prefix string) *ImageNotesTable {
|
||||
return newImageNotesTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new ImageNotesTable with assigned table suffix
|
||||
func (a ImageNotesTable) WithSuffix(suffix string) *ImageNotesTable {
|
||||
return newImageNotesTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newImageNotesTable(schemaName, tableName, alias string) *ImageNotesTable {
|
||||
return &ImageNotesTable{
|
||||
imageNotesTable: newImageNotesTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newImageNotesTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newImageNotesTableImpl(schemaName, tableName, alias string) imageNotesTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
ImageIDColumn = postgres.StringColumn("image_id")
|
||||
NoteIDColumn = postgres.StringColumn("note_id")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, NoteIDColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{ImageIDColumn, NoteIDColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return imageNotesTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
NoteID: NoteIDColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
84
backend/.gen/haystack/haystack/table/image_schema_items.go
Normal file
84
backend/.gen/haystack/haystack/table/image_schema_items.go
Normal file
@ -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 ImageSchemaItems = newImageSchemaItemsTable("haystack", "image_schema_items", "")
|
||||
|
||||
type imageSchemaItemsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
Value postgres.ColumnString
|
||||
SchemaItemID postgres.ColumnString
|
||||
ImageID postgres.ColumnString
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type ImageSchemaItemsTable struct {
|
||||
imageSchemaItemsTable
|
||||
|
||||
EXCLUDED imageSchemaItemsTable
|
||||
}
|
||||
|
||||
// AS creates new ImageSchemaItemsTable with assigned alias
|
||||
func (a ImageSchemaItemsTable) AS(alias string) *ImageSchemaItemsTable {
|
||||
return newImageSchemaItemsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new ImageSchemaItemsTable with assigned schema name
|
||||
func (a ImageSchemaItemsTable) FromSchema(schemaName string) *ImageSchemaItemsTable {
|
||||
return newImageSchemaItemsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new ImageSchemaItemsTable with assigned table prefix
|
||||
func (a ImageSchemaItemsTable) WithPrefix(prefix string) *ImageSchemaItemsTable {
|
||||
return newImageSchemaItemsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new ImageSchemaItemsTable with assigned table suffix
|
||||
func (a ImageSchemaItemsTable) WithSuffix(suffix string) *ImageSchemaItemsTable {
|
||||
return newImageSchemaItemsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newImageSchemaItemsTable(schemaName, tableName, alias string) *ImageSchemaItemsTable {
|
||||
return &ImageSchemaItemsTable{
|
||||
imageSchemaItemsTable: newImageSchemaItemsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newImageSchemaItemsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newImageSchemaItemsTableImpl(schemaName, tableName, alias string) imageSchemaItemsTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
ValueColumn = postgres.StringColumn("value")
|
||||
SchemaItemIDColumn = postgres.StringColumn("schema_item_id")
|
||||
ImageIDColumn = postgres.StringColumn("image_id")
|
||||
allColumns = postgres.ColumnList{IDColumn, ValueColumn, SchemaItemIDColumn, ImageIDColumn}
|
||||
mutableColumns = postgres.ColumnList{ValueColumn, SchemaItemIDColumn, ImageIDColumn}
|
||||
)
|
||||
|
||||
return imageSchemaItemsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
Value: ValueColumn,
|
||||
SchemaItemID: SchemaItemIDColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
81
backend/.gen/haystack/haystack/table/image_stacks.go
Normal file
81
backend/.gen/haystack/haystack/table/image_stacks.go
Normal file
@ -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 ImageStacks = newImageStacksTable("haystack", "image_stacks", "")
|
||||
|
||||
type imageStacksTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
ImageID postgres.ColumnString
|
||||
StackID postgres.ColumnString
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type ImageStacksTable struct {
|
||||
imageStacksTable
|
||||
|
||||
EXCLUDED imageStacksTable
|
||||
}
|
||||
|
||||
// AS creates new ImageStacksTable with assigned alias
|
||||
func (a ImageStacksTable) AS(alias string) *ImageStacksTable {
|
||||
return newImageStacksTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new ImageStacksTable with assigned schema name
|
||||
func (a ImageStacksTable) FromSchema(schemaName string) *ImageStacksTable {
|
||||
return newImageStacksTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new ImageStacksTable with assigned table prefix
|
||||
func (a ImageStacksTable) WithPrefix(prefix string) *ImageStacksTable {
|
||||
return newImageStacksTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new ImageStacksTable with assigned table suffix
|
||||
func (a ImageStacksTable) WithSuffix(suffix string) *ImageStacksTable {
|
||||
return newImageStacksTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newImageStacksTable(schemaName, tableName, alias string) *ImageStacksTable {
|
||||
return &ImageStacksTable{
|
||||
imageStacksTable: newImageStacksTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newImageStacksTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newImageStacksTableImpl(schemaName, tableName, alias string) imageStacksTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
ImageIDColumn = postgres.StringColumn("image_id")
|
||||
StackIDColumn = postgres.StringColumn("stack_id")
|
||||
allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, StackIDColumn}
|
||||
mutableColumns = postgres.ColumnList{ImageIDColumn, StackIDColumn}
|
||||
)
|
||||
|
||||
return imageStacksTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
StackID: StackIDColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
ID postgres.ColumnString
|
||||
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 (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
TagIDColumn = postgres.StringColumn("tag_id")
|
||||
ImageIDColumn = postgres.StringColumn("image_id")
|
||||
allColumns = postgres.ColumnList{IDColumn, TagIDColumn, ImageIDColumn}
|
||||
mutableColumns = postgres.ColumnList{TagIDColumn, ImageIDColumn}
|
||||
)
|
||||
|
||||
return imageTagsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
TagID: TagIDColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
//
|
||||
// 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,
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
//
|
||||
// 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 Locations = newLocationsTable("haystack", "locations", "")
|
||||
|
||||
type locationsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
Name postgres.ColumnString
|
||||
Address postgres.ColumnString
|
||||
Description postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type LocationsTable struct {
|
||||
locationsTable
|
||||
|
||||
EXCLUDED locationsTable
|
||||
}
|
||||
|
||||
// AS creates new LocationsTable with assigned alias
|
||||
func (a LocationsTable) AS(alias string) *LocationsTable {
|
||||
return newLocationsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new LocationsTable with assigned schema name
|
||||
func (a LocationsTable) FromSchema(schemaName string) *LocationsTable {
|
||||
return newLocationsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new LocationsTable with assigned table prefix
|
||||
func (a LocationsTable) WithPrefix(prefix string) *LocationsTable {
|
||||
return newLocationsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new LocationsTable with assigned table suffix
|
||||
func (a LocationsTable) WithSuffix(suffix string) *LocationsTable {
|
||||
return newLocationsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newLocationsTable(schemaName, tableName, alias string) *LocationsTable {
|
||||
return &LocationsTable{
|
||||
locationsTable: newLocationsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newLocationsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newLocationsTableImpl(schemaName, tableName, alias string) locationsTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
NameColumn = postgres.StringColumn("name")
|
||||
AddressColumn = postgres.StringColumn("address")
|
||||
DescriptionColumn = postgres.StringColumn("description")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, NameColumn, AddressColumn, DescriptionColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{NameColumn, AddressColumn, DescriptionColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return locationsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
Name: NameColumn,
|
||||
Address: AddressColumn,
|
||||
Description: DescriptionColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
//
|
||||
// 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 Logs = newLogsTable("haystack", "logs", "")
|
||||
|
||||
type logsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
Log postgres.ColumnString
|
||||
ImageID postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type LogsTable struct {
|
||||
logsTable
|
||||
|
||||
EXCLUDED logsTable
|
||||
}
|
||||
|
||||
// AS creates new LogsTable with assigned alias
|
||||
func (a LogsTable) AS(alias string) *LogsTable {
|
||||
return newLogsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new LogsTable with assigned schema name
|
||||
func (a LogsTable) FromSchema(schemaName string) *LogsTable {
|
||||
return newLogsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new LogsTable with assigned table prefix
|
||||
func (a LogsTable) WithPrefix(prefix string) *LogsTable {
|
||||
return newLogsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new LogsTable with assigned table suffix
|
||||
func (a LogsTable) WithSuffix(suffix string) *LogsTable {
|
||||
return newLogsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newLogsTable(schemaName, tableName, alias string) *LogsTable {
|
||||
return &LogsTable{
|
||||
logsTable: newLogsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newLogsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newLogsTableImpl(schemaName, tableName, alias string) logsTable {
|
||||
var (
|
||||
LogColumn = postgres.StringColumn("log")
|
||||
ImageIDColumn = postgres.StringColumn("image_id")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{LogColumn, ImageIDColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{LogColumn, ImageIDColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return logsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
Log: LogColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
//
|
||||
// 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 Notes = newNotesTable("haystack", "notes", "")
|
||||
|
||||
type notesTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
Name postgres.ColumnString
|
||||
Description postgres.ColumnString
|
||||
Content postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type NotesTable struct {
|
||||
notesTable
|
||||
|
||||
EXCLUDED notesTable
|
||||
}
|
||||
|
||||
// AS creates new NotesTable with assigned alias
|
||||
func (a NotesTable) AS(alias string) *NotesTable {
|
||||
return newNotesTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new NotesTable with assigned schema name
|
||||
func (a NotesTable) FromSchema(schemaName string) *NotesTable {
|
||||
return newNotesTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new NotesTable with assigned table prefix
|
||||
func (a NotesTable) WithPrefix(prefix string) *NotesTable {
|
||||
return newNotesTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new NotesTable with assigned table suffix
|
||||
func (a NotesTable) WithSuffix(suffix string) *NotesTable {
|
||||
return newNotesTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newNotesTable(schemaName, tableName, alias string) *NotesTable {
|
||||
return &NotesTable{
|
||||
notesTable: newNotesTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newNotesTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newNotesTableImpl(schemaName, tableName, alias string) notesTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
NameColumn = postgres.StringColumn("name")
|
||||
DescriptionColumn = postgres.StringColumn("description")
|
||||
ContentColumn = postgres.StringColumn("content")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, NameColumn, DescriptionColumn, ContentColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{NameColumn, DescriptionColumn, ContentColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return notesTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
Name: NameColumn,
|
||||
Description: DescriptionColumn,
|
||||
Content: ContentColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
87
backend/.gen/haystack/haystack/table/schema_items.go
Normal file
87
backend/.gen/haystack/haystack/table/schema_items.go
Normal file
@ -0,0 +1,87 @@
|
||||
//
|
||||
// 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 SchemaItems = newSchemaItemsTable("haystack", "schema_items", "")
|
||||
|
||||
type schemaItemsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
Item postgres.ColumnString
|
||||
Value postgres.ColumnString
|
||||
Description postgres.ColumnString
|
||||
StackID postgres.ColumnString
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type SchemaItemsTable struct {
|
||||
schemaItemsTable
|
||||
|
||||
EXCLUDED schemaItemsTable
|
||||
}
|
||||
|
||||
// AS creates new SchemaItemsTable with assigned alias
|
||||
func (a SchemaItemsTable) AS(alias string) *SchemaItemsTable {
|
||||
return newSchemaItemsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new SchemaItemsTable with assigned schema name
|
||||
func (a SchemaItemsTable) FromSchema(schemaName string) *SchemaItemsTable {
|
||||
return newSchemaItemsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new SchemaItemsTable with assigned table prefix
|
||||
func (a SchemaItemsTable) WithPrefix(prefix string) *SchemaItemsTable {
|
||||
return newSchemaItemsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new SchemaItemsTable with assigned table suffix
|
||||
func (a SchemaItemsTable) WithSuffix(suffix string) *SchemaItemsTable {
|
||||
return newSchemaItemsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newSchemaItemsTable(schemaName, tableName, alias string) *SchemaItemsTable {
|
||||
return &SchemaItemsTable{
|
||||
schemaItemsTable: newSchemaItemsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newSchemaItemsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newSchemaItemsTableImpl(schemaName, tableName, alias string) schemaItemsTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
ItemColumn = postgres.StringColumn("item")
|
||||
ValueColumn = postgres.StringColumn("value")
|
||||
DescriptionColumn = postgres.StringColumn("description")
|
||||
StackIDColumn = postgres.StringColumn("stack_id")
|
||||
allColumns = postgres.ColumnList{IDColumn, ItemColumn, ValueColumn, DescriptionColumn, StackIDColumn}
|
||||
mutableColumns = postgres.ColumnList{ItemColumn, ValueColumn, DescriptionColumn, StackIDColumn}
|
||||
)
|
||||
|
||||
return schemaItemsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
Item: ItemColumn,
|
||||
Value: ValueColumn,
|
||||
Description: DescriptionColumn,
|
||||
StackID: StackIDColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
90
backend/.gen/haystack/haystack/table/stacks.go
Normal file
90
backend/.gen/haystack/haystack/table/stacks.go
Normal file
@ -0,0 +1,90 @@
|
||||
//
|
||||
// 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 Stacks = newStacksTable("haystack", "stacks", "")
|
||||
|
||||
type stacksTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
UserID postgres.ColumnString
|
||||
Status postgres.ColumnString
|
||||
Name postgres.ColumnString
|
||||
Description postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type StacksTable struct {
|
||||
stacksTable
|
||||
|
||||
EXCLUDED stacksTable
|
||||
}
|
||||
|
||||
// AS creates new StacksTable with assigned alias
|
||||
func (a StacksTable) AS(alias string) *StacksTable {
|
||||
return newStacksTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new StacksTable with assigned schema name
|
||||
func (a StacksTable) FromSchema(schemaName string) *StacksTable {
|
||||
return newStacksTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new StacksTable with assigned table prefix
|
||||
func (a StacksTable) WithPrefix(prefix string) *StacksTable {
|
||||
return newStacksTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new StacksTable with assigned table suffix
|
||||
func (a StacksTable) WithSuffix(suffix string) *StacksTable {
|
||||
return newStacksTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newStacksTable(schemaName, tableName, alias string) *StacksTable {
|
||||
return &StacksTable{
|
||||
stacksTable: newStacksTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newStacksTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newStacksTableImpl(schemaName, tableName, alias string) stacksTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
UserIDColumn = postgres.StringColumn("user_id")
|
||||
StatusColumn = postgres.StringColumn("status")
|
||||
NameColumn = postgres.StringColumn("name")
|
||||
DescriptionColumn = postgres.StringColumn("description")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, UserIDColumn, StatusColumn, NameColumn, DescriptionColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{UserIDColumn, StatusColumn, NameColumn, DescriptionColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return stacksTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
UserID: UserIDColumn,
|
||||
Status: StatusColumn,
|
||||
Name: NameColumn,
|
||||
Description: DescriptionColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
@ -10,25 +10,10 @@ 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) {
|
||||
Contacts = Contacts.FromSchema(schema)
|
||||
Events = Events.FromSchema(schema)
|
||||
Image = Image.FromSchema(schema)
|
||||
ImageContacts = ImageContacts.FromSchema(schema)
|
||||
ImageEvents = ImageEvents.FromSchema(schema)
|
||||
ImageLinks = ImageLinks.FromSchema(schema)
|
||||
ImageLocations = ImageLocations.FromSchema(schema)
|
||||
ImageNotes = ImageNotes.FromSchema(schema)
|
||||
ImageTags = ImageTags.FromSchema(schema)
|
||||
ImageText = ImageText.FromSchema(schema)
|
||||
Locations = Locations.FromSchema(schema)
|
||||
Logs = Logs.FromSchema(schema)
|
||||
Notes = Notes.FromSchema(schema)
|
||||
UserContacts = UserContacts.FromSchema(schema)
|
||||
UserEvents = UserEvents.FromSchema(schema)
|
||||
UserImages = UserImages.FromSchema(schema)
|
||||
UserImagesToProcess = UserImagesToProcess.FromSchema(schema)
|
||||
UserLocations = UserLocations.FromSchema(schema)
|
||||
UserNotes = UserNotes.FromSchema(schema)
|
||||
UserTags = UserTags.FromSchema(schema)
|
||||
ImageSchemaItems = ImageSchemaItems.FromSchema(schema)
|
||||
ImageStacks = ImageStacks.FromSchema(schema)
|
||||
SchemaItems = SchemaItems.FromSchema(schema)
|
||||
Stacks = Stacks.FromSchema(schema)
|
||||
Users = Users.FromSchema(schema)
|
||||
}
|
||||
|
@ -1,84 +0,0 @@
|
||||
//
|
||||
// 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 UserContacts = newUserContactsTable("haystack", "user_contacts", "")
|
||||
|
||||
type userContactsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
UserID postgres.ColumnString
|
||||
ContactID postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type UserContactsTable struct {
|
||||
userContactsTable
|
||||
|
||||
EXCLUDED userContactsTable
|
||||
}
|
||||
|
||||
// AS creates new UserContactsTable with assigned alias
|
||||
func (a UserContactsTable) AS(alias string) *UserContactsTable {
|
||||
return newUserContactsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new UserContactsTable with assigned schema name
|
||||
func (a UserContactsTable) FromSchema(schemaName string) *UserContactsTable {
|
||||
return newUserContactsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new UserContactsTable with assigned table prefix
|
||||
func (a UserContactsTable) WithPrefix(prefix string) *UserContactsTable {
|
||||
return newUserContactsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new UserContactsTable with assigned table suffix
|
||||
func (a UserContactsTable) WithSuffix(suffix string) *UserContactsTable {
|
||||
return newUserContactsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newUserContactsTable(schemaName, tableName, alias string) *UserContactsTable {
|
||||
return &UserContactsTable{
|
||||
userContactsTable: newUserContactsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newUserContactsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newUserContactsTableImpl(schemaName, tableName, alias string) userContactsTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
UserIDColumn = postgres.StringColumn("user_id")
|
||||
ContactIDColumn = postgres.StringColumn("contact_id")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, UserIDColumn, ContactIDColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{UserIDColumn, ContactIDColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return userContactsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
UserID: UserIDColumn,
|
||||
ContactID: ContactIDColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
//
|
||||
// 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 UserEvents = newUserEventsTable("haystack", "user_events", "")
|
||||
|
||||
type userEventsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
EventID postgres.ColumnString
|
||||
UserID postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type UserEventsTable struct {
|
||||
userEventsTable
|
||||
|
||||
EXCLUDED userEventsTable
|
||||
}
|
||||
|
||||
// AS creates new UserEventsTable with assigned alias
|
||||
func (a UserEventsTable) AS(alias string) *UserEventsTable {
|
||||
return newUserEventsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new UserEventsTable with assigned schema name
|
||||
func (a UserEventsTable) FromSchema(schemaName string) *UserEventsTable {
|
||||
return newUserEventsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new UserEventsTable with assigned table prefix
|
||||
func (a UserEventsTable) WithPrefix(prefix string) *UserEventsTable {
|
||||
return newUserEventsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new UserEventsTable with assigned table suffix
|
||||
func (a UserEventsTable) WithSuffix(suffix string) *UserEventsTable {
|
||||
return newUserEventsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newUserEventsTable(schemaName, tableName, alias string) *UserEventsTable {
|
||||
return &UserEventsTable{
|
||||
userEventsTable: newUserEventsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newUserEventsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newUserEventsTableImpl(schemaName, tableName, alias string) userEventsTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
EventIDColumn = postgres.StringColumn("event_id")
|
||||
UserIDColumn = postgres.StringColumn("user_id")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, EventIDColumn, UserIDColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{EventIDColumn, UserIDColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return userEventsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
EventID: EventIDColumn,
|
||||
UserID: UserIDColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
ImageID postgres.ColumnString
|
||||
UserID postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
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")
|
||||
ImageIDColumn = postgres.StringColumn("image_id")
|
||||
UserIDColumn = postgres.StringColumn("user_id")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, UserIDColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{ImageIDColumn, UserIDColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return userImagesTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
UserID: UserIDColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
//
|
||||
// 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 UserImagesToProcess = newUserImagesToProcessTable("haystack", "user_images_to_process", "")
|
||||
|
||||
type userImagesToProcessTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
Status postgres.ColumnString
|
||||
ImageID postgres.ColumnString
|
||||
UserID postgres.ColumnString
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type UserImagesToProcessTable struct {
|
||||
userImagesToProcessTable
|
||||
|
||||
EXCLUDED userImagesToProcessTable
|
||||
}
|
||||
|
||||
// AS creates new UserImagesToProcessTable with assigned alias
|
||||
func (a UserImagesToProcessTable) AS(alias string) *UserImagesToProcessTable {
|
||||
return newUserImagesToProcessTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new UserImagesToProcessTable with assigned schema name
|
||||
func (a UserImagesToProcessTable) FromSchema(schemaName string) *UserImagesToProcessTable {
|
||||
return newUserImagesToProcessTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new UserImagesToProcessTable with assigned table prefix
|
||||
func (a UserImagesToProcessTable) WithPrefix(prefix string) *UserImagesToProcessTable {
|
||||
return newUserImagesToProcessTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new UserImagesToProcessTable with assigned table suffix
|
||||
func (a UserImagesToProcessTable) WithSuffix(suffix string) *UserImagesToProcessTable {
|
||||
return newUserImagesToProcessTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newUserImagesToProcessTable(schemaName, tableName, alias string) *UserImagesToProcessTable {
|
||||
return &UserImagesToProcessTable{
|
||||
userImagesToProcessTable: newUserImagesToProcessTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newUserImagesToProcessTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newUserImagesToProcessTableImpl(schemaName, tableName, alias string) userImagesToProcessTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
StatusColumn = postgres.StringColumn("status")
|
||||
ImageIDColumn = postgres.StringColumn("image_id")
|
||||
UserIDColumn = postgres.StringColumn("user_id")
|
||||
allColumns = postgres.ColumnList{IDColumn, StatusColumn, ImageIDColumn, UserIDColumn}
|
||||
mutableColumns = postgres.ColumnList{StatusColumn, ImageIDColumn, UserIDColumn}
|
||||
)
|
||||
|
||||
return userImagesToProcessTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
Status: StatusColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
UserID: UserIDColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
//
|
||||
// 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 UserLocations = newUserLocationsTable("haystack", "user_locations", "")
|
||||
|
||||
type userLocationsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
LocationID postgres.ColumnString
|
||||
UserID postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type UserLocationsTable struct {
|
||||
userLocationsTable
|
||||
|
||||
EXCLUDED userLocationsTable
|
||||
}
|
||||
|
||||
// AS creates new UserLocationsTable with assigned alias
|
||||
func (a UserLocationsTable) AS(alias string) *UserLocationsTable {
|
||||
return newUserLocationsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new UserLocationsTable with assigned schema name
|
||||
func (a UserLocationsTable) FromSchema(schemaName string) *UserLocationsTable {
|
||||
return newUserLocationsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new UserLocationsTable with assigned table prefix
|
||||
func (a UserLocationsTable) WithPrefix(prefix string) *UserLocationsTable {
|
||||
return newUserLocationsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new UserLocationsTable with assigned table suffix
|
||||
func (a UserLocationsTable) WithSuffix(suffix string) *UserLocationsTable {
|
||||
return newUserLocationsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newUserLocationsTable(schemaName, tableName, alias string) *UserLocationsTable {
|
||||
return &UserLocationsTable{
|
||||
userLocationsTable: newUserLocationsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newUserLocationsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newUserLocationsTableImpl(schemaName, tableName, alias string) userLocationsTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
LocationIDColumn = postgres.StringColumn("location_id")
|
||||
UserIDColumn = postgres.StringColumn("user_id")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, LocationIDColumn, UserIDColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{LocationIDColumn, UserIDColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return userLocationsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
LocationID: LocationIDColumn,
|
||||
UserID: UserIDColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
//
|
||||
// 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 UserNotes = newUserNotesTable("haystack", "user_notes", "")
|
||||
|
||||
type userNotesTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
UserID postgres.ColumnString
|
||||
NoteID postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type UserNotesTable struct {
|
||||
userNotesTable
|
||||
|
||||
EXCLUDED userNotesTable
|
||||
}
|
||||
|
||||
// AS creates new UserNotesTable with assigned alias
|
||||
func (a UserNotesTable) AS(alias string) *UserNotesTable {
|
||||
return newUserNotesTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new UserNotesTable with assigned schema name
|
||||
func (a UserNotesTable) FromSchema(schemaName string) *UserNotesTable {
|
||||
return newUserNotesTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new UserNotesTable with assigned table prefix
|
||||
func (a UserNotesTable) WithPrefix(prefix string) *UserNotesTable {
|
||||
return newUserNotesTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new UserNotesTable with assigned table suffix
|
||||
func (a UserNotesTable) WithSuffix(suffix string) *UserNotesTable {
|
||||
return newUserNotesTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newUserNotesTable(schemaName, tableName, alias string) *UserNotesTable {
|
||||
return &UserNotesTable{
|
||||
userNotesTable: newUserNotesTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newUserNotesTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newUserNotesTableImpl(schemaName, tableName, alias string) userNotesTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
UserIDColumn = postgres.StringColumn("user_id")
|
||||
NoteIDColumn = postgres.StringColumn("note_id")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, UserIDColumn, NoteIDColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{UserIDColumn, NoteIDColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return userNotesTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
UserID: UserIDColumn,
|
||||
NoteID: NoteIDColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
//
|
||||
// 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,
|
||||
}
|
||||
}
|
@ -17,8 +17,9 @@ type usersTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
Email postgres.ColumnString
|
||||
ID postgres.ColumnString
|
||||
Email postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
@ -59,18 +60,20 @@ func newUsersTable(schemaName, tableName, alias string) *UsersTable {
|
||||
|
||||
func newUsersTableImpl(schemaName, tableName, alias string) usersTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
EmailColumn = postgres.StringColumn("email")
|
||||
allColumns = postgres.ColumnList{IDColumn, EmailColumn}
|
||||
mutableColumns = postgres.ColumnList{EmailColumn}
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
EmailColumn = postgres.StringColumn("email")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, EmailColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{EmailColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return usersTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
Email: EmailColumn,
|
||||
ID: IDColumn,
|
||||
Email: EmailColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
|
@ -187,6 +187,15 @@ func (chat *Chat) AddSystem(prompt string) {
|
||||
})
|
||||
}
|
||||
|
||||
func (chat *Chat) AddUser(msg string) {
|
||||
chat.Messages = append(chat.Messages, ChatUserMessage{
|
||||
Role: User,
|
||||
MessageContent: SingleMessage{
|
||||
Content: msg,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (chat *Chat) AddImage(imageName string, image []byte, query *string) error {
|
||||
extension := filepath.Ext(imageName)
|
||||
if len(extension) == 0 {
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -101,7 +102,7 @@ func CreateAgentClient(options CreateAgentClientOptions) AgentClient {
|
||||
|
||||
return AgentClient{
|
||||
apiKey: apiKey,
|
||||
url: "https://api.openai.com/v1/chat/completions",
|
||||
url: "https://router.requesty.ai/v1/chat/completions",
|
||||
Do: func(req *http.Request) (*http.Response, error) {
|
||||
client := &http.Client{}
|
||||
return client.Do(req)
|
||||
@ -132,29 +133,29 @@ func (client AgentClient) getRequest(body []byte) (*http.Request, error) {
|
||||
func (client AgentClient) Request(req *AgentRequestBody) (AgentResponse, error) {
|
||||
jsonAiRequest, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return AgentResponse{}, err
|
||||
return AgentResponse{}, fmt.Errorf("Could not format JSON: %w", err)
|
||||
}
|
||||
|
||||
httpRequest, err := client.getRequest(jsonAiRequest)
|
||||
if err != nil {
|
||||
return AgentResponse{}, err
|
||||
return AgentResponse{}, fmt.Errorf("Could not get request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return AgentResponse{}, err
|
||||
return AgentResponse{}, fmt.Errorf("Could not send request: %w", err)
|
||||
}
|
||||
|
||||
response, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return AgentResponse{}, err
|
||||
return AgentResponse{}, fmt.Errorf("Could not read body: %w", err)
|
||||
}
|
||||
|
||||
agentResponse := AgentResponse{}
|
||||
err = json.Unmarshal(response, &agentResponse)
|
||||
|
||||
if err != nil {
|
||||
return AgentResponse{}, err
|
||||
return AgentResponse{}, fmt.Errorf("Could not unmarshal response, response: %s: %w", string(response), err)
|
||||
}
|
||||
|
||||
if len(agentResponse.Choices) != 1 {
|
||||
@ -245,7 +246,7 @@ func (client *AgentClient) RunAgent(userId uuid.UUID, imageId uuid.UUID, imageNa
|
||||
request := AgentRequestBody{
|
||||
Tools: &tools,
|
||||
ToolChoice: &toolChoice,
|
||||
Model: "gpt-4.1-mini",
|
||||
Model: "policy/images",
|
||||
RandomSeed: &seed,
|
||||
Temperature: 0.3,
|
||||
EndToolCall: client.Options.EndToolCall,
|
||||
@ -261,7 +262,7 @@ func (client *AgentClient) RunAgent(userId uuid.UUID, imageId uuid.UUID, imageNa
|
||||
request.Chat.AddImage(imageName, imageData, client.Options.Query)
|
||||
|
||||
toolHandlerInfo := ToolHandlerInfo{
|
||||
ImageId: imageId,
|
||||
ImageID: imageId,
|
||||
ImageName: imageName,
|
||||
UserId: userId,
|
||||
Image: &imageData,
|
||||
@ -269,3 +270,38 @@ func (client *AgentClient) RunAgent(userId uuid.UUID, imageId uuid.UUID, imageNa
|
||||
|
||||
return client.ToolLoop(toolHandlerInfo, &request)
|
||||
}
|
||||
|
||||
func (client *AgentClient) RunAgentAlone(userID uuid.UUID, userReq string) error {
|
||||
var tools any
|
||||
err := json.Unmarshal([]byte(client.Options.JsonTools), &tools)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
toolChoice := "auto"
|
||||
seed := 42
|
||||
|
||||
request := AgentRequestBody{
|
||||
Tools: &tools,
|
||||
ToolChoice: &toolChoice,
|
||||
Model: "policy/images",
|
||||
RandomSeed: &seed,
|
||||
Temperature: 0.3,
|
||||
EndToolCall: client.Options.EndToolCall,
|
||||
ResponseFormat: ResponseFormat{
|
||||
Type: "text",
|
||||
},
|
||||
Chat: &Chat{
|
||||
Messages: make([]ChatMessage, 0),
|
||||
},
|
||||
}
|
||||
|
||||
request.Chat.AddSystem(client.Options.SystemPrompt)
|
||||
request.Chat.AddUser(userReq)
|
||||
|
||||
toolHandlerInfo := ToolHandlerInfo{
|
||||
UserId: userID,
|
||||
}
|
||||
|
||||
return client.ToolLoop(toolHandlerInfo, &request)
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
|
||||
type ToolHandlerInfo struct {
|
||||
UserId uuid.UUID
|
||||
ImageId uuid.UUID
|
||||
ImageID uuid.UUID
|
||||
ImageName string
|
||||
|
||||
// Pointer because we don't want to copy this around too much.
|
||||
|
@ -40,7 +40,7 @@ func (suite *ToolTestSuite) TestSingleToolCall() {
|
||||
response := suite.handler.Handle(
|
||||
ToolHandlerInfo{
|
||||
UserId: uuid.Nil,
|
||||
ImageId: uuid.Nil,
|
||||
ImageID: uuid.Nil,
|
||||
},
|
||||
ToolCall{
|
||||
Index: 0,
|
||||
@ -91,7 +91,7 @@ func (suite *ToolTestSuite) TestMultipleToolCalls() {
|
||||
err := suite.client.Process(
|
||||
ToolHandlerInfo{
|
||||
UserId: uuid.Nil,
|
||||
ImageId: uuid.Nil,
|
||||
ImageID: uuid.Nil,
|
||||
},
|
||||
&AgentRequestBody{
|
||||
Chat: &chat,
|
||||
@ -154,7 +154,7 @@ func (suite *ToolTestSuite) TestMultipleToolCallsWithErrors() {
|
||||
err := suite.client.Process(
|
||||
ToolHandlerInfo{
|
||||
UserId: uuid.Nil,
|
||||
ImageId: uuid.Nil,
|
||||
ImageID: uuid.Nil,
|
||||
},
|
||||
&AgentRequestBody{
|
||||
Chat: &chat,
|
||||
|
@ -1,220 +0,0 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/models"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const contactPrompt = `
|
||||
**Role:** AI Contact Processor from Images.
|
||||
|
||||
**Goal:** Extract contacts from an image, check against existing list using listContacts, add *only* new contacts using createContact, and call stopAgent when finished. Avoid duplicates.
|
||||
|
||||
**Input:** Image potentially containing contact info (Name, Phone, Email, Address).
|
||||
|
||||
**Workflow:**
|
||||
1. **Scan Image:** Extract all contact details. If none, call stopAgent.
|
||||
2. **Think:** Using the think tool, you must layout your thoughts about the contacts on the image. If they are duplicates or not, and what your next action should be,
|
||||
3. **Check Duplicates:** If contacts found, *first* call listContacts. Compare extracted info to list. If all found contacts already exist, use createExistingContact.
|
||||
4. **Add New:** If you detect a new contact on the image, call createContact to create a new contact.
|
||||
5. **Finish:** Call stopAgent once all new contacts are created OR if steps 1 or 2 determined no action/creation was needed.
|
||||
|
||||
**Tools:**
|
||||
* listContacts: Check existing contacts (Use first if contacts found).
|
||||
* createContact: Add a NEW contact (Name required).
|
||||
* createExistingContact: Adds this image to an existing contact, if one is found in listContacts.
|
||||
* stopAgent: Signal task completion.
|
||||
`
|
||||
|
||||
const contactTools = `
|
||||
[
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "think",
|
||||
"description": "Use this tool to think through the image, evaluating the contact and whether or not it exists in the users listContacts.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"thought": {
|
||||
"type": "string",
|
||||
"description": "A singular thought about the image"
|
||||
}
|
||||
},
|
||||
"required": ["thought"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "listContacts",
|
||||
"description": "Retrieves the complete list of the user's currently saved contacts (e.g., names, phone numbers, emails if available in the stored data). This tool is essential and **must** be called *before* attempting to create a new contact if potential contact info is found in the image, to check if the person already exists and prevent duplicate entries.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "createContact",
|
||||
"description": "Saves a new contact to the user's contact list. Only use this function **after** confirming the contact does not already exist by checking the output of listContacts. Provide all available extracted information for the new contact. Process one new contact per call.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"contactId": {
|
||||
"type": "string",
|
||||
"description": "The UUID of the contact. You should only provide this IF you believe the contact already exists, from listContacts."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The full name of the person being added as a contact. This field is mandatory."
|
||||
},
|
||||
"phoneNumber": {
|
||||
"type": "string",
|
||||
"description": "The contact's primary phone number, including area or country code if available. Provide this if extracted from the image. Only include this if you see a phone number, do not make it up."
|
||||
},
|
||||
"address": {
|
||||
"type": "string",
|
||||
"description": "The complete physical mailing address of the contact (e.g., street number, street name, city, state/province, postal code, country). Provide this if extracted from the image. Only include this if you see an address. No not make it up."
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "The contact's primary email address. Provide this if extracted from the image. Only include this if you see an email, do not make it up."
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "createExistingContact",
|
||||
"description": "Called when a contact already exists in the users list, from listContas. Only call this to indicate this image contains a duplicate.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"contactId": {
|
||||
"type": "string",
|
||||
"description": "The UUID of the contact"
|
||||
}
|
||||
},
|
||||
"required": ["contactId"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "stopAgent",
|
||||
"description": "Use this tool to signal that the contact processing for the current image is complete. Call this *only* when: 1) No contact info was found initially, OR 2) All found contacts were confirmed to already exist after calling listContacts, OR 3) All necessary createContact calls for new individuals have been completed.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
`
|
||||
|
||||
type listContactsArguments struct{}
|
||||
type createContactsArguments struct {
|
||||
Name string `json:"name"`
|
||||
ContactID *string `json:"contactId"`
|
||||
PhoneNumber *string `json:"phoneNumber"`
|
||||
Address *string `json:"address"`
|
||||
Email *string `json:"email"`
|
||||
}
|
||||
type createExistingContactArguments struct {
|
||||
ContactID string `json:"contactId"`
|
||||
}
|
||||
|
||||
func NewContactAgent(log *log.Logger, contactModel models.ContactModel) client.AgentClient {
|
||||
agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{
|
||||
SystemPrompt: contactPrompt,
|
||||
JsonTools: contactTools,
|
||||
Log: log,
|
||||
EndToolCall: "stopAgent",
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("think", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
return "Thought", nil
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("listContacts", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
return contactModel.List(context.Background(), info.UserId)
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("createContact", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
|
||||
args := createContactsArguments{}
|
||||
err := json.Unmarshal([]byte(_args), &args)
|
||||
if err != nil {
|
||||
return model.Contacts{}, err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
contactId := uuid.Nil
|
||||
if args.ContactID != nil {
|
||||
contactUuid, err := uuid.Parse(*args.ContactID)
|
||||
if err != nil {
|
||||
return model.Contacts{}, err
|
||||
}
|
||||
|
||||
contactId = contactUuid
|
||||
}
|
||||
|
||||
contact, err := contactModel.Save(ctx, info.UserId, model.Contacts{
|
||||
ID: contactId,
|
||||
Name: args.Name,
|
||||
PhoneNumber: args.PhoneNumber,
|
||||
Email: args.Email,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return model.Contacts{}, err
|
||||
}
|
||||
|
||||
_, err = contactModel.SaveToImage(ctx, info.ImageId, contact.ID)
|
||||
if err != nil {
|
||||
return model.Contacts{}, err
|
||||
}
|
||||
|
||||
return contact, nil
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("createExistingContact", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
|
||||
args := createExistingContactArguments{}
|
||||
err := json.Unmarshal([]byte(_args), &args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
contactId, err := uuid.Parse(args.ContactID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = contactModel.SaveToImage(ctx, info.ImageId, contactId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "", nil
|
||||
})
|
||||
|
||||
return agentClient
|
||||
}
|
157
backend/agents/create_list_agent.go
Normal file
157
backend/agents/create_list_agent.go
Normal file
@ -0,0 +1,157 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/models"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const createListAgentPrompt = `
|
||||
You are an agent who's job is to produce a reasonable output for an unstructured input.
|
||||
|
||||
Your job is to create lists for the user, the user will give you a title and some fields they want
|
||||
as part of the list. Your job is to take these fields, adjust their names so they have good names,
|
||||
and add a good description for each one.
|
||||
|
||||
You can add fields if you think they make a lot of sense.
|
||||
You can remove fields if they are not correct, but be sure before you do this.
|
||||
|
||||
You must respond in json format, do not add backticks to the json. ONLY valid json.
|
||||
`
|
||||
|
||||
const listJsonSchema = `
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "the title of the list"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "the description of the list"
|
||||
},
|
||||
"fields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the field."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "A description of the field."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"description"
|
||||
]
|
||||
},
|
||||
"description": "An array of field objects."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fields"
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
type createNewListArguments struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
|
||||
Fields []struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
} `json:"fields"`
|
||||
}
|
||||
|
||||
type CreateListAgent struct {
|
||||
client client.AgentClient
|
||||
|
||||
stackModel models.StackModel
|
||||
}
|
||||
|
||||
func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, stackID uuid.UUID, title string, userReq string) error {
|
||||
request := client.AgentRequestBody{
|
||||
Model: "policy/images",
|
||||
Temperature: 0.3,
|
||||
ResponseFormat: client.ResponseFormat{
|
||||
Type: "json_schema",
|
||||
JsonSchema: listJsonSchema,
|
||||
},
|
||||
Chat: &client.Chat{
|
||||
Messages: make([]client.ChatMessage, 0),
|
||||
},
|
||||
}
|
||||
|
||||
request.Chat.AddSystem(agent.client.Options.SystemPrompt)
|
||||
|
||||
req := fmt.Sprintf("List title: %s | Users list description: %s", title, userReq)
|
||||
|
||||
request.Chat.AddUser(req)
|
||||
|
||||
resp, err := agent.client.Request(&request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request: %w", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
content := resp.Choices[0].Message.Content
|
||||
|
||||
if strings.HasPrefix(content, "```json") {
|
||||
content = content[len("```json") : len(content)-3]
|
||||
}
|
||||
|
||||
log.Info("", "res", content)
|
||||
|
||||
var createListArgs createNewListArguments
|
||||
err = json.Unmarshal([]byte(content), &createListArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schemaItems := make([]model.SchemaItems, 0)
|
||||
for _, field := range createListArgs.Fields {
|
||||
schemaItems = append(schemaItems, model.SchemaItems{
|
||||
StackID: stackID,
|
||||
|
||||
Item: field.Name,
|
||||
Description: field.Description,
|
||||
|
||||
Value: "string", // keep it simple for now.
|
||||
})
|
||||
}
|
||||
|
||||
err = agent.stackModel.SaveItems(ctx, schemaItems)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating list agent, saving items: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewCreateListAgent(log *log.Logger, listModel models.StackModel) CreateListAgent {
|
||||
client := client.CreateAgentClient(client.CreateAgentClientOptions{
|
||||
SystemPrompt: createListAgentPrompt,
|
||||
Log: log,
|
||||
})
|
||||
|
||||
agent := CreateListAgent{
|
||||
client,
|
||||
listModel,
|
||||
}
|
||||
|
||||
return agent
|
||||
}
|
74
backend/agents/description_agent.go
Normal file
74
backend/agents/description_agent.go
Normal file
@ -0,0 +1,74 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/models"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const noteAgentPrompt = `
|
||||
You are an AI agent who's job is to describe the image you see.
|
||||
|
||||
You should also add any text you see in the image, if no text exists, just add a description.
|
||||
Be consise and don't add too much extra information or formatting characters, simple text.
|
||||
|
||||
You must write this text in Markdown. You can add extra information for the user.
|
||||
You must organise this text nicely, not be all over the place.
|
||||
`
|
||||
|
||||
type DescriptionAgent struct {
|
||||
client client.AgentClient
|
||||
|
||||
imageModel models.ImageModel
|
||||
}
|
||||
|
||||
func (agent DescriptionAgent) Describe(log *log.Logger, imageID uuid.UUID, imageName string, imageData []byte) error {
|
||||
request := client.AgentRequestBody{
|
||||
Model: "policy/images",
|
||||
Temperature: 0.3,
|
||||
ResponseFormat: client.ResponseFormat{
|
||||
Type: "text",
|
||||
},
|
||||
Chat: &client.Chat{
|
||||
Messages: make([]client.ChatMessage, 0),
|
||||
},
|
||||
}
|
||||
|
||||
request.Chat.AddSystem(noteAgentPrompt)
|
||||
request.Chat.AddImage(imageName, imageData, nil)
|
||||
|
||||
log.Debug("Sending description request")
|
||||
resp, err := agent.client.Request(&request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not request. %s", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
description := resp.Choices[0].Message.Content
|
||||
|
||||
err = agent.imageModel.UpdateDescription(ctx, imageID, description)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewDescriptionAgent(log *log.Logger, imageModel models.ImageModel) DescriptionAgent {
|
||||
client := client.CreateAgentClient(client.CreateAgentClientOptions{
|
||||
SystemPrompt: noteAgentPrompt,
|
||||
Log: log,
|
||||
})
|
||||
|
||||
agent := DescriptionAgent{
|
||||
client: client,
|
||||
imageModel: imageModel,
|
||||
}
|
||||
|
||||
return agent
|
||||
}
|
@ -1,267 +0,0 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/models"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const eventPrompt = `
|
||||
**You are an AI processing events from images using internal thought.**
|
||||
|
||||
**Task:** Extract event details (Name, Date/Time, Location). Use think before deciding actions. Check duplicates with listEvents. Handle new events via getEventLocationId (if location exists) and createEvent. Use finish if no event or duplicate found.
|
||||
1. **Analyze Image & Think:** Extract details. Use think to confirm if a valid event exists. If not -> stopAgent.
|
||||
2. **Event Confirmed?** -> *Must* call listEvents, to check for existing events and prevent duplicates.
|
||||
3. **Detect Duplicates** -> If the input contains an event that already exists from listEvents, then you should call stopAgent.
|
||||
4. **New Events**
|
||||
* If you think the input contains a location, then you can use getEventLocationId to retrieve the ID of the location. Only use this IF the input contains a location.
|
||||
* Call createEvent.
|
||||
5. **Multiple Events:** Process sequentially using this logic.
|
||||
|
||||
**Tools:**
|
||||
* think: Internal reasoning/planning step.
|
||||
* listEvents: Check for duplicates (mandatory first step for found events).
|
||||
* getEventLocationId: Get ID for location text.
|
||||
* createEvent: Add new event (Name req.). Terminal action for new events.
|
||||
* stopAgent: Signal completion (no event/duplicate found). Terminal action.
|
||||
`
|
||||
|
||||
const eventTools = `
|
||||
[
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "think",
|
||||
"description": "Use this tool to think through the image, evaluating the event and whether or not it exists in the users listEvents.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"thought": {
|
||||
"type": "string",
|
||||
"description": "A singular thought about the image"
|
||||
}
|
||||
},
|
||||
"required": ["thought"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "listEvents",
|
||||
"description": "Retrieves the list of the user's currently scheduled events. Essential for checking if an event identified in the image already exists to prevent duplicates. Must be called before potentially creating an event.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "createEvent",
|
||||
"description": "Creates a new event in the user's calendar or list. Use only after listEvents confirms the event is new. Provide all extracted details.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name or title of the event. This field is mandatory."
|
||||
},
|
||||
"startDateTime": {
|
||||
"type": "string",
|
||||
"description": "The event's start date and time in ISO 8601 format (e.g., '2025-04-18T10:00:00Z'). Include if available."
|
||||
},
|
||||
"endDateTime": {
|
||||
"type": "string",
|
||||
"description": "The event's end date and time in ISO 8601 format. Optional, include if available and different from startDateTime."
|
||||
},
|
||||
"locationId": {
|
||||
"type": "string",
|
||||
"description": "The unique identifier (UUID or similar) for the event's location. Use this if available, do not invent it."
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "updateEvent",
|
||||
"description": "Updates an existing event record identified by its eventId. Use this tool when listEvents indicates a match for the event details found in the current input.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"eventId": {
|
||||
"type": "string",
|
||||
"description": "The UUID of the existing event"
|
||||
}
|
||||
},
|
||||
"required": ["eventId"]
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getEventLocationId",
|
||||
"description": "Retrieves a unique identifier for a location description associated with an event. Use this before createEvent if a new event specifies a location.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"locationDescription": {
|
||||
"type": "string",
|
||||
"description": "The text describing the location extracted from the image (e.g., 'Conference Room B', '123 Main St, Anytown', 'Zoom Link details')."
|
||||
}
|
||||
},
|
||||
"required": ["locationDescription"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "stopAgent",
|
||||
"description": "Call this tool only when event processing for the current image is fully complete. This occurs if: 1) No event info was found, OR 2) The found event already exists, OR 3) A new event has been successfully created.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]`
|
||||
|
||||
type listEventArguments struct{}
|
||||
type createEventArguments struct {
|
||||
Name string `json:"name"`
|
||||
StartDateTime *string `json:"startDateTime"`
|
||||
EndDateTime *string `json:"endDateTime"`
|
||||
OrganizerName *string `json:"organizerName"`
|
||||
LocationID *string `json:"locationId"`
|
||||
}
|
||||
type updateEventArguments struct {
|
||||
EventID string `json:"eventId"`
|
||||
}
|
||||
|
||||
const layout = "2006-01-02T15:04:05Z"
|
||||
|
||||
func getArguments(args createEventArguments) (model.Events, error) {
|
||||
event := model.Events{
|
||||
Name: args.Name,
|
||||
}
|
||||
|
||||
if args.StartDateTime != nil {
|
||||
startTime, err := time.Parse(layout, *args.StartDateTime)
|
||||
if err != nil {
|
||||
return event, err
|
||||
}
|
||||
|
||||
event.StartDateTime = &startTime
|
||||
}
|
||||
|
||||
if args.EndDateTime != nil {
|
||||
endTime, err := time.Parse(layout, *args.EndDateTime)
|
||||
if err != nil {
|
||||
return event, err
|
||||
}
|
||||
|
||||
event.EndDateTime = &endTime
|
||||
}
|
||||
|
||||
if args.LocationID != nil {
|
||||
locationId, err := uuid.Parse(*args.LocationID)
|
||||
if err != nil {
|
||||
return model.Events{}, err
|
||||
}
|
||||
|
||||
event.LocationID = &locationId
|
||||
}
|
||||
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func NewEventAgent(log *log.Logger, eventsModel models.EventModel, locationModel models.LocationModel) client.AgentClient {
|
||||
agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{
|
||||
SystemPrompt: eventPrompt,
|
||||
JsonTools: eventTools,
|
||||
Log: log,
|
||||
EndToolCall: "stopAgent",
|
||||
})
|
||||
|
||||
locationAgent := NewLocationAgentWithComm(log.WithPrefix("Events 📅 > Locations 📍"), locationModel)
|
||||
locationQuery := "Can you get me the ID of the location present in this image?"
|
||||
locationAgent.Options.Query = &locationQuery
|
||||
|
||||
agentClient.ToolHandler.AddTool("think", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
return "Thought", nil
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("listEvents", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
return eventsModel.List(context.Background(), info.UserId)
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("createEvent", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
|
||||
args := createEventArguments{}
|
||||
err := json.Unmarshal([]byte(_args), &args)
|
||||
if err != nil {
|
||||
return model.Events{}, err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
event, err := getArguments(args)
|
||||
if err != nil {
|
||||
return model.Events{}, err
|
||||
}
|
||||
|
||||
events, err := eventsModel.Save(ctx, info.UserId, event)
|
||||
|
||||
if err != nil {
|
||||
return model.Events{}, err
|
||||
}
|
||||
|
||||
_, err = eventsModel.SaveToImage(ctx, info.ImageId, events.ID)
|
||||
if err != nil {
|
||||
return model.Events{}, err
|
||||
}
|
||||
|
||||
return events, nil
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("updateEvent", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
|
||||
args := updateEventArguments{}
|
||||
err := json.Unmarshal([]byte(_args), &args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
contactUuid, err := uuid.Parse(args.EventID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
eventsModel.SaveToImage(ctx, info.ImageId, contactUuid)
|
||||
return "Saved", nil
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("getEventLocationId", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
// TODO: reenable this when I'm creating the agent locally instead of getting it from above.
|
||||
locationAgent.RunAgent(info.UserId, info.ImageId, info.ImageName, *info.Image)
|
||||
|
||||
log.Debugf("Reply from location %s\n", locationAgent.Reply)
|
||||
return locationAgent.Reply, nil
|
||||
})
|
||||
|
||||
return agentClient
|
||||
}
|
259
backend/agents/list_agent.go
Normal file
259
backend/agents/list_agent.go
Normal file
@ -0,0 +1,259 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/limits"
|
||||
"screenmark/screenmark/models"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const listPrompt = `
|
||||
**You are an AI used to classify what list a certain image belongs in**
|
||||
|
||||
You will need to decide using tool calls, if you must create a new list, or use an existing one.
|
||||
You must be specific enough so it is useful, but not too specific such that all images belong on seperate lists.
|
||||
|
||||
An example of lists are:
|
||||
- Locations
|
||||
- Events
|
||||
- TV Shows
|
||||
- Movies
|
||||
- Books
|
||||
|
||||
Another one of your tasks is to create a schema for this list. This should contain information that this, and following
|
||||
pictures contain. Be specific but also generic. You should use the parameters in "createList" to create this schema.
|
||||
|
||||
This schema should not be super specific. You must be able to understand the image, and if the content of the image doesnt seem relevant, try
|
||||
and extract some meaning about what the image is.
|
||||
|
||||
You must call "listLists" to see which available lists are already available.
|
||||
Use "createList" only once, don't create multiple lists for one image.
|
||||
|
||||
You can add an image to multiple lists, this is also true if you already created a list. But only add to a list if it makes sense to do so.
|
||||
|
||||
**Tools:**
|
||||
* think: Internal reasoning/planning step.
|
||||
* listLists: Get existing lists
|
||||
* createList: Creates a new list with a name and description. Only use this once.
|
||||
* addToList: Add to an existing list. This will also mean extracting information from this image, and inserting it, fitting the schema.
|
||||
* stopAgent: Signal task completion.
|
||||
`
|
||||
|
||||
const listTools = `
|
||||
[
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "think",
|
||||
"description": "Use this tool to think through the image, evaluating the event and whether or not it exists in the users listEvents.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"thought": {
|
||||
"type": "string",
|
||||
"description": "A singular thought about the image."
|
||||
}
|
||||
},
|
||||
"required": ["thought"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "listLists",
|
||||
"description": "Retrieves the list of the user's existing lists.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "createList",
|
||||
"description": "Creates a new list",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of this new list."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "A simple description of this list."
|
||||
},
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Item": {
|
||||
"type": "string",
|
||||
"description": "The name of the key for this specific field. Similar to a column in a database"
|
||||
},
|
||||
"Value": {
|
||||
"type": "string",
|
||||
"enum": ["string", "number", "boolean"]
|
||||
},
|
||||
"Description": {
|
||||
"type": "string",
|
||||
"description": "The description for this item"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "description", "schema"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "addToList",
|
||||
"description": "Adds an image to a list, this could be a new one you just created or not.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"listId": {
|
||||
"type": "string",
|
||||
"description": "The UUID of the existing list"
|
||||
},
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"description": "A key-value of ID - value from this image to fit the schema. any of the values can be null",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The UUID of the schema item."
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "the concrete value for this field"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["listId", "schema"]
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "stopAgent",
|
||||
"description": "Use this tool to signal that the contact processing for the current image is complete. Call this *only* when: 1) No contact info was found initially, OR 2) All found contacts were confirmed to already exist after calling listContacts, OR 3) All necessary createContact calls for new individuals have been completed.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]`
|
||||
|
||||
type createListArguments struct {
|
||||
Name string `json:"name"`
|
||||
Desription string `json:"description"`
|
||||
Schema []model.SchemaItems
|
||||
}
|
||||
type addToListArguments struct {
|
||||
ListID string `json:"listId"`
|
||||
Schema []models.IDValue
|
||||
}
|
||||
|
||||
func NewStackAgent(log *log.Logger, stackModel models.StackModel, limitsMethods limits.LimitsManagerMethods) client.AgentClient {
|
||||
agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{
|
||||
SystemPrompt: listPrompt,
|
||||
JsonTools: listTools,
|
||||
Log: log,
|
||||
EndToolCall: "stopAgent",
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("think", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
return "Thought", nil
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("createList", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
|
||||
args := createListArguments{}
|
||||
err := json.Unmarshal([]byte(_args), &args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hasReachedLimit, err := limitsMethods.HasReachedStackLimit(info.UserId)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error checking stack limits: %w", err)
|
||||
}
|
||||
|
||||
if hasReachedLimit {
|
||||
log.Warn("User has reached limits", "userID", info.UserId)
|
||||
return "", fmt.Errorf("reached stack limits")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
savedList, err := stackModel.Save(ctx, info.UserId, args.Name, args.Desription, model.Progress_Complete)
|
||||
if err != nil {
|
||||
log.Error("saving list", "err", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
for i := range args.Schema {
|
||||
args.Schema[i].StackID = savedList.ID
|
||||
}
|
||||
|
||||
err = stackModel.SaveItems(ctx, args.Schema)
|
||||
if err != nil {
|
||||
log.Error("saving items", "err", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return savedList, nil
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("listLists", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
return stackModel.List(context.Background(), info.UserId)
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("addToList", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
|
||||
args := addToListArguments{}
|
||||
err := json.Unmarshal([]byte(_args), &args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
listUUID, err := uuid.Parse(args.ListID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
imageStack, err := stackModel.SaveImage(ctx, info.ImageID, listUUID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := stackModel.SaveSchemaItems(ctx, imageStack.ID, args.Schema); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "Saved", nil
|
||||
})
|
||||
|
||||
return agentClient
|
||||
}
|
@ -1,248 +0,0 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/models"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const locationPrompt = `
|
||||
Role: Location AI Assistant
|
||||
|
||||
Objective: Identify locations from images/text, manage a saved list, and answer user queries about saved locations using the provided tools.
|
||||
The user does not want to have duplicate entries on their saved location list. So you should only create a new location if listLocation doesnt return
|
||||
what would be a duplicate.
|
||||
|
||||
Core Logic:
|
||||
|
||||
**Extract Location Details:** Attempt to extract location details (like InputName, InputAddress) from the user's input.
|
||||
* If no details can be extracted, inform the user and use stopAgent.
|
||||
|
||||
**Check for Existing Location:** If details *were* extracted:
|
||||
* Use listLocations with the extracted InputName and/or InputAddress to search for potentially matching locations already saved in the list.
|
||||
|
||||
Action loop:
|
||||
**Thinking**
|
||||
* Use the think tool to analytise the image.
|
||||
* You should think about whether listLocations already contains this location, or if it is a new location.
|
||||
* You should always call this after listLocations.
|
||||
* You must think about whether or not listLocations already has this location.
|
||||
|
||||
**Decide Action based on Search Results:**
|
||||
* If no existing location looks like the location on the input. You should use createLocation.
|
||||
* Do not use this tool if this location already exists.
|
||||
* If the input contains a location that already exists, you should use createExistingLocation.
|
||||
* If there is a similar location in listLocation, you should use this tool. It doesnt have to be an exact match.
|
||||
* Lastly, if the user asked a specific question about a location. You must do all the actions but also always use the reply tool to answer the user.
|
||||
* This is the only way you can communicate with the user if they asked a query.
|
||||
|
||||
You should repeat the action loop until all locations on the image are done.
|
||||
Once you are done, use stopAgent.
|
||||
`
|
||||
|
||||
const replyTool = `
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "reply",
|
||||
"description": "Signals intent to provide information about a specific known location in response to a user's query. Use only if the user asked a question and the location's ID was found via listLocations.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"locationId": {
|
||||
"type": "string",
|
||||
"description": "The UUID of the saved location that the user is asking about."
|
||||
}
|
||||
},
|
||||
"required": ["locationId"]
|
||||
}
|
||||
}
|
||||
},`
|
||||
|
||||
const locationTools = `
|
||||
[
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "think",
|
||||
"description": "Use this tool to think through the image, evaluating the location and whether or not it exists in the users listLocations. You should also ask yourself if the user has asked a query, and if you've used the correct tool to reply to them.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"thought": {
|
||||
"type": "string",
|
||||
"description": "A singular thought about the image"
|
||||
}
|
||||
},
|
||||
"required": ["thought"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "listLocations",
|
||||
"description": "Retrieves the list of the user's currently saved locations (names, addresses, IDs). Use this first to check if a location from an image already exists, or to find the ID of a location the user is asking about.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "createLocation",
|
||||
"description": "Creates a new location with as much information as you can extract. Be precise. You should only add the parameters you can actually see on the image.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The primary name of the location"
|
||||
},
|
||||
"address": {
|
||||
"type": "string",
|
||||
"description": "The address of the location"
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "createExistingLocation",
|
||||
"description": "Called when a location already exists in the users list, from listLocations. Only call this to indicate this image contains a duplicate.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"locationId": {
|
||||
"type": "string",
|
||||
"description": "The UUID of the location, from listLocations"
|
||||
}
|
||||
},
|
||||
"required": ["locationId"]
|
||||
}
|
||||
}
|
||||
},
|
||||
%s
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "stopAgent",
|
||||
"description": "Use this tool to signal that the contact processing for the current image is complete.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]`
|
||||
|
||||
func getLocationAgentTools(allowReply bool) string {
|
||||
if allowReply {
|
||||
return fmt.Sprintf(locationTools, replyTool)
|
||||
} else {
|
||||
return fmt.Sprintf(locationTools, "")
|
||||
}
|
||||
}
|
||||
|
||||
type listLocationArguments struct{}
|
||||
type createLocationArguments struct {
|
||||
Name string `json:"name"`
|
||||
Address *string `json:"address"`
|
||||
}
|
||||
type createExistingLocationArguments struct {
|
||||
LocationID string `json:"locationId"`
|
||||
}
|
||||
|
||||
func NewLocationAgentWithComm(log *log.Logger, locationModel models.LocationModel) client.AgentClient {
|
||||
client := NewLocationAgent(log, locationModel)
|
||||
|
||||
client.Options.JsonTools = getLocationAgentTools(true)
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
func NewLocationAgent(log *log.Logger, locationModel models.LocationModel) client.AgentClient {
|
||||
agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{
|
||||
SystemPrompt: locationPrompt,
|
||||
JsonTools: getLocationAgentTools(false),
|
||||
Log: log,
|
||||
EndToolCall: "stopAgent",
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("listLocations", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
return locationModel.List(context.Background(), info.UserId)
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("createLocation", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
|
||||
args := createLocationArguments{}
|
||||
err := json.Unmarshal([]byte(_args), &args)
|
||||
if err != nil {
|
||||
return model.Locations{}, err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// TODO: this tool could be simplier, as the model could have a SaveToImage joined with the save.
|
||||
|
||||
location, err := locationModel.Save(ctx, info.UserId, model.Locations{
|
||||
Name: args.Name,
|
||||
Address: args.Address,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return model.Locations{}, err
|
||||
}
|
||||
|
||||
_, err = locationModel.SaveToImage(ctx, info.ImageId, location.ID)
|
||||
if err != nil {
|
||||
return model.Locations{}, err
|
||||
}
|
||||
|
||||
return location, nil
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("createExistingLocation", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
|
||||
args := createExistingLocationArguments{}
|
||||
err := json.Unmarshal([]byte(_args), &args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
locationId, err := uuid.Parse(args.LocationID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = locationModel.SaveToImage(ctx, info.ImageId, locationId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "", nil
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("reply", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
return "ok", nil
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("think", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
return "ok", nil
|
||||
})
|
||||
|
||||
return agentClient
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/models"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const noteAgentPrompt = `
|
||||
You are a helpful agent, who's job is to extract notes from images.
|
||||
Not all images contain notes, in such cases there's not need to create them.
|
||||
|
||||
An image can have more than one note.
|
||||
|
||||
You must return markdown, and adapt the text to best fit markdown.
|
||||
Do not return anything except markdown.
|
||||
|
||||
If the image contains code, add this inside code blocks. You must try and correctly guess the language too.
|
||||
`
|
||||
|
||||
type NoteAgent struct {
|
||||
client client.AgentClient
|
||||
|
||||
noteModel models.NoteModel
|
||||
}
|
||||
|
||||
func (agent NoteAgent) GetNotes(userId uuid.UUID, imageId uuid.UUID, imageName string, imageData []byte) error {
|
||||
request := client.AgentRequestBody{
|
||||
Model: "gpt-4.1-nano",
|
||||
Temperature: 0.3,
|
||||
ResponseFormat: client.ResponseFormat{
|
||||
Type: "text",
|
||||
},
|
||||
Chat: &client.Chat{
|
||||
Messages: make([]client.ChatMessage, 0),
|
||||
},
|
||||
}
|
||||
|
||||
request.Chat.AddSystem(noteAgentPrompt)
|
||||
request.Chat.AddImage(imageName, imageData, nil)
|
||||
|
||||
resp, err := agent.client.Request(&request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
markdown := resp.Choices[0].Message.Content
|
||||
|
||||
note, err := agent.noteModel.Save(ctx, userId, model.Notes{
|
||||
Name: "the note", // TODO: add some json schema
|
||||
Content: markdown,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = agent.noteModel.SaveToImage(ctx, imageId, note.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewNoteAgent(log *log.Logger, noteModel models.NoteModel) NoteAgent {
|
||||
client := client.CreateAgentClient(client.CreateAgentClientOptions{
|
||||
SystemPrompt: noteAgentPrompt,
|
||||
Log: log,
|
||||
})
|
||||
|
||||
agent := NoteAgent{
|
||||
client: client,
|
||||
noteModel: noteModel,
|
||||
}
|
||||
|
||||
return agent
|
||||
}
|
@ -1,171 +0,0 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"screenmark/screenmark/agents/client"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
const orchestratorPrompt = `
|
||||
**Role:** You are an Orchestrator AI responsible for analyzing images provided by the user.
|
||||
|
||||
**Primary Task:** Examine the input image and determine which specialized AI agent(s), available as tool calls, should be invoked to process the relevant information within the image, or if no specialized processing is needed. Your goal is to either extract and structure useful information for the user by selecting the most appropriate tool(s) or explicitly indicate that no specific action is required.
|
||||
|
||||
**Analysis Process & Decision Logic:**
|
||||
|
||||
1. **Analyze Image Content:** Scrutinize the image for distinct types of information:
|
||||
* General text/writing (including code, formulas)
|
||||
* Information about a person or contact details
|
||||
* Information about a place, location, or address
|
||||
* Information about an event
|
||||
* Content that doesn't fit any specific category or lacks actionable information.
|
||||
|
||||
2. **Thinking**
|
||||
* You should use the think tool to allow you to think your way through the image.
|
||||
* You should call this as many times as you need to in order to describe and analyse the image correctly.
|
||||
|
||||
3. **Agent Selection - Determine ALL that apply:**
|
||||
* **contactAgent:** Is there information specifically related to a person or their contact details (e.g., business card, name/email/phone)?
|
||||
* **locationAgent:** Is there information specifically identifying a place, location, city, or address (e.g., map, street sign, address text)?
|
||||
* **eventAgent:** Is there information specifically related to an event (e.g., invitation, poster with date/time, schedule)?
|
||||
* **noteAgent** Does the image contain *any* text/writing (including code, formulas)?
|
||||
* **noAgent**: Call this when you are done working on this image.
|
||||
|
||||
* Call all applicable specialized agents (noteAgent, contactAgent, locationAgent, eventAgent) simultaneously (in parallel).
|
||||
* Do not call agents if you dont think they are relevant. It's better to be sound than complete.
|
||||
`
|
||||
|
||||
const orchestratorTools = `
|
||||
[
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "think",
|
||||
"description": "Use to layout all your thoughts about the image, roughly describing it, and specially describing if the image contains anything relevant to your available agents",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"thought": {
|
||||
"type": "string",
|
||||
"description": "A singular thought about the image"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "noteAgent",
|
||||
"description": "Extracts general textual content like handwritten notes, paragraphs in documents, presentation slides, code snippets, or mathematical formulas. Use this for significant text that isn't primarily contact details, an address, or specific event information.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "contactAgent",
|
||||
"description": "Extracts personal contact information. Use when the image clearly shows details like names, phone numbers, email addresses, job titles, or company names, especially from sources like business cards, email signatures, or contact lists.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "locationAgent",
|
||||
"description": "Use when the input has anything to do with a place. This could be a city, an address, a postcode, a virtual meeting location, or a geographical location.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "eventAgent",
|
||||
"description": "Extracts details related to scheduled events, appointments, or specific occasions. Use when the image contains information like event titles, dates, times, venues, agendas, or descriptions, typically found on invitations, posters, calendar entries, or schedules.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "noAgent",
|
||||
"description": "Extracts details related to scheduled events, appointments, or specific occasions. Use when the image contains information like event titles, dates, times, venues, agendas, or descriptions, typically found on invitations, posters, calendar entries, or schedules.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
`
|
||||
|
||||
type OrchestratorAgent struct {
|
||||
Client client.AgentClient
|
||||
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
Ok bool `json:"ok"`
|
||||
}
|
||||
|
||||
func NewOrchestratorAgent(log *log.Logger, noteAgent NoteAgent, contactAgent client.AgentClient, locationAgent client.AgentClient, eventAgent client.AgentClient, imageName string, imageData []byte) client.AgentClient {
|
||||
agent := client.CreateAgentClient(client.CreateAgentClientOptions{
|
||||
SystemPrompt: orchestratorPrompt,
|
||||
JsonTools: orchestratorTools,
|
||||
Log: log,
|
||||
EndToolCall: "noAgent",
|
||||
})
|
||||
|
||||
agent.ToolHandler.AddTool("think", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
return "Thought", nil
|
||||
})
|
||||
|
||||
agent.ToolHandler.AddTool("noteAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
noteAgent.GetNotes(info.UserId, info.ImageId, imageName, imageData)
|
||||
|
||||
return "noteAgent called successfully", nil
|
||||
})
|
||||
|
||||
agent.ToolHandler.AddTool("contactAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
contactAgent.RunAgent(info.UserId, info.ImageId, imageName, imageData)
|
||||
|
||||
return "contactAgent called successfully", nil
|
||||
})
|
||||
|
||||
agent.ToolHandler.AddTool("locationAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
locationAgent.RunAgent(info.UserId, info.ImageId, imageName, imageData)
|
||||
|
||||
return "locationAgent called successfully", nil
|
||||
})
|
||||
|
||||
agent.ToolHandler.AddTool("eventAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
eventAgent.RunAgent(info.UserId, info.ImageId, imageName, imageData)
|
||||
|
||||
return "eventAgent called successfully", nil
|
||||
})
|
||||
|
||||
agent.ToolHandler.AddTool("noAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
return "ok", nil
|
||||
})
|
||||
|
||||
return agent
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
@ -1,9 +1,9 @@
|
||||
package main
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/wneessen/go-mail"
|
||||
)
|
||||
|
||||
@ -11,7 +11,9 @@ type MailClient struct {
|
||||
client *mail.Client
|
||||
}
|
||||
|
||||
type TestMailClient struct{}
|
||||
type TestMailClient struct {
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
type Mailer interface {
|
||||
SendCode(to string, code string) error
|
||||
@ -43,15 +45,17 @@ func (m MailClient) SendCode(to string, code string) error {
|
||||
}
|
||||
|
||||
func (m TestMailClient) SendCode(to string, code string) error {
|
||||
fmt.Printf("Email: %s | Code %s\n", to, code)
|
||||
m.logger.Info("Auth Code", "email", to, "code", code)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateMailClient() (Mailer, error) {
|
||||
func CreateMailClient(log *log.Logger) (Mailer, error) {
|
||||
mode := os.Getenv("MODE")
|
||||
if mode == "DEV" {
|
||||
return TestMailClient{}, nil
|
||||
return TestMailClient{
|
||||
log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
client, err := mail.NewClient(
|
135
backend/auth/handler.go
Normal file
135
backend/auth/handler.go
Normal file
@ -0,0 +1,135 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"os"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/middleware"
|
||||
"screenmark/screenmark/models"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
logger *log.Logger
|
||||
|
||||
user models.UserModel
|
||||
|
||||
auth Auth
|
||||
|
||||
jwtManager *middleware.JwtManager
|
||||
}
|
||||
|
||||
type loginBody struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
type codeBody struct {
|
||||
Email string `json:"email"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
type codeReturn struct {
|
||||
Access string `json:"access"`
|
||||
Refresh string `json:"refresh"`
|
||||
}
|
||||
|
||||
type refreshBody struct {
|
||||
Refresh string `json:"refresh"`
|
||||
}
|
||||
|
||||
type refreshReturn struct {
|
||||
Access string `json:"access"`
|
||||
}
|
||||
|
||||
func (h *AuthHandler) login(body loginBody, w http.ResponseWriter, r *http.Request) {
|
||||
err := h.auth.CreateCode(body.Email)
|
||||
if err != nil {
|
||||
middleware.WriteErrorInternal(h.logger, "could not create a code", w)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) code(body codeBody, w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.auth.UseCode(body.Email, body.Code); err != nil {
|
||||
middleware.WriteErrorBadRequest(h.logger, "email or code are incorrect", w)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: we should only keep emails around for a little bit.
|
||||
// Time to first login should be less than 10 minutes.
|
||||
// So actually, they shouldn't be written to our database.
|
||||
if exists := h.user.DoesUserExist(r.Context(), body.Email); !exists {
|
||||
h.user.Save(r.Context(), model.Users{
|
||||
Email: body.Email,
|
||||
})
|
||||
}
|
||||
|
||||
uuid, err := h.user.GetUserIdFromEmail(r.Context(), body.Email)
|
||||
if err != nil {
|
||||
middleware.WriteErrorBadRequest(h.logger, "failed to get user", w)
|
||||
return
|
||||
}
|
||||
|
||||
refresh := h.jwtManager.CreateRefreshToken(uuid)
|
||||
access := h.jwtManager.CreateAccessToken(uuid)
|
||||
|
||||
codeReturn := codeReturn{
|
||||
Access: access,
|
||||
Refresh: refresh,
|
||||
}
|
||||
|
||||
middleware.WriteJsonOrError(h.logger, codeReturn, w)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) refresh(body refreshBody, w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("token", "refresh", body.Refresh)
|
||||
userId, err := h.jwtManager.GetUserIdFromRefresh(body.Refresh)
|
||||
if err != nil {
|
||||
middleware.WriteErrorBadRequest(h.logger, "invalid refresh token: "+err.Error(), w)
|
||||
return
|
||||
}
|
||||
|
||||
access := h.jwtManager.CreateAccessToken(userId)
|
||||
|
||||
refreshReturn := refreshReturn{
|
||||
Access: access,
|
||||
}
|
||||
|
||||
middleware.WriteJsonOrError(h.logger, refreshReturn, w)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) CreateRoutes(r chi.Router) {
|
||||
h.logger.Info("Mounting auth router")
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.SetJson)
|
||||
|
||||
r.Post("/login", middleware.WithValidatedPost(h.login))
|
||||
r.Post("/code", middleware.WithValidatedPost(h.code))
|
||||
r.Post("/refresh", middleware.WithValidatedPost(h.refresh))
|
||||
})
|
||||
}
|
||||
|
||||
func CreateAuthHandler(db *sql.DB, jwtManager *middleware.JwtManager) AuthHandler {
|
||||
userModel := models.NewUserModel(db)
|
||||
logger := log.New(os.Stdout).WithPrefix("Auth")
|
||||
|
||||
mailer, err := CreateMailClient(logger)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
auth := CreateAuth(mailer)
|
||||
|
||||
return AuthHandler{
|
||||
logger: logger,
|
||||
user: userModel,
|
||||
auth: auth,
|
||||
jwtManager: jwtManager,
|
||||
}
|
||||
}
|
@ -1,146 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"screenmark/screenmark/agents"
|
||||
"screenmark/screenmark/models"
|
||||
"screenmark/screenmark/middleware"
|
||||
"screenmark/screenmark/notifications"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
type Notification struct {
|
||||
ImageID uuid.UUID
|
||||
ImageName string
|
||||
Status string
|
||||
}
|
||||
|
||||
func ListenNewImageEvents(db *sql.DB, notifier *Notifier[Notification]) {
|
||||
listener := pq.NewListener(os.Getenv("DB_CONNECTION"), time.Second, time.Second, func(event pq.ListenerEventType, err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
defer listener.Close()
|
||||
|
||||
locationModel := models.NewLocationModel(db)
|
||||
eventModel := models.NewEventModel(db)
|
||||
noteModel := models.NewNoteModel(db)
|
||||
imageModel := models.NewImageModel(db)
|
||||
contactModel := models.NewContactModel(db)
|
||||
|
||||
databaseEventLog := createLogger("Database Events 🤖", os.Stdout)
|
||||
databaseEventLog.SetLevel(log.DebugLevel)
|
||||
|
||||
err := listener.Listen("new_image")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for parameters := range listener.Notify {
|
||||
imageId := uuid.MustParse(parameters.Extra)
|
||||
|
||||
databaseEventLog.Debug("Starting processing image", "ImageID", imageId)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
go func() {
|
||||
image, err := imageModel.GetToProcessWithData(ctx, imageId)
|
||||
if err != nil {
|
||||
databaseEventLog.Error("Failed to GetToProcessWithData", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
splitWriter := createDbStdoutWriter(db, image.ImageID)
|
||||
|
||||
noteAgent := agents.NewNoteAgent(createLogger("Notes 📝", splitWriter), noteModel)
|
||||
contactAgent := agents.NewContactAgent(createLogger("Contacts 👥", splitWriter), contactModel)
|
||||
locationAgent := agents.NewLocationAgent(createLogger("Locations 📍", splitWriter), locationModel)
|
||||
eventAgent := agents.NewEventAgent(createLogger("Events 📅", splitWriter), eventModel, locationModel)
|
||||
|
||||
if err := imageModel.StartProcessing(ctx, image.ID); err != nil {
|
||||
databaseEventLog.Error("Failed to FinishProcessing", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
orchestrator := agents.NewOrchestratorAgent(createLogger("Orchestrator 🎼", splitWriter), noteAgent, contactAgent, locationAgent, eventAgent, image.Image.ImageName, image.Image.Image)
|
||||
orchestrator.RunAgent(image.UserID, image.ImageID, image.Image.ImageName, image.Image.Image)
|
||||
_, err = imageModel.FinishProcessing(ctx, image.ID)
|
||||
if err != nil {
|
||||
databaseEventLog.Error("Failed to finish processing", "ImageID", imageId, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
databaseEventLog.Debug("Finished processing image", "ImageID", imageId)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func ListenProcessingImageStatus(db *sql.DB, images models.ImageModel, notifier *Notifier[Notification]) {
|
||||
listener := pq.NewListener(os.Getenv("DB_CONNECTION"), time.Second, time.Second, func(event pq.ListenerEventType, err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
defer listener.Close()
|
||||
|
||||
logger := createLogger("Image Status 📊", os.Stdout)
|
||||
|
||||
if err := listener.Listen("new_processing_image_status"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for data := range listener.Notify {
|
||||
imageStringUuid := data.Extra[0:36]
|
||||
status := data.Extra[36:]
|
||||
|
||||
imageUuid, err := uuid.Parse(imageStringUuid)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
processingImage, err := images.GetToProcess(context.Background(), imageUuid)
|
||||
if err != nil {
|
||||
logger.Error("GetToProcess failed", "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info("Update", "id", imageStringUuid, "status", status)
|
||||
|
||||
notification := Notification{
|
||||
ImageID: processingImage.ImageID,
|
||||
ImageName: processingImage.Image.ImageName,
|
||||
Status: status,
|
||||
}
|
||||
|
||||
if err := notifier.SendAndCreate(processingImage.UserID.String(), notification); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO: We have channels open every a user sends an image.
|
||||
* We never close these channels.
|
||||
*
|
||||
* What is a reasonable default? Close the channel after 1 minute of inactivity?
|
||||
*/
|
||||
func CreateEventsHandler(notifier *Notifier[Notification]) http.HandlerFunc {
|
||||
func CreateEventsHandler(notifier *notifications.Notifier[notifications.Notification]) http.HandlerFunc {
|
||||
counter := 0
|
||||
|
||||
userSplitters := make(map[string]*ChannelSplitter[Notification])
|
||||
userSplitters := make(map[string]*notifications.ChannelSplitter[notifications.Notification])
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
_userId := r.Context().Value(USER_ID).(uuid.UUID)
|
||||
_userId := r.Context().Value(middleware.USER_ID).(uuid.UUID)
|
||||
if _userId == uuid.Nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
@ -160,7 +43,7 @@ func CreateEventsHandler(notifier *Notifier[Notification]) http.HandlerFunc {
|
||||
userNotifications := notifier.Listeners[userId]
|
||||
|
||||
if _, exists := userSplitters[userId]; !exists {
|
||||
splitter := NewChannelSplitter(userNotifications)
|
||||
splitter := notifications.NewChannelSplitter(userNotifications)
|
||||
|
||||
userSplitters[userId] = &splitter
|
||||
splitter.Listen()
|
||||
@ -188,7 +71,8 @@ func CreateEventsHandler(notifier *Notifier[Notification]) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Sending msg %s\n", msg)
|
||||
fmt.Printf("Sending msg %s\n", msgString)
|
||||
|
||||
fmt.Fprintf(w, "event: data\ndata: %s\n\n", string(msgString))
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ require (
|
||||
github.com/charmbracelet/x/ansi v0.4.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.1 // indirect
|
||||
github.com/go-jet/jet/v2 v2.12.0 // indirect
|
||||
github.com/go-jet/jet/v2 v2.13.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
|
@ -13,6 +13,8 @@ github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
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/go-jet/jet/v2 v2.13.0 h1:DcD2IJRGos+4X40IQRV6S6q9onoOfZY/GPdvU6ImZcQ=
|
||||
github.com/go-jet/jet/v2 v2.13.0/go.mod h1:YhT75U1FoYAxFOObbQliHmXVYQeffkBKWT7ZilZ3zPc=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
|
232
backend/images/handler.go
Normal file
232
backend/images/handler.go
Normal file
@ -0,0 +1,232 @@
|
||||
package images
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/limits"
|
||||
"screenmark/screenmark/middleware"
|
||||
"screenmark/screenmark/models"
|
||||
"screenmark/screenmark/processor"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ImageHandler struct {
|
||||
logger *log.Logger
|
||||
|
||||
imageModel models.ImageModel
|
||||
userModel models.UserModel
|
||||
|
||||
limitsManager limits.LimitsManagerMethods
|
||||
|
||||
jwtManager *middleware.JwtManager
|
||||
|
||||
processor *processor.Processor[model.Image]
|
||||
}
|
||||
|
||||
type ImagesReturn struct {
|
||||
UserImages []models.UserImageWithImage
|
||||
Stacks []models.ListsWithImages
|
||||
}
|
||||
|
||||
func (h *ImageHandler) serveImage(w http.ResponseWriter, r *http.Request) {
|
||||
imageID, err := middleware.GetPathParamID(h.logger, "id", w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
userID, err := middleware.GetUserID(ctx, h.logger, w)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
image, exists, err := h.imageModel.Get(r.Context(), imageID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprintf(w, "Could not get image")
|
||||
return
|
||||
}
|
||||
|
||||
// Do not leak that this ID exists.
|
||||
if !exists || image.UserID != userID {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: this could be part of the db table
|
||||
extension := filepath.Ext(image.ImageName)
|
||||
if len(extension) == 0 {
|
||||
// Same hack
|
||||
extension = "png"
|
||||
}
|
||||
extension = extension[1:]
|
||||
|
||||
w.Header().Add("Content-Type", "image/"+extension)
|
||||
w.Write(image.Image)
|
||||
}
|
||||
|
||||
func (h *ImageHandler) listImages(w http.ResponseWriter, r *http.Request) {
|
||||
userId, err := middleware.GetUserID(r.Context(), h.logger, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
images, err := h.userModel.GetUserImages(r.Context(), userId)
|
||||
if err != nil {
|
||||
middleware.WriteErrorInternal(h.logger, "could not get user images", w)
|
||||
return
|
||||
}
|
||||
|
||||
stacksWithImages, err := h.userModel.ListWithImages(r.Context(), userId)
|
||||
if err != nil {
|
||||
middleware.WriteErrorInternal(h.logger, "could not get lists with images", w)
|
||||
return
|
||||
}
|
||||
|
||||
imagesReturn := ImagesReturn{
|
||||
UserImages: images,
|
||||
Stacks: stacksWithImages,
|
||||
}
|
||||
|
||||
middleware.WriteJsonOrError(h.logger, imagesReturn, w)
|
||||
}
|
||||
|
||||
func (h *ImageHandler) uploadImage(w http.ResponseWriter, r *http.Request) {
|
||||
imageName := chi.URLParam(r, "name")
|
||||
if len(imageName) == 0 {
|
||||
middleware.WriteErrorBadRequest(h.logger, "you need to provide a name in the path", w)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := middleware.GetUserID(r.Context(), h.logger, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
|
||||
image := make([]byte, 0)
|
||||
switch contentType {
|
||||
case "application/base64":
|
||||
decoder := base64.NewDecoder(base64.StdEncoding, r.Body)
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
_, err := io.Copy(buf, decoder)
|
||||
if err != nil {
|
||||
middleware.WriteErrorBadRequest(h.logger, "base64 decoding failed", w)
|
||||
return
|
||||
}
|
||||
|
||||
image = buf.Bytes()
|
||||
case "application/oclet-stream", "image/png":
|
||||
bodyData, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
middleware.WriteErrorBadRequest(h.logger, "binary data reading failed", w)
|
||||
return
|
||||
}
|
||||
// TODO: check headers
|
||||
|
||||
image = bodyData
|
||||
default:
|
||||
middleware.WriteErrorBadRequest(h.logger, "unsupported content type, need octet-stream or base64", w)
|
||||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
|
||||
newImage, err := h.imageModel.Save(ctx, imageName, image, userID)
|
||||
if err != nil {
|
||||
middleware.WriteErrorInternal(h.logger, "could not save image to DB: "+err.Error(), w)
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("About to add image")
|
||||
h.processor.Add(newImage)
|
||||
|
||||
// We nullify the image's data, so we're not transferring all that
|
||||
// data back to the frontend.
|
||||
newImage.Image = nil
|
||||
|
||||
middleware.WriteJsonOrError(h.logger, newImage, w)
|
||||
}
|
||||
|
||||
func (h *ImageHandler) deleteImage(w http.ResponseWriter, r *http.Request) {
|
||||
stringImageID := chi.URLParam(r, "image-id")
|
||||
imageID, err := uuid.Parse(stringImageID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
userID, err := middleware.GetUserID(ctx, h.logger, w)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
exists, err := h.imageModel.Delete(ctx, imageID, userID)
|
||||
if err != nil {
|
||||
h.logger.Warn("cannot delete image", "error", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Don't leak if the image exists or not
|
||||
if !exists {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *ImageHandler) CreateRoutes(r chi.Router) {
|
||||
h.logger.Info("Mounting image router")
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.ProtectedRouteURL(h.jwtManager))
|
||||
r.Get("/{id}", h.serveImage)
|
||||
})
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.ProtectedRoute(h.jwtManager))
|
||||
r.Use(middleware.SetJson)
|
||||
|
||||
r.Get("/", h.listImages)
|
||||
r.Post("/{name}", middleware.WithLimit(h.logger, h.limitsManager.HasReachedImageLimit, h.uploadImage))
|
||||
r.Delete("/{image-id}", h.deleteImage)
|
||||
})
|
||||
}
|
||||
|
||||
func CreateImageHandler(
|
||||
db *sql.DB,
|
||||
limitsManager limits.LimitsManagerMethods,
|
||||
jwtManager *middleware.JwtManager,
|
||||
processor *processor.Processor[model.Image],
|
||||
) ImageHandler {
|
||||
imageModel := models.NewImageModel(db)
|
||||
userModel := models.NewUserModel(db)
|
||||
logger := log.New(os.Stdout).WithPrefix("Images")
|
||||
|
||||
return ImageHandler{
|
||||
logger: logger,
|
||||
imageModel: imageModel,
|
||||
userModel: userModel,
|
||||
limitsManager: limitsManager,
|
||||
jwtManager: jwtManager,
|
||||
processor: processor,
|
||||
}
|
||||
}
|
803
backend/integration_test.go
Normal file
803
backend/integration_test.go
Normal file
@ -0,0 +1,803 @@
|
||||
// Integration Tests for Haystack Backend
|
||||
//
|
||||
// These tests provide comprehensive end-to-end testing of all API endpoints.
|
||||
//
|
||||
// Requirements:
|
||||
// - Docker must be installed and running
|
||||
// - PostgreSQL Docker image will be automatically pulled and started
|
||||
//
|
||||
// To run the integration tests:
|
||||
//
|
||||
// 1. Start Docker daemon
|
||||
// 2. Run: go test -v ./integration_test.go
|
||||
//
|
||||
// The tests will:
|
||||
// - Start a PostgreSQL container on port 5433
|
||||
// - Set up the database schema
|
||||
// - Test all auth, stack, and image endpoints
|
||||
// - Clean up the container after tests complete
|
||||
//
|
||||
// Note: These tests require Docker and will be skipped if Docker is not available.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"screenmark/screenmark/middleware"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
testDBName = "test_haystack"
|
||||
testDBUser = "test_user"
|
||||
testDBPassword = "test_password"
|
||||
testDBHost = "localhost"
|
||||
testDBPort = "5433"
|
||||
testDBSSLMode = "disable"
|
||||
)
|
||||
|
||||
type TestUser struct {
|
||||
ID uuid.UUID
|
||||
Email string
|
||||
Token string
|
||||
}
|
||||
|
||||
type TestContext struct {
|
||||
db *sql.DB
|
||||
router chi.Router
|
||||
server *httptest.Server
|
||||
users []TestUser
|
||||
cleanup func()
|
||||
jwtManager *middleware.JwtManager
|
||||
}
|
||||
|
||||
func setupTestDatabase() (*sql.DB, func(), error) {
|
||||
// Check if Docker daemon is running
|
||||
checkCmd := exec.Command("docker", "info")
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
return nil, nil, fmt.Errorf("docker daemon is not running: %w", err)
|
||||
}
|
||||
|
||||
// Start PostgreSQL container
|
||||
containerName := "test_postgres_haystack"
|
||||
|
||||
// Clean up any existing container
|
||||
exec.Command("docker", "rm", "-f", containerName).Run()
|
||||
|
||||
// Start new PostgreSQL container
|
||||
cmd := exec.Command("docker", "run", "-d",
|
||||
"--name", containerName,
|
||||
"-e", "POSTGRES_DB="+testDBName,
|
||||
"-e", "POSTGRES_USER="+testDBUser,
|
||||
"-e", "POSTGRES_PASSWORD="+testDBPassword,
|
||||
"-p", testDBPort+":5432",
|
||||
"postgres:15-alpine",
|
||||
)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to start postgres container: %w, output: %s", err, string(output))
|
||||
}
|
||||
|
||||
// Wait for database to be ready with retries
|
||||
maxRetries := 15
|
||||
for i := range maxRetries {
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Test connection
|
||||
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
|
||||
testDBHost, testDBPort, testDBUser, testDBPassword, testDBName, testDBSSLMode)
|
||||
|
||||
testDB, testErr := sql.Open("postgres", connStr)
|
||||
if testErr == nil {
|
||||
if pingErr := testDB.Ping(); pingErr == nil {
|
||||
testDB.Close()
|
||||
break
|
||||
}
|
||||
testDB.Close()
|
||||
}
|
||||
|
||||
if i == maxRetries-1 {
|
||||
return nil, nil, fmt.Errorf("database failed to become ready after %d retries", maxRetries)
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to database
|
||||
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
|
||||
testDBHost, testDBPort, testDBUser, testDBPassword, testDBName, testDBSSLMode)
|
||||
|
||||
db, err := sql.Open("postgres", connStr)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to connect to test database: %w", err)
|
||||
}
|
||||
|
||||
// Test connection
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to ping test database: %w", err)
|
||||
}
|
||||
|
||||
// Load and execute schema
|
||||
schema, err := os.ReadFile("schema.sql")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read schema file: %w", err)
|
||||
}
|
||||
|
||||
if _, err := db.Exec(string(schema)); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to execute schema: %w", err)
|
||||
}
|
||||
|
||||
// Cleanup function
|
||||
cleanup := func() {
|
||||
db.Close()
|
||||
exec.Command("docker", "rm", "-f", containerName).Run()
|
||||
}
|
||||
|
||||
return db, cleanup, nil
|
||||
}
|
||||
|
||||
func setupTestContext(t *testing.T) *TestContext {
|
||||
// Set environment variables for test environment
|
||||
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
|
||||
testDBHost, testDBPort, testDBUser, testDBPassword, testDBName, testDBSSLMode)
|
||||
|
||||
originalDBConn := os.Getenv("DB_CONNECTION")
|
||||
originalTestEnv := os.Getenv("GO_TEST_ENVIRONMENT")
|
||||
|
||||
os.Setenv("DB_CONNECTION", connStr)
|
||||
os.Setenv("GO_TEST_ENVIRONMENT", "true")
|
||||
|
||||
defer func() {
|
||||
if originalDBConn != "" {
|
||||
os.Setenv("DB_CONNECTION", originalDBConn)
|
||||
} else {
|
||||
os.Unsetenv("DB_CONNECTION")
|
||||
}
|
||||
if originalTestEnv != "" {
|
||||
os.Setenv("GO_TEST_ENVIRONMENT", originalTestEnv)
|
||||
} else {
|
||||
os.Unsetenv("GO_TEST_ENVIRONMENT")
|
||||
}
|
||||
}()
|
||||
|
||||
tc := &TestContext{}
|
||||
|
||||
db, cleanup, err := setupTestDatabase()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to setup test database: %v", err)
|
||||
}
|
||||
|
||||
jwtManager := middleware.NewJwtManager([]byte("test-jwt-secret"))
|
||||
router, err := setupRouter(db, jwtManager)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
server := httptest.NewServer(router)
|
||||
|
||||
tc.db = db
|
||||
tc.router = router
|
||||
tc.server = server
|
||||
tc.jwtManager = jwtManager
|
||||
tc.cleanup = func() {
|
||||
server.Close()
|
||||
cleanup()
|
||||
}
|
||||
|
||||
return tc
|
||||
}
|
||||
|
||||
func (tc *TestContext) createTestUser(email string) TestUser {
|
||||
// Insert user into database
|
||||
var userID uuid.UUID
|
||||
err := tc.db.QueryRow("INSERT INTO haystack.users (email) VALUES ($1) RETURNING id", email).Scan(&userID)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to create test user: %v", err))
|
||||
}
|
||||
|
||||
// Create access token for the user
|
||||
accessToken := tc.jwtManager.CreateAccessToken(userID)
|
||||
|
||||
user := TestUser{
|
||||
ID: userID,
|
||||
Email: email,
|
||||
Token: accessToken,
|
||||
}
|
||||
|
||||
tc.users = append(tc.users, user)
|
||||
return user
|
||||
}
|
||||
|
||||
func (tc *TestContext) makeRequest(t *testing.T, method, path, token string, body io.Reader) *http.Response {
|
||||
url := tc.server.URL + path
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (tc *TestContext) makeJSONRequest(t *testing.T, method, path, token string, data any) *http.Response {
|
||||
var body io.Reader
|
||||
if data != nil {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal JSON: %v", err)
|
||||
}
|
||||
body = bytes.NewReader(jsonData)
|
||||
}
|
||||
|
||||
return tc.makeRequest(t, method, path, token, body)
|
||||
}
|
||||
|
||||
// Comprehensive integration test suite - single database setup for all tests
|
||||
func TestAllRoutes(t *testing.T) {
|
||||
tc := setupTestContext(t)
|
||||
defer tc.cleanup()
|
||||
|
||||
// Create test users for different test scenarios
|
||||
stackUser := tc.createTestUser("stacktest@example.com")
|
||||
imageUser := tc.createTestUser("imagetest@example.com")
|
||||
flowUser := tc.createTestUser("flowtest@example.com")
|
||||
|
||||
t.Run("Auth Routes", func(t *testing.T) {
|
||||
t.Run("Login endpoint", func(t *testing.T) {
|
||||
loginData := map[string]string{
|
||||
"email": "test@example.com",
|
||||
}
|
||||
|
||||
resp := tc.makeJSONRequest(t, "POST", "/auth/login", "", loginData)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Code endpoint with valid email", func(t *testing.T) {
|
||||
// First create a login request to set up the email
|
||||
loginData := map[string]string{
|
||||
"email": "test@example.com",
|
||||
}
|
||||
tc.makeJSONRequest(t, "POST", "/auth/login", "", loginData)
|
||||
|
||||
// Then try to use a code (this will fail with invalid code, but tests the endpoint)
|
||||
codeData := map[string]string{
|
||||
"email": "test@example.com",
|
||||
"code": "invalid",
|
||||
}
|
||||
|
||||
resp := tc.makeJSONRequest(t, "POST", "/auth/code", "", codeData)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// The auth system creates a user for new emails, so this returns 200
|
||||
// We're testing that the endpoint works, not necessarily the code validation
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200 for code endpoint, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Protected route without token", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "GET", "/images/image", "", nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected status 401 for protected route without token, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Stack Routes", func(t *testing.T) {
|
||||
t.Run("Get stacks without authentication", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "GET", "/stacks/", "", nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected status 401, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Get stacks with authentication", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "GET", "/stacks/", stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var stacks []interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&stacks); err != nil {
|
||||
t.Errorf("Failed to decode response: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Create stack", func(t *testing.T) {
|
||||
stackData := map[string]string{
|
||||
"title": "Test Stack",
|
||||
"fields": "name,description,value",
|
||||
}
|
||||
|
||||
resp := tc.makeJSONRequest(t, "POST", "/stacks/", stackUser.Token, stackData)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Get stack items with invalid ID", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "GET", "/stacks/invalid-id", stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400 for invalid ID, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete stack without authentication", func(t *testing.T) {
|
||||
fakeUUID := uuid.New()
|
||||
resp := tc.makeRequest(t, "DELETE", "/stacks/"+fakeUUID.String(), "", nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected status 401 for unauthenticated delete, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete stack with invalid ID", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "DELETE", "/stacks/invalid-id", stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400 for invalid ID, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete non-existent stack", func(t *testing.T) {
|
||||
fakeUUID := uuid.New()
|
||||
resp := tc.makeRequest(t, "DELETE", "/stacks/"+fakeUUID.String(), stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400 for non-existent stack, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Create and delete stack successfully", func(t *testing.T) {
|
||||
// First create a stack
|
||||
stackData := map[string]string{
|
||||
"title": "Stack to Delete",
|
||||
"fields": "name,description,value",
|
||||
}
|
||||
|
||||
resp := tc.makeJSONRequest(t, "POST", "/stacks/", stackUser.Token, stackData)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Failed to create stack for deletion test, got %d", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the list of stacks to find the created stack ID
|
||||
resp = tc.makeRequest(t, "GET", "/stacks/", stackUser.Token, nil)
|
||||
|
||||
var stacks []map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&stacks); err != nil {
|
||||
t.Errorf("Failed to decode stacks response: %v", err)
|
||||
resp.Body.Close()
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if len(stacks) == 0 {
|
||||
t.Errorf("No stacks found after creation")
|
||||
return
|
||||
}
|
||||
|
||||
// Find the stack we just created
|
||||
var stackToDelete map[string]interface{}
|
||||
for _, stack := range stacks {
|
||||
if name, ok := stack["Name"].(string); ok && name == "Stack to Delete" {
|
||||
stackToDelete = stack
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if stackToDelete == nil {
|
||||
t.Errorf("Could not find created stack")
|
||||
return
|
||||
}
|
||||
|
||||
stackID, ok := stackToDelete["ID"].(string)
|
||||
if !ok {
|
||||
t.Errorf("Stack ID not found or not a string")
|
||||
return
|
||||
}
|
||||
|
||||
// Now delete the stack
|
||||
resp = tc.makeRequest(t, "DELETE", "/stacks/"+stackID, stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200 for successful delete, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Verify the stack is gone by trying to get it again
|
||||
resp = tc.makeRequest(t, "GET", "/stacks/", stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var stacksAfterDelete []map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&stacksAfterDelete); err != nil {
|
||||
t.Errorf("Failed to decode stacks response after delete: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check that the deleted stack is no longer in the list
|
||||
for _, stack := range stacksAfterDelete {
|
||||
if id, ok := stack["ID"].(string); ok && id == stackID {
|
||||
t.Errorf("Stack still exists after deletion")
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete stack belonging to different user", func(t *testing.T) {
|
||||
// Create a stack with stackUser
|
||||
stackData := map[string]string{
|
||||
"title": "Other User's Stack",
|
||||
"fields": "name,description,value",
|
||||
}
|
||||
|
||||
resp := tc.makeJSONRequest(t, "POST", "/stacks/", stackUser.Token, stackData)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Failed to create stack for ownership test, got %d", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the stack ID
|
||||
resp = tc.makeRequest(t, "GET", "/stacks/", stackUser.Token, nil)
|
||||
|
||||
var stacks []map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&stacks); err != nil {
|
||||
t.Errorf("Failed to decode stacks response: %v", err)
|
||||
resp.Body.Close()
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
var stackID string
|
||||
for _, stack := range stacks {
|
||||
if name, ok := stack["Name"].(string); ok && name == "Other User's Stack" {
|
||||
if id, ok := stack["ID"].(string); ok {
|
||||
stackID = id
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if stackID == "" {
|
||||
t.Errorf("Could not find created stack ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Try to delete the stack with a different user (imageUser)
|
||||
resp = tc.makeRequest(t, "DELETE", "/stacks/"+stackID, imageUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400 when deleting another user's stack, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Image Routes", func(t *testing.T) {
|
||||
t.Run("Get images without authentication", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "GET", "/images/", "", nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected status 401, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Get images with authentication", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "GET", "/images/", imageUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var imageData interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&imageData); err != nil {
|
||||
t.Errorf("Failed to decode response: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Upload image with base64", func(t *testing.T) {
|
||||
// Create a simple valid base64 string for testing
|
||||
testImageBase64 := "dGVzdCBkYXRh" // "test data" in base64
|
||||
|
||||
req, err := http.NewRequest("POST", tc.server.URL+"/images/test.png", strings.NewReader(testImageBase64))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+imageUser.Token)
|
||||
req.Header.Set("Content-Type", "application/base64")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// The API might return 200 for successful operations
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Errorf("Expected status 200 or 201, got %d. Response: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Upload image with binary data", func(t *testing.T) {
|
||||
// Create a small test image (minimal PNG)
|
||||
testImageBinary := []byte{
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D,
|
||||
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
||||
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00,
|
||||
0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0x99, 0x01, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x37, 0x6E, 0xF9, 0x5F, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x49,
|
||||
0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", tc.server.URL+"/images/test2.png", bytes.NewReader(testImageBinary))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+imageUser.Token)
|
||||
req.Header.Set("Content-Type", "image/png")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// The API might return 200 for successful operations
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Errorf("Expected status 200 or 201, got %d. Response: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Upload image without name", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "POST", "/images/", imageUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Route pattern doesn't match empty names, so returns 404
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
t.Errorf("Expected status 404 for missing name, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Serve non-existent image", func(t *testing.T) {
|
||||
fakeUUID := uuid.New()
|
||||
resp := tc.makeRequest(t, "GET", "/images/"+fakeUUID.String(), "", nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
t.Errorf("Expected status 404 for non-existent image, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Complete User Flow", func(t *testing.T) {
|
||||
// Step 1: Test authentication is working
|
||||
resp := tc.makeRequest(t, "GET", "/images/", flowUser.Token, nil)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Authentication failed, expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Step 2: Upload an image
|
||||
testImageBinary := []byte{
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D,
|
||||
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
||||
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00,
|
||||
0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0x99, 0x01, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x37, 0x6E, 0xF9, 0x5F, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x49,
|
||||
0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", tc.server.URL+"/images/test_flow.png", bytes.NewReader(testImageBinary))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create upload request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+flowUser.Token)
|
||||
req.Header.Set("Content-Type", "image/png")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to upload image: %v", err)
|
||||
}
|
||||
|
||||
// The API returns 200 for successful image uploads
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Errorf("Image upload failed, expected 200, got %d. Response: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Step 3: Verify image appears in user's image list
|
||||
resp = tc.makeRequest(t, "GET", "/images/", flowUser.Token, nil)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Failed to get user images, expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var imageData map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&imageData); err != nil {
|
||||
t.Errorf("Failed to decode image list: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Check that we have user images
|
||||
if userImages, ok := imageData["userImages"].([]interface{}); ok {
|
||||
if len(userImages) == 0 {
|
||||
t.Log("Warning: No user images found, but upload succeeded")
|
||||
} else {
|
||||
t.Logf("Found %d user images", len(userImages))
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Test stack creation
|
||||
stackData := map[string]string{
|
||||
"title": "Integration Test Stack",
|
||||
"fields": "name,description,value",
|
||||
}
|
||||
|
||||
resp = tc.makeJSONRequest(t, "POST", "/stacks/", flowUser.Token, stackData)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Stack creation failed, expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Step 5: Verify stack appears in user's stack list
|
||||
resp = tc.makeRequest(t, "GET", "/stacks/", flowUser.Token, nil)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Failed to get user stacks, expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var stacks []interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&stacks); err != nil {
|
||||
t.Errorf("Failed to decode stack list: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if len(stacks) == 0 {
|
||||
t.Log("Warning: No stacks found, but creation succeeded")
|
||||
} else {
|
||||
t.Logf("Found %d stacks", len(stacks))
|
||||
}
|
||||
|
||||
t.Log("Complete user flow test passed!")
|
||||
})
|
||||
}
|
||||
|
||||
// Simple test that doesn't require Docker
|
||||
func TestIntegrationTestSetup(t *testing.T) {
|
||||
// This test verifies that the test structure is correct
|
||||
// It doesn't require Docker to be running
|
||||
|
||||
t.Run("Test structure validation", func(t *testing.T) {
|
||||
// This test verifies that the test structure is correct
|
||||
// It doesn't require Docker to be running
|
||||
|
||||
// Verify that our test types are properly defined
|
||||
var _ TestUser
|
||||
var _ TestContext
|
||||
|
||||
// Verify that our constants are defined
|
||||
if testDBName == "" {
|
||||
t.Error("testDBName constant is not defined")
|
||||
}
|
||||
|
||||
if testDBPort == "" {
|
||||
t.Error("testDBPort constant is not defined")
|
||||
}
|
||||
|
||||
t.Log("Test structure is valid")
|
||||
})
|
||||
|
||||
t.Run("Database and router setup", func(t *testing.T) {
|
||||
// This test verifies that the database and router can be set up without SSL errors
|
||||
tc := setupTestContext(t)
|
||||
defer tc.cleanup()
|
||||
|
||||
// Verify that the router was created successfully
|
||||
if tc.router == nil {
|
||||
t.Error("Router was not created successfully")
|
||||
}
|
||||
|
||||
// Verify that the server was created successfully
|
||||
if tc.server == nil {
|
||||
t.Error("Server was not created successfully")
|
||||
}
|
||||
|
||||
// Verify that the database connection is working
|
||||
if err := tc.db.Ping(); err != nil {
|
||||
t.Errorf("Database connection failed: %v", err)
|
||||
}
|
||||
|
||||
t.Log("Database and router setup successful - no SSL errors!")
|
||||
})
|
||||
|
||||
t.Run("Docker availability check", func(t *testing.T) {
|
||||
// Check if Docker is available but don't fail the test
|
||||
if _, err := exec.LookPath("docker"); err != nil {
|
||||
t.Skip("Docker not found, skipping Docker-dependent tests")
|
||||
}
|
||||
|
||||
// Check if Docker daemon is running
|
||||
checkCmd := exec.Command("docker", "info")
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
t.Skip("Docker daemon is not running, skipping Docker-dependent tests")
|
||||
}
|
||||
|
||||
t.Log("Docker is available and running")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// Check if Docker is available
|
||||
if _, err := exec.LookPath("docker"); err != nil {
|
||||
fmt.Println("Docker not found, skipping integration tests")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Check if Docker daemon is running
|
||||
checkCmd := exec.Command("docker", "info")
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
fmt.Println("Docker daemon is not running, skipping integration tests")
|
||||
fmt.Println("To run integration tests, start Docker daemon and try again")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Run tests
|
||||
code := m.Run()
|
||||
os.Exit(code)
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type JwtType string
|
||||
|
||||
const (
|
||||
Access JwtType = "access"
|
||||
Refresh JwtType = "refresh"
|
||||
)
|
||||
|
||||
type JwtClaims struct {
|
||||
UserID string
|
||||
Type JwtType
|
||||
Expire time.Time
|
||||
}
|
||||
|
||||
// obviously this is very not secure. TODO: extract to env
|
||||
var JWT_SECRET = []byte("very secret")
|
||||
|
||||
func createToken(claims JwtClaims) *jwt.Token {
|
||||
return jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"UserID": claims.UserID,
|
||||
"Type": claims.Type,
|
||||
"Expire": claims.Expire,
|
||||
})
|
||||
}
|
||||
|
||||
func CreateRefreshToken(userId uuid.UUID) string {
|
||||
token := createToken(JwtClaims{
|
||||
UserID: userId.String(),
|
||||
Type: Refresh,
|
||||
Expire: time.Now().Add(time.Hour * 24 * 7),
|
||||
})
|
||||
|
||||
// TODO: bruh what is this
|
||||
tokenString, err := token.SignedString(JWT_SECRET)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return tokenString
|
||||
}
|
||||
|
||||
func CreateAccessToken(userId uuid.UUID) string {
|
||||
token := createToken(JwtClaims{
|
||||
UserID: userId.String(),
|
||||
Type: Access,
|
||||
Expire: time.Now().Add(time.Hour),
|
||||
})
|
||||
|
||||
// TODO: bruh what is this
|
||||
tokenString, err := token.SignedString(JWT_SECRET)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return tokenString
|
||||
}
|
||||
|
||||
var NotValidToken = errors.New("Not a valid token")
|
||||
|
||||
func GetUserIdFromAccess(accessToken string) (uuid.UUID, error) {
|
||||
token, err := jwt.Parse(accessToken, func(token *jwt.Token) (any, error) {
|
||||
return JWT_SECRET, nil
|
||||
}, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))
|
||||
|
||||
if err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
|
||||
// Blah blah, check expiry and stuff
|
||||
|
||||
// this function is stupid
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok {
|
||||
tokenType, ok := claims["Type"]
|
||||
if !ok || tokenType.(string) != "access" {
|
||||
return uuid.Nil, NotValidToken
|
||||
}
|
||||
|
||||
userId, err := uuid.Parse(claims["UserID"].(string))
|
||||
if err != nil {
|
||||
return uuid.Nil, NotValidToken
|
||||
}
|
||||
|
||||
return userId, nil
|
||||
} else {
|
||||
return uuid.Nil, NotValidToken
|
||||
}
|
||||
}
|
61
backend/limits/limits.go
Normal file
61
backend/limits/limits.go
Normal file
@ -0,0 +1,61 @@
|
||||
package limits
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
LISTS_LIMIT = 10
|
||||
IMAGE_LIMIT = 10
|
||||
)
|
||||
|
||||
type LimitsManager struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
type LimitsManagerMethods interface {
|
||||
HasReachedStackLimit(userID uuid.UUID) (bool, error)
|
||||
HasReachedImageLimit(userID uuid.UUID) (bool, error)
|
||||
}
|
||||
|
||||
type listCount struct {
|
||||
ListCount int `alias:"list_count"`
|
||||
}
|
||||
|
||||
func (m *LimitsManager) HasReachedStackLimit(userID uuid.UUID) (bool, error) {
|
||||
getStacks := Stacks.
|
||||
SELECT(COUNT(Stacks.UserID).AS("listCount.ListCount")).
|
||||
WHERE(Stacks.UserID.EQ(UUID(userID)))
|
||||
|
||||
var count listCount
|
||||
err := getStacks.Query(m.dbPool, &count)
|
||||
|
||||
return count.ListCount >= LISTS_LIMIT, err
|
||||
}
|
||||
|
||||
type imageCount struct {
|
||||
ImageCount int `alias:"image_count"`
|
||||
}
|
||||
|
||||
func (m *LimitsManager) HasReachedImageLimit(userID uuid.UUID) (bool, error) {
|
||||
getStacks := Image.
|
||||
SELECT(COUNT(Image.UserID).AS("imageCount.ImageCount")).
|
||||
WHERE(Image.UserID.EQ(UUID(userID)))
|
||||
|
||||
var count imageCount
|
||||
err := getStacks.Query(m.dbPool, &count)
|
||||
|
||||
return count.ImageCount >= IMAGE_LIMIT, err
|
||||
}
|
||||
|
||||
func CreateLimitsManager(db *sql.DB) *LimitsManager {
|
||||
return &LimitsManager{
|
||||
db,
|
||||
}
|
||||
}
|
@ -1,21 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
"github.com/robert-nix/ansihtml"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
"github.com/muesli/termenv"
|
||||
@ -31,12 +21,6 @@ func (w *DatabaseWriter) Write(p []byte) (n int, err error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
insertLogStmt := Logs.
|
||||
INSERT(Logs.Log, Logs.ImageID).
|
||||
VALUES(string(p), w.imageId)
|
||||
|
||||
_, err = insertLogStmt.Exec(w.dbPool)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else {
|
||||
@ -44,85 +28,6 @@ func (w *DatabaseWriter) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *DatabaseWriter) GetImageLogs(ctx context.Context, imageId uuid.UUID) ([]string, error) {
|
||||
getImageLogsStmt := Logs.
|
||||
SELECT(Logs.Log).
|
||||
WHERE(Logs.ImageID.EQ(UUID(imageId)))
|
||||
|
||||
logs := []model.Logs{}
|
||||
err := getImageLogsStmt.QueryContext(ctx, w.dbPool, &logs)
|
||||
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
stringLogs := make([]string, len(logs))
|
||||
for i, log := range logs {
|
||||
stringLogs[i] = log.Log
|
||||
}
|
||||
|
||||
return stringLogs, nil
|
||||
}
|
||||
|
||||
func createLogHandler(logWriter *DatabaseWriter) func(r chi.Router) {
|
||||
return func(r chi.Router) {
|
||||
r.Get("/{imageId}", func(w http.ResponseWriter, r *http.Request) {
|
||||
stringImageId := r.PathValue("imageId")
|
||||
imageId, err := uuid.Parse(stringImageId)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
logs, err := logWriter.GetImageLogs(r.Context(), imageId)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
html := ""
|
||||
|
||||
imageTag := fmt.Sprintf(`<image src="https://haystack.johncosta.tech/image/%s">`, stringImageId)
|
||||
|
||||
for _, log := range logs {
|
||||
html += fmt.Sprintf("<div>%s</div>", string(ansihtml.ConvertToHTML([]byte(log)))+"\n")
|
||||
}
|
||||
|
||||
css := `
|
||||
<style>
|
||||
body {
|
||||
background-color: #1e1e1e;
|
||||
color: #f0f0f0;
|
||||
font-family: sans-serif;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Basic styling for code blocks often used for logs */
|
||||
pre {
|
||||
background-color: #2a2a2a;
|
||||
padding: 1em;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
`
|
||||
|
||||
fullHtml := fmt.Sprintf("<html><head><title>Logs</title>%s</head><body>%s%s</body></html>", css, imageTag, html)
|
||||
|
||||
w.Header().Add("Content-Type", "text/html")
|
||||
w.Write([]byte(fullHtml))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newDatabaseWriter(dbPool *sql.DB, imageId uuid.UUID) *DatabaseWriter {
|
||||
return &DatabaseWriter{
|
||||
dbPool: dbPool,
|
||||
|
409
backend/main.go
409
backend/main.go
@ -1,421 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"os"
|
||||
"screenmark/screenmark/middleware"
|
||||
"screenmark/screenmark/models"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/google/uuid"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
type TestAiClient struct {
|
||||
ImageInfo client.ImageMessageContent
|
||||
}
|
||||
|
||||
func (client TestAiClient) GetImageInfo(imageName string, imageData []byte) (client.ImageMessageContent, error) {
|
||||
return client.ImageInfo, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
jwtSecret := os.Getenv("JWT_SECRET")
|
||||
if jwtSecret == "" {
|
||||
panic("JWT_SECRET environment variable not set")
|
||||
}
|
||||
|
||||
jwtManager := middleware.NewJwtManager([]byte(jwtSecret))
|
||||
|
||||
db, err := models.InitDatabase()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
imageModel := models.NewImageModel(db)
|
||||
userModel := models.NewUserModel(db)
|
||||
|
||||
mail, err := CreateMailClient()
|
||||
router, err := setupRouter(db, jwtManager)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
auth := CreateAuth(mail)
|
||||
|
||||
notifier := NewNotifier[Notification](10)
|
||||
|
||||
go ListenNewImageEvents(db, ¬ifier)
|
||||
go ListenProcessingImageStatus(db, imageModel, ¬ifier)
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(CorsMiddleware)
|
||||
r.Options("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
// Temporarily not in protect route because we aren't using cookies.
|
||||
// Therefore they don't get automatically attached to the request.
|
||||
// So <img src=""> cannot send the tokensend the token
|
||||
r.Get("/image/{id}", func(w http.ResponseWriter, r *http.Request) {
|
||||
stringImageId := r.PathValue("id")
|
||||
// userId := r.Context().Value(USER_ID).(uuid.UUID)
|
||||
|
||||
imageId, err := uuid.Parse(stringImageId)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
fmt.Fprintf(w, "You cannot read this")
|
||||
return
|
||||
}
|
||||
|
||||
// if authorized := imageModel.IsUserAuthorized(r.Context(), imageId, userId); !authorized {
|
||||
// w.WriteHeader(http.StatusForbidden)
|
||||
// fmt.Fprintf(w, "You cannot read this")
|
||||
// return
|
||||
// }
|
||||
|
||||
image, err := imageModel.Get(r.Context(), imageId)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprintf(w, "Could not get image")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: this could be part of the db table
|
||||
extension := filepath.Ext(image.ImageName)
|
||||
if len(extension) == 0 {
|
||||
// Same hack
|
||||
extension = "png"
|
||||
}
|
||||
extension = extension[1:]
|
||||
|
||||
w.Header().Add("Content-Type", "image/"+extension)
|
||||
w.Write(image.Image)
|
||||
})
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(ProtectedRoute)
|
||||
r.Use(func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
})
|
||||
|
||||
r.Get("/image", func(w http.ResponseWriter, r *http.Request) {
|
||||
userId := r.Context().Value(USER_ID).(uuid.UUID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
fmt.Fprintf(w, "You cannot read this")
|
||||
return
|
||||
}
|
||||
|
||||
imageProperties, err := userModel.ListWithProperties(r.Context(), userId)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprintf(w, "Something went wrong")
|
||||
return
|
||||
}
|
||||
|
||||
images, err := userModel.GetUserImages(r.Context(), userId)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprintf(w, "Something went wrong")
|
||||
return
|
||||
}
|
||||
|
||||
processingImages, err := imageModel.GetProcessing(r.Context(), userId)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprintf(w, "Something went wrong")
|
||||
return
|
||||
}
|
||||
|
||||
type ImagesReturn struct {
|
||||
UserImages []models.UserImageWithImage
|
||||
ImageProperties []models.TypedProperties
|
||||
ProcessingImages []models.UserProcessingImage
|
||||
}
|
||||
|
||||
imagesReturn := ImagesReturn{
|
||||
UserImages: images,
|
||||
ImageProperties: models.GetTypedImageProperties(imageProperties),
|
||||
ProcessingImages: processingImages,
|
||||
}
|
||||
|
||||
jsonImages, err := json.Marshal(imagesReturn)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(w, "Could not create JSON response for this image")
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(jsonImages)
|
||||
})
|
||||
|
||||
r.Get("/image-properties/{id}", func(w http.ResponseWriter, r *http.Request) {
|
||||
userId := r.Context().Value(USER_ID).(uuid.UUID)
|
||||
stringImageId := r.PathValue("id")
|
||||
|
||||
imageId, err := uuid.Parse(stringImageId)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
fmt.Fprintf(w, "You cannot read this")
|
||||
return
|
||||
}
|
||||
|
||||
image, err := userModel.ListImageWithProperties(r.Context(), userId, imageId)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprintf(w, "Something went wrong")
|
||||
return
|
||||
}
|
||||
|
||||
jsonImages, err := json.Marshal(models.GetTypedImageProperties([]models.ImageWithProperties{image})[0])
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(w, "Could not create JSON response for this image")
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(jsonImages)
|
||||
})
|
||||
|
||||
r.Post("/image/{name}", func(w http.ResponseWriter, r *http.Request) {
|
||||
imageName := r.PathValue("name")
|
||||
userId := r.Context().Value(USER_ID).(uuid.UUID)
|
||||
|
||||
if len(imageName) == 0 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(w, "You need to provide a name in the path")
|
||||
return
|
||||
}
|
||||
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
|
||||
fmt.Printf("Content-Type: %s\n", contentType)
|
||||
|
||||
// TODO: length checks on body
|
||||
// TODO: extract this shit out
|
||||
image := make([]byte, 0)
|
||||
if contentType == "application/base64" {
|
||||
decoder := base64.NewDecoder(base64.StdEncoding, r.Body)
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
decodedIamge, err := io.Copy(buf, decoder)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(w, "bruh, base64 aint decoding")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println(string(image))
|
||||
fmt.Println(decodedIamge)
|
||||
|
||||
image = buf.Bytes()
|
||||
} else if contentType == "application/oclet-stream" || contentType == "image/png" {
|
||||
bodyData, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(w, "bruh, binary aint binaring")
|
||||
return
|
||||
}
|
||||
// TODO: check headers
|
||||
|
||||
image = bodyData
|
||||
} else {
|
||||
log.Println("bad stuff?")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(w, "Bruh, you need oclet stream or base64")
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Println("First case")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(w, "Couldnt read the image from the request body")
|
||||
return
|
||||
}
|
||||
|
||||
userImage, err := imageModel.Process(r.Context(), userId, model.Image{
|
||||
Image: image,
|
||||
ImageName: imageName,
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("Second case")
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(w, "Could not save image to DB")
|
||||
return
|
||||
}
|
||||
|
||||
jsonUserImage, err := json.Marshal(userImage)
|
||||
if err != nil {
|
||||
log.Println("Third case")
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(w, "Could not create JSON response for this image")
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
|
||||
fmt.Fprint(w, string(jsonUserImage))
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
r.Route("/notifications", func(r chi.Router) {
|
||||
r.Use(GetUserIdFromUrl)
|
||||
|
||||
r.Get("/", CreateEventsHandler(¬ifier))
|
||||
})
|
||||
|
||||
r.Post("/login", func(w http.ResponseWriter, r *http.Request) {
|
||||
type LoginBody struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
loginBody := LoginBody{}
|
||||
err := json.NewDecoder(r.Body).Decode(&loginBody)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(w, "Request body was not correct")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: validate it's an email
|
||||
|
||||
auth.CreateCode(loginBody.Email)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
type CodeReturn struct {
|
||||
Access string `json:"access"`
|
||||
Refresh string `json:"refresh"`
|
||||
port, exists := os.LookupEnv("PORT")
|
||||
if !exists {
|
||||
panic("no port can be found")
|
||||
}
|
||||
|
||||
r.Post("/code", func(w http.ResponseWriter, r *http.Request) {
|
||||
type CodeBody struct {
|
||||
Email string `json:"email"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
portWithColon := fmt.Sprintf(":%s", port)
|
||||
|
||||
codeBody := CodeBody{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&codeBody); err != nil {
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(w, "Request body was not correct")
|
||||
return
|
||||
}
|
||||
logger := createLogger("Main", os.Stdout)
|
||||
|
||||
if err := auth.UseCode(codeBody.Email, codeBody.Code); err != nil {
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(w, "email or code are incorrect")
|
||||
return
|
||||
}
|
||||
|
||||
if exists := userModel.DoesUserExist(r.Context(), codeBody.Email); !exists {
|
||||
userModel.Save(r.Context(), model.Users{
|
||||
Email: codeBody.Email,
|
||||
})
|
||||
}
|
||||
|
||||
uuid, err := userModel.GetUserIdFromEmail(r.Context(), codeBody.Email)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "Something went wrong.")
|
||||
return
|
||||
}
|
||||
|
||||
refresh := CreateRefreshToken(uuid)
|
||||
access := CreateAccessToken(uuid)
|
||||
|
||||
codeReturn := CodeReturn{
|
||||
Access: access,
|
||||
Refresh: refresh,
|
||||
}
|
||||
|
||||
fmt.Println(codeReturn)
|
||||
|
||||
json, err := json.Marshal(codeReturn)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "Something went wrong.")
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
||||
fmt.Fprint(w, string(json))
|
||||
})
|
||||
|
||||
r.Get("/demo-login", func(w http.ResponseWriter, r *http.Request) {
|
||||
uuid, err := userModel.GetUserIdFromEmail(r.Context(), "demo@email.com")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "Something went wrong.")
|
||||
return
|
||||
}
|
||||
|
||||
refresh := CreateRefreshToken(uuid)
|
||||
access := CreateAccessToken(uuid)
|
||||
|
||||
codeReturn := CodeReturn{
|
||||
Access: access,
|
||||
Refresh: refresh,
|
||||
}
|
||||
|
||||
fmt.Println(codeReturn)
|
||||
|
||||
json, err := json.Marshal(codeReturn)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
fmt.Fprintf(w, "Something went wrong.")
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
||||
fmt.Fprint(w, string(json))
|
||||
})
|
||||
|
||||
logWriter := DatabaseWriter{
|
||||
dbPool: db,
|
||||
}
|
||||
|
||||
r.Route("/logs", createLogHandler(&logWriter))
|
||||
|
||||
log.Println("Listening and serving on port 3040.")
|
||||
if err := http.ListenAndServe(":3040", r); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
logger.Info("Serving router", "port", portWithColon)
|
||||
err = http.ListenAndServe(portWithColon, router)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
@ -1,61 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func CorsMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(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", "*")
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
const USER_ID = "UserID"
|
||||
|
||||
func ProtectedRoute(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get("Authorization")
|
||||
if len(token) < len("Bearer ") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
userId, err := GetUserIdFromAccess(token[len("Bearer "):])
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
contextWithUserId := context.WithValue(r.Context(), USER_ID, userId)
|
||||
|
||||
newR := r.WithContext(contextWithUserId)
|
||||
next.ServeHTTP(w, newR)
|
||||
})
|
||||
}
|
||||
|
||||
func GetUserIdFromUrl(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.URL.Query().Get("token")
|
||||
|
||||
if len(token) == 0 {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
userId, err := GetUserIdFromAccess(token)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
contextWithUserId := context.WithValue(r.Context(), USER_ID, userId)
|
||||
|
||||
newR := r.WithContext(contextWithUserId)
|
||||
next.ServeHTTP(w, newR)
|
||||
})
|
||||
}
|
29
backend/middleware/body.go
Normal file
29
backend/middleware/body.go
Normal file
@ -0,0 +1,29 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func WithValidatedPost[K any](
|
||||
fn func(request K, w http.ResponseWriter, r *http.Request),
|
||||
) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
request := new(K)
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, request)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
fn(*request, w, r)
|
||||
}
|
||||
}
|
11
backend/middleware/json.go
Normal file
11
backend/middleware/json.go
Normal file
@ -0,0 +1,11 @@
|
||||
package middleware
|
||||
|
||||
import "net/http"
|
||||
|
||||
func SetJson(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
136
backend/middleware/jwt.go
Normal file
136
backend/middleware/jwt.go
Normal file
@ -0,0 +1,136 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type JwtType string
|
||||
|
||||
const (
|
||||
Access JwtType = "access"
|
||||
Refresh JwtType = "refresh"
|
||||
)
|
||||
|
||||
type JwtClaims struct {
|
||||
UserID string
|
||||
Type JwtType
|
||||
Expiry time.Time
|
||||
}
|
||||
|
||||
type JwtManager struct {
|
||||
secret []byte
|
||||
}
|
||||
|
||||
func NewJwtManager(secret []byte) *JwtManager {
|
||||
return &JwtManager{secret: secret}
|
||||
}
|
||||
|
||||
func (jm *JwtManager) createToken(claims JwtClaims) *jwt.Token {
|
||||
return jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"UserID": claims.UserID,
|
||||
"Type": claims.Type,
|
||||
"exp": claims.Expiry.Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
func (jm *JwtManager) CreateRefreshToken(userId uuid.UUID) string {
|
||||
token := jm.createToken(JwtClaims{
|
||||
UserID: userId.String(),
|
||||
Type: Refresh,
|
||||
Expiry: time.Now().Add(time.Hour * 24 * 30),
|
||||
})
|
||||
|
||||
tokenString, err := token.SignedString(jm.secret)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return tokenString
|
||||
}
|
||||
|
||||
func (jm *JwtManager) CreateAccessToken(userId uuid.UUID) string {
|
||||
token := jm.createToken(JwtClaims{
|
||||
UserID: userId.String(),
|
||||
Type: Access,
|
||||
Expiry: time.Now().Add(time.Minute),
|
||||
})
|
||||
|
||||
tokenString, err := token.SignedString(jm.secret)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return tokenString
|
||||
}
|
||||
|
||||
var NotValidToken = errors.New("Not a valid token")
|
||||
|
||||
func (jm *JwtManager) GetUserIdFromAccess(accessToken string) (uuid.UUID, error) {
|
||||
token, err := jwt.Parse(accessToken, func(token *jwt.Token) (any, error) {
|
||||
return jm.secret, nil
|
||||
}, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))
|
||||
|
||||
if err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
|
||||
// Check if token is valid (JWT library validates exp claim automatically)
|
||||
if !token.Valid {
|
||||
return uuid.Nil, NotValidToken
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok {
|
||||
tokenType, ok := claims["Type"]
|
||||
if !ok || tokenType.(string) != "access" {
|
||||
return uuid.Nil, NotValidToken
|
||||
}
|
||||
|
||||
userId, err := uuid.Parse(claims["UserID"].(string))
|
||||
if err != nil {
|
||||
return uuid.Nil, NotValidToken
|
||||
}
|
||||
|
||||
return userId, nil
|
||||
} else {
|
||||
return uuid.Nil, NotValidToken
|
||||
}
|
||||
}
|
||||
|
||||
func (jm *JwtManager) GetUserIdFromRefresh(refreshToken string) (uuid.UUID, error) {
|
||||
token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (any, error) {
|
||||
return jm.secret, nil
|
||||
}, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))
|
||||
|
||||
if err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
|
||||
// Check if token is valid (JWT library validates exp claim automatically)
|
||||
if !token.Valid {
|
||||
return uuid.Nil, NotValidToken
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok {
|
||||
tokenType, ok := claims["Type"]
|
||||
if !ok || tokenType.(string) != "refresh" {
|
||||
return uuid.Nil, NotValidToken
|
||||
}
|
||||
|
||||
userId, err := uuid.Parse(claims["UserID"].(string))
|
||||
if err != nil {
|
||||
return uuid.Nil, NotValidToken
|
||||
}
|
||||
|
||||
return userId, nil
|
||||
} else {
|
||||
return uuid.Nil, NotValidToken
|
||||
}
|
||||
}
|
||||
|
||||
func GetUserIdFromAccess(jm *JwtManager, accessToken string) (uuid.UUID, error) {
|
||||
return jm.GetUserIdFromAccess(accessToken)
|
||||
}
|
36
backend/middleware/limits.go
Normal file
36
backend/middleware/limits.go
Normal file
@ -0,0 +1,36 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func WithLimit(logger *log.Logger, getLimit func(userID uuid.UUID) (bool, error), next func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
userID, err := GetUserID(ctx, logger, w)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
hasReachedLimit, err := getLimit(userID)
|
||||
if err != nil {
|
||||
logger.Error("failed to image limit", "err", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("Limits", "hasReachedLimit", hasReachedLimit)
|
||||
|
||||
if hasReachedLimit {
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
140
backend/middleware/middleware.go
Normal file
140
backend/middleware/middleware.go
Normal file
@ -0,0 +1,140 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func CorsMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Add("Access-Control-Allow-Headers", "*")
|
||||
|
||||
// Access-Control-Allow-Methods is often needed for preflight OPTIONS requests
|
||||
w.Header().Add("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||||
|
||||
// The client makes an OPTIONS preflight request before a complex request.
|
||||
// We must handle this and respond with the appropriate headers.
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
const USER_ID = "UserID"
|
||||
|
||||
func GetUserID(ctx context.Context, logger *log.Logger, w http.ResponseWriter) (uuid.UUID, error) {
|
||||
userId := ctx.Value(USER_ID)
|
||||
|
||||
if userId == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
logger.Warn("UserID not present in request")
|
||||
return uuid.Nil, errors.New("context does not contain a user id")
|
||||
}
|
||||
|
||||
userIdUuid, ok := userId.(uuid.UUID)
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
logger.Warn("UserID not of correct type")
|
||||
return uuid.Nil, fmt.Errorf("context user id is not of type uuid, got: %t", userId)
|
||||
}
|
||||
|
||||
return userIdUuid, nil
|
||||
}
|
||||
|
||||
func ProtectedRouteURL(jm *JwtManager) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.URL.Query().Get("token")
|
||||
|
||||
userId, err := GetUserIdFromAccess(jm, token)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
contextWithUserId := context.WithValue(r.Context(), USER_ID, userId)
|
||||
|
||||
newR := r.WithContext(contextWithUserId)
|
||||
next.ServeHTTP(w, newR)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ProtectedRoute(jm *JwtManager) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get("Authorization")
|
||||
|
||||
if len(token) < len("Bearer ") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
userId, err := GetUserIdFromAccess(jm, token[len("Bearer "):])
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
contextWithUserId := context.WithValue(r.Context(), USER_ID, userId)
|
||||
|
||||
newR := r.WithContext(contextWithUserId)
|
||||
next.ServeHTTP(w, newR)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func GetUserIdFromUrl(jm *JwtManager) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.URL.Query().Get("token")
|
||||
|
||||
if len(token) == 0 {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
userId, err := GetUserIdFromAccess(jm, token)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
contextWithUserId := context.WithValue(r.Context(), USER_ID, userId)
|
||||
|
||||
newR := r.WithContext(contextWithUserId)
|
||||
next.ServeHTTP(w, newR)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func GetPathParamID(logger *log.Logger, param string, w http.ResponseWriter, r *http.Request) (uuid.UUID, error) {
|
||||
pathParam := r.PathValue(param)
|
||||
if len(pathParam) == 0 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
err := fmt.Errorf("%s was not present", param)
|
||||
logger.Warn(err)
|
||||
return uuid.Nil, err
|
||||
}
|
||||
|
||||
uuidParam, err := uuid.Parse(pathParam)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
err := fmt.Errorf("could not parse param: %w", err)
|
||||
logger.Warn(err)
|
||||
return uuid.Nil, err
|
||||
}
|
||||
|
||||
return uuidParam, nil
|
||||
}
|
49
backend/middleware/util.go
Normal file
49
backend/middleware/util.go
Normal file
@ -0,0 +1,49 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
func WriteJsonOrError[K any](logger *log.Logger, object K, w http.ResponseWriter) {
|
||||
jsonObject, err := json.Marshal(object)
|
||||
if err != nil {
|
||||
logger.Warn("could not marshal json object", "err", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(jsonObject)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
type ErrorObject struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func writeError(logger *log.Logger, error string, w http.ResponseWriter, code int) {
|
||||
e := ErrorObject{
|
||||
error,
|
||||
}
|
||||
|
||||
jsonObject, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
logger.Warn("could not marshal json object", "err", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error("writing error", "error", error)
|
||||
w.Write(jsonObject)
|
||||
w.WriteHeader(code)
|
||||
}
|
||||
|
||||
func WriteErrorBadRequest(logger *log.Logger, error string, w http.ResponseWriter) {
|
||||
writeError(logger, error, w, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func WriteErrorInternal(logger *log.Logger, error string, w http.ResponseWriter) {
|
||||
writeError(logger, error, w, http.StatusInternalServerError)
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ContactModel struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
func (m ContactModel) List(ctx context.Context, userId uuid.UUID) ([]model.Contacts, error) {
|
||||
listContactsStmt := SELECT(Contacts.AllColumns).
|
||||
FROM(
|
||||
Contacts.
|
||||
INNER_JOIN(UserContacts, UserContacts.ContactID.EQ(Contacts.ID)),
|
||||
).
|
||||
WHERE(UserContacts.UserID.EQ(UUID(userId)))
|
||||
|
||||
locations := []model.Contacts{}
|
||||
|
||||
err := listContactsStmt.QueryContext(ctx, m.dbPool, &locations)
|
||||
return locations, err
|
||||
}
|
||||
|
||||
func (m ContactModel) Get(ctx context.Context, contactId uuid.UUID) (model.Contacts, error) {
|
||||
getContactStmt := Contacts.
|
||||
SELECT(Contacts.AllColumns).
|
||||
WHERE(Contacts.ID.EQ(UUID(contactId)))
|
||||
|
||||
contact := model.Contacts{}
|
||||
err := getContactStmt.QueryContext(ctx, m.dbPool, &contact)
|
||||
|
||||
return contact, err
|
||||
}
|
||||
|
||||
func (m ContactModel) Update(ctx context.Context, contact model.Contacts) (model.Contacts, error) {
|
||||
existingContact, err := m.Get(ctx, contact.ID)
|
||||
if err != nil {
|
||||
return model.Contacts{}, err
|
||||
}
|
||||
|
||||
existingContact.Name = contact.Name
|
||||
|
||||
if contact.Description != nil {
|
||||
existingContact.Description = contact.Description
|
||||
}
|
||||
|
||||
if contact.PhoneNumber != nil {
|
||||
existingContact.PhoneNumber = contact.PhoneNumber
|
||||
}
|
||||
|
||||
if contact.Email != nil {
|
||||
existingContact.Email = contact.Email
|
||||
}
|
||||
|
||||
updateContactStmt := Contacts.
|
||||
UPDATE(Contacts.MutableColumns).
|
||||
MODEL(existingContact).
|
||||
WHERE(Contacts.ID.EQ(UUID(contact.ID))).
|
||||
RETURNING(Contacts.AllColumns)
|
||||
|
||||
updatedContact := model.Contacts{}
|
||||
err = updateContactStmt.QueryContext(ctx, m.dbPool, &updatedContact)
|
||||
|
||||
return updatedContact, err
|
||||
}
|
||||
|
||||
func (m ContactModel) Save(ctx context.Context, userId uuid.UUID, contact model.Contacts) (model.Contacts, error) {
|
||||
// TODO: make this a transaction
|
||||
|
||||
if contact.ID != uuid.Nil {
|
||||
return m.Update(ctx, contact)
|
||||
}
|
||||
|
||||
insertContactStmt := Contacts.
|
||||
INSERT(Contacts.Name, Contacts.Description, Contacts.PhoneNumber, Contacts.Email).
|
||||
VALUES(contact.Name, contact.Description, contact.PhoneNumber, contact.Email).
|
||||
RETURNING(Contacts.AllColumns)
|
||||
|
||||
insertedContact := model.Contacts{}
|
||||
err := insertContactStmt.QueryContext(ctx, m.dbPool, &insertedContact)
|
||||
|
||||
if err != nil {
|
||||
return insertedContact, err
|
||||
}
|
||||
|
||||
insertUserContactStmt := UserContacts.
|
||||
INSERT(UserContacts.UserID, UserContacts.ContactID).
|
||||
VALUES(userId, insertedContact.ID)
|
||||
|
||||
_, err = insertUserContactStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return insertedContact, err
|
||||
}
|
||||
|
||||
func (m ContactModel) SaveToImage(ctx context.Context, imageId uuid.UUID, contactId uuid.UUID) (model.ImageContacts, error) {
|
||||
insertImageContactStmt := ImageContacts.
|
||||
INSERT(ImageContacts.ImageID, ImageContacts.ContactID).
|
||||
VALUES(imageId, contactId).
|
||||
RETURNING(ImageContacts.AllColumns)
|
||||
|
||||
imageContact := model.ImageContacts{}
|
||||
err := insertImageContactStmt.QueryContext(ctx, m.dbPool, &imageContact)
|
||||
|
||||
return imageContact, err
|
||||
}
|
||||
|
||||
func NewContactModel(db *sql.DB) ContactModel {
|
||||
return ContactModel{dbPool: db}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type EventModel struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
func (m EventModel) List(ctx context.Context, userId uuid.UUID) ([]model.Events, error) {
|
||||
listEventsStmt := SELECT(Events.AllColumns).
|
||||
FROM(
|
||||
Events.
|
||||
INNER_JOIN(UserEvents, UserEvents.EventID.EQ(Events.ID)),
|
||||
).
|
||||
WHERE(UserEvents.UserID.EQ(UUID(userId)))
|
||||
|
||||
events := []model.Events{}
|
||||
|
||||
err := listEventsStmt.QueryContext(ctx, m.dbPool, &events)
|
||||
return events, err
|
||||
}
|
||||
|
||||
func (m EventModel) Save(ctx context.Context, userId uuid.UUID, event model.Events) (model.Events, error) {
|
||||
// TODO tx here
|
||||
insertEventStmt := Events.
|
||||
INSERT(Events.MutableColumns).
|
||||
MODEL(event).
|
||||
RETURNING(Events.AllColumns)
|
||||
|
||||
insertedEvent := model.Events{}
|
||||
err := insertEventStmt.QueryContext(ctx, m.dbPool, &insertedEvent)
|
||||
|
||||
if err != nil {
|
||||
return insertedEvent, err
|
||||
}
|
||||
|
||||
insertUserEventStmt := UserEvents.
|
||||
INSERT(UserEvents.UserID, UserEvents.EventID).
|
||||
VALUES(userId, insertedEvent.ID)
|
||||
|
||||
_, err = insertUserEventStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return insertedEvent, err
|
||||
}
|
||||
|
||||
func (m EventModel) SaveToImage(ctx context.Context, imageId uuid.UUID, eventId uuid.UUID) (model.ImageEvents, error) {
|
||||
insertImageEventStmt := ImageEvents.
|
||||
INSERT(ImageEvents.ImageID, ImageEvents.EventID).
|
||||
VALUES(imageId, eventId).
|
||||
RETURNING(ImageEvents.AllColumns)
|
||||
|
||||
imageEvent := model.ImageEvents{}
|
||||
err := insertImageEventStmt.QueryContext(ctx, m.dbPool, &imageEvent)
|
||||
|
||||
return imageEvent, err
|
||||
}
|
||||
|
||||
func (m EventModel) UpdateLocation(ctx context.Context, eventId uuid.UUID, locationId uuid.UUID) (model.Events, error) {
|
||||
updateEventLocationStmt := Events.
|
||||
UPDATE(Events.LocationID).
|
||||
SET(locationId).
|
||||
WHERE(Events.ID.EQ(UUID(eventId))).
|
||||
RETURNING(Events.AllColumns)
|
||||
|
||||
updatedEvent := model.Events{}
|
||||
err := updateEventLocationStmt.QueryContext(ctx, m.dbPool, &updatedEvent)
|
||||
|
||||
return updatedEvent, err
|
||||
}
|
||||
|
||||
func (m EventModel) UpdateOrganizer(ctx context.Context, eventId uuid.UUID, organizerId uuid.UUID) (model.Events, error) {
|
||||
updateEventContactStmt := Events.
|
||||
UPDATE(Events.OrganizerID).
|
||||
SET(organizerId).
|
||||
WHERE(Events.ID.EQ(UUID(eventId))).
|
||||
RETURNING(Events.AllColumns)
|
||||
|
||||
updatedEvent := model.Events{}
|
||||
err := updateEventContactStmt.QueryContext(ctx, m.dbPool, &updatedEvent)
|
||||
|
||||
return updatedEvent, err
|
||||
}
|
||||
|
||||
func NewEventModel(db *sql.DB) EventModel {
|
||||
return EventModel{dbPool: db}
|
||||
}
|
@ -4,11 +4,11 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/enum"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
"github.com/go-jet/jet/v2/qrm"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
@ -17,194 +17,72 @@ type ImageModel struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
type ImageData struct {
|
||||
model.UserImages
|
||||
func (m ImageModel) Save(ctx context.Context, name string, image []byte, userID uuid.UUID) (model.Image, error) {
|
||||
saveImageStmt := Image.INSERT(Image.ImageName, Image.Image, Image.Description, Image.UserID).
|
||||
VALUES(name, image, "", userID).
|
||||
RETURNING(Image.AllColumns)
|
||||
|
||||
Image model.Image
|
||||
newImage := model.Image{}
|
||||
err := saveImageStmt.QueryContext(ctx, m.dbPool, &newImage)
|
||||
|
||||
return newImage, err
|
||||
}
|
||||
|
||||
type ProcessingImageData struct {
|
||||
model.UserImagesToProcess
|
||||
|
||||
Image model.Image
|
||||
}
|
||||
|
||||
type UserProcessingImage struct {
|
||||
model.UserImagesToProcess
|
||||
|
||||
Image model.Image
|
||||
}
|
||||
|
||||
func (m ImageModel) Process(ctx context.Context, userId uuid.UUID, image model.Image) (model.UserImagesToProcess, error) {
|
||||
tx, err := m.dbPool.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return model.UserImagesToProcess{}, err
|
||||
}
|
||||
|
||||
insertImageStmt := Image.
|
||||
INSERT(Image.ImageName, Image.Image).
|
||||
VALUES(image.ImageName, image.Image).
|
||||
RETURNING(Image.ID)
|
||||
|
||||
insertedImage := model.Image{}
|
||||
err = insertImageStmt.QueryContext(ctx, tx, &insertedImage)
|
||||
if err != nil {
|
||||
return model.UserImagesToProcess{}, err
|
||||
}
|
||||
|
||||
stmt := UserImagesToProcess.
|
||||
INSERT(UserImagesToProcess.UserID, UserImagesToProcess.ImageID).
|
||||
VALUES(userId, insertedImage.ID).
|
||||
RETURNING(UserImagesToProcess.AllColumns)
|
||||
|
||||
userImage := model.UserImagesToProcess{}
|
||||
err = stmt.QueryContext(ctx, tx, &userImage)
|
||||
if err != nil {
|
||||
return model.UserImagesToProcess{}, err
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
|
||||
return userImage, err
|
||||
}
|
||||
|
||||
func (m ImageModel) GetToProcess(ctx context.Context, imageId uuid.UUID) (UserProcessingImage, error) {
|
||||
getToProcessStmt := SELECT(UserImagesToProcess.AllColumns, Image.ID, Image.ImageName).
|
||||
FROM(
|
||||
UserImagesToProcess.INNER_JOIN(
|
||||
Image, Image.ID.EQ(UserImagesToProcess.ImageID),
|
||||
),
|
||||
).
|
||||
WHERE(UserImagesToProcess.ID.EQ(UUID(imageId)))
|
||||
|
||||
images := []UserProcessingImage{}
|
||||
err := getToProcessStmt.QueryContext(ctx, m.dbPool, &images)
|
||||
|
||||
if len(images) != 1 {
|
||||
return UserProcessingImage{}, fmt.Errorf("Expected 1, got %d\n", len(images))
|
||||
}
|
||||
|
||||
return images[0], err
|
||||
}
|
||||
|
||||
func (m ImageModel) GetToProcessWithData(ctx context.Context, imageId uuid.UUID) (ProcessingImageData, error) {
|
||||
stmt := SELECT(UserImagesToProcess.AllColumns, Image.AllColumns).
|
||||
FROM(
|
||||
UserImagesToProcess.INNER_JOIN(
|
||||
Image, Image.ID.EQ(UserImagesToProcess.ImageID),
|
||||
),
|
||||
).WHERE(UserImagesToProcess.ID.EQ(UUID(imageId)))
|
||||
|
||||
images := []ProcessingImageData{}
|
||||
err := stmt.QueryContext(ctx, m.dbPool, &images)
|
||||
|
||||
if len(images) != 1 {
|
||||
return ProcessingImageData{}, fmt.Errorf("Expected 1, got %d\n", len(images))
|
||||
}
|
||||
|
||||
return images[0], err
|
||||
}
|
||||
|
||||
func (m ImageModel) FinishProcessing(ctx context.Context, imageId uuid.UUID) (model.UserImages, error) {
|
||||
imageToProcess, err := m.GetToProcess(ctx, imageId)
|
||||
if err != nil {
|
||||
return model.UserImages{}, err
|
||||
}
|
||||
|
||||
tx, err := m.dbPool.Begin()
|
||||
if err != nil {
|
||||
return model.UserImages{}, err
|
||||
}
|
||||
|
||||
insertImageStmt := UserImages.
|
||||
INSERT(UserImages.UserID, UserImages.ImageID).
|
||||
VALUES(imageToProcess.UserID, imageToProcess.ImageID).
|
||||
RETURNING(UserImages.ID, UserImages.UserID, UserImages.ImageID)
|
||||
|
||||
userImage := model.UserImages{}
|
||||
err = insertImageStmt.QueryContext(ctx, tx, &userImage)
|
||||
if err != nil {
|
||||
return model.UserImages{}, err
|
||||
}
|
||||
|
||||
// Hacky. Update the status before removing so we can get our regular triggers
|
||||
// to work.
|
||||
|
||||
updateStatusStmt := UserImagesToProcess.
|
||||
UPDATE(UserImagesToProcess.Status).
|
||||
SET(model.Progress_Complete).
|
||||
WHERE(UserImagesToProcess.ID.EQ(UUID(imageToProcess.ID)))
|
||||
|
||||
_, err = updateStatusStmt.ExecContext(ctx, tx)
|
||||
if err != nil {
|
||||
return model.UserImages{}, err
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// We cannot delete the image to process because our events rely on it.
|
||||
// This indicates our DB structure with the two tables might need some adjusting.
|
||||
// Or re-doing all together perhaps.
|
||||
// (switching to a one table (user_images) could work)
|
||||
// But for now, we can just not delete the images to process and set them to complete
|
||||
|
||||
// removeProcessingStmt := UserImagesToProcess.
|
||||
// DELETE().
|
||||
// WHERE(UserImagesToProcess.ID.EQ(UUID(imageToProcess.ID)))
|
||||
//
|
||||
// _, err = removeProcessingStmt.ExecContext(ctx, tx)
|
||||
// if err != nil {
|
||||
// return model.UserImages{}, err
|
||||
// }
|
||||
|
||||
err = tx.Commit()
|
||||
return userImage, err
|
||||
}
|
||||
|
||||
func (m ImageModel) StartProcessing(ctx context.Context, processingImageId uuid.UUID) error {
|
||||
startProcessingStmt := UserImagesToProcess.
|
||||
UPDATE(UserImagesToProcess.Status).
|
||||
SET(model.Progress_InProgress).
|
||||
WHERE(UserImagesToProcess.ID.EQ(UUID(processingImageId)))
|
||||
|
||||
_, err := startProcessingStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m ImageModel) Get(ctx context.Context, imageId uuid.UUID) (model.Image, error) {
|
||||
getImageStmt := Image.SELECT(Image.AllColumns).
|
||||
WHERE(Image.ID.EQ(UUID(imageId)))
|
||||
func (m ImageModel) Get(ctx context.Context, imageID uuid.UUID) (model.Image, bool, error) {
|
||||
getImageStmt := Image.SELECT(Image.AllColumns).WHERE(Image.ID.EQ(UUID(imageID)))
|
||||
|
||||
image := model.Image{}
|
||||
err := getImageStmt.QueryContext(ctx, m.dbPool, &image)
|
||||
|
||||
return image, err
|
||||
return image, err != qrm.ErrNoRows, err
|
||||
}
|
||||
|
||||
func (m ImageModel) GetProcessing(ctx context.Context, userId uuid.UUID) ([]UserProcessingImage, error) {
|
||||
getProcessingStmt := SELECT(UserImagesToProcess.AllColumns, Image.ID, Image.ImageName).
|
||||
FROM(
|
||||
UserImagesToProcess.INNER_JOIN(
|
||||
Image, Image.ID.EQ(UserImagesToProcess.ImageID),
|
||||
),
|
||||
).WHERE(
|
||||
UserImagesToProcess.UserID.EQ(UUID(userId)).
|
||||
AND(UserImagesToProcess.Status.NOT_EQ(enum.Progress.Complete)),
|
||||
)
|
||||
func (m ImageModel) UpdateDescription(ctx context.Context, imageID uuid.UUID, description string) error {
|
||||
updateImageDescriptionStmt := Image.UPDATE(Image.Description).
|
||||
SET(Image.Description.SET(String(description))).
|
||||
WHERE(Image.ID.EQ(UUID(imageID)))
|
||||
|
||||
images := []UserProcessingImage{}
|
||||
err := getProcessingStmt.QueryContext(ctx, m.dbPool, &images)
|
||||
_, err := updateImageDescriptionStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return images, err
|
||||
return err
|
||||
}
|
||||
|
||||
func (m ImageModel) IsUserAuthorized(ctx context.Context, imageId uuid.UUID, userId uuid.UUID) bool {
|
||||
getImageUserId := UserImages.SELECT(UserImages.UserID).WHERE(UserImages.ImageID.EQ(UUID(imageId)))
|
||||
func (m ImageModel) UpdateProcess(ctx context.Context, imageID uuid.UUID, process model.Progress) error {
|
||||
updateImageDescriptionStmt := Image.UPDATE(Image.Status).
|
||||
SET(process).
|
||||
WHERE(Image.ID.EQ(UUID(imageID)))
|
||||
|
||||
userImage := model.UserImages{}
|
||||
err := getImageUserId.QueryContext(ctx, m.dbPool, &userImage)
|
||||
_, err := updateImageDescriptionStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err != nil && userImage.UserID.String() == userId.String()
|
||||
return err
|
||||
}
|
||||
|
||||
func (m ImageModel) Update(ctx context.Context, image model.Image) (model.Image, error) {
|
||||
updateImageStmt := Image.UPDATE(Image.MutableColumns.Except(Image.Image)).
|
||||
MODEL(image).
|
||||
WHERE(Image.ID.EQ(UUID(image.ID))).
|
||||
RETURNING(Image.AllColumns.Except(Image.Image))
|
||||
|
||||
updatedImage := model.Image{}
|
||||
err := updateImageStmt.QueryContext(ctx, m.dbPool, &updatedImage)
|
||||
|
||||
return updatedImage, err
|
||||
}
|
||||
|
||||
func (m ImageModel) Delete(ctx context.Context, imageID, userID uuid.UUID) (bool, error) {
|
||||
deleteImageStmt := Image.DELETE().WHERE(Image.ID.EQ(UUID(imageID)).AND(Image.UserID.EQ(UUID(userID))))
|
||||
|
||||
r, err := deleteImageStmt.ExecContext(ctx, m.dbPool)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("deleting image: %w", err)
|
||||
}
|
||||
|
||||
rowsAffected, err := r.RowsAffected()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("unreachable: %w", err)
|
||||
}
|
||||
|
||||
return rowsAffected > 0, nil
|
||||
}
|
||||
|
||||
func NewImageModel(db *sql.DB) ImageModel {
|
||||
|
@ -1,33 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type LinkModel struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
func (m LinkModel) Save(ctx context.Context, imageId uuid.UUID, links []string) error {
|
||||
if len(links) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
stmt := ImageLinks.INSERT(ImageLinks.ImageID, ImageLinks.Link)
|
||||
|
||||
for _, link := range links {
|
||||
stmt = stmt.VALUES(imageId, link)
|
||||
}
|
||||
|
||||
_, err := stmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func NewLinkModel(db *sql.DB) LinkModel {
|
||||
return LinkModel{dbPool: db}
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
"github.com/go-jet/jet/v2/qrm"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type LocationModel struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
func (m LocationModel) List(ctx context.Context, userId uuid.UUID) ([]model.Locations, error) {
|
||||
listLocationsStmt := SELECT(Locations.AllColumns).
|
||||
FROM(
|
||||
Locations.
|
||||
INNER_JOIN(UserLocations, UserLocations.LocationID.EQ(Locations.ID)),
|
||||
).
|
||||
WHERE(UserLocations.UserID.EQ(UUID(userId)))
|
||||
|
||||
locations := []model.Locations{}
|
||||
|
||||
err := listLocationsStmt.QueryContext(ctx, m.dbPool, &locations)
|
||||
return locations, err
|
||||
}
|
||||
|
||||
func (m LocationModel) Get(ctx context.Context, locationId uuid.UUID) (model.Locations, error) {
|
||||
getLocationStmt := Locations.
|
||||
SELECT(Locations.AllColumns).
|
||||
WHERE(Locations.ID.EQ(UUID(locationId)))
|
||||
|
||||
location := model.Locations{}
|
||||
err := getLocationStmt.QueryContext(ctx, m.dbPool, &location)
|
||||
|
||||
return location, err
|
||||
}
|
||||
|
||||
func (m LocationModel) Update(ctx context.Context, location model.Locations) (model.Locations, error) {
|
||||
existingLocation, err := m.Get(ctx, location.ID)
|
||||
if err != nil {
|
||||
return model.Locations{}, err
|
||||
}
|
||||
|
||||
existingLocation.Name = location.Name
|
||||
|
||||
if location.Description != nil {
|
||||
existingLocation.Description = location.Description
|
||||
}
|
||||
|
||||
if location.Address != nil {
|
||||
existingLocation.Address = location.Address
|
||||
}
|
||||
|
||||
updateLocationStmt := Locations.
|
||||
UPDATE(Locations.MutableColumns).
|
||||
MODEL(existingLocation).
|
||||
WHERE(Locations.ID.EQ(UUID(location.ID))).
|
||||
RETURNING(Locations.AllColumns)
|
||||
|
||||
updatedLocation := model.Locations{}
|
||||
err = updateLocationStmt.QueryContext(ctx, m.dbPool, &updatedLocation)
|
||||
|
||||
return updatedLocation, err
|
||||
}
|
||||
|
||||
func (m LocationModel) Save(ctx context.Context, userId uuid.UUID, location model.Locations) (model.Locations, error) {
|
||||
if location.ID != uuid.Nil {
|
||||
return m.Update(ctx, location)
|
||||
}
|
||||
|
||||
insertLocationStmt := Locations.
|
||||
INSERT(Locations.Name, Locations.Address, Locations.Description).
|
||||
VALUES(location.Name, location.Address, location.Description).
|
||||
RETURNING(Locations.AllColumns)
|
||||
|
||||
insertedLocation := model.Locations{}
|
||||
err := insertLocationStmt.QueryContext(ctx, m.dbPool, &insertedLocation)
|
||||
if err != nil {
|
||||
return model.Locations{}, err
|
||||
}
|
||||
|
||||
insertUserLocationStmt := UserLocations.
|
||||
INSERT(UserLocations.UserID, UserLocations.LocationID).
|
||||
VALUES(userId, insertedLocation.ID)
|
||||
|
||||
_, err = insertUserLocationStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return insertedLocation, err
|
||||
}
|
||||
|
||||
func (m LocationModel) SaveToImage(ctx context.Context, imageId uuid.UUID, locationId uuid.UUID) (model.ImageLocations, error) {
|
||||
imageLocation := model.ImageLocations{}
|
||||
|
||||
checkExistingStmt := ImageLocations.
|
||||
SELECT(ImageLocations.AllColumns).
|
||||
WHERE(
|
||||
ImageLocations.ImageID.EQ(UUID(imageId)).
|
||||
AND(ImageLocations.LocationID.EQ(UUID(locationId))),
|
||||
)
|
||||
|
||||
err := checkExistingStmt.QueryContext(ctx, m.dbPool, &imageLocation)
|
||||
if err != nil && err != qrm.ErrNoRows {
|
||||
// A real error
|
||||
return model.ImageLocations{}, err
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
// Already exists.
|
||||
return imageLocation, nil
|
||||
}
|
||||
|
||||
insertImageLocationStmt := ImageLocations.
|
||||
INSERT(ImageLocations.ImageID, ImageLocations.LocationID).
|
||||
VALUES(imageId, locationId).
|
||||
RETURNING(ImageLocations.AllColumns)
|
||||
|
||||
err = insertImageLocationStmt.QueryContext(ctx, m.dbPool, &imageLocation)
|
||||
|
||||
return imageLocation, err
|
||||
}
|
||||
|
||||
func NewLocationModel(db *sql.DB) LocationModel {
|
||||
return LocationModel{dbPool: db}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type NoteModel struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
func (m NoteModel) List(ctx context.Context, userId uuid.UUID) ([]model.Notes, error) {
|
||||
listNotesStmt := SELECT(Notes.AllColumns).
|
||||
FROM(
|
||||
Notes.
|
||||
INNER_JOIN(UserNotes, UserNotes.NoteID.EQ(Notes.ID)),
|
||||
).
|
||||
WHERE(UserNotes.UserID.EQ(UUID(userId)))
|
||||
|
||||
locations := []model.Notes{}
|
||||
|
||||
err := listNotesStmt.QueryContext(ctx, m.dbPool, &locations)
|
||||
return locations, err
|
||||
}
|
||||
|
||||
func (m NoteModel) Save(ctx context.Context, userId uuid.UUID, note model.Notes) (model.Notes, error) {
|
||||
insertNoteStmt := Notes.
|
||||
INSERT(Notes.Name, Notes.Description, Notes.Content).
|
||||
VALUES(note.Name, note.Description, note.Content).
|
||||
RETURNING(Notes.AllColumns)
|
||||
|
||||
insertedNote := model.Notes{}
|
||||
err := insertNoteStmt.QueryContext(ctx, m.dbPool, &insertedNote)
|
||||
if err != nil {
|
||||
return model.Notes{}, err
|
||||
}
|
||||
|
||||
insertUserNoteStmt := UserNotes.
|
||||
INSERT(UserNotes.UserID, UserNotes.NoteID).
|
||||
VALUES(userId, insertedNote.ID)
|
||||
|
||||
_, err = insertUserNoteStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return insertedNote, err
|
||||
}
|
||||
|
||||
func (m NoteModel) SaveToImage(ctx context.Context, imageId uuid.UUID, noteId uuid.UUID) (model.ImageNotes, error) {
|
||||
insertImageNoteStmt := ImageNotes.
|
||||
INSERT(ImageNotes.ImageID, ImageNotes.NoteID).
|
||||
VALUES(imageId, noteId).
|
||||
RETURNING(ImageNotes.AllColumns)
|
||||
|
||||
imageNote := model.ImageNotes{}
|
||||
err := insertImageNoteStmt.QueryContext(ctx, m.dbPool, &imageNote)
|
||||
|
||||
return imageNote, err
|
||||
}
|
||||
|
||||
func NewNoteModel(db *sql.DB) NoteModel {
|
||||
return NoteModel{dbPool: db}
|
||||
}
|
200
backend/models/stacks.go
Normal file
200
backend/models/stacks.go
Normal file
@ -0,0 +1,200 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type StackModel struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
type StackWithItems struct {
|
||||
model.Stacks
|
||||
|
||||
SchemaItems []model.SchemaItems
|
||||
}
|
||||
|
||||
type ImageWithSchema struct {
|
||||
model.ImageStacks
|
||||
|
||||
Items []model.ImageSchemaItems
|
||||
}
|
||||
|
||||
type IDValue struct {
|
||||
ID string `json:"id"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SELECT for lists
|
||||
// ========================================
|
||||
|
||||
func (m StackModel) List(ctx context.Context, userId uuid.UUID) ([]StackWithItems, error) {
|
||||
getStacksWithItems := SELECT(
|
||||
Stacks.AllColumns,
|
||||
SchemaItems.AllColumns,
|
||||
).
|
||||
FROM(
|
||||
Stacks.
|
||||
INNER_JOIN(SchemaItems, SchemaItems.StackID.EQ(Stacks.ID)),
|
||||
).
|
||||
WHERE(Stacks.UserID.EQ(UUID(userId)))
|
||||
|
||||
lists := []StackWithItems{}
|
||||
err := getStacksWithItems.QueryContext(ctx, m.dbPool, &lists)
|
||||
|
||||
return lists, err
|
||||
}
|
||||
|
||||
func (m StackModel) ListItems(ctx context.Context, stackID uuid.UUID) ([]ImageWithSchema, error) {
|
||||
getListItems := SELECT(
|
||||
ImageStacks.AllColumns,
|
||||
ImageSchemaItems.AllColumns,
|
||||
).
|
||||
FROM(
|
||||
ImageStacks.
|
||||
INNER_JOIN(ImageSchemaItems, ImageSchemaItems.ImageID.EQ(ImageStacks.ImageID)),
|
||||
).
|
||||
WHERE(ImageStacks.StackID.EQ(UUID(stackID)))
|
||||
|
||||
listItems := make([]ImageWithSchema, 0)
|
||||
err := getListItems.QueryContext(ctx, m.dbPool, &listItems)
|
||||
|
||||
return listItems, err
|
||||
}
|
||||
|
||||
func (m StackModel) Get(ctx context.Context, stackID uuid.UUID) (model.Stacks, error) {
|
||||
getStackStmt := Stacks.SELECT(Stacks.AllColumns).WHERE(Stacks.ID.EQ(UUID(stackID)))
|
||||
|
||||
stack := model.Stacks{}
|
||||
err := getStackStmt.QueryContext(ctx, m.dbPool, &stack)
|
||||
|
||||
return stack, err
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// INSERT methods
|
||||
// ========================================
|
||||
|
||||
func (m StackModel) Save(ctx context.Context, userID uuid.UUID, name string, description string, status model.Progress) (model.Stacks, error) {
|
||||
saveListStmt := Stacks.
|
||||
INSERT(Stacks.UserID, Stacks.Name, Stacks.Description, Stacks.Status).
|
||||
VALUES(userID, name, description, status).
|
||||
RETURNING(Stacks.ID, Stacks.UserID, Stacks.Name, Stacks.Description, Stacks.Status, Stacks.CreatedAt)
|
||||
|
||||
list := model.Stacks{}
|
||||
err := saveListStmt.QueryContext(ctx, m.dbPool, &list)
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (m StackModel) SaveItems(ctx context.Context, items []model.SchemaItems) error {
|
||||
saveItemsStmt := SchemaItems.INSERT(SchemaItems.MutableColumns).MODELS(items)
|
||||
|
||||
_, err := saveItemsStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m StackModel) SaveImage(ctx context.Context, imageID uuid.UUID, stackID uuid.UUID) (model.ImageStacks, error) {
|
||||
saveImageStmt := ImageStacks.
|
||||
INSERT(ImageStacks.ImageID, ImageStacks.StackID).
|
||||
VALUES(imageID, stackID).
|
||||
RETURNING(ImageStacks.AllColumns)
|
||||
|
||||
imageStack := model.ImageStacks{}
|
||||
|
||||
err := saveImageStmt.QueryContext(ctx, m.dbPool, &imageStack)
|
||||
|
||||
return imageStack, err
|
||||
}
|
||||
|
||||
func (m StackModel) SaveSchemaItems(ctx context.Context, imageID uuid.UUID, items []IDValue) error {
|
||||
if len(items) == 0 {
|
||||
return fmt.Errorf("items cannot be empty")
|
||||
}
|
||||
|
||||
saveSchemaItemStmt := ImageSchemaItems.
|
||||
INSERT(
|
||||
ImageSchemaItems.ImageID,
|
||||
ImageSchemaItems.SchemaItemID,
|
||||
ImageSchemaItems.Value,
|
||||
)
|
||||
|
||||
for _, item := range items {
|
||||
saveSchemaItemStmt = saveSchemaItemStmt.VALUES(
|
||||
imageID,
|
||||
item.ID,
|
||||
item.Value,
|
||||
)
|
||||
}
|
||||
|
||||
_, err := saveSchemaItemStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// UPDATE methods
|
||||
// ========================================
|
||||
|
||||
func (m StackModel) UpdateProcess(ctx context.Context, stackID uuid.UUID, process model.Progress) error {
|
||||
updateStackProgressStmt := Stacks.UPDATE(Stacks.Status).
|
||||
SET(process).
|
||||
WHERE(Stacks.ID.EQ(UUID(stackID)))
|
||||
|
||||
_, err := updateStackProgressStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// DELETE methods
|
||||
// ========================================
|
||||
|
||||
func (m StackModel) DeleteSchemaItem(ctx context.Context, stackID uuid.UUID, schemaItemID uuid.UUID) error {
|
||||
deleteImageListStmt := SchemaItems.DELETE().
|
||||
WHERE(
|
||||
SchemaItems.ID.EQ(UUID(schemaItemID)).
|
||||
// The StackID check is a sanity check.
|
||||
// We don't technically need it, but it adds extra protection
|
||||
// in case we make a mistake later on
|
||||
AND(SchemaItems.StackID.EQ(UUID(stackID))),
|
||||
)
|
||||
|
||||
_, err := deleteImageListStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m StackModel) DeleteImage(ctx context.Context, stackID uuid.UUID, imageID uuid.UUID) error {
|
||||
deleteImageListStmt := ImageStacks.DELETE().
|
||||
WHERE(
|
||||
ImageStacks.StackID.EQ(UUID(stackID)).
|
||||
AND(ImageStacks.ImageID.EQ(UUID(imageID))),
|
||||
)
|
||||
|
||||
_, err := deleteImageListStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m StackModel) Delete(ctx context.Context, stackID uuid.UUID, userID uuid.UUID) error {
|
||||
deleteStackStmt := Stacks.DELETE().WHERE(Stacks.ID.EQ(UUID(stackID)).AND(Stacks.UserID.EQ(UUID(userID))))
|
||||
|
||||
_, err := deleteStackStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func NewStackModel(db *sql.DB) StackModel {
|
||||
return StackModel{dbPool: db}
|
||||
}
|
@ -1,156 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type TagModel struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
// Raw dogging SQL is kinda based though?
|
||||
//
|
||||
// | nO, usE OrM!!
|
||||
//
|
||||
// | RAW - RAW
|
||||
// | SQL | \ SQL
|
||||
// | GOOD | \ GOOD
|
||||
// | - -
|
||||
// | -- --
|
||||
// | -- --
|
||||
// | ---- IQ ----
|
||||
func (m TagModel) getNonExistantTags(ctx context.Context, userId uuid.UUID, tags []string) ([]string, error) {
|
||||
if len(tags) == 0 {
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
values := ""
|
||||
counter := 1
|
||||
// big big SQL injection problem here?
|
||||
for counter = 1; counter <= len(tags); counter++ {
|
||||
values += fmt.Sprintf("($%d),", counter)
|
||||
}
|
||||
values = values[0 : len(values)-1]
|
||||
|
||||
getNonExistingTags := fmt.Sprintf(`WITH given_tags
|
||||
AS (SELECT given_tags.tag FROM (VALUES `+values+`) AS given_tags (tag)),
|
||||
this_user_tags AS
|
||||
(SELECT id, tag FROM haystack.user_tags WHERE user_tags.user_id = $%d)
|
||||
SELECT given_tags.tag
|
||||
FROM given_tags
|
||||
LEFT OUTER JOIN haystack.user_tags ON haystack.user_tags.tag = given_tags.tag
|
||||
where user_tags.tag is null`, counter)
|
||||
|
||||
getNonExistingTagsStmt, err := m.dbPool.PrepareContext(ctx, getNonExistingTags)
|
||||
defer getNonExistingTagsStmt.Close()
|
||||
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
args := make([]any, counter)
|
||||
for i, v := range tags {
|
||||
args[i] = v
|
||||
}
|
||||
args[counter-1] = userId.String()
|
||||
|
||||
rows, err := getNonExistingTagsStmt.QueryContext(ctx, args...)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
nonExistantTags := make([]string, 0)
|
||||
|
||||
for rows.Next() {
|
||||
var tag string
|
||||
rows.Scan(&tag)
|
||||
|
||||
nonExistantTags = append(nonExistantTags, tag)
|
||||
}
|
||||
|
||||
return nonExistantTags, nil
|
||||
}
|
||||
|
||||
func (m TagModel) Save(ctx context.Context, userId uuid.UUID, tags []string) error {
|
||||
tagsToInsert, err := m.getNonExistantTags(ctx, userId, tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(tagsToInsert) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
stmt := UserTags.INSERT(UserTags.UserID, UserTags.Tag)
|
||||
|
||||
for _, tag := range tagsToInsert {
|
||||
stmt = stmt.VALUES(UUID(userId), tag)
|
||||
}
|
||||
|
||||
_, err = stmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m TagModel) List(ctx context.Context, userId uuid.UUID) ([]model.UserTags, error) {
|
||||
listTagsStmt := UserTags.SELECT(UserTags.AllColumns).WHERE(UserTags.UserID.EQ(UUID(userId)))
|
||||
|
||||
userTags := []model.UserTags{}
|
||||
|
||||
err := listTagsStmt.QueryContext(ctx, m.dbPool, &userTags)
|
||||
|
||||
return userTags, err
|
||||
}
|
||||
|
||||
func (m TagModel) SaveToImage(ctx context.Context, imageId uuid.UUID, tags []string) error {
|
||||
if len(tags) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
userId, err := getUserIdFromImage(ctx, m.dbPool, imageId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.Save(ctx, userId, tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userTagsExpression := make([]Expression, 0)
|
||||
for _, tag := range tags {
|
||||
userTagsExpression = append(userTagsExpression, String(tag))
|
||||
}
|
||||
|
||||
userTags := make([]model.UserTags, 0)
|
||||
|
||||
getTagsStmt := UserTags.SELECT(
|
||||
UserTags.ID, UserTags.Tag,
|
||||
).WHERE(UserTags.Tag.IN(userTagsExpression...))
|
||||
err = getTagsStmt.Query(m.dbPool, &userTags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stmt := ImageTags.INSERT(ImageTags.ImageID, ImageTags.TagID)
|
||||
|
||||
for _, t := range userTags {
|
||||
stmt = stmt.VALUES(imageId, t.ID)
|
||||
}
|
||||
|
||||
_, err = stmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func NewTagModel(db *sql.DB) TagModel {
|
||||
return TagModel{dbPool: db}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type TextModel struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
func (m TextModel) Save(ctx context.Context, imageId uuid.UUID, texts []string) error {
|
||||
if len(texts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
saveImageTextStmt := ImageText.INSERT(ImageText.ImageID, ImageText.ImageText)
|
||||
|
||||
for _, t := range texts {
|
||||
saveImageTextStmt = saveImageTextStmt.VALUES(imageId, t)
|
||||
}
|
||||
|
||||
saveImageTextStmt.RETURNING(ImageText.AllColumns)
|
||||
|
||||
_, err := saveImageTextStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func NewTextModel(db *sql.DB) TextModel {
|
||||
return TextModel{dbPool: db}
|
||||
}
|
@ -3,8 +3,6 @@ package models
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"log"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
@ -21,251 +19,6 @@ type ImageWithProperties struct {
|
||||
ID uuid.UUID
|
||||
|
||||
Image model.Image
|
||||
|
||||
Locations []model.Locations
|
||||
Events []model.Events
|
||||
Notes []model.Notes
|
||||
Contacts []model.Contacts
|
||||
}
|
||||
|
||||
type PropertiesWithImage struct {
|
||||
Locations []struct {
|
||||
model.Locations
|
||||
|
||||
Images uuid.UUIDs
|
||||
}
|
||||
Contacts []struct {
|
||||
model.Contacts
|
||||
|
||||
Images uuid.UUIDs
|
||||
}
|
||||
Events []struct {
|
||||
model.Events
|
||||
|
||||
Images uuid.UUIDs
|
||||
}
|
||||
Notes []struct {
|
||||
model.Notes
|
||||
|
||||
Images uuid.UUIDs
|
||||
}
|
||||
}
|
||||
|
||||
func transpose(imageProperties []ImageWithProperties) PropertiesWithImage {
|
||||
// EntityID -> []ImageIDs
|
||||
dependencies := make(map[uuid.UUID]uuid.UUIDs)
|
||||
|
||||
addDependency := func(entityId uuid.UUID, imageId uuid.UUID) {
|
||||
deps, exists := dependencies[entityId]
|
||||
if !exists {
|
||||
dep := uuid.UUIDs{imageId}
|
||||
dependencies[entityId] = dep
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
dependencies[entityId] = append(deps, imageId)
|
||||
}
|
||||
|
||||
contactMap := make(map[uuid.UUID]model.Contacts)
|
||||
locationMap := make(map[uuid.UUID]model.Locations)
|
||||
noteMap := make(map[uuid.UUID]model.Notes)
|
||||
eventMap := make(map[uuid.UUID]model.Events)
|
||||
|
||||
for _, image := range imageProperties {
|
||||
for _, contact := range image.Contacts {
|
||||
contactMap[contact.ID] = contact
|
||||
addDependency(contact.ID, image.Image.ID)
|
||||
}
|
||||
for _, location := range image.Locations {
|
||||
locationMap[location.ID] = location
|
||||
addDependency(location.ID, image.Image.ID)
|
||||
}
|
||||
for _, note := range image.Notes {
|
||||
noteMap[note.ID] = note
|
||||
addDependency(note.ID, image.Image.ID)
|
||||
}
|
||||
for _, event := range image.Events {
|
||||
eventMap[event.ID] = event
|
||||
addDependency(event.ID, image.Image.ID)
|
||||
}
|
||||
}
|
||||
|
||||
properties := PropertiesWithImage{
|
||||
Contacts: make([]struct {
|
||||
model.Contacts
|
||||
Images uuid.UUIDs
|
||||
}, 0),
|
||||
Locations: make([]struct {
|
||||
model.Locations
|
||||
Images uuid.UUIDs
|
||||
}, 0),
|
||||
Notes: make([]struct {
|
||||
model.Notes
|
||||
Images uuid.UUIDs
|
||||
}, 0),
|
||||
Events: make([]struct {
|
||||
model.Events
|
||||
Images uuid.UUIDs
|
||||
}, 0),
|
||||
}
|
||||
|
||||
for contactId, contact := range contactMap {
|
||||
properties.Contacts = append(properties.Contacts, struct {
|
||||
model.Contacts
|
||||
Images uuid.UUIDs
|
||||
}{
|
||||
Contacts: contact,
|
||||
Images: dependencies[contactId],
|
||||
})
|
||||
}
|
||||
|
||||
for locationId, location := range locationMap {
|
||||
properties.Locations = append(properties.Locations, struct {
|
||||
model.Locations
|
||||
Images uuid.UUIDs
|
||||
}{
|
||||
Locations: location,
|
||||
Images: dependencies[locationId],
|
||||
})
|
||||
}
|
||||
|
||||
for noteId, note := range noteMap {
|
||||
properties.Notes = append(properties.Notes, struct {
|
||||
model.Notes
|
||||
Images uuid.UUIDs
|
||||
}{
|
||||
Notes: note,
|
||||
Images: dependencies[noteId],
|
||||
})
|
||||
}
|
||||
|
||||
for eventId, event := range eventMap {
|
||||
properties.Events = append(properties.Events, struct {
|
||||
model.Events
|
||||
Images uuid.UUIDs
|
||||
}{
|
||||
Events: event,
|
||||
Images: dependencies[eventId],
|
||||
})
|
||||
}
|
||||
|
||||
return properties
|
||||
}
|
||||
|
||||
type TypedProperties struct {
|
||||
Type string `json:"type"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
func propertiesToTypeArray(properties PropertiesWithImage) []TypedProperties {
|
||||
typedProperties := make([]TypedProperties, 0)
|
||||
|
||||
for _, location := range properties.Locations {
|
||||
typedProperties = append(typedProperties, TypedProperties{
|
||||
Type: "location",
|
||||
Data: location,
|
||||
})
|
||||
}
|
||||
|
||||
for _, contact := range properties.Contacts {
|
||||
typedProperties = append(typedProperties, TypedProperties{
|
||||
Type: "contact",
|
||||
Data: contact,
|
||||
})
|
||||
}
|
||||
|
||||
for _, note := range properties.Notes {
|
||||
typedProperties = append(typedProperties, TypedProperties{
|
||||
Type: "note",
|
||||
Data: note,
|
||||
})
|
||||
}
|
||||
|
||||
for _, event := range properties.Events {
|
||||
typedProperties = append(typedProperties, TypedProperties{
|
||||
Type: "event",
|
||||
Data: event,
|
||||
})
|
||||
}
|
||||
|
||||
return typedProperties
|
||||
}
|
||||
|
||||
func GetTypedImageProperties(imageProperties []ImageWithProperties) []TypedProperties {
|
||||
return propertiesToTypeArray(transpose(imageProperties))
|
||||
}
|
||||
|
||||
func getUserIdFromImage(ctx context.Context, dbPool *sql.DB, imageId uuid.UUID) (uuid.UUID, error) {
|
||||
getUserIdStmt := UserImages.SELECT(UserImages.UserID).WHERE(UserImages.ImageID.EQ(UUID(imageId)))
|
||||
|
||||
log.Println(getUserIdStmt.DebugSql())
|
||||
|
||||
userImages := []model.UserImages{}
|
||||
err := getUserIdStmt.QueryContext(ctx, dbPool, &userImages)
|
||||
if err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
|
||||
if len(userImages) != 1 {
|
||||
return uuid.Nil, errors.New("Expected exactly one choice.")
|
||||
}
|
||||
|
||||
return userImages[0].UserID, nil
|
||||
}
|
||||
|
||||
func getListImagesStmt() SelectStatement {
|
||||
return SELECT(
|
||||
UserImages.ID.AS("ImageWithProperties.ID"),
|
||||
Image.ID,
|
||||
Image.ImageName,
|
||||
ImageTags.AllColumns,
|
||||
UserTags.AllColumns,
|
||||
ImageText.AllColumns,
|
||||
ImageLinks.AllColumns,
|
||||
ImageLocations.AllColumns,
|
||||
Locations.AllColumns,
|
||||
ImageEvents.AllColumns,
|
||||
Events.AllColumns,
|
||||
ImageContacts.AllColumns,
|
||||
Contacts.AllColumns,
|
||||
ImageNotes.AllColumns,
|
||||
Notes.AllColumns,
|
||||
).
|
||||
FROM(
|
||||
UserImages.INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID)).
|
||||
LEFT_JOIN(ImageTags, ImageTags.ImageID.EQ(Image.ID)).
|
||||
LEFT_JOIN(UserTags, UserTags.ID.EQ(ImageTags.TagID)).
|
||||
LEFT_JOIN(ImageText, ImageText.ImageID.EQ(Image.ID)).
|
||||
LEFT_JOIN(ImageLinks, ImageLinks.ImageID.EQ(Image.ID)).
|
||||
LEFT_JOIN(ImageLocations, ImageLocations.ImageID.EQ(UserImages.ImageID)).
|
||||
LEFT_JOIN(Locations, Locations.ID.EQ(ImageLocations.LocationID)).
|
||||
LEFT_JOIN(ImageEvents, ImageEvents.ImageID.EQ(UserImages.ImageID)).
|
||||
LEFT_JOIN(Events, Events.ID.EQ(ImageEvents.EventID)).
|
||||
LEFT_JOIN(ImageContacts, ImageContacts.ImageID.EQ(UserImages.ImageID)).
|
||||
LEFT_JOIN(Contacts, Contacts.ID.EQ(ImageContacts.ContactID)).
|
||||
LEFT_JOIN(ImageNotes, ImageNotes.ImageID.EQ(UserImages.ImageID)).
|
||||
LEFT_JOIN(Notes, Notes.ID.EQ(ImageNotes.NoteID)))
|
||||
}
|
||||
|
||||
func (m UserModel) ListImageWithProperties(ctx context.Context, userId uuid.UUID, imageId uuid.UUID) (ImageWithProperties, error) {
|
||||
listImagePropertiesStmt := getListImagesStmt().
|
||||
WHERE(UserImages.ImageID.EQ(UUID(imageId)))
|
||||
|
||||
image := ImageWithProperties{}
|
||||
err := listImagePropertiesStmt.QueryContext(ctx, m.dbPool, &image)
|
||||
|
||||
return image, err
|
||||
}
|
||||
|
||||
func (m UserModel) ListWithProperties(ctx context.Context, userId uuid.UUID) ([]ImageWithProperties, error) {
|
||||
listWithPropertiesStmt := getListImagesStmt().
|
||||
WHERE(UserImages.UserID.EQ(UUID(userId)))
|
||||
|
||||
images := []ImageWithProperties{}
|
||||
err := listWithPropertiesStmt.QueryContext(ctx, m.dbPool, &images)
|
||||
|
||||
return images, err
|
||||
}
|
||||
|
||||
func (m UserModel) GetUserIdFromEmail(ctx context.Context, email string) (uuid.UUID, error) {
|
||||
@ -296,19 +49,20 @@ func (m UserModel) Save(ctx context.Context, user model.Users) (model.Users, err
|
||||
}
|
||||
|
||||
type UserImageWithImage struct {
|
||||
model.UserImages
|
||||
|
||||
Image model.Image
|
||||
model.Image
|
||||
ImageStacks []model.ImageStacks
|
||||
}
|
||||
|
||||
func (m UserModel) GetUserImages(ctx context.Context, userId uuid.UUID) ([]UserImageWithImage, error) {
|
||||
getUserImagesStmt := SELECT(
|
||||
UserImages.AllColumns,
|
||||
Image.ID,
|
||||
Image.ImageName,
|
||||
Image.AllColumns.Except(Image.Image),
|
||||
ImageStacks.AllColumns,
|
||||
).
|
||||
FROM(UserImages.INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID))).
|
||||
WHERE(UserImages.UserID.EQ(UUID(userId)))
|
||||
FROM(
|
||||
Image.
|
||||
LEFT_JOIN(ImageStacks, ImageStacks.ImageID.EQ(Image.ID)),
|
||||
).
|
||||
WHERE(Image.UserID.EQ(UUID(userId)))
|
||||
|
||||
userImages := []UserImageWithImage{}
|
||||
err := getUserImagesStmt.QueryContext(ctx, m.dbPool, &userImages)
|
||||
@ -316,6 +70,39 @@ func (m UserModel) GetUserImages(ctx context.Context, userId uuid.UUID) ([]UserI
|
||||
return userImages, err
|
||||
}
|
||||
|
||||
type ListsWithImages struct {
|
||||
model.Stacks
|
||||
|
||||
SchemaItems []model.SchemaItems
|
||||
|
||||
Images []struct {
|
||||
model.ImageStacks
|
||||
|
||||
Items []model.ImageSchemaItems
|
||||
}
|
||||
}
|
||||
|
||||
func (m UserModel) ListWithImages(ctx context.Context, userId uuid.UUID) ([]ListsWithImages, error) {
|
||||
stmt := SELECT(
|
||||
Stacks.AllColumns,
|
||||
ImageStacks.AllColumns,
|
||||
SchemaItems.AllColumns,
|
||||
ImageSchemaItems.AllColumns,
|
||||
).
|
||||
FROM(
|
||||
Stacks.
|
||||
INNER_JOIN(SchemaItems, SchemaItems.StackID.EQ(Stacks.ID)).
|
||||
LEFT_JOIN(ImageStacks, ImageStacks.StackID.EQ(Stacks.ID)).
|
||||
LEFT_JOIN(ImageSchemaItems, ImageSchemaItems.ImageID.EQ(ImageStacks.ID)),
|
||||
).
|
||||
WHERE(Stacks.UserID.EQ(UUID(userId)))
|
||||
|
||||
lists := []ListsWithImages{}
|
||||
err := stmt.QueryContext(ctx, m.dbPool, &lists)
|
||||
|
||||
return lists, err
|
||||
}
|
||||
|
||||
func NewUserModel(db *sql.DB) UserModel {
|
||||
return UserModel{dbPool: db}
|
||||
}
|
||||
|
38
backend/notifications/channel_splitter.go
Normal file
38
backend/notifications/channel_splitter.go
Normal file
@ -0,0 +1,38 @@
|
||||
package notifications
|
||||
|
||||
type ChannelSplitter[TNotification any] struct {
|
||||
ch chan TNotification
|
||||
|
||||
Listeners map[string]chan TNotification
|
||||
}
|
||||
|
||||
func (s *ChannelSplitter[TNotification]) Listen() {
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case msg := <-s.ch:
|
||||
for _, v := range s.Listeners {
|
||||
v <- msg
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *ChannelSplitter[TNotification]) Add(id string) chan TNotification {
|
||||
ch := make(chan TNotification)
|
||||
s.Listeners[id] = ch
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
func (s *ChannelSplitter[TNotification]) Remove(id string) {
|
||||
delete(s.Listeners, id)
|
||||
}
|
||||
|
||||
func NewChannelSplitter[TNotification any](ch chan TNotification) ChannelSplitter[TNotification] {
|
||||
return ChannelSplitter[TNotification]{
|
||||
ch: ch,
|
||||
Listeners: make(map[string]chan TNotification),
|
||||
}
|
||||
}
|
64
backend/notifications/entities_notification.go
Normal file
64
backend/notifications/entities_notification.go
Normal file
@ -0,0 +1,64 @@
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
IMAGE_TYPE = "image"
|
||||
STACK_TYPE = "stack"
|
||||
)
|
||||
|
||||
type ImageNotification struct {
|
||||
Type string
|
||||
|
||||
ImageID uuid.UUID
|
||||
ImageName string
|
||||
|
||||
Status string
|
||||
}
|
||||
|
||||
type StackNotification struct {
|
||||
Type string
|
||||
|
||||
StackID uuid.UUID
|
||||
Name string
|
||||
|
||||
Status string
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
image *ImageNotification
|
||||
stack *StackNotification
|
||||
}
|
||||
|
||||
func GetImageNotification(image ImageNotification) Notification {
|
||||
return Notification{
|
||||
image: &image,
|
||||
}
|
||||
}
|
||||
|
||||
func GetStackNotification(list StackNotification) Notification {
|
||||
return Notification{
|
||||
stack: &list,
|
||||
}
|
||||
}
|
||||
|
||||
func (n Notification) MarshalJSON() ([]byte, error) {
|
||||
if n.image != nil {
|
||||
return json.Marshal(n.image)
|
||||
}
|
||||
|
||||
if n.stack != nil {
|
||||
return json.Marshal(n.stack)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no image or list present")
|
||||
}
|
||||
|
||||
func (n *Notification) UnmarshalJSON(data []byte) error {
|
||||
return fmt.Errorf("unimplemented")
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@ -56,42 +56,3 @@ func NewNotifier[TNotification any](bufferSize int) Notifier[TNotification] {
|
||||
Listeners: make(map[string]chan TNotification),
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
|
||||
type ChannelSplitter[TNotification any] struct {
|
||||
ch chan TNotification
|
||||
|
||||
Listeners map[string]chan TNotification
|
||||
}
|
||||
|
||||
func (s *ChannelSplitter[TNotification]) Listen() {
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case msg := <-s.ch:
|
||||
for _, v := range s.Listeners {
|
||||
v <- msg
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *ChannelSplitter[TNotification]) Add(id string) chan TNotification {
|
||||
ch := make(chan TNotification)
|
||||
s.Listeners[id] = ch
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
func (s *ChannelSplitter[TNotification]) Remove(id string) {
|
||||
delete(s.Listeners, id)
|
||||
}
|
||||
|
||||
func NewChannelSplitter[TNotification any](ch chan TNotification) ChannelSplitter[TNotification] {
|
||||
return ChannelSplitter[TNotification]{
|
||||
ch: ch,
|
||||
Listeners: make(map[string]chan TNotification),
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"testing"
|
157
backend/processor/image.go
Normal file
157
backend/processor/image.go
Normal file
@ -0,0 +1,157 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/agents"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/limits"
|
||||
"screenmark/screenmark/models"
|
||||
"screenmark/screenmark/notifications"
|
||||
"sync"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
const IMAGE_PROCESS_AT_A_TIME = 10
|
||||
|
||||
type ImageProcessor struct {
|
||||
imageModel models.ImageModel
|
||||
logger *log.Logger
|
||||
|
||||
descriptionAgent agents.DescriptionAgent
|
||||
stackAgent client.AgentClient
|
||||
|
||||
Processor *Processor[model.Image]
|
||||
notifier *notifications.Notifier[notifications.Notification]
|
||||
}
|
||||
|
||||
func (p *ImageProcessor) setImageToProcess(ctx context.Context, image model.Image) {
|
||||
err := p.imageModel.UpdateProcess(ctx, image.ID, model.Progress_InProgress)
|
||||
if err != nil {
|
||||
// TODO: what can we actually do here for the errors?
|
||||
// We can't stop the work for the others
|
||||
|
||||
p.logger.Error("failed to update image", "err", err)
|
||||
|
||||
// TODO: we can use context here to actually pass some information through
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ImageProcessor) setImageToDone(ctx context.Context, image model.Image) {
|
||||
err := p.imageModel.UpdateProcess(ctx, image.ID, model.Progress_Complete)
|
||||
if err != nil {
|
||||
// TODO: what can we actually do here for the errors?
|
||||
// We can't stop the work for the others
|
||||
|
||||
p.logger.Error("failed to update image", "err", err)
|
||||
|
||||
// TODO: we can use context here to actually pass some information through
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ImageProcessor) describe(ctx context.Context, image model.Image) {
|
||||
descriptionSubLogger := p.logger.With("describe image", image.ID)
|
||||
|
||||
err := p.descriptionAgent.Describe(descriptionSubLogger, image.ID, image.ImageName, image.Image)
|
||||
if err != nil {
|
||||
// Again, wtf do we do?
|
||||
// Although i think the agent actually returns an error when it's finished
|
||||
p.logger.Error("failed to describe image", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ImageProcessor) extractInfo(ctx context.Context, image model.Image) {
|
||||
err := p.stackAgent.RunAgent(image.UserID, image.ID, image.ImageName, image.Image)
|
||||
if err != nil {
|
||||
// Again, wtf do we do?
|
||||
// Although i think the agent actually returns an error when it's finished
|
||||
p.logger.Error("failed to process image", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ImageProcessor) processImage(image model.Image) {
|
||||
p.logger.Info("Processing image", "ID", image.ID)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
p.setImageToProcess(ctx, image)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
imageNotification := notifications.GetImageNotification(notifications.ImageNotification{
|
||||
Type: notifications.IMAGE_TYPE,
|
||||
ImageID: image.ID,
|
||||
ImageName: image.ImageName,
|
||||
Status: string(model.Progress_InProgress),
|
||||
})
|
||||
|
||||
err := p.notifier.SendAndCreate(image.UserID.String(), imageNotification)
|
||||
if err != nil {
|
||||
p.logger.Error("sending in progress notification", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
p.describe(ctx, image)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
p.extractInfo(ctx, image)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
p.setImageToDone(ctx, image)
|
||||
|
||||
// TODO: there is some repeated code here. The ergonomicts of the notifications,
|
||||
// isn't the best.
|
||||
imageNotification = notifications.GetImageNotification(notifications.ImageNotification{
|
||||
Type: notifications.IMAGE_TYPE,
|
||||
ImageID: image.ID,
|
||||
ImageName: image.ImageName,
|
||||
Status: string(model.Progress_Complete),
|
||||
})
|
||||
|
||||
err = p.notifier.SendAndCreate(image.UserID.String(), imageNotification)
|
||||
if err != nil {
|
||||
p.logger.Error("sending done notification", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func NewImageProcessor(
|
||||
logger *log.Logger,
|
||||
imageModel models.ImageModel,
|
||||
listModel models.StackModel,
|
||||
limitsManager limits.LimitsManagerMethods,
|
||||
notifier *notifications.Notifier[notifications.Notification],
|
||||
) (ImageProcessor, error) {
|
||||
if notifier == nil {
|
||||
return ImageProcessor{}, fmt.Errorf("notifier is nil")
|
||||
}
|
||||
|
||||
descriptionAgent := agents.NewDescriptionAgent(logger, imageModel)
|
||||
stackAgent := agents.NewStackAgent(logger, listModel, limitsManager)
|
||||
|
||||
imageProcessor := ImageProcessor{
|
||||
imageModel: imageModel,
|
||||
logger: logger,
|
||||
descriptionAgent: descriptionAgent,
|
||||
stackAgent: stackAgent,
|
||||
|
||||
notifier: notifier,
|
||||
}
|
||||
|
||||
imageProcessor.Processor = NewProcessor(int(IMAGE_PROCESS_AT_A_TIME), imageProcessor.processImage)
|
||||
|
||||
return imageProcessor, nil
|
||||
}
|
23
backend/processor/processor.go
Normal file
23
backend/processor/processor.go
Normal file
@ -0,0 +1,23 @@
|
||||
package processor
|
||||
|
||||
type Processor[TMessage any] struct {
|
||||
queue chan TMessage
|
||||
process func(message TMessage)
|
||||
}
|
||||
|
||||
func (p *Processor[TMessage]) Work() {
|
||||
for msg := range p.queue {
|
||||
p.process(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor[TMessage]) Add(msg TMessage) {
|
||||
p.queue <- msg
|
||||
}
|
||||
|
||||
func NewProcessor[TMessage any](bufferSize int, process func(message TMessage)) *Processor[TMessage] {
|
||||
return &Processor[TMessage]{
|
||||
queue: make(chan TMessage, bufferSize),
|
||||
process: process,
|
||||
}
|
||||
}
|
142
backend/processor/stack.go
Normal file
142
backend/processor/stack.go
Normal file
@ -0,0 +1,142 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/agents"
|
||||
"screenmark/screenmark/models"
|
||||
"screenmark/screenmark/notifications"
|
||||
"sync"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
const STACK_PROCESS_AT_A_TIME = 10
|
||||
|
||||
// TODO:
|
||||
// This processor contains a lot of shared stuff.
|
||||
// If we ever want to do more generic stuff with "in-progress" and stuff
|
||||
// we can extract that into a common thing
|
||||
//
|
||||
// However, this will require a pretty big DB shuffle.
|
||||
|
||||
type StackProcessor struct {
|
||||
stackModel models.StackModel
|
||||
logger *log.Logger
|
||||
|
||||
stackAgent agents.CreateListAgent
|
||||
|
||||
Processor *Processor[model.Stacks]
|
||||
|
||||
notifier *notifications.Notifier[notifications.Notification]
|
||||
}
|
||||
|
||||
func (p *StackProcessor) setStackToProcess(ctx context.Context, stack model.Stacks) {
|
||||
err := p.stackModel.UpdateProcess(ctx, stack.ID, model.Progress_InProgress)
|
||||
if err != nil {
|
||||
// TODO: what can we actually do here for the errors?
|
||||
// We can't stop the work for the others
|
||||
|
||||
p.logger.Error("failed to update stack", "err", err)
|
||||
|
||||
// TODO: we can use context here to actually pass some information through
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (p *StackProcessor) setStackToDone(ctx context.Context, stack model.Stacks) {
|
||||
err := p.stackModel.UpdateProcess(ctx, stack.ID, model.Progress_Complete)
|
||||
if err != nil {
|
||||
// TODO: what can we actually do here for the errors?
|
||||
// We can't stop the work for the others
|
||||
|
||||
p.logger.Error("failed to update stack", "err", err)
|
||||
|
||||
// TODO: we can use context here to actually pass some information through
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (p *StackProcessor) extractInfo(ctx context.Context, stack model.Stacks) {
|
||||
err := p.stackAgent.CreateList(p.logger, stack.UserID, stack.ID, stack.Name, stack.Description)
|
||||
if err != nil {
|
||||
// Again, wtf do we do?
|
||||
// Although i think the agent actually returns an error when it's finished
|
||||
p.logger.Error("failed to process image", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (p *StackProcessor) processImage(stack model.Stacks) {
|
||||
p.logger.Info("Processing image", "ID", stack.ID)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
p.setStackToProcess(ctx, stack)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Future proofing!
|
||||
wg.Add(1)
|
||||
|
||||
stackNotification := notifications.GetStackNotification(notifications.StackNotification{
|
||||
Type: notifications.STACK_TYPE,
|
||||
Status: string(model.Progress_InProgress),
|
||||
StackID: stack.ID,
|
||||
Name: stack.Name,
|
||||
})
|
||||
|
||||
err := p.notifier.SendAndCreate(stack.UserID.String(), stackNotification)
|
||||
if err != nil {
|
||||
p.logger.Error("sending in progress notification", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
p.extractInfo(ctx, stack)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
p.setStackToDone(ctx, stack)
|
||||
|
||||
// TODO: there is some repeated code here. The ergonomicts of the notifications,
|
||||
// isn't the best.
|
||||
stackNotification = notifications.GetStackNotification(notifications.StackNotification{
|
||||
Type: notifications.STACK_TYPE,
|
||||
Status: string(model.Progress_Complete),
|
||||
StackID: stack.ID,
|
||||
Name: stack.Name,
|
||||
})
|
||||
|
||||
err = p.notifier.SendAndCreate(stack.UserID.String(), stackNotification)
|
||||
if err != nil {
|
||||
p.logger.Error("sending done notification", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func NewStackProcessor(
|
||||
logger *log.Logger,
|
||||
stackModel models.StackModel,
|
||||
notifier *notifications.Notifier[notifications.Notification],
|
||||
) (StackProcessor, error) {
|
||||
if notifier == nil {
|
||||
return StackProcessor{}, fmt.Errorf("notifier is nil")
|
||||
}
|
||||
|
||||
stackAgent := agents.NewCreateListAgent(logger, stackModel)
|
||||
|
||||
imageProcessor := StackProcessor{
|
||||
logger: logger,
|
||||
stackModel: stackModel,
|
||||
stackAgent: stackAgent,
|
||||
notifier: notifier,
|
||||
}
|
||||
|
||||
imageProcessor.Processor = NewProcessor(int(IMAGE_PROCESS_AT_A_TIME), imageProcessor.processImage)
|
||||
|
||||
return imageProcessor, nil
|
||||
}
|
73
backend/router.go
Normal file
73
backend/router.go
Normal file
@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/auth"
|
||||
"screenmark/screenmark/images"
|
||||
"screenmark/screenmark/limits"
|
||||
"screenmark/screenmark/models"
|
||||
"screenmark/screenmark/notifications"
|
||||
"screenmark/screenmark/processor"
|
||||
"screenmark/screenmark/stacks"
|
||||
|
||||
ourmiddleware "screenmark/screenmark/middleware"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
)
|
||||
|
||||
type TestAiClient struct {
|
||||
ImageInfo client.ImageMessageContent
|
||||
}
|
||||
|
||||
func (client TestAiClient) GetImageInfo(imageName string, imageData []byte) (client.ImageMessageContent, error) {
|
||||
return client.ImageInfo, nil
|
||||
}
|
||||
|
||||
func setupRouter(db *sql.DB, jwtManager *ourmiddleware.JwtManager) (chi.Router, error) {
|
||||
limitsManager := limits.CreateLimitsManager(db)
|
||||
|
||||
imageModel := models.NewImageModel(db)
|
||||
stackModel := models.NewStackModel(db)
|
||||
|
||||
notifier := notifications.NewNotifier[notifications.Notification](10)
|
||||
|
||||
imageProcessorLogger := createLogger("Image Processor", os.Stdout)
|
||||
imageProcessor, err := processor.NewImageProcessor(imageProcessorLogger, imageModel, stackModel, limitsManager, ¬ifier)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("processor: %w", err)
|
||||
}
|
||||
|
||||
stackProcessorLog := createLogger("Stack Processor", os.Stdout)
|
||||
stackProcessor, err := processor.NewStackProcessor(stackProcessorLog, stackModel, ¬ifier)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("processor: %w", err)
|
||||
}
|
||||
|
||||
go imageProcessor.Processor.Work()
|
||||
go stackProcessor.Processor.Work()
|
||||
|
||||
stackHandler := stacks.CreateStackHandler(db, limitsManager, jwtManager, stackProcessor.Processor)
|
||||
authHandler := auth.CreateAuthHandler(db, jwtManager)
|
||||
imageHandler := images.CreateImageHandler(db, limitsManager, jwtManager, imageProcessor.Processor)
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(ourmiddleware.CorsMiddleware)
|
||||
|
||||
r.Route("/stacks", stackHandler.CreateRoutes)
|
||||
r.Route("/auth", authHandler.CreateRoutes)
|
||||
r.Route("/images", imageHandler.CreateRoutes)
|
||||
|
||||
r.Route("/notifications", func(r chi.Router) {
|
||||
r.Use(ourmiddleware.GetUserIdFromUrl(jwtManager))
|
||||
|
||||
r.Get("/", CreateEventsHandler(¬ifier))
|
||||
})
|
||||
|
||||
return r, nil
|
||||
}
|
@ -9,311 +9,62 @@ CREATE TYPE haystack.progress AS ENUM('not-started','in-progress', 'complete');
|
||||
/* -----| Schema tables |----- */
|
||||
|
||||
CREATE TABLE haystack.users (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email TEXT NOT NULL
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email TEXT NOT NULL,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.image (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES haystack.users (id),
|
||||
|
||||
image_name TEXT NOT NULL,
|
||||
image BYTEA NOT NULL
|
||||
);
|
||||
description TEXT NOT NULL,
|
||||
|
||||
CREATE TABLE haystack.user_images_to_process (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
status haystack.progress NOT NULL DEFAULT 'not-started',
|
||||
image_id uuid NOT NULL UNIQUE REFERENCES haystack.image (id),
|
||||
user_id uuid NOT NULL REFERENCES haystack.users (id)
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.user_images (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
image_id uuid NOT NULL UNIQUE REFERENCES haystack.image (id),
|
||||
user_id uuid NOT NULL REFERENCES haystack.users (id),
|
||||
image BYTEA NOT NULL,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
/* ===== DEPRECATED ===== */
|
||||
|
||||
CREATE TABLE haystack.user_tags (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tag VARCHAR(32) UNIQUE NOT NULL,
|
||||
user_id uuid NOT NULL REFERENCES haystack.users (id)
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.image_tags (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tag_id UUID NOT NULL REFERENCES haystack.user_tags (id),
|
||||
image_id UUID NOT NULL REFERENCES haystack.image (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.image (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.image (id)
|
||||
);
|
||||
|
||||
/* ===== END DEPRECATED ===== */
|
||||
|
||||
CREATE TABLE haystack.locations (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
address TEXT,
|
||||
description TEXT,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.image_locations (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
location_id UUID NOT NULL REFERENCES haystack.locations (id),
|
||||
image_id UUID NOT NULL REFERENCES haystack.image (id),
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.user_locations (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
location_id UUID NOT NULL REFERENCES haystack.locations (id),
|
||||
CREATE TABLE haystack.stacks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES haystack.users (id),
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
status haystack.progress NOT NULL DEFAULT 'not-started',
|
||||
|
||||
CREATE TABLE haystack.contacts (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- It seems name and description are frequent. We could use table inheritance.
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
phone_number TEXT,
|
||||
email TEXT,
|
||||
description TEXT NOT NULL,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.user_contacts (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES haystack.users (id),
|
||||
contact_id UUID NOT NULL REFERENCES haystack.contacts (id),
|
||||
CREATE TABLE haystack.image_stacks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
image_id UUID NOT NULL REFERENCES haystack.image (id) ON DELETE CASCADE,
|
||||
stack_id UUID NOT NULL REFERENCES haystack.stacks (id) ON DELETE CASCADE,
|
||||
|
||||
UNIQUE(image_id, stack_id)
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.image_contacts (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
image_id UUID NOT NULL REFERENCES haystack.image (id),
|
||||
contact_id UUID NOT NULL REFERENCES haystack.contacts (id),
|
||||
CREATE TABLE haystack.schema_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
item TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
|
||||
stack_id UUID NOT NULL REFERENCES haystack.stacks (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.events (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
CREATE TABLE haystack.image_schema_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- It seems name and description are frequent. We could use table inheritance.
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
value TEXT,
|
||||
|
||||
start_date_time TIMESTAMP,
|
||||
end_date_time TIMESTAMP,
|
||||
|
||||
location_id UUID REFERENCES haystack.locations (id),
|
||||
organizer_id UUID REFERENCES haystack.contacts (id),
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
schema_item_id UUID NOT NULL REFERENCES haystack.schema_items (id) ON DELETE CASCADE,
|
||||
image_id UUID NOT NULL REFERENCES haystack.image_stacks (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.image_events (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_id UUID NOT NULL REFERENCES haystack.events (id),
|
||||
image_id UUID NOT NULL REFERENCES haystack.image (id),
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.user_events (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_id UUID NOT NULL REFERENCES haystack.events (id),
|
||||
user_id UUID NOT NULL REFERENCES haystack.users (id),
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.notes (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- It seems name and description are frequent. We could use table inheritance.
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
content TEXT NOT NULL,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.image_notes (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
image_id UUID NOT NULL REFERENCES haystack.image (id),
|
||||
note_id UUID NOT NULL REFERENCES haystack.notes (id),
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.user_notes (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES haystack.users (id),
|
||||
note_id UUID NOT NULL REFERENCES haystack.notes (id),
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.logs (
|
||||
log TEXT NOT NULL,
|
||||
image_id UUID NOT NULL REFERENCES haystack.image (id),
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
/* -----| Indexes |----- */
|
||||
|
||||
CREATE INDEX user_tags_index ON haystack.user_tags(tag);
|
||||
|
||||
/* -----| 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;
|
||||
|
||||
CREATE OR REPLACE FUNCTION notify_new_processing_image_status()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify('new_processing_image_status', NEW.id::text || NEW.status::text);
|
||||
RETURN NEW;
|
||||
END
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
/* -----| Triggers |----- */
|
||||
|
||||
CREATE OR REPLACE TRIGGER on_new_image AFTER INSERT
|
||||
ON haystack.user_images_to_process
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE notify_new_image();
|
||||
|
||||
CREATE OR REPLACE TRIGGER on_update_image_progress
|
||||
AFTER UPDATE OF status
|
||||
ON haystack.user_images_to_process
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE notify_new_processing_image_status();
|
||||
|
||||
/* -----| Test Data |----- */
|
||||
|
||||
-- Insert a user
|
||||
INSERT INTO haystack.users (id, email) VALUES ('1db09f34-b155-4bf2-b606-dda25365fc89', 'me@email.com');
|
||||
|
||||
-- Insert images
|
||||
INSERT INTO haystack.image (id, image_name, image) VALUES
|
||||
('3bd3fa04-e4b4-4ffb-b282-d573a092eb71', 'Sample Image 1', 'sample_image_1_bytes'),
|
||||
('f4560a78-d5d3-433e-8d90-b75c66e25423', 'Sample Image 2', 'sample_image_2_bytes');
|
||||
|
||||
-- Insert user images to process
|
||||
INSERT INTO haystack.user_images_to_process (id, image_id, user_id) VALUES
|
||||
('abe3679c-e787-4670-b5da-570453938f18', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71', '1db09f34-b155-4bf2-b606-dda25365fc89'),
|
||||
('8f3727e8-03fa-49bf-b0fe-ba8762df0902', 'f4560a78-d5d3-433e-8d90-b75c66e25423', '1db09f34-b155-4bf2-b606-dda25365fc89');
|
||||
|
||||
-- Insert user images
|
||||
INSERT INTO haystack.user_images (id, image_id, user_id) VALUES
|
||||
('28ade3a5-30c0-4f0a-93ff-5d062ba5c253', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71', '1db09f34-b155-4bf2-b606-dda25365fc89'),
|
||||
('c9425f01-a496-4c0a-919e-54b58c8ba600', 'f4560a78-d5d3-433e-8d90-b75c66e25423', '1db09f34-b155-4bf2-b606-dda25365fc89');
|
||||
|
||||
-- Insert user tags
|
||||
INSERT INTO haystack.user_tags (id, tag, user_id) VALUES
|
||||
('118c9491-a1ea-4930-88ee-33edfbc61cd3', 'vacation', '1db09f34-b155-4bf2-b606-dda25365fc89'),
|
||||
('c3e8c00a-4af6-45c6-acc3-53aa7ce2024a', 'family', '1db09f34-b155-4bf2-b606-dda25365fc89');
|
||||
|
||||
-- Insert image tags
|
||||
INSERT INTO haystack.image_tags (id, tag_id, image_id) VALUES
|
||||
('38ec5481-7b09-4e50-98b8-a85bbd5f6c6e', '118c9491-a1ea-4930-88ee-33edfbc61cd3', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71'),
|
||||
('9d64f58e-1d61-4c97-ae8b-a38bc3519fe1', 'c3e8c00a-4af6-45c6-acc3-53aa7ce2024a', 'f4560a78-d5d3-433e-8d90-b75c66e25423');
|
||||
|
||||
-- Insert image text
|
||||
INSERT INTO haystack.image_text (id, image_text, image_id) VALUES
|
||||
('fdd7a9f4-2a9a-494e-89d2-a63df8e45d62', 'Sample text for image 1', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71'),
|
||||
('95516f15-575c-485b-92ab-22eb18a306c1', 'Sample text for image 2', 'f4560a78-d5d3-433e-8d90-b75c66e25423');
|
||||
|
||||
-- Insert image links
|
||||
INSERT INTO haystack.image_links (id, link, image_id) VALUES
|
||||
('bbcc284f-c1f6-47ac-8d54-65b7729f03be', 'http://example.com/image1', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71'),
|
||||
('7391b2d1-6141-4195-8a4c-9c8ba4491b5a', 'http://example.com/image2', 'f4560a78-d5d3-433e-8d90-b75c66e25423');
|
||||
|
||||
-- Insert locations
|
||||
INSERT INTO haystack.locations (id, name, address, description) VALUES
|
||||
('5ac6f116-c21a-408b-9d2b-e8227a9a8503', 'Sample Location 1', '123 Sample St', 'A sample location'),
|
||||
('cd4b1815-5019-406d-9f1d-e9e5ac34c5f1', 'Sample Location 2', '456 Sample Ave', 'Another sample location');
|
||||
|
||||
-- Insert image locations
|
||||
INSERT INTO haystack.image_locations (id, location_id, image_id) VALUES
|
||||
('0e0c5cc2-b5b3-4b26-9d9c-2517b9358eb3', '5ac6f116-c21a-408b-9d2b-e8227a9a8503', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71'),
|
||||
('98facc74-cfc0-41cd-87e1-5e3822ae3407', 'cd4b1815-5019-406d-9f1d-e9e5ac34c5f1', 'f4560a78-d5d3-433e-8d90-b75c66e25423');
|
||||
|
||||
-- Insert user locations
|
||||
INSERT INTO haystack.user_locations (id, location_id, user_id) VALUES
|
||||
('1427ca1c-293f-4fab-b813-2acf145715f5', '5ac6f116-c21a-408b-9d2b-e8227a9a8503', '1db09f34-b155-4bf2-b606-dda25365fc89'),
|
||||
('343f9321-f63d-4248-aaab-3a1264d9cb5e', 'cd4b1815-5019-406d-9f1d-e9e5ac34c5f1', '1db09f34-b155-4bf2-b606-dda25365fc89');
|
||||
|
||||
-- Insert contacts
|
||||
INSERT INTO haystack.contacts (id, name, description, phone_number, email) VALUES
|
||||
('943be2ab-4db4-4e4e-bd1c-b78ad96df0d1', 'Contact 1', 'Sample contact description', '123-456-7890', 'contact1@example.com'),
|
||||
('09e2bf18-09b7-4553-971e-45136bd5b12f', 'Contact 2', 'Another sample contact description', '098-765-4321', 'contact2@example.com');
|
||||
|
||||
-- Insert user contacts
|
||||
INSERT INTO haystack.user_contacts (id, user_id, contact_id) VALUES
|
||||
('d74125e4-cbe4-4b83-8432-e0a3206af91c', '1db09f34-b155-4bf2-b606-dda25365fc89', '943be2ab-4db4-4e4e-bd1c-b78ad96df0d1'),
|
||||
('46e8cbd4-46a6-4499-9575-d3aad003fd1c', '1db09f34-b155-4bf2-b606-dda25365fc89', '09e2bf18-09b7-4553-971e-45136bd5b12f');
|
||||
|
||||
-- Insert image contacts
|
||||
INSERT INTO haystack.image_contacts (id, image_id, contact_id) VALUES
|
||||
('db075381-e89b-4582-800e-07561f9139e8', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71', '943be2ab-4db4-4e4e-bd1c-b78ad96df0d1'),
|
||||
('7384970d-3d3c-4e29-b158-edf200c53169', 'f4560a78-d5d3-433e-8d90-b75c66e25423', '09e2bf18-09b7-4553-971e-45136bd5b12f');
|
||||
|
||||
-- Insert events
|
||||
INSERT INTO haystack.events (id, name, description, start_date_time, end_date_time, location_id, organizer_id) VALUES
|
||||
('24a9dcbc-f8dc-4fca-835b-7ea57850d0b7', 'Sample Event 1', 'A sample event description', '2023-01-01 10:00:00', '2023-01-01 12:00:00', '5ac6f116-c21a-408b-9d2b-e8227a9a8503', '943be2ab-4db4-4e4e-bd1c-b78ad96df0d1'),
|
||||
('9cb6b0ae-3b02-4343-9858-5a07dd248562', 'Sample Event 2', 'Another sample event description', '2023-02-01 14:00:00', '2023-02-01 16:00:00', 'cd4b1815-5019-406d-9f1d-e9e5ac34c5f1', '09e2bf18-09b7-4553-971e-45136bd5b12f');
|
||||
|
||||
-- Insert image events
|
||||
INSERT INTO haystack.image_events (id, event_id, image_id) VALUES
|
||||
('5268a005-b3eb-4a30-8823-c8e9666507bb', '24a9dcbc-f8dc-4fca-835b-7ea57850d0b7', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71'),
|
||||
('9d6d4d26-c2a2-427f-92ed-34dc8c2d3e5f', '9cb6b0ae-3b02-4343-9858-5a07dd248562', 'f4560a78-d5d3-433e-8d90-b75c66e25423');
|
||||
|
||||
-- Insert user events
|
||||
INSERT INTO haystack.user_events (id, event_id, user_id) VALUES
|
||||
('16d815e4-6387-4fe9-b31d-5baff0567345', '24a9dcbc-f8dc-4fca-835b-7ea57850d0b7', '1db09f34-b155-4bf2-b606-dda25365fc89'),
|
||||
('43078366-d265-4ff9-9210-e11680bd6bcd', '9cb6b0ae-3b02-4343-9858-5a07dd248562', '1db09f34-b155-4bf2-b606-dda25365fc89');
|
||||
|
||||
-- Insert notes
|
||||
INSERT INTO haystack.notes (id, name, description, content) VALUES
|
||||
('6524f6b9-c659-409e-b2a0-abd3c3f5b5bb', 'Sample Note 1', 'A sample note description', 'This is the content of the sample note 1'),
|
||||
('a274b9b3-024f-457d-b4a0-d4535c2cca54', 'Sample Note 2', 'Another sample note description', 'This is the content of the sample note 2');
|
||||
|
||||
-- Insert image notes
|
||||
INSERT INTO haystack.image_notes (id, image_id, note_id) VALUES
|
||||
('6062fceb-7b3f-41fb-8509-489218968204', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71', '6524f6b9-c659-409e-b2a0-abd3c3f5b5bb'),
|
||||
('956dd3f6-4513-4cbc-9a5e-03dbec769402', 'f4560a78-d5d3-433e-8d90-b75c66e25423', 'a274b9b3-024f-457d-b4a0-d4535c2cca54');
|
||||
|
||||
-- Insert user notes
|
||||
INSERT INTO haystack.user_notes (id, user_id, note_id) VALUES
|
||||
('e3fa7a74-acbf-4aa9-930b-f10bd8a6ced5', '1db09f34-b155-4bf2-b606-dda25365fc89', '6524f6b9-c659-409e-b2a0-abd3c3f5b5bb'),
|
||||
('ebaef76b-3b78-491c-93f7-19510080284d', '1db09f34-b155-4bf2-b606-dda25365fc89', 'a274b9b3-024f-457d-b4a0-d4535c2cca54');
|
||||
|
263
backend/stacks/handler.go
Normal file
263
backend/stacks/handler.go
Normal file
@ -0,0 +1,263 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"os"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/limits"
|
||||
"screenmark/screenmark/middleware"
|
||||
"screenmark/screenmark/models"
|
||||
"screenmark/screenmark/processor"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type StackHandler struct {
|
||||
logger *log.Logger
|
||||
|
||||
imageModel models.ImageModel
|
||||
stackModel models.StackModel
|
||||
|
||||
limitsManager limits.LimitsManagerMethods
|
||||
|
||||
jwtManager *middleware.JwtManager
|
||||
|
||||
processor *processor.Processor[model.Stacks]
|
||||
}
|
||||
|
||||
func (h *StackHandler) getAllStacks(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
userID, err := middleware.GetUserID(ctx, h.logger, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
stacks, err := h.stackModel.List(ctx, userID)
|
||||
if err != nil {
|
||||
h.logger.Warn("could not get stacks", "err", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
middleware.WriteJsonOrError(h.logger, stacks, w)
|
||||
}
|
||||
|
||||
func (h *StackHandler) getStackItems(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
_, err := middleware.GetUserID(ctx, h.logger, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
stackID, err := middleware.GetPathParamID(h.logger, "stackID", w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: must check for permission here.
|
||||
|
||||
lists, err := h.stackModel.ListItems(ctx, stackID)
|
||||
if err != nil {
|
||||
h.logger.Warn("could not get list items", "err", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
middleware.WriteJsonOrError(h.logger, lists, w)
|
||||
}
|
||||
|
||||
type EditStack struct {
|
||||
Hello string `json:"hello"`
|
||||
}
|
||||
|
||||
func (h *StackHandler) editStack(req EditStack, w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
func (h *StackHandler) deleteStack(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
userID, err := middleware.GetUserID(ctx, h.logger, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
stackID, err := middleware.GetPathParamID(h.logger, "stackID", w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = h.stackModel.Delete(ctx, stackID, userID)
|
||||
if err != nil {
|
||||
h.logger.Warn("could not delete stack", "err", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *StackHandler) deleteImageFromStack(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
stringListID := chi.URLParam(r, "stackID")
|
||||
stringImageID := chi.URLParam(r, "imageID")
|
||||
|
||||
imageID, err := uuid.Parse(stringImageID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
stackID, err := uuid.Parse(stringListID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: this should be extracted into a middleware of sorts
|
||||
userID, err := middleware.GetUserID(ctx, h.logger, w)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
stack, err := h.stackModel.Get(ctx, stackID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if stack.UserID != userID {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.stackModel.DeleteImage(ctx, stackID, imageID)
|
||||
if err != nil {
|
||||
h.logger.Warn("failed to delete image from list", "error", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *StackHandler) deleteImageStackSchemaItem(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
stringStackID := chi.URLParam(r, "stackID")
|
||||
stringSchemaItemID := chi.URLParam(r, "schemaItemID")
|
||||
|
||||
stackID, err := uuid.Parse(stringStackID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
schemaItemID, err := uuid.Parse(stringSchemaItemID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: this should be extracted into a middleware of sorts
|
||||
userID, err := middleware.GetUserID(ctx, h.logger, w)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
stack, err := h.stackModel.Get(ctx, stackID)
|
||||
if err != nil {
|
||||
h.logger.Error("could not get stack model", "err", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if stack.UserID != userID {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// The code above is repeated, because it contains stack & image
|
||||
// manipulations. So we could create a middleware.
|
||||
// If you repeat this 3 times, then organise it :)
|
||||
|
||||
err = h.stackModel.DeleteSchemaItem(ctx, stackID, schemaItemID)
|
||||
if err != nil {
|
||||
h.logger.Warn("failed to delete image from list", "error", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
type CreateStackBody struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func (h *StackHandler) createStack(body CreateStackBody, w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
userID, err := middleware.GetUserID(ctx, h.logger, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Add the stack processor here
|
||||
stack, err := h.stackModel.Save(ctx, userID, body.Title, body.Description, model.Progress_NotStarted)
|
||||
if err != nil {
|
||||
h.logger.Warn("could not save stack", "err", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
h.processor.Add(stack)
|
||||
|
||||
middleware.WriteJsonOrError(h.logger, stack, w)
|
||||
}
|
||||
|
||||
func (h *StackHandler) CreateRoutes(r chi.Router) {
|
||||
h.logger.Info("Mounting stack router")
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.ProtectedRoute(h.jwtManager))
|
||||
r.Use(middleware.SetJson)
|
||||
|
||||
r.Get("/", h.getAllStacks)
|
||||
r.Get("/{stackID}", h.getStackItems)
|
||||
|
||||
r.Post("/", middleware.WithLimit(h.logger, h.limitsManager.HasReachedStackLimit, middleware.WithValidatedPost(h.createStack)))
|
||||
r.Patch("/{stackID}", middleware.WithValidatedPost(h.editStack))
|
||||
r.Delete("/{stackID}", h.deleteStack)
|
||||
r.Delete("/{stackID}/{imageID}", h.deleteImageFromStack)
|
||||
r.Delete("/{stackID}/{schemaItemID}", h.deleteImageStackSchemaItem)
|
||||
})
|
||||
}
|
||||
|
||||
func CreateStackHandler(
|
||||
db *sql.DB,
|
||||
limitsManager limits.LimitsManagerMethods,
|
||||
jwtManager *middleware.JwtManager,
|
||||
processor *processor.Processor[model.Stacks],
|
||||
) StackHandler {
|
||||
stackModel := models.NewStackModel(db)
|
||||
imageModel := models.NewImageModel(db)
|
||||
logger := log.New(os.Stdout).WithPrefix("Stacks")
|
||||
|
||||
return StackHandler{
|
||||
logger: logger,
|
||||
imageModel: imageModel,
|
||||
stackModel: stackModel,
|
||||
limitsManager: limitsManager,
|
||||
jwtManager: jwtManager,
|
||||
processor: processor,
|
||||
}
|
||||
}
|
2
frontend/.gitignore
vendored
2
frontend/.gitignore
vendored
@ -3,3 +3,5 @@ db
|
||||
screenmark
|
||||
node_modules
|
||||
dist
|
||||
tsconfig.node.tsbuildinfo
|
||||
tsconfig.tsbuildinfo
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user