Compare commits
28 Commits
fa71f68de5
...
main
Author | SHA1 | Date | |
---|---|---|---|
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 |
@ -1,22 +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 Contacts struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
Name string
|
||||
Description *string
|
||||
PhoneNumber *string
|
||||
Email *string
|
||||
CreatedAt *time.Time
|
||||
}
|
@ -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
|
||||
}
|
@ -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 ImageContacts struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
ImageID uuid.UUID
|
||||
ContactID uuid.UUID
|
||||
CreatedAt *time.Time
|
||||
type ImageSchemaItems struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
Value *string
|
||||
SchemaItemID uuid.UUID
|
||||
ImageID uuid.UUID
|
||||
}
|
@ -12,9 +12,11 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserContacts struct {
|
||||
type ProcessingLists struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
UserID uuid.UUID
|
||||
ContactID uuid.UUID
|
||||
Title string
|
||||
Fields string
|
||||
Status Progress
|
||||
CreatedAt *time.Time
|
||||
}
|
@ -9,13 +9,12 @@ package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Locations struct {
|
||||
type SchemaItems struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
Name string
|
||||
Address *string
|
||||
Description *string
|
||||
CreatedAt *time.Time
|
||||
Item string
|
||||
Value string
|
||||
Description string
|
||||
SchemaID uuid.UUID
|
||||
}
|
@ -9,12 +9,9 @@ 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
|
||||
type Schemas struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
ListID 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,93 +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
|
||||
DefaultColumns 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}
|
||||
defaultColumns = postgres.ColumnList{IDColumn, 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,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
@ -1,99 +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
|
||||
DefaultColumns 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}
|
||||
defaultColumns = postgres.ColumnList{IDColumn, 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,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
@ -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 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
|
||||
DefaultColumns 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}
|
||||
defaultColumns = postgres.ColumnList{IDColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return imageContactsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
ContactID: ContactIDColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
@ -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 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
|
||||
DefaultColumns 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}
|
||||
defaultColumns = postgres.ColumnList{IDColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return imageEventsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
EventID: EventIDColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
@ -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 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
|
||||
DefaultColumns 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}
|
||||
defaultColumns = postgres.ColumnList{IDColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return imageLocationsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
LocationID: LocationIDColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
87
backend/.gen/haystack/haystack/table/image_schema_items.go
Normal file
87
backend/.gen/haystack/haystack/table/image_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 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
|
||||
DefaultColumns 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}
|
||||
defaultColumns = postgres.ColumnList{IDColumn}
|
||||
)
|
||||
|
||||
return imageSchemaItemsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
Value: ValueColumn,
|
||||
SchemaItemID: SchemaItemIDColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
@ -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 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
|
||||
DefaultColumns 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}
|
||||
defaultColumns = postgres.ColumnList{IDColumn, 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,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
93
backend/.gen/haystack/haystack/table/processing_lists.go
Normal file
93
backend/.gen/haystack/haystack/table/processing_lists.go
Normal file
@ -0,0 +1,93 @@
|
||||
//
|
||||
// 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 ProcessingLists = newProcessingListsTable("haystack", "processing_lists", "")
|
||||
|
||||
type processingListsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
UserID postgres.ColumnString
|
||||
Title postgres.ColumnString
|
||||
Fields postgres.ColumnString
|
||||
Status postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type ProcessingListsTable struct {
|
||||
processingListsTable
|
||||
|
||||
EXCLUDED processingListsTable
|
||||
}
|
||||
|
||||
// AS creates new ProcessingListsTable with assigned alias
|
||||
func (a ProcessingListsTable) AS(alias string) *ProcessingListsTable {
|
||||
return newProcessingListsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new ProcessingListsTable with assigned schema name
|
||||
func (a ProcessingListsTable) FromSchema(schemaName string) *ProcessingListsTable {
|
||||
return newProcessingListsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new ProcessingListsTable with assigned table prefix
|
||||
func (a ProcessingListsTable) WithPrefix(prefix string) *ProcessingListsTable {
|
||||
return newProcessingListsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new ProcessingListsTable with assigned table suffix
|
||||
func (a ProcessingListsTable) WithSuffix(suffix string) *ProcessingListsTable {
|
||||
return newProcessingListsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newProcessingListsTable(schemaName, tableName, alias string) *ProcessingListsTable {
|
||||
return &ProcessingListsTable{
|
||||
processingListsTable: newProcessingListsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newProcessingListsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newProcessingListsTableImpl(schemaName, tableName, alias string) processingListsTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
UserIDColumn = postgres.StringColumn("user_id")
|
||||
TitleColumn = postgres.StringColumn("title")
|
||||
FieldsColumn = postgres.StringColumn("fields")
|
||||
StatusColumn = postgres.StringColumn("status")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, UserIDColumn, TitleColumn, FieldsColumn, StatusColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{UserIDColumn, TitleColumn, FieldsColumn, StatusColumn, CreatedAtColumn}
|
||||
defaultColumns = postgres.ColumnList{IDColumn, StatusColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return processingListsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
UserID: UserIDColumn,
|
||||
Title: TitleColumn,
|
||||
Fields: FieldsColumn,
|
||||
Status: StatusColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
90
backend/.gen/haystack/haystack/table/schema_items.go
Normal file
90
backend/.gen/haystack/haystack/table/schema_items.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 SchemaItems = newSchemaItemsTable("haystack", "schema_items", "")
|
||||
|
||||
type schemaItemsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
Item postgres.ColumnString
|
||||
Value postgres.ColumnString
|
||||
Description postgres.ColumnString
|
||||
SchemaID postgres.ColumnString
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns 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")
|
||||
SchemaIDColumn = postgres.StringColumn("schema_id")
|
||||
allColumns = postgres.ColumnList{IDColumn, ItemColumn, ValueColumn, DescriptionColumn, SchemaIDColumn}
|
||||
mutableColumns = postgres.ColumnList{ItemColumn, ValueColumn, DescriptionColumn, SchemaIDColumn}
|
||||
defaultColumns = postgres.ColumnList{IDColumn}
|
||||
)
|
||||
|
||||
return schemaItemsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
Item: ItemColumn,
|
||||
Value: ValueColumn,
|
||||
Description: DescriptionColumn,
|
||||
SchemaID: SchemaIDColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
81
backend/.gen/haystack/haystack/table/schemas.go
Normal file
81
backend/.gen/haystack/haystack/table/schemas.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 Schemas = newSchemasTable("haystack", "schemas", "")
|
||||
|
||||
type schemasTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
ListID postgres.ColumnString
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type SchemasTable struct {
|
||||
schemasTable
|
||||
|
||||
EXCLUDED schemasTable
|
||||
}
|
||||
|
||||
// AS creates new SchemasTable with assigned alias
|
||||
func (a SchemasTable) AS(alias string) *SchemasTable {
|
||||
return newSchemasTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new SchemasTable with assigned schema name
|
||||
func (a SchemasTable) FromSchema(schemaName string) *SchemasTable {
|
||||
return newSchemasTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new SchemasTable with assigned table prefix
|
||||
func (a SchemasTable) WithPrefix(prefix string) *SchemasTable {
|
||||
return newSchemasTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new SchemasTable with assigned table suffix
|
||||
func (a SchemasTable) WithSuffix(suffix string) *SchemasTable {
|
||||
return newSchemasTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newSchemasTable(schemaName, tableName, alias string) *SchemasTable {
|
||||
return &SchemasTable{
|
||||
schemasTable: newSchemasTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newSchemasTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newSchemasTableImpl(schemaName, tableName, alias string) schemasTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
ListIDColumn = postgres.StringColumn("list_id")
|
||||
allColumns = postgres.ColumnList{IDColumn, ListIDColumn}
|
||||
mutableColumns = postgres.ColumnList{ListIDColumn}
|
||||
defaultColumns = postgres.ColumnList{IDColumn}
|
||||
)
|
||||
|
||||
return schemasTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
ListID: ListIDColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
@ -10,20 +10,15 @@ 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)
|
||||
ImageLists = ImageLists.FromSchema(schema)
|
||||
ImageLocations = ImageLocations.FromSchema(schema)
|
||||
ImageSchemaItems = ImageSchemaItems.FromSchema(schema)
|
||||
Lists = Lists.FromSchema(schema)
|
||||
Locations = Locations.FromSchema(schema)
|
||||
Logs = Logs.FromSchema(schema)
|
||||
UserContacts = UserContacts.FromSchema(schema)
|
||||
UserEvents = UserEvents.FromSchema(schema)
|
||||
ProcessingLists = ProcessingLists.FromSchema(schema)
|
||||
SchemaItems = SchemaItems.FromSchema(schema)
|
||||
Schemas = Schemas.FromSchema(schema)
|
||||
UserImages = UserImages.FromSchema(schema)
|
||||
UserImagesToProcess = UserImagesToProcess.FromSchema(schema)
|
||||
UserLocations = UserLocations.FromSchema(schema)
|
||||
Users = Users.FromSchema(schema)
|
||||
}
|
||||
|
@ -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 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
|
||||
DefaultColumns 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}
|
||||
defaultColumns = postgres.ColumnList{IDColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return userContactsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
UserID: UserIDColumn,
|
||||
ContactID: ContactIDColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
@ -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 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
|
||||
DefaultColumns 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}
|
||||
defaultColumns = postgres.ColumnList{IDColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return userEventsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
EventID: EventIDColumn,
|
||||
UserID: UserIDColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
@ -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 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
|
||||
DefaultColumns 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}
|
||||
defaultColumns = postgres.ColumnList{IDColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return userLocationsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
LocationID: LocationIDColumn,
|
||||
UserID: UserIDColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -133,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{}, fmt.Errorf("Could not format JSON", err)
|
||||
return AgentResponse{}, fmt.Errorf("Could not format JSON: %w", err)
|
||||
}
|
||||
|
||||
httpRequest, err := client.getRequest(jsonAiRequest)
|
||||
if err != nil {
|
||||
return AgentResponse{}, fmt.Errorf("Could not get request", err)
|
||||
return AgentResponse{}, fmt.Errorf("Could not get request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return AgentResponse{}, fmt.Errorf("Could not send request", err)
|
||||
return AgentResponse{}, fmt.Errorf("Could not send request: %w", err)
|
||||
}
|
||||
|
||||
response, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return AgentResponse{}, fmt.Errorf("Could not read body", err)
|
||||
return AgentResponse{}, fmt.Errorf("Could not read body: %w", err)
|
||||
}
|
||||
|
||||
agentResponse := AgentResponse{}
|
||||
err = json.Unmarshal(response, &agentResponse)
|
||||
|
||||
if err != nil {
|
||||
return AgentResponse{}, fmt.Errorf("Could not unmarshal response, response: %s", string(response), err)
|
||||
return AgentResponse{}, fmt.Errorf("Could not unmarshal response, response: %s: %w", string(response), err)
|
||||
}
|
||||
|
||||
if len(agentResponse.Choices) != 1 {
|
||||
@ -270,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: "google/gemini-2.5-flash",
|
||||
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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
140
backend/agents/create_list_agent.go
Normal file
140
backend/agents/create_list_agent.go
Normal file
@ -0,0 +1,140 @@
|
||||
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 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.
|
||||
`
|
||||
|
||||
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
|
||||
|
||||
listModel models.ListModel
|
||||
}
|
||||
|
||||
func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, userReq string) error {
|
||||
request := client.AgentRequestBody{
|
||||
Model: "google/gemini-2.5-flash",
|
||||
Temperature: 0.3,
|
||||
ResponseFormat: client.ResponseFormat{
|
||||
Type: "json_object",
|
||||
JsonSchema: listJsonSchema,
|
||||
},
|
||||
Chat: &client.Chat{
|
||||
Messages: make([]client.ChatMessage, 0),
|
||||
},
|
||||
}
|
||||
|
||||
request.Chat.AddSystem(agent.client.Options.SystemPrompt)
|
||||
request.Chat.AddUser(userReq)
|
||||
|
||||
resp, err := agent.client.Request(&request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request: %w", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
structuredOutput := resp.Choices[0].Message.Content
|
||||
|
||||
var createListArgs createNewListArguments
|
||||
err = json.Unmarshal([]byte(structuredOutput), &createListArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schemaItems := make([]model.SchemaItems, 0)
|
||||
for _, field := range createListArgs.Fields {
|
||||
schemaItems = append(schemaItems, model.SchemaItems{
|
||||
Item: field.Name,
|
||||
Description: field.Description,
|
||||
|
||||
Value: "string", // keep it simple for now.
|
||||
})
|
||||
}
|
||||
|
||||
agent.listModel.Save(ctx, userID, createListArgs.Title, createListArgs.Description, schemaItems)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewCreateListAgent(log *log.Logger, listModel models.ListModel) CreateListAgent {
|
||||
client := client.CreateAgentClient(client.CreateAgentClientOptions{
|
||||
SystemPrompt: createListAgentPrompt,
|
||||
Log: log,
|
||||
})
|
||||
|
||||
agent := CreateListAgent{
|
||||
client,
|
||||
listModel,
|
||||
}
|
||||
|
||||
return agent
|
||||
}
|
@ -15,6 +15,9 @@ 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 {
|
||||
@ -41,15 +44,13 @@ func (agent DescriptionAgent) Describe(log *log.Logger, imageId uuid.UUID, image
|
||||
log.Debug("Sending description request")
|
||||
resp, err := agent.client.Request(&request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not request", err)
|
||||
return fmt.Errorf("Could not request. %s", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
markdown := resp.Choices[0].Message.Content
|
||||
|
||||
log.Debugf("Response %s", markdown)
|
||||
|
||||
err = agent.imageModel.AddDescription(ctx, imageId, markdown)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -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
|
||||
}
|
@ -24,17 +24,22 @@ An example of lists are:
|
||||
- Movies
|
||||
- Books
|
||||
|
||||
You must call "listLists" to see which available lists are already available.
|
||||
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.
|
||||
|
||||
*Important*
|
||||
You must not create lists with the names Locations, Events, Contacts or Notes. You can create lists adjacent to those, but
|
||||
those lists are dealt with seperately.
|
||||
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.
|
||||
* addToList: Add to an existing list.
|
||||
* 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.
|
||||
`
|
||||
|
||||
@ -84,9 +89,29 @@ const listTools = `
|
||||
"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"]
|
||||
"required": ["name", "description", "schema"]
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -101,9 +126,26 @@ const listTools = `
|
||||
"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"]
|
||||
"required": ["listId", "schema"]
|
||||
}
|
||||
|
||||
}
|
||||
@ -122,13 +164,14 @@ const listTools = `
|
||||
}
|
||||
]`
|
||||
|
||||
type listListsArguments struct{}
|
||||
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 NewListAgent(log *log.Logger, listModel models.ListModel) client.AgentClient {
|
||||
@ -143,27 +186,30 @@ func NewListAgent(log *log.Logger, listModel models.ListModel) client.AgentClien
|
||||
return "Thought", nil
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("listLists", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
return listModel.List(context.Background(), info.UserId)
|
||||
})
|
||||
|
||||
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 model.Events{}, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
savedList, err := listModel.Save(ctx, info.UserId, args.Name, args.Desription)
|
||||
savedList, err := listModel.Save(ctx, info.UserId, args.Name, args.Desription, args.Schema)
|
||||
|
||||
if err != nil {
|
||||
return model.Lists{}, err
|
||||
log.Error(err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Debug(savedList)
|
||||
|
||||
return savedList, nil
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("listLists", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
return listModel.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)
|
||||
@ -178,7 +224,7 @@ func NewListAgent(log *log.Logger, listModel models.ListModel) client.AgentClien
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := listModel.SaveInto(ctx, listUuid, info.ImageId); err != nil {
|
||||
if err := listModel.SaveInto(ctx, listUuid, info.ImageId, args.Schema); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
|
@ -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,165 +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, 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("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(
|
106
backend/auth/handler.go
Normal file
106
backend/auth/handler.go
Normal file
@ -0,0 +1,106 @@
|
||||
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
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
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 := middleware.CreateRefreshToken(uuid)
|
||||
access := middleware.CreateAccessToken(uuid)
|
||||
|
||||
codeReturn := codeReturn{
|
||||
Access: access,
|
||||
Refresh: refresh,
|
||||
}
|
||||
|
||||
middleware.WriteJsonOrError(h.logger, codeReturn, 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))
|
||||
})
|
||||
}
|
||||
|
||||
func CreateAuthHandler(db *sql.DB) 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,
|
||||
userModel,
|
||||
auth,
|
||||
}
|
||||
}
|
@ -8,8 +8,10 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"screenmark/screenmark/agents"
|
||||
"screenmark/screenmark/middleware"
|
||||
"screenmark/screenmark/models"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
@ -17,13 +19,63 @@ import (
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
type Notification struct {
|
||||
const (
|
||||
IMAGE_TYPE = "image"
|
||||
LIST_TYPE = "list"
|
||||
)
|
||||
|
||||
type imageNotification struct {
|
||||
Type string
|
||||
|
||||
ImageID uuid.UUID
|
||||
ImageName string
|
||||
Status string
|
||||
|
||||
Status string
|
||||
}
|
||||
|
||||
func ListenNewImageEvents(db *sql.DB, notifier *Notifier[Notification]) {
|
||||
type listNotification struct {
|
||||
Type string
|
||||
|
||||
ListID uuid.UUID
|
||||
Name string
|
||||
|
||||
Status string
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
image *imageNotification
|
||||
list *listNotification
|
||||
}
|
||||
|
||||
func getImageNotification(image imageNotification) Notification {
|
||||
return Notification{
|
||||
image: &image,
|
||||
}
|
||||
}
|
||||
|
||||
func getListNotification(list listNotification) Notification {
|
||||
return Notification{
|
||||
list: &list,
|
||||
}
|
||||
}
|
||||
|
||||
func (n Notification) MarshalJSON() ([]byte, error) {
|
||||
if n.image != nil {
|
||||
return json.Marshal(n.image)
|
||||
}
|
||||
|
||||
if n.list != nil {
|
||||
return json.Marshal(n.list)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no image or list present")
|
||||
}
|
||||
|
||||
func (n *Notification) UnmarshalJSON(data []byte) error {
|
||||
return fmt.Errorf("unimplemented")
|
||||
}
|
||||
|
||||
func ListenNewImageEvents(db *sql.DB) {
|
||||
listener := pq.NewListener(os.Getenv("DB_CONNECTION"), time.Second, time.Second, func(event pq.ListenerEventType, err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -31,11 +83,7 @@ func ListenNewImageEvents(db *sql.DB, notifier *Notifier[Notification]) {
|
||||
})
|
||||
defer listener.Close()
|
||||
|
||||
locationModel := models.NewLocationModel(db)
|
||||
eventModel := models.NewEventModel(db)
|
||||
imageModel := models.NewImageModel(db)
|
||||
contactModel := models.NewContactModel(db)
|
||||
|
||||
listModel := models.NewListModel(db)
|
||||
|
||||
databaseEventLog := createLogger("Database Events 🤖", os.Stdout)
|
||||
@ -62,27 +110,30 @@ func ListenNewImageEvents(db *sql.DB, notifier *Notifier[Notification]) {
|
||||
|
||||
splitWriter := createDbStdoutWriter(db, image.ImageID)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
descriptionAgent := agents.NewDescriptionAgent(createLogger("Description 📝", splitWriter), imageModel)
|
||||
err = descriptionAgent.Describe(createLogger("Description 📓", splitWriter), image.Image.ID, image.Image.ImageName, image.Image.Image)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
listAgent := agents.NewListAgent(createLogger("Lists 🖋️", splitWriter), listModel)
|
||||
listAgent.RunAgent(image.UserID, image.ImageID, image.Image.ImageName, image.Image.Image)
|
||||
|
||||
orchestrator := agents.NewOrchestratorAgent(createLogger("Orchestrator 🎼", splitWriter), contactAgent, locationAgent, eventAgent, image.Image.ImageName, image.Image.Image)
|
||||
orchestrator.RunAgent(image.UserID, image.ImageID, image.Image.ImageName, image.Image.Image)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
descriptionAgent.Describe(createLogger("Description 📓", splitWriter), image.Image.ID, image.Image.ImageName, image.Image.Image)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
listAgent.RunAgent(image.UserID, image.ImageID, image.Image.ImageName, image.Image.Image)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
_, err = imageModel.FinishProcessing(ctx, image.ID)
|
||||
if err != nil {
|
||||
@ -127,11 +178,12 @@ func ListenProcessingImageStatus(db *sql.DB, images models.ImageModel, notifier
|
||||
|
||||
logger.Info("Update", "id", imageStringUuid, "status", status)
|
||||
|
||||
notification := Notification{
|
||||
notification := getImageNotification(imageNotification{
|
||||
Type: IMAGE_TYPE,
|
||||
ImageID: processingImage.ImageID,
|
||||
ImageName: processingImage.Image.ImageName,
|
||||
Status: status,
|
||||
}
|
||||
})
|
||||
|
||||
if err := notifier.SendAndCreate(processingImage.UserID.String(), notification); err != nil {
|
||||
logger.Error(err)
|
||||
@ -139,6 +191,107 @@ func ListenProcessingImageStatus(db *sql.DB, images models.ImageModel, notifier
|
||||
}
|
||||
}
|
||||
|
||||
func ListenNewStackEvents(db *sql.DB) {
|
||||
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()
|
||||
|
||||
stackModel := models.NewListModel(db)
|
||||
|
||||
newStacksLogger := createLogger("New Stacks 🤖", os.Stdout)
|
||||
newStacksLogger.SetLevel(log.DebugLevel)
|
||||
|
||||
err := listener.Listen("new_stack")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for parameters := range listener.Notify {
|
||||
stackID := uuid.MustParse(parameters.Extra)
|
||||
|
||||
newStacksLogger.Debug("Starting processing stack", "StackID", stackID)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
go func() {
|
||||
stack, err := stackModel.GetProcessing(ctx, stackID)
|
||||
if err != nil {
|
||||
newStacksLogger.Error("failed to get processing", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := stackModel.StartProcessing(ctx, stackID); err != nil {
|
||||
newStacksLogger.Error("failed to start processing", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
listAgent := agents.NewCreateListAgent(newStacksLogger, stackModel)
|
||||
userListRequest := fmt.Sprintf("title=%s,fields=%s", stack.Title, stack.Fields)
|
||||
|
||||
err = listAgent.CreateList(newStacksLogger, stack.UserID, userListRequest)
|
||||
if err != nil {
|
||||
newStacksLogger.Error("running agent", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := stackModel.EndProcessing(ctx, stackID); err != nil {
|
||||
newStacksLogger.Error("failed to finish processing", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
newStacksLogger.Debug("Finished processing stack", "StackID", stackID)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func ListenProcessingStackStatus(db *sql.DB, stacks models.ListModel, 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("Stack Status 📊", os.Stdout)
|
||||
|
||||
if err := listener.Listen("new_processing_stack_status"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for data := range listener.Notify {
|
||||
stackStringUUID := data.Extra[0:36]
|
||||
status := data.Extra[36:]
|
||||
|
||||
stackUUID, err := uuid.Parse(stackStringUUID)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
processingStack, err := stacks.GetToProcess(context.Background(), stackUUID)
|
||||
if err != nil {
|
||||
logger.Error("GetToProcess failed", "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info("Update", "id", stackStringUUID, "status", status)
|
||||
|
||||
notification := getListNotification(listNotification{
|
||||
Type: LIST_TYPE,
|
||||
Name: processingStack.Title,
|
||||
ListID: stackUUID,
|
||||
Status: status,
|
||||
})
|
||||
|
||||
if err := notifier.SendAndCreate(processingStack.UserID.String(), notification); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO: We have channels open every a user sends an image.
|
||||
* We never close these channels.
|
||||
@ -151,7 +304,7 @@ func CreateEventsHandler(notifier *Notifier[Notification]) http.HandlerFunc {
|
||||
userSplitters := make(map[string]*ChannelSplitter[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
|
||||
@ -199,7 +352,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()
|
||||
}
|
||||
|
170
backend/images/handler.go
Normal file
170
backend/images/handler.go
Normal file
@ -0,0 +1,170 @@
|
||||
package images
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/middleware"
|
||||
"screenmark/screenmark/models"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type ImageHandler struct {
|
||||
logger *log.Logger
|
||||
imageModel models.ImageModel
|
||||
userModel models.UserModel
|
||||
}
|
||||
|
||||
type ImagesReturn struct {
|
||||
UserImages []models.UserImageWithImage `json:"userImages"`
|
||||
ProcessingImages []models.UserProcessingImage `json:"processingImages"`
|
||||
Lists []models.ListsWithImages `json:"lists"`
|
||||
}
|
||||
|
||||
func (h *ImageHandler) serveImage(w http.ResponseWriter, r *http.Request) {
|
||||
imageId, err := middleware.GetPathParamID(h.logger, "id", w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
image, err := h.imageModel.Get(r.Context(), imageId)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
processingImages, err := h.imageModel.GetProcessing(r.Context(), userId)
|
||||
if err != nil {
|
||||
middleware.WriteErrorInternal(h.logger, "could not get processing images", w)
|
||||
return
|
||||
}
|
||||
|
||||
listsWithImages, 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,
|
||||
ProcessingImages: processingImages,
|
||||
Lists: listsWithImages,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
userImage, err := h.imageModel.Process(r.Context(), userId, model.Image{
|
||||
Image: image,
|
||||
ImageName: imageName,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
middleware.WriteErrorInternal(h.logger, "could not save image to DB", w)
|
||||
return
|
||||
}
|
||||
|
||||
middleware.WriteJsonOrError(h.logger, userImage, w)
|
||||
}
|
||||
|
||||
func (h *ImageHandler) CreateRoutes(r chi.Router) {
|
||||
h.logger.Info("Mounting image router")
|
||||
|
||||
// Public route for serving images (not protected)
|
||||
r.Get("/{id}", h.serveImage)
|
||||
|
||||
// Protected routes
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.ProtectedRoute)
|
||||
r.Use(middleware.SetJson)
|
||||
|
||||
r.Get("/", h.listImages)
|
||||
r.Post("/{name}", h.uploadImage)
|
||||
})
|
||||
}
|
||||
|
||||
func CreateImageHandler(db *sql.DB) ImageHandler {
|
||||
imageModel := models.NewImageModel(db)
|
||||
userModel := models.NewUserModel(db)
|
||||
logger := log.New(os.Stdout).WithPrefix("Images")
|
||||
|
||||
return ImageHandler{
|
||||
logger: logger,
|
||||
imageModel: imageModel,
|
||||
userModel: userModel,
|
||||
}
|
||||
}
|
796
backend/integration_test.go
Normal file
796
backend/integration_test.go
Normal file
@ -0,0 +1,796 @@
|
||||
// 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()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
router := setupRouter(db)
|
||||
server := httptest.NewServer(router)
|
||||
|
||||
tc.db = db
|
||||
tc.router = router
|
||||
tc.server = server
|
||||
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 := middleware.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)
|
||||
}
|
420
backend/main.go
420
backend/main.go
@ -1,32 +1,14 @@
|
||||
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/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 {
|
||||
@ -38,396 +20,20 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
imageModel := models.NewImageModel(db)
|
||||
userModel := models.NewUserModel(db)
|
||||
router := setupRouter(db)
|
||||
|
||||
mail, err := CreateMailClient()
|
||||
port, exists := os.LookupEnv("PORT")
|
||||
if !exists {
|
||||
panic("no port can be found")
|
||||
}
|
||||
|
||||
portWithColon := fmt.Sprintf(":%s", port)
|
||||
|
||||
logger := createLogger("Main", os.Stdout)
|
||||
|
||||
logger.Info("Serving router", "port", portWithColon)
|
||||
err = http.ListenAndServe(portWithColon, router)
|
||||
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
|
||||
}
|
||||
|
||||
listsWithImages, err := userModel.ListWithImages(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
|
||||
Lists []models.ListsWithImages
|
||||
}
|
||||
|
||||
imagesReturn := ImagesReturn{
|
||||
UserImages: images,
|
||||
ImageProperties: models.GetTypedImageProperties(imageProperties),
|
||||
ProcessingImages: processingImages,
|
||||
Lists: listsWithImages,
|
||||
}
|
||||
|
||||
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)
|
||||
switch contentType {
|
||||
case "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()
|
||||
case "application/oclet-stream", "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
|
||||
default:
|
||||
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,
|
||||
Description: "",
|
||||
})
|
||||
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"`
|
||||
}
|
||||
|
||||
r.Post("/code", func(w http.ResponseWriter, r *http.Request) {
|
||||
type CodeBody struct {
|
||||
Email string `json:"email"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
116
backend/middleware/middleware.go
Normal file
116
backend/middleware/middleware.go
Normal file
@ -0,0 +1,116 @@
|
||||
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 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)
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
48
backend/middleware/util.go
Normal file
48
backend/middleware/util.go
Normal file
@ -0,0 +1,48 @@
|
||||
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
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
@ -38,7 +38,7 @@ type UserProcessingImage struct {
|
||||
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{}, fmt.Errorf("Failed to begin transaction", err)
|
||||
return model.UserImagesToProcess{}, fmt.Errorf("Failed to begin transaction: %w", err)
|
||||
}
|
||||
|
||||
insertImageStmt := Image.
|
||||
@ -49,7 +49,7 @@ func (m ImageModel) Process(ctx context.Context, userId uuid.UUID, image model.I
|
||||
insertedImage := model.Image{}
|
||||
err = insertImageStmt.QueryContext(ctx, tx, &insertedImage)
|
||||
if err != nil {
|
||||
return model.UserImagesToProcess{}, fmt.Errorf("Could not insert/query new image. SQL %s.", insertImageStmt.DebugSql(), err)
|
||||
return model.UserImagesToProcess{}, fmt.Errorf("Could not insert/query new image. SQL %s: %w", insertImageStmt.DebugSql(), err)
|
||||
}
|
||||
|
||||
stmt := UserImagesToProcess.
|
||||
@ -60,7 +60,7 @@ func (m ImageModel) Process(ctx context.Context, userId uuid.UUID, image model.I
|
||||
userImage := model.UserImagesToProcess{}
|
||||
err = stmt.QueryContext(ctx, tx, &userImage)
|
||||
if err != nil {
|
||||
return model.UserImagesToProcess{}, fmt.Errorf("Could not insert user_image", err)
|
||||
return model.UserImagesToProcess{}, fmt.Errorf("Could not insert user_image: %w", err)
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
|
@ -3,10 +3,12 @@ package models
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
"fmt"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@ -14,36 +16,295 @@ type ListModel struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
func (m ListModel) Save(ctx context.Context, userId uuid.UUID, name string, description string) (model.Lists, error) {
|
||||
type ListWithItems struct {
|
||||
model.Lists
|
||||
|
||||
Schema struct {
|
||||
model.Schemas
|
||||
|
||||
SchemaItems []model.SchemaItems
|
||||
}
|
||||
}
|
||||
|
||||
type ImageWithSchema struct {
|
||||
model.ImageLists
|
||||
|
||||
Items []model.ImageSchemaItems
|
||||
}
|
||||
|
||||
type IDValue struct {
|
||||
ID string `json:"id"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SELECT for lists
|
||||
// ========================================
|
||||
|
||||
func (m ListModel) List(ctx context.Context, userId uuid.UUID) ([]ListWithItems, error) {
|
||||
getListsWithItems := SELECT(
|
||||
Lists.AllColumns,
|
||||
Schemas.AllColumns,
|
||||
SchemaItems.AllColumns,
|
||||
).
|
||||
FROM(
|
||||
Lists.
|
||||
INNER_JOIN(Schemas, Schemas.ListID.EQ(Lists.ID)).
|
||||
INNER_JOIN(SchemaItems, SchemaItems.SchemaID.EQ(Schemas.ID)),
|
||||
).
|
||||
WHERE(Lists.UserID.EQ(UUID(userId)))
|
||||
|
||||
lists := []ListWithItems{}
|
||||
err := getListsWithItems.QueryContext(ctx, m.dbPool, &lists)
|
||||
|
||||
return lists, err
|
||||
}
|
||||
|
||||
func (m ListModel) ListItems(ctx context.Context, listID uuid.UUID) ([]ImageWithSchema, error) {
|
||||
getListItems := SELECT(
|
||||
ImageLists.AllColumns,
|
||||
ImageSchemaItems.AllColumns,
|
||||
).
|
||||
FROM(
|
||||
ImageLists.
|
||||
INNER_JOIN(ImageSchemaItems, ImageSchemaItems.ImageID.EQ(ImageLists.ImageID)),
|
||||
).
|
||||
WHERE(ImageLists.ListID.EQ(UUID(listID)))
|
||||
|
||||
listItems := make([]ImageWithSchema, 0)
|
||||
err := getListItems.QueryContext(ctx, m.dbPool, &listItems)
|
||||
|
||||
return listItems, err
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SELECT for specific items
|
||||
// ========================================
|
||||
|
||||
func (m ListModel) GetProcessing(ctx context.Context, processingListID uuid.UUID) (model.ProcessingLists, error) {
|
||||
getProcessingListStmt := ProcessingLists.
|
||||
SELECT(ProcessingLists.AllColumns).
|
||||
WHERE(ProcessingLists.ID.EQ(UUID(processingListID)))
|
||||
|
||||
list := model.ProcessingLists{}
|
||||
err := getProcessingListStmt.QueryContext(ctx, m.dbPool, &list)
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (m ListModel) GetToProcess(ctx context.Context, listID uuid.UUID) (model.ProcessingLists, error) {
|
||||
getToProcessStmt := ProcessingLists.
|
||||
SELECT(ProcessingLists.AllColumns).
|
||||
WHERE(ProcessingLists.ID.EQ(UUID(listID)))
|
||||
|
||||
stack := []model.ProcessingLists{}
|
||||
err := getToProcessStmt.QueryContext(ctx, m.dbPool, &stack)
|
||||
|
||||
if len(stack) != 1 {
|
||||
return model.ProcessingLists{}, fmt.Errorf("Expected 1, got %d\n", len(stack))
|
||||
}
|
||||
|
||||
return stack[0], err
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// UPDATE
|
||||
// ========================================
|
||||
|
||||
func (m ListModel) StartProcessing(ctx context.Context, processingListID uuid.UUID) error {
|
||||
startProcessingStmt := ProcessingLists.
|
||||
UPDATE(ProcessingLists.Status).
|
||||
SET(model.Progress_InProgress).
|
||||
WHERE(ProcessingLists.ID.EQ(UUID(processingListID)))
|
||||
|
||||
_, err := startProcessingStmt.ExecContext(ctx, m.dbPool)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m ListModel) EndProcessing(ctx context.Context, processingListID uuid.UUID) error {
|
||||
startProcessingStmt := ProcessingLists.
|
||||
UPDATE(ProcessingLists.Status).
|
||||
SET(model.Progress_Complete).
|
||||
WHERE(ProcessingLists.ID.EQ(UUID(processingListID)))
|
||||
|
||||
_, err := startProcessingStmt.ExecContext(ctx, m.dbPool)
|
||||
return err
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// INSERT methods
|
||||
// ========================================
|
||||
|
||||
func (m ListModel) Save(ctx context.Context, userId uuid.UUID, name string, description string, schemaItems []model.SchemaItems) (ListWithItems, error) {
|
||||
tx, err := m.dbPool.BeginTx(ctx, nil)
|
||||
|
||||
stmt := Lists.INSERT(Lists.UserID, Lists.Name, Lists.Description).
|
||||
VALUES(userId, name, description).
|
||||
RETURNING(Lists.ID, Lists.Name, Lists.Description)
|
||||
|
||||
newList := model.Lists{}
|
||||
err := stmt.QueryContext(ctx, m.dbPool, &newList)
|
||||
err = stmt.QueryContext(ctx, tx, &newList)
|
||||
|
||||
return newList, err
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return ListWithItems{}, fmt.Errorf("Could not save new list. %s", err)
|
||||
}
|
||||
|
||||
insertSchemaStmt := Schemas.INSERT(Schemas.ListID).
|
||||
VALUES(newList.ID).
|
||||
RETURNING(Schemas.ID)
|
||||
|
||||
newSchema := model.Schemas{}
|
||||
err = insertSchemaStmt.QueryContext(ctx, tx, &newSchema)
|
||||
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return ListWithItems{}, fmt.Errorf("Could not save new schema. %s", err)
|
||||
}
|
||||
|
||||
// This is very interesting...
|
||||
for i := range schemaItems {
|
||||
schemaItems[i].SchemaID = newSchema.ID
|
||||
}
|
||||
|
||||
insertSchemaItemsStmt := SchemaItems.INSERT(SchemaItems.Item, SchemaItems.Value, SchemaItems.Description, SchemaItems.SchemaID).
|
||||
MODELS(schemaItems)
|
||||
_, err = insertSchemaItemsStmt.ExecContext(ctx, tx)
|
||||
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return ListWithItems{}, fmt.Errorf("Could not save schema items. %s", err)
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return ListWithItems{}, fmt.Errorf("Could not commit transaction. %s", err)
|
||||
}
|
||||
|
||||
getListAndItems := SELECT(Lists.AllColumns, Schemas.AllColumns, SchemaItems.AllColumns).
|
||||
FROM(
|
||||
Lists.
|
||||
INNER_JOIN(Schemas, Schemas.ListID.EQ(Lists.ID)).
|
||||
INNER_JOIN(SchemaItems, SchemaItems.SchemaID.EQ(Schemas.ID)),
|
||||
).
|
||||
WHERE(Lists.ID.EQ(UUID(newList.ID)))
|
||||
|
||||
listWithItems := ListWithItems{}
|
||||
err = getListAndItems.QueryContext(ctx, m.dbPool, &listWithItems)
|
||||
|
||||
return listWithItems, err
|
||||
}
|
||||
|
||||
func (m ListModel) List(ctx context.Context, userId uuid.UUID) ([]model.Lists, error) {
|
||||
stmt := Lists.SELECT(Lists.AllColumns).
|
||||
WHERE(Lists.UserID.EQ(UUID(userId)))
|
||||
func (m ListModel) SaveInto(ctx context.Context, listId uuid.UUID, imageId uuid.UUID, schemaValues []IDValue) error {
|
||||
imageSchemaItems := make([]model.ImageSchemaItems, len(schemaValues))
|
||||
|
||||
lists := []model.Lists{}
|
||||
err := stmt.QueryContext(ctx, m.dbPool, &lists)
|
||||
for i, v := range schemaValues {
|
||||
parsedId, err := uuid.Parse(v.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return lists, err
|
||||
}
|
||||
imageSchemaItems[i].SchemaItemID = parsedId
|
||||
imageSchemaItems[i].ImageID = imageId
|
||||
imageSchemaItems[i].Value = &v.Value
|
||||
}
|
||||
|
||||
tx, err := m.dbPool.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func (m ListModel) SaveInto(ctx context.Context, listId uuid.UUID, imageId uuid.UUID) error {
|
||||
stmt := ImageLists.INSERT(ImageLists.ListID, ImageLists.ImageID).
|
||||
VALUES(listId, imageId)
|
||||
|
||||
_, err := stmt.ExecContext(ctx, m.dbPool)
|
||||
_, err = stmt.ExecContext(ctx, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("Could not insert new list. %s", err)
|
||||
}
|
||||
|
||||
insertSchemaItemsStmt := ImageSchemaItems.
|
||||
INSERT(ImageSchemaItems.Value, ImageSchemaItems.SchemaItemID, ImageSchemaItems.ImageID).
|
||||
MODELS(imageSchemaItems)
|
||||
|
||||
_, err = insertSchemaItemsStmt.ExecContext(ctx, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("Could not insert schema items. %s", err)
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
return err
|
||||
}
|
||||
|
||||
func (m ListModel) SaveProcessing(ctx context.Context, userID uuid.UUID, title string, fields string) error {
|
||||
insertListToProcess := ProcessingLists.
|
||||
INSERT(ProcessingLists.UserID, ProcessingLists.Title, ProcessingLists.Fields).
|
||||
VALUES(userID, title, fields)
|
||||
|
||||
_, err := insertListToProcess.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// DELETE methods
|
||||
// ========================================
|
||||
|
||||
func (m ListModel) Delete(ctx context.Context, listID uuid.UUID, userID uuid.UUID) error {
|
||||
// First verify the list belongs to the user
|
||||
checkOwnershipStmt := Lists.
|
||||
SELECT(Lists.ID).
|
||||
WHERE(Lists.ID.EQ(UUID(listID)).AND(Lists.UserID.EQ(UUID(userID))))
|
||||
|
||||
var existingList model.Lists
|
||||
err := checkOwnershipStmt.QueryContext(ctx, m.dbPool, &existingList)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not verify list ownership: %w", err)
|
||||
}
|
||||
|
||||
// Start a transaction to ensure all deletions happen atomically
|
||||
tx, err := m.dbPool.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not start transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Delete in reverse order of dependencies:
|
||||
// 1. Delete schema items first
|
||||
deleteSchemaItemsStmt := SchemaItems.DELETE().
|
||||
WHERE(SchemaItems.SchemaID.IN(
|
||||
Schemas.SELECT(Schemas.ID).
|
||||
WHERE(Schemas.ListID.EQ(UUID(listID))),
|
||||
))
|
||||
_, err = deleteSchemaItemsStmt.ExecContext(ctx, tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not delete schema items: %w", err)
|
||||
}
|
||||
|
||||
// 2. Delete schemas
|
||||
deleteSchemasStmt := Schemas.DELETE().WHERE(Schemas.ListID.EQ(UUID(listID)))
|
||||
_, err = deleteSchemasStmt.ExecContext(ctx, tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not delete schemas: %w", err)
|
||||
}
|
||||
|
||||
// 3. Delete the list itself
|
||||
deleteListStmt := Lists.DELETE().WHERE(Lists.ID.EQ(UUID(listID)))
|
||||
_, err = deleteListStmt.ExecContext(ctx, tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not delete list: %w", err)
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not commit transaction: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewListModel(db *sql.DB) ListModel {
|
||||
return ListModel{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}
|
||||
}
|
@ -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,207 +19,6 @@ type ImageWithProperties struct {
|
||||
ID uuid.UUID
|
||||
|
||||
Image model.Image
|
||||
|
||||
Locations []model.Locations
|
||||
Events []model.Events
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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 _, 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),
|
||||
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 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 _, 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,
|
||||
ImageLocations.AllColumns,
|
||||
Locations.AllColumns,
|
||||
ImageEvents.AllColumns,
|
||||
Events.AllColumns,
|
||||
ImageContacts.AllColumns,
|
||||
Contacts.AllColumns,
|
||||
).
|
||||
FROM(
|
||||
UserImages.INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID)).
|
||||
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)))
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -254,7 +51,10 @@ func (m UserModel) Save(ctx context.Context, user model.Users) (model.Users, err
|
||||
type UserImageWithImage struct {
|
||||
model.UserImages
|
||||
|
||||
Image model.Image
|
||||
Image struct {
|
||||
model.Image
|
||||
ImageLists []model.ImageLists
|
||||
}
|
||||
}
|
||||
|
||||
func (m UserModel) GetUserImages(ctx context.Context, userId uuid.UUID) ([]UserImageWithImage, error) {
|
||||
@ -263,8 +63,13 @@ func (m UserModel) GetUserImages(ctx context.Context, userId uuid.UUID) ([]UserI
|
||||
Image.ID,
|
||||
Image.ImageName,
|
||||
Image.Description,
|
||||
ImageLists.AllColumns,
|
||||
).
|
||||
FROM(UserImages.INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID))).
|
||||
FROM(
|
||||
UserImages.
|
||||
INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID)).
|
||||
INNER_JOIN(ImageLists, ImageLists.ImageID.EQ(UserImages.ImageID)),
|
||||
).
|
||||
WHERE(UserImages.UserID.EQ(UUID(userId)))
|
||||
|
||||
userImages := []UserImageWithImage{}
|
||||
@ -276,14 +81,33 @@ func (m UserModel) GetUserImages(ctx context.Context, userId uuid.UUID) ([]UserI
|
||||
type ListsWithImages struct {
|
||||
model.Lists
|
||||
|
||||
Images []model.ImageLists
|
||||
Schema struct {
|
||||
model.Schemas
|
||||
|
||||
SchemaItems []model.SchemaItems
|
||||
}
|
||||
|
||||
Images []struct {
|
||||
model.ImageLists
|
||||
|
||||
Items []model.ImageSchemaItems
|
||||
}
|
||||
}
|
||||
|
||||
func (m UserModel) ListWithImages(ctx context.Context, userId uuid.UUID) ([]ListsWithImages, error) {
|
||||
stmt := SELECT(Lists.AllColumns, ImageLists.AllColumns).
|
||||
stmt := SELECT(
|
||||
Lists.AllColumns,
|
||||
ImageLists.AllColumns,
|
||||
Schemas.AllColumns,
|
||||
SchemaItems.AllColumns,
|
||||
ImageSchemaItems.AllColumns,
|
||||
).
|
||||
FROM(
|
||||
Lists.
|
||||
INNER_JOIN(ImageLists, ImageLists.ListID.EQ(Lists.ID)),
|
||||
INNER_JOIN(Schemas, Schemas.ListID.EQ(Lists.ID)).
|
||||
INNER_JOIN(SchemaItems, SchemaItems.SchemaID.EQ(Schemas.ID)).
|
||||
LEFT_JOIN(ImageLists, ImageLists.ListID.EQ(Lists.ID)).
|
||||
LEFT_JOIN(ImageSchemaItems, ImageSchemaItems.ImageID.EQ(ImageLists.ImageID)),
|
||||
).
|
||||
WHERE(Lists.UserID.EQ(UUID(userId)))
|
||||
|
||||
|
71
backend/router.go
Normal file
71
backend/router.go
Normal file
@ -0,0 +1,71 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/auth"
|
||||
"screenmark/screenmark/images"
|
||||
"screenmark/screenmark/models"
|
||||
"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) chi.Router {
|
||||
imageModel := models.NewImageModel(db)
|
||||
stackModel := models.NewListModel(db)
|
||||
|
||||
stackHandler := stacks.CreateStackHandler(db)
|
||||
authHandler := auth.CreateAuthHandler(db)
|
||||
imageHandler := images.CreateImageHandler(db)
|
||||
|
||||
notifier := NewNotifier[Notification](10)
|
||||
|
||||
// Only start event listeners if not in test environment
|
||||
if os.Getenv("GO_TEST_ENVIRONMENT") != "true" {
|
||||
|
||||
// TODO: should extract these into a notification manager
|
||||
// And actually make them the same code.
|
||||
// The events are basically the same.
|
||||
|
||||
go ListenNewImageEvents(db)
|
||||
go ListenProcessingImageStatus(db, imageModel, ¬ifier)
|
||||
go ListenNewStackEvents(db)
|
||||
go ListenProcessingStackStatus(db, stackModel, ¬ifier)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
r.Get("/", CreateEventsHandler(¬ifier))
|
||||
})
|
||||
|
||||
logWriter := DatabaseWriter{
|
||||
dbPool: db,
|
||||
}
|
||||
|
||||
r.Route("/logs", createLogHandler(&logWriter))
|
||||
|
||||
return r
|
||||
}
|
@ -35,92 +35,6 @@ CREATE TABLE haystack.user_images (
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
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),
|
||||
user_id UUID NOT NULL REFERENCES haystack.users (id),
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
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,
|
||||
|
||||
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),
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
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),
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.events (
|
||||
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,
|
||||
|
||||
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()
|
||||
);
|
||||
|
||||
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.logs (
|
||||
log TEXT NOT NULL,
|
||||
image_id UUID NOT NULL REFERENCES haystack.image (id),
|
||||
@ -138,6 +52,18 @@ CREATE TABLE haystack.lists (
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.processing_lists (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES haystack.users (id),
|
||||
|
||||
title TEXT NOT NULL,
|
||||
fields TEXT NOT NULL,
|
||||
|
||||
status haystack.progress NOT NULL DEFAULT 'not-started',
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.image_lists (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
@ -145,6 +71,31 @@ CREATE TABLE haystack.image_lists (
|
||||
list_id UUID NOT NULL REFERENCES haystack.lists (id)
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.schemas (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
list_id UUID NOT NULL REFERENCES haystack.lists (id)
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.schema_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
item TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
|
||||
schema_id UUID NOT NULL REFERENCES haystack.schemas (id)
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.image_schema_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
value TEXT,
|
||||
|
||||
schema_item_id UUID NOT NULL REFERENCES haystack.schema_items (id),
|
||||
image_id UUID NOT NULL REFERENCES haystack.image (id)
|
||||
);
|
||||
|
||||
/* -----| Indexes |----- */
|
||||
|
||||
/* -----| Stored Procedures |----- */
|
||||
@ -165,6 +116,22 @@ PERFORM pg_notify('new_processing_image_status', NEW.id::text || NEW.status::tex
|
||||
END
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION notify_new_stacks()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify('new_stack', NEW.id::text);
|
||||
RETURN NEW;
|
||||
END
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION notify_new_processing_stack_status()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify('new_processing_stack_status', NEW.id::text || NEW.status::text);
|
||||
RETURN NEW;
|
||||
END
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
/* -----| Triggers |----- */
|
||||
|
||||
CREATE OR REPLACE TRIGGER on_new_image AFTER INSERT
|
||||
@ -178,4 +145,15 @@ ON haystack.user_images_to_process
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE notify_new_processing_image_status();
|
||||
|
||||
CREATE OR REPLACE TRIGGER on_new_image AFTER INSERT
|
||||
ON haystack.processing_lists
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE notify_new_stacks();
|
||||
|
||||
CREATE OR REPLACE TRIGGER on_update_stack_progress
|
||||
AFTER UPDATE OF status
|
||||
ON haystack.processing_lists
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE notify_new_processing_stack_status();
|
||||
|
||||
/* -----| Test Data |----- */
|
||||
|
161
backend/stacks/handler.go
Normal file
161
backend/stacks/handler.go
Normal file
@ -0,0 +1,161 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/middleware"
|
||||
"screenmark/screenmark/models"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type StackHandler struct {
|
||||
logger *log.Logger
|
||||
stackModel models.ListModel
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
lists, 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, lists, 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
|
||||
}
|
||||
|
||||
listID, err := middleware.GetPathParamID(h.logger, "listID", w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: must check for permission here.
|
||||
|
||||
lists, err := h.stackModel.ListItems(ctx, listID)
|
||||
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
|
||||
}
|
||||
|
||||
listID, err := middleware.GetPathParamID(h.logger, "listID", w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = h.stackModel.Delete(ctx, listID, userID)
|
||||
if err != nil {
|
||||
h.logger.Warn("could not delete stack", "err", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
type CreateStackBody struct {
|
||||
Title string `json:"title"`
|
||||
|
||||
// We want a regular string because AI will take care of creating these for us.
|
||||
Fields string `json:"fields"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Convert fields string to basic schema items
|
||||
// For now, create a simple schema item for each field
|
||||
var schemaItems []SchemaItems
|
||||
if body.Fields != "" {
|
||||
fields := strings.Split(body.Fields, ",")
|
||||
for i, field := range fields {
|
||||
field = strings.TrimSpace(field)
|
||||
if field != "" {
|
||||
schemaItems = append(schemaItems, SchemaItems{
|
||||
Item: field,
|
||||
Value: "",
|
||||
Description: fmt.Sprintf("Field %d: %s", i+1, field),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use empty description for now since the API doesn't provide one
|
||||
_, err = h.stackModel.Save(ctx, userID, body.Title, "", schemaItems)
|
||||
if err != nil {
|
||||
h.logger.Warn("could not save stack", "err", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *StackHandler) CreateRoutes(r chi.Router) {
|
||||
h.logger.Info("Mounting stack router")
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.ProtectedRoute)
|
||||
r.Use(middleware.SetJson)
|
||||
|
||||
r.Get("/", h.getAllStacks)
|
||||
r.Get("/{listID}", h.getStackItems)
|
||||
|
||||
r.Post("/", middleware.WithValidatedPost(h.createStack))
|
||||
r.Patch("/{listID}", middleware.WithValidatedPost(h.editStack))
|
||||
r.Delete("/{listID}", h.deleteStack)
|
||||
})
|
||||
}
|
||||
|
||||
func CreateStackHandler(db *sql.DB) StackHandler {
|
||||
stackModel := models.NewListModel(db)
|
||||
logger := log.New(os.Stdout).WithPrefix("Stacks")
|
||||
|
||||
return StackHandler{
|
||||
logger,
|
||||
stackModel,
|
||||
}
|
||||
}
|
@ -2,13 +2,12 @@ import { Navigate, Route, Router } from "@solidjs/router";
|
||||
import { onAndroidMount } from "./mobile";
|
||||
import {
|
||||
FrontPage,
|
||||
Gallery,
|
||||
ImagePage,
|
||||
Login,
|
||||
Settings,
|
||||
Entity,
|
||||
SearchPage,
|
||||
AllImages,
|
||||
List,
|
||||
} from "./pages";
|
||||
import { SearchImageContextProvider } from "@contexts/SearchImageContext";
|
||||
import { WithNotifications } from "@contexts/Notifications";
|
||||
@ -34,8 +33,7 @@ export const App = () => {
|
||||
<Route path="/search" component={SearchPage} />
|
||||
<Route path="/all-images" component={AllImages} />
|
||||
<Route path="/image/:imageId" component={ImagePage} />
|
||||
<Route path="/entity/:entityId" component={Entity} />
|
||||
<Route path="/gallery/:entity" component={Gallery} />
|
||||
<Route path="/list/:listId" component={List} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
@ -4,10 +4,21 @@ import { A } from "@solidjs/router";
|
||||
|
||||
export const ImageComponent: Component<{ ID: string }> = (props) => {
|
||||
return (
|
||||
<A href={`/image/${props.ID}`} class="w-full h-full flex justify-center">
|
||||
<A href={`/image/${props.ID}`} class="w-full flex justify-center h-[300px]">
|
||||
<img
|
||||
class="w-full object-contain rounded-xl max-w-[700px] max-h-[400px]"
|
||||
src={`${base}/image/${props.ID}`}
|
||||
class="flex w-full object-cover rounded-xl"
|
||||
src={`${base}/images/${props.ID}`}
|
||||
/>
|
||||
</A>
|
||||
);
|
||||
};
|
||||
|
||||
export const ImageComponentFullHeight: Component<{ ID: string }> = (props) => {
|
||||
return (
|
||||
<A href={`/image/${props.ID}`} class="w-full flex justify-center">
|
||||
<img
|
||||
class="flex w-full object-cover rounded-xl"
|
||||
src={`${base}/images/${props.ID}`}
|
||||
/>
|
||||
</A>
|
||||
);
|
||||
|
@ -1,82 +0,0 @@
|
||||
import type { UserImage } from "../../network";
|
||||
import { Show, type Component } from "solid-js";
|
||||
import SolidjsMarkdown from "solidjs-markdown";
|
||||
|
||||
type Props = {
|
||||
item: UserImage;
|
||||
};
|
||||
|
||||
const NullableParagraph: Component<{
|
||||
item: string | null;
|
||||
itemTitle: string;
|
||||
}> = (props) => {
|
||||
return (
|
||||
<Show when={props.item}>
|
||||
{(item) => (
|
||||
<>
|
||||
<p class="font-semibold text-xl">{props.itemTitle}</p>
|
||||
<p class="text-md">{item()}</p>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
const ConcreteItemModal: Component<Props> = (props) => {
|
||||
switch (props.item.type) {
|
||||
case "note":
|
||||
return (
|
||||
<SolidjsMarkdown>
|
||||
{props.item.data.Content.slice(
|
||||
"```markdown".length,
|
||||
props.item.data.Content.length - "```".length,
|
||||
)}
|
||||
</SolidjsMarkdown>
|
||||
);
|
||||
case "location":
|
||||
return (
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="font-semibold text-xl">Address</p>
|
||||
<p class="text-md">{props.item.data.Address}</p>
|
||||
</div>
|
||||
);
|
||||
case "event":
|
||||
return (
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="font-semibold text-xl">Event</p>
|
||||
<p class="text-md">{props.item.data.Name}</p>
|
||||
|
||||
<NullableParagraph
|
||||
itemTitle="Start Time"
|
||||
item={props.item.data.StartDateTime}
|
||||
/>
|
||||
<NullableParagraph
|
||||
itemTitle="End Time"
|
||||
item={props.item.data.EndDateTime}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case "contact":
|
||||
return (
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="font-semibold text-xl">Contact</p>
|
||||
<p class="text-md">{props.item.data.Name}</p>
|
||||
|
||||
<NullableParagraph itemTitle="Email" item={props.item.data.Email} />
|
||||
|
||||
<NullableParagraph
|
||||
itemTitle="Phone Number"
|
||||
item={props.item.data.PhoneNumber}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const ItemModal: Component<Props> = (props) => {
|
||||
return (
|
||||
<div class="rounded-2xl p-4 bg-white border border-neutral-300 flex flex-col gap-2 mb-2">
|
||||
<ConcreteItemModal item={props.item} />
|
||||
</div>
|
||||
);
|
||||
};
|
35
frontend/src/components/list-card/index.tsx
Normal file
35
frontend/src/components/list-card/index.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { List } from "@network/index";
|
||||
import { Component } from "solid-js";
|
||||
import fastHashCode from "../../utils/hash";
|
||||
import { A } from "@solidjs/router";
|
||||
|
||||
const colors = [
|
||||
"bg-emerald-50",
|
||||
"bg-lime-50",
|
||||
|
||||
"bg-indigo-50",
|
||||
"bg-sky-50",
|
||||
|
||||
"bg-amber-50",
|
||||
"bg-teal-50",
|
||||
|
||||
"bg-fuchsia-50",
|
||||
"bg-pink-50",
|
||||
];
|
||||
|
||||
export const ListCard: Component<{ list: List }> = (props) => {
|
||||
return (
|
||||
<A
|
||||
href={`/list/${props.list.ID}`}
|
||||
class={
|
||||
"flex flex-col p-4 border border-neutral-200 rounded-lg " +
|
||||
colors[
|
||||
fastHashCode(props.list.Name, { forcePositive: true }) % colors.length
|
||||
]
|
||||
}
|
||||
>
|
||||
<p class="text-xl font-bold">{props.list.Name}</p>
|
||||
<p class="text-lg">{props.list.Images.length}</p>
|
||||
</A>
|
||||
);
|
||||
};
|
@ -8,7 +8,8 @@ export const ProcessingImages: Component = () => {
|
||||
const notifications = useNotifications();
|
||||
|
||||
const processingNumber = () =>
|
||||
Object.keys(notifications.state.ProcessingImages).length;
|
||||
Object.keys(notifications.state.ProcessingImages).length +
|
||||
Object.keys(notifications.state.ProcessingLists).length;
|
||||
|
||||
return (
|
||||
<Popover sameWidth gutter={4}>
|
||||
@ -16,7 +17,7 @@ export const ProcessingImages: Component = () => {
|
||||
<Show when={processingNumber() > 0}>
|
||||
<p class="text-md">
|
||||
Processing {processingNumber()}{" "}
|
||||
{processingNumber() === 1 ? "image" : "images"}
|
||||
{processingNumber() === 1 ? "item" : "items"}
|
||||
...
|
||||
</p>
|
||||
</Show>
|
||||
@ -30,10 +31,8 @@ export const ProcessingImages: Component = () => {
|
||||
<Popover.Portal>
|
||||
<Popover.Content class="shadow-2xl flex flex-col gap-2 bg-white rounded-xl p-2">
|
||||
<Show
|
||||
when={
|
||||
Object.entries(notifications.state.ProcessingImages).length > 0
|
||||
}
|
||||
fallback={<p>No images to process</p>}
|
||||
when={processingNumber() > 0}
|
||||
fallback={<p>No items to process</p>}
|
||||
>
|
||||
<For each={Object.entries(notifications.state.ProcessingImages)}>
|
||||
{([id, _image]) => (
|
||||
@ -43,7 +42,7 @@ export const ProcessingImages: Component = () => {
|
||||
<img
|
||||
class="w-16 h-16 aspect-square rounded"
|
||||
alt="processing"
|
||||
src={`${base}/image/${id}`}
|
||||
src={`${base}/images/${id}`}
|
||||
/>
|
||||
<div class="flex flex-col gap-1">
|
||||
<p class="text-slate-100">{image().ImageName}</p>
|
||||
@ -57,6 +56,24 @@ export const ProcessingImages: Component = () => {
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<For each={Object.entries(notifications.state.ProcessingLists)}>
|
||||
{([, _list]) => (
|
||||
<Show when={_list}>
|
||||
{(list) => (
|
||||
<div class="flex gap-2 w-full justify-center">
|
||||
<div class="flex flex-col gap-1">
|
||||
<p class="text-slate-900">New Stack: {list().Name}</p>
|
||||
</div>
|
||||
<LoadingCircle
|
||||
status="loading"
|
||||
class="ml-auto self-center"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
|
@ -1,31 +0,0 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import type { UserImage } from "../../network";
|
||||
import { SearchCardContact } from "./SearchCardContact";
|
||||
import { SearchCardEvent } from "./SearchCardEvent";
|
||||
import { SearchCardLocation } from "./SearchCardLocation";
|
||||
|
||||
const UnwrappedSearchCard = (props: { item: UserImage }) => {
|
||||
const { item } = props;
|
||||
|
||||
switch (item.type) {
|
||||
case "location":
|
||||
return <SearchCardLocation item={item} />;
|
||||
case "event":
|
||||
return <SearchCardEvent item={item} />;
|
||||
case "contact":
|
||||
return <SearchCardContact item={item} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const SearchCard = (props: { item: UserImage }) => {
|
||||
return (
|
||||
<A
|
||||
href={`/entity/${props.item.data.ID}`}
|
||||
class="w-full h-[144px] border relative border-neutral-200 cursor-pointer overflow-hidden rounded-xl"
|
||||
>
|
||||
<UnwrappedSearchCard item={props.item} />
|
||||
</A>
|
||||
);
|
||||
};
|
@ -1,24 +0,0 @@
|
||||
import { IconUser } from "@tabler/icons-solidjs";
|
||||
import type { UserImage } from "../../network";
|
||||
|
||||
type Props = {
|
||||
item: Extract<UserImage, { type: "contact" }>;
|
||||
};
|
||||
|
||||
export const SearchCardContact = ({ item }: Props) => {
|
||||
const { data } = item;
|
||||
|
||||
return (
|
||||
<div class="h-full inset-0 p-3 bg-orange-50">
|
||||
<div class="flex mb-1 items-center gap-1">
|
||||
<IconUser size={14} class="text-neutral-500" />
|
||||
<p class="text-xs text-neutral-500">Contact</p>
|
||||
</div>
|
||||
<p class="text-sm text-neutral-900 font-bold mb-1">
|
||||
{data.Name.length > 0 ? data.Name : "Unknown 🐞"}
|
||||
</p>
|
||||
<p class="text-xs text-neutral-700">Phone: {data.PhoneNumber}</p>
|
||||
<p class="text-xs text-neutral-700">Mail: {data.Email}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,32 +0,0 @@
|
||||
import { IconCalendar } from "@tabler/icons-solidjs";
|
||||
import type { UserImage } from "../../network";
|
||||
|
||||
type Props = {
|
||||
item: Extract<UserImage, { type: "event" }>;
|
||||
};
|
||||
|
||||
export const SearchCardEvent = ({ item }: Props) => {
|
||||
const { data } = item;
|
||||
|
||||
return (
|
||||
<div class="h-full inset-0 p-3 bg-purple-50">
|
||||
<div class="flex mb-1 items-center gap-1">
|
||||
<IconCalendar size={14} class="text-neutral-500" />
|
||||
<p class="text-xs text-neutral-500">Event</p>
|
||||
</div>
|
||||
<p class="text-sm text-neutral-900 font-bold mb-1">
|
||||
{data.Name.length > 0 ? data.Name : "Unknown 🐞"}
|
||||
</p>
|
||||
<p class="text-xs text-neutral-700">
|
||||
On{" "}
|
||||
{data.StartDateTime
|
||||
? new Date(data.StartDateTime).toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})
|
||||
: "unknown date"}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,23 +0,0 @@
|
||||
import { IconMapPin } from "@tabler/icons-solidjs";
|
||||
import type { UserImage } from "../../network";
|
||||
|
||||
type Props = {
|
||||
item: Extract<UserImage, { type: "location" }>;
|
||||
};
|
||||
|
||||
export const SearchCardLocation = ({ item }: Props) => {
|
||||
const { data } = item;
|
||||
|
||||
return (
|
||||
<div class="h-full inset-0 p-3 bg-red-50">
|
||||
<div class="flex mb-1 items-center gap-1">
|
||||
<IconMapPin size={14} class="text-neutral-500" />
|
||||
<p class="text-xs text-neutral-500">Location</p>
|
||||
</div>
|
||||
<p class="text-sm text-neutral-900 font-bold mb-1">
|
||||
{data.Name.length > 0 ? data.Name : "Unknown 🐞"}
|
||||
</p>
|
||||
<p class="text-xs text-neutral-700">Address: {data.Address}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -10,18 +10,27 @@ import {
|
||||
useContext,
|
||||
} from "solid-js";
|
||||
import { base } from "@network/index";
|
||||
import { processingImagesValidator } from "@network/notifications";
|
||||
import {
|
||||
notificationValidator,
|
||||
processingImagesValidator,
|
||||
processingListValidator,
|
||||
} from "@network/notifications";
|
||||
|
||||
type NotificationState = {
|
||||
ProcessingImages: Record<
|
||||
string,
|
||||
InferOutput<typeof processingImagesValidator> | undefined
|
||||
>;
|
||||
ProcessingLists: Record<
|
||||
string,
|
||||
InferOutput<typeof processingListValidator> | undefined
|
||||
>;
|
||||
};
|
||||
|
||||
export const Notifications = (onCompleteImage: () => void) => {
|
||||
const [state, setState] = createStore<NotificationState>({
|
||||
ProcessingImages: {},
|
||||
ProcessingLists: {},
|
||||
});
|
||||
|
||||
const { processingImages } = useSearchImageContext();
|
||||
@ -45,21 +54,32 @@ export const Notifications = (onCompleteImage: () => void) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const processingImage = safeParse(processingImagesValidator, jsonData);
|
||||
if (!processingImage.success) {
|
||||
const notification = safeParse(notificationValidator, jsonData);
|
||||
if (!notification.success) {
|
||||
console.error("Processing image could not be parsed.", e.data);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("SSE: ", processingImage);
|
||||
console.log("SSE: ", notification);
|
||||
|
||||
const { ImageID, Status } = processingImage.output;
|
||||
if (notification.output.Type === "image") {
|
||||
const { ImageID, Status } = notification.output;
|
||||
|
||||
if (Status === "complete") {
|
||||
setState("ProcessingImages", ImageID, undefined);
|
||||
onCompleteImage();
|
||||
} else {
|
||||
setState("ProcessingImages", ImageID, processingImage.output);
|
||||
if (Status === "complete") {
|
||||
setState("ProcessingImages", ImageID, undefined);
|
||||
onCompleteImage();
|
||||
} else {
|
||||
setState("ProcessingImages", ImageID, notification.output);
|
||||
}
|
||||
} else if (notification.output.Type === "list") {
|
||||
const { ListID, Status } = notification.output;
|
||||
|
||||
if (Status === "complete") {
|
||||
setState("ProcessingLists", ListID, undefined);
|
||||
onCompleteImage();
|
||||
} else {
|
||||
setState("ProcessingLists", ListID, notification.output);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -83,6 +103,7 @@ export const Notifications = (onCompleteImage: () => void) => {
|
||||
images.map((i) => [
|
||||
i.ImageID,
|
||||
{
|
||||
Type: "image",
|
||||
ImageID: i.ImageID,
|
||||
ImageName: i.Image.ImageName,
|
||||
Status: i.Status,
|
||||
|
@ -3,44 +3,26 @@ import {
|
||||
type Component,
|
||||
type ParentProps,
|
||||
createContext,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createResource,
|
||||
useContext,
|
||||
} from "solid-js";
|
||||
import {
|
||||
CategoryUnion,
|
||||
getUserImages,
|
||||
JustTheImageWhatAreTheseNames,
|
||||
UserImage,
|
||||
} from "../network";
|
||||
import { groupPropertiesWithImage } from "../utils/groupPropertiesWithImage";
|
||||
|
||||
type TaggedCategory<T extends CategoryUnion["type"]> = Extract<
|
||||
CategoryUnion,
|
||||
{ type: T }
|
||||
>["data"];
|
||||
|
||||
type CategoriesSpecificData = {
|
||||
[K in CategoryUnion["type"]]: Array<TaggedCategory<K>>;
|
||||
};
|
||||
import { getUserImages, JustTheImageWhatAreTheseNames } from "../network";
|
||||
|
||||
export type SearchImageStore = {
|
||||
images: Accessor<UserImage[]>;
|
||||
imagesByDate: Accessor<
|
||||
Array<{ date: Date; images: JustTheImageWhatAreTheseNames }>
|
||||
>;
|
||||
|
||||
lists: Accessor<Awaited<ReturnType<typeof getUserImages>>["Lists"]>;
|
||||
lists: Accessor<Awaited<ReturnType<typeof getUserImages>>["lists"]>;
|
||||
|
||||
userImages: Accessor<JustTheImageWhatAreTheseNames>;
|
||||
|
||||
imagesWithProperties: Accessor<ReturnType<typeof groupPropertiesWithImage>>;
|
||||
processingImages: Accessor<
|
||||
Awaited<ReturnType<typeof getUserImages>>["ProcessingImages"] | undefined
|
||||
Awaited<ReturnType<typeof getUserImages>>["processingImages"] | undefined
|
||||
>;
|
||||
|
||||
categories: Accessor<CategoriesSpecificData>;
|
||||
|
||||
onRefetchImages: () => void;
|
||||
};
|
||||
|
||||
@ -48,13 +30,8 @@ const SearchImageContext = createContext<SearchImageStore>();
|
||||
export const SearchImageContextProvider: Component<ParentProps> = (props) => {
|
||||
const [data, { refetch }] = createResource(getUserImages);
|
||||
|
||||
const imageData = createMemo(() => {
|
||||
const d = data();
|
||||
if (d == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return d.ImageProperties;
|
||||
createEffect(() => {
|
||||
console.log(data());
|
||||
});
|
||||
|
||||
const sortedImages = createMemo<ReturnType<SearchImageStore["imagesByDate"]>>(
|
||||
@ -67,7 +44,7 @@ export const SearchImageContextProvider: Component<ParentProps> = (props) => {
|
||||
// Sorted by day. But we could potentially add more in the future.
|
||||
const buckets: Record<string, JustTheImageWhatAreTheseNames> = {};
|
||||
|
||||
for (const image of d.UserImages) {
|
||||
for (const image of d.userImages) {
|
||||
if (image.CreatedAt == null) {
|
||||
continue;
|
||||
}
|
||||
@ -86,44 +63,16 @@ export const SearchImageContextProvider: Component<ParentProps> = (props) => {
|
||||
},
|
||||
);
|
||||
|
||||
const processingImages = () => data()?.ProcessingImages ?? [];
|
||||
|
||||
const imagesWithProperties = createMemo<
|
||||
ReturnType<typeof groupPropertiesWithImage>
|
||||
>(() => {
|
||||
const d = data();
|
||||
if (d == null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return groupPropertiesWithImage(d);
|
||||
});
|
||||
|
||||
const categories = createMemo(() => {
|
||||
const c: ReturnType<SearchImageStore["categories"]> = {
|
||||
contact: [],
|
||||
event: [],
|
||||
location: [],
|
||||
};
|
||||
|
||||
for (const category of data()?.ImageProperties ?? []) {
|
||||
c[category.type].push(category.data as any);
|
||||
}
|
||||
|
||||
return c;
|
||||
});
|
||||
const processingImages = () => data()?.processingImages ?? [];
|
||||
|
||||
return (
|
||||
<SearchImageContext.Provider
|
||||
value={{
|
||||
images: imageData,
|
||||
imagesByDate: sortedImages,
|
||||
lists: () => data()?.Lists ?? [],
|
||||
imagesWithProperties: imagesWithProperties,
|
||||
userImages: () => data()?.UserImages ?? [],
|
||||
lists: () => data()?.lists ?? [],
|
||||
userImages: () => data()?.userImages ?? [],
|
||||
processingImages,
|
||||
onRefetchImages: refetch,
|
||||
categories,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
|
@ -10,9 +10,9 @@ import {
|
||||
pipe,
|
||||
strictObject,
|
||||
string,
|
||||
transform,
|
||||
union,
|
||||
uuid,
|
||||
variant,
|
||||
} from "valibot";
|
||||
|
||||
type BaseRequestParams = Partial<{
|
||||
@ -56,7 +56,7 @@ export const sendImageFile = async (
|
||||
file: File,
|
||||
): Promise<InferOutput<typeof sendImageResponseValidator>> => {
|
||||
const request = getBaseAuthorizedRequest({
|
||||
path: `image/${imageName}`,
|
||||
path: `images/${imageName}`,
|
||||
body: file,
|
||||
method: "POST",
|
||||
});
|
||||
@ -73,7 +73,7 @@ export const sendImage = async (
|
||||
base64Image: string,
|
||||
): Promise<InferOutput<typeof sendImageResponseValidator>> => {
|
||||
const request = getBaseAuthorizedRequest({
|
||||
path: `image/${imageName}`,
|
||||
path: `images/${imageName}`,
|
||||
body: base64Image,
|
||||
method: "POST",
|
||||
});
|
||||
@ -85,62 +85,6 @@ export const sendImage = async (
|
||||
return parse(sendImageResponseValidator, res);
|
||||
};
|
||||
|
||||
const locationValidator = strictObject({
|
||||
ID: pipe(string(), uuid()),
|
||||
CreatedAt: pipe(string()),
|
||||
Name: string(),
|
||||
Address: nullable(string()),
|
||||
Description: nullable(string()),
|
||||
Images: array(pipe(string(), uuid())),
|
||||
});
|
||||
|
||||
const contactValidator = strictObject({
|
||||
ID: pipe(string(), uuid()),
|
||||
CreatedAt: pipe(string()),
|
||||
Name: string(),
|
||||
Description: nullable(string()),
|
||||
PhoneNumber: nullable(string()),
|
||||
Email: nullable(string()),
|
||||
Images: array(pipe(string(), uuid())),
|
||||
});
|
||||
|
||||
const eventValidator = strictObject({
|
||||
ID: pipe(string(), uuid()),
|
||||
CreatedAt: nullable(pipe(string())),
|
||||
Name: string(),
|
||||
StartDateTime: nullable(pipe(string())),
|
||||
EndDateTime: nullable(pipe(string())),
|
||||
Description: nullable(string()),
|
||||
LocationID: nullable(pipe(string(), uuid())),
|
||||
// Location: nullable(locationValidator),
|
||||
OrganizerID: nullable(pipe(string(), uuid())),
|
||||
// Organizer: nullable(contactValidator),
|
||||
Images: array(pipe(string(), uuid())),
|
||||
});
|
||||
|
||||
const locationDataType = strictObject({
|
||||
type: literal("location"),
|
||||
data: locationValidator,
|
||||
});
|
||||
|
||||
const eventDataType = strictObject({
|
||||
type: literal("event"),
|
||||
data: eventValidator,
|
||||
});
|
||||
|
||||
const contactDataType = strictObject({
|
||||
type: literal("contact"),
|
||||
data: contactValidator,
|
||||
});
|
||||
|
||||
const dataTypeValidator = variant("type", [
|
||||
locationDataType,
|
||||
eventDataType,
|
||||
contactDataType,
|
||||
]);
|
||||
|
||||
export type CategoryUnion = InferOutput<typeof dataTypeValidator>;
|
||||
|
||||
const imageMetaValidator = strictObject({
|
||||
ID: pipe(string(), uuid()),
|
||||
ImageName: string(),
|
||||
@ -153,7 +97,16 @@ const userImageValidator = strictObject({
|
||||
CreatedAt: pipe(string()),
|
||||
ImageID: pipe(string(), uuid()),
|
||||
UserID: pipe(string(), uuid()),
|
||||
Image: imageMetaValidator,
|
||||
Image: strictObject({
|
||||
...imageMetaValidator.entries,
|
||||
ImageLists: array(
|
||||
strictObject({
|
||||
ID: pipe(string(), uuid()),
|
||||
ImageID: pipe(string(), uuid()),
|
||||
ListID: pipe(string(), uuid()),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
const userProcessingImageValidator = strictObject({
|
||||
@ -168,8 +121,6 @@ const userProcessingImageValidator = strictObject({
|
||||
]),
|
||||
});
|
||||
|
||||
export type UserImage = InferOutput<typeof dataTypeValidator>;
|
||||
|
||||
const listValidator = strictObject({
|
||||
ID: pipe(string(), uuid()),
|
||||
UserID: pipe(string(), uuid()),
|
||||
@ -177,20 +128,48 @@ const listValidator = strictObject({
|
||||
Name: string(),
|
||||
Description: nullable(string()),
|
||||
|
||||
Images: array(
|
||||
strictObject({
|
||||
ID: pipe(string(), uuid()),
|
||||
ImageID: pipe(string(), uuid()),
|
||||
ListID: pipe(string(), uuid()),
|
||||
}),
|
||||
Images: pipe(
|
||||
nullable(
|
||||
array(
|
||||
strictObject({
|
||||
ID: pipe(string(), uuid()),
|
||||
ImageID: pipe(string(), uuid()),
|
||||
ListID: pipe(string(), uuid()),
|
||||
Items: array(
|
||||
strictObject({
|
||||
ID: pipe(string(), uuid()),
|
||||
ImageID: pipe(string(), uuid()),
|
||||
SchemaItemID: pipe(string(), uuid()),
|
||||
Value: string(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
),
|
||||
transform((n) => n ?? []),
|
||||
),
|
||||
|
||||
Schema: strictObject({
|
||||
ID: pipe(string(), uuid()),
|
||||
ListID: pipe(string(), uuid()),
|
||||
SchemaItems: array(
|
||||
strictObject({
|
||||
ID: pipe(string(), uuid()),
|
||||
SchemaID: pipe(string(), uuid()),
|
||||
Item: string(),
|
||||
Value: nullable(string()),
|
||||
Description: string(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
export type List = InferOutput<typeof listValidator>;
|
||||
|
||||
const imageRequestValidator = strictObject({
|
||||
UserImages: array(userImageValidator),
|
||||
ImageProperties: array(dataTypeValidator),
|
||||
ProcessingImages: array(userProcessingImageValidator),
|
||||
Lists: array(listValidator),
|
||||
userImages: array(userImageValidator),
|
||||
processingImages: array(userProcessingImageValidator),
|
||||
lists: array(listValidator),
|
||||
});
|
||||
|
||||
export type JustTheImageWhatAreTheseNames = InferOutput<
|
||||
@ -200,27 +179,16 @@ export type JustTheImageWhatAreTheseNames = InferOutput<
|
||||
export const getUserImages = async (): Promise<
|
||||
InferOutput<typeof imageRequestValidator>
|
||||
> => {
|
||||
const request = getBaseAuthorizedRequest({ path: "image" });
|
||||
const request = getBaseAuthorizedRequest({ path: "images" });
|
||||
|
||||
const res = await fetch(request).then((res) => res.json());
|
||||
|
||||
console.log("BACKEND RESPONSE: ", res);
|
||||
|
||||
return parse(imageRequestValidator, res);
|
||||
};
|
||||
|
||||
export const getImage = async (imageId: string): Promise<UserImage> => {
|
||||
const request = getBaseAuthorizedRequest({
|
||||
path: `image-properties/${imageId}`,
|
||||
});
|
||||
|
||||
const res = await fetch(request).then((res) => res.json());
|
||||
return parse(dataTypeValidator, res);
|
||||
};
|
||||
|
||||
export const postLogin = async (email: string): Promise<void> => {
|
||||
const request = getBaseRequest({
|
||||
path: "login",
|
||||
path: "auth/login",
|
||||
body: JSON.stringify({ email }),
|
||||
method: "POST",
|
||||
});
|
||||
@ -228,18 +196,6 @@ export const postLogin = async (email: string): Promise<void> => {
|
||||
await fetch(request);
|
||||
};
|
||||
|
||||
export const postDemoLogin = async (): Promise<
|
||||
InferOutput<typeof codeValidator>
|
||||
> => {
|
||||
const request = getBaseRequest({
|
||||
path: "demo-login",
|
||||
});
|
||||
|
||||
const res = await fetch(request).then((res) => res.json());
|
||||
|
||||
return parse(codeValidator, res);
|
||||
};
|
||||
|
||||
const codeValidator = strictObject({
|
||||
access: string(),
|
||||
refresh: string(),
|
||||
@ -250,7 +206,7 @@ export const postCode = async (
|
||||
code: string,
|
||||
): Promise<InferOutput<typeof codeValidator>> => {
|
||||
const request = getBaseRequest({
|
||||
path: "code",
|
||||
path: "auth/code",
|
||||
body: JSON.stringify({ email, code }),
|
||||
method: "POST",
|
||||
});
|
||||
@ -259,3 +215,18 @@ export const postCode = async (
|
||||
|
||||
return parse(codeValidator, res);
|
||||
};
|
||||
|
||||
export const createList = async (
|
||||
title: string,
|
||||
description: string,
|
||||
): Promise<void> => {
|
||||
const request = getBaseAuthorizedRequest({
|
||||
path: "stacks",
|
||||
method: "POST",
|
||||
body: JSON.stringify({ title, description }),
|
||||
});
|
||||
|
||||
request.headers.set("Content-Type", "application/json");
|
||||
|
||||
await fetch(request);
|
||||
};
|
||||
|
@ -1,6 +1,21 @@
|
||||
import { literal, pipe, strictObject, string, union, uuid } from "valibot";
|
||||
|
||||
export const processingListValidator = strictObject({
|
||||
Type: literal("list"),
|
||||
|
||||
Name: string(),
|
||||
ListID: pipe(string(), uuid()),
|
||||
|
||||
Status: union([
|
||||
literal("not-started"),
|
||||
literal("in-progress"),
|
||||
literal("complete"),
|
||||
]),
|
||||
});
|
||||
|
||||
export const processingImagesValidator = strictObject({
|
||||
Type: literal("image"),
|
||||
|
||||
ImageID: pipe(string(), uuid()),
|
||||
ImageName: string(),
|
||||
Status: union([
|
||||
@ -9,3 +24,8 @@ export const processingImagesValidator = strictObject({
|
||||
literal("complete"),
|
||||
]),
|
||||
});
|
||||
|
||||
export const notificationValidator = union([
|
||||
processingListValidator,
|
||||
processingImagesValidator,
|
||||
]);
|
||||
|
@ -1,28 +0,0 @@
|
||||
import { ImageComponent } from "@components/image";
|
||||
import { ItemModal } from "@components/item-modal/ItemModal";
|
||||
import { useSearchImageContext } from "@contexts/SearchImageContext";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { Component, For, Show } from "solid-js";
|
||||
|
||||
export const Entity: Component = () => {
|
||||
const params = useParams<{ entityId: string }>();
|
||||
|
||||
const { images } = useSearchImageContext();
|
||||
|
||||
const entity = () => images().find((i) => i.data.ID === params.entityId);
|
||||
|
||||
return (
|
||||
<Show when={entity()} fallback={<>Sorry, this entity could not be found</>}>
|
||||
{(e) => (
|
||||
<div>
|
||||
<ItemModal item={e()} />
|
||||
<div class="w-full grid grid-cols-4 auto-rows-[minmax(100px,1fr)] gap-4 bg-white p-4 rounded-xl border border-neutral-200">
|
||||
<For each={e().data.Images}>
|
||||
{(imageId) => <ImageComponent ID={imageId} />}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
);
|
||||
};
|
@ -1,80 +1,136 @@
|
||||
import { Component, For } from "solid-js";
|
||||
import { A } from "@solidjs/router";
|
||||
import {
|
||||
SearchImageStore,
|
||||
useSearchImageContext,
|
||||
} from "@contexts/SearchImageContext";
|
||||
import fastHashCode from "../../utils/hash";
|
||||
|
||||
// TODO: lots of stuff to do with Entities, this could be seperated into a centralized place.
|
||||
const CategoryColor: Record<
|
||||
keyof ReturnType<SearchImageStore["categories"]>,
|
||||
string
|
||||
> = {
|
||||
contact: "bg-orange-50",
|
||||
location: "bg-red-50",
|
||||
event: "bg-purple-50",
|
||||
};
|
||||
|
||||
const colors = [
|
||||
"bg-emerald-50",
|
||||
"bg-lime-50",
|
||||
|
||||
"bg-indigo-50",
|
||||
"bg-sky-50",
|
||||
|
||||
"bg-amber-50",
|
||||
"bg-teal-50",
|
||||
|
||||
"bg-fuchsia-50",
|
||||
"bg-pink-50",
|
||||
];
|
||||
import { Component, For, createSignal } from "solid-js";
|
||||
import { useSearchImageContext } from "@contexts/SearchImageContext";
|
||||
import { ListCard } from "@components/list-card";
|
||||
import { Button } from "@kobalte/core/button";
|
||||
import { Dialog } from "@kobalte/core/dialog";
|
||||
import { createList } from "../../network";
|
||||
|
||||
export const Categories: Component = () => {
|
||||
const { categories, lists } = useSearchImageContext();
|
||||
const { lists, onRefetchImages } = useSearchImageContext();
|
||||
|
||||
return (
|
||||
<div class="rounded-xl bg-white p-4 flex flex-col gap-2">
|
||||
<h2 class="text-xl font-bold">Entities</h2>
|
||||
<div class="w-full grid grid-cols-4 auto-rows-[minmax(100px,1fr)] gap-4">
|
||||
<For each={Object.entries(categories())}>
|
||||
{([category, group]) => (
|
||||
<A
|
||||
href={`/gallery/${category}`}
|
||||
class={
|
||||
"col-span-2 flex flex-col justify-center items-center rounded-lg p-4 border border-neutral-200 " +
|
||||
"capitalize " +
|
||||
CategoryColor[category as keyof typeof CategoryColor] +
|
||||
" " +
|
||||
(group.length === 0 ? "row-span-1 order-10" : "row-span-2")
|
||||
}
|
||||
>
|
||||
<p class="text-xl font-bold">{category}s</p>
|
||||
<p class="text-lg">{group.length}</p>
|
||||
</A>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<h2 class="text-xl font-bold">Generated Lists</h2>
|
||||
<div class="w-full grid grid-cols-3 auto-rows-[minmax(100px,1fr)] gap-4">
|
||||
<For each={lists()}>
|
||||
{(list) => (
|
||||
<A
|
||||
href="/"
|
||||
class={
|
||||
"flex flex-col p-4 border border-neutral-200 rounded-lg " +
|
||||
colors[
|
||||
fastHashCode(list.Name, { forcePositive: true }) %
|
||||
colors.length
|
||||
]
|
||||
}
|
||||
>
|
||||
<p class="text-xl font-bold">{list.Name}</p>
|
||||
<p class="text-lg">{list.Images.length}</p>
|
||||
</A>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const [title, setTitle] = createSignal("");
|
||||
const [description, setDescription] = createSignal("");
|
||||
|
||||
const [isCreating, setIsCreating] = createSignal(false);
|
||||
const [showForm, setShowForm] = createSignal(false);
|
||||
|
||||
const handleCreateList = async () => {
|
||||
if (description().trim().length === 0 || title().trim().length === 0)
|
||||
return;
|
||||
|
||||
setIsCreating(true);
|
||||
try {
|
||||
await createList(title().trim(), description().trim());
|
||||
setTitle("");
|
||||
setDescription("");
|
||||
setShowForm(false);
|
||||
onRefetchImages(); // Refresh the lists
|
||||
} catch (error) {
|
||||
console.error("Failed to create list:", error);
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="rounded-xl bg-white p-4 flex flex-col gap-2">
|
||||
<h2 class="text-xl font-bold">Generated Lists</h2>
|
||||
<div class="w-full grid grid-cols-3 auto-rows-[minmax(100px,1fr)] gap-4">
|
||||
<For each={lists()}>{(list) => <ListCard list={list} />}</For>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<Button
|
||||
class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors font-medium shadow-sm hover:shadow-md"
|
||||
onClick={() => setShowForm(true)}
|
||||
>
|
||||
+ Create List
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Dialog open={showForm()} onOpenChange={setShowForm}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class="fixed inset-0 bg-black/50 z-50" />
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<Dialog.Content class="bg-white rounded-lg shadow-xl max-w-md w-full max-h-[90vh] overflow-y-auto">
|
||||
<div class="p-6">
|
||||
<Dialog.Title class="text-xl font-bold text-neutral-900 mb-4">
|
||||
Create New List
|
||||
</Dialog.Title>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
for="list-title"
|
||||
class="block text-sm font-medium text-neutral-700 mb-2"
|
||||
>
|
||||
List Title
|
||||
</label>
|
||||
<input
|
||||
id="list-title"
|
||||
type="text"
|
||||
value={title()}
|
||||
onInput={(e) =>
|
||||
setTitle(e.target.value)
|
||||
}
|
||||
placeholder="Enter a title for your list"
|
||||
class="w-full p-3 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-indigo-600 focus:border-transparent transition-colors"
|
||||
disabled={isCreating()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="list-description"
|
||||
class="block text-sm font-medium text-neutral-700 mb-2"
|
||||
>
|
||||
List Description
|
||||
</label>
|
||||
<textarea
|
||||
id="list-description"
|
||||
value={description()}
|
||||
onInput={(e) =>
|
||||
setDescription(e.target.value)
|
||||
}
|
||||
placeholder="Describe what kind of list you want to create (e.g., 'A list of my favorite recipes' or 'Photos from my vacation')"
|
||||
class="w-full p-3 border border-neutral-300 rounded-lg resize-none focus:ring-2 focus:ring-indigo-600 focus:border-transparent transition-colors"
|
||||
rows="4"
|
||||
disabled={isCreating()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 mt-6">
|
||||
<Button
|
||||
class="flex-1 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-50 font-medium shadow-sm hover:shadow-md"
|
||||
onClick={handleCreateList}
|
||||
disabled={
|
||||
isCreating() ||
|
||||
!title().trim() ||
|
||||
!description().trim()
|
||||
}
|
||||
>
|
||||
{isCreating()
|
||||
? "Creating..."
|
||||
: "Create List"}
|
||||
</Button>
|
||||
<Button
|
||||
class="px-4 py-2 bg-neutral-300 text-neutral-700 rounded-lg hover:bg-neutral-400 transition-colors font-medium"
|
||||
onClick={() => {
|
||||
setShowForm(false);
|
||||
setTitle("");
|
||||
setDescription("");
|
||||
}}
|
||||
disabled={isCreating()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,52 +0,0 @@
|
||||
import { Component, For, Show } from "solid-js";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { union, literal, safeParse, InferOutput, parse } from "valibot";
|
||||
import { useSearchImageContext } from "@contexts/SearchImageContext";
|
||||
import { SearchCard } from "@components/search-card/SearchCard";
|
||||
|
||||
const entityValidator = union([
|
||||
literal("event"),
|
||||
literal("note"),
|
||||
literal("location"),
|
||||
literal("contact"),
|
||||
]);
|
||||
|
||||
const EntityGallery: Component<{
|
||||
entity: InferOutput<typeof entityValidator>;
|
||||
}> = (props) => {
|
||||
// Just to be doubly sure.
|
||||
parse(entityValidator, props.entity);
|
||||
|
||||
// These names are being silly. Entity or Category?
|
||||
const { images } = useSearchImageContext();
|
||||
|
||||
const filteredCategories = () =>
|
||||
images().filter((i) => i.type === props.entity);
|
||||
|
||||
return (
|
||||
<div class="w-full flex flex-col gap-4 capitalize bg-white rounded-xl p-4">
|
||||
<h2 class="font-bold text-xl">
|
||||
{props.entity}s ({filteredCategories().length})
|
||||
</h2>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<For each={filteredCategories()}>
|
||||
{(category) => <SearchCard item={category} />}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Gallery: Component = () => {
|
||||
const params = useParams();
|
||||
const validated = safeParse(entityValidator, params.entity);
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={validated.success}
|
||||
fallback={<p>Sorry, this entity is not supported</p>}
|
||||
>
|
||||
<EntityGallery entity={validated.output as any} />
|
||||
</Show>
|
||||
);
|
||||
};
|
@ -1,43 +1,38 @@
|
||||
import { ImageComponent } from "@components/image";
|
||||
import { SearchCard } from "@components/search-card/SearchCard";
|
||||
import { ImageComponentFullHeight } from "@components/image";
|
||||
import { useSearchImageContext } from "@contexts/SearchImageContext";
|
||||
import { UserImage } from "@network/index";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { createEffect, For, Show, type Component } from "solid-js";
|
||||
import { For, type Component } from "solid-js";
|
||||
import SolidjsMarkdown from "solidjs-markdown";
|
||||
import { ListCard } from "@components/list-card";
|
||||
|
||||
export const ImagePage: Component = () => {
|
||||
const { imageId } = useParams<{ imageId: string }>();
|
||||
|
||||
const { imagesWithProperties, userImages } = useSearchImageContext();
|
||||
const { userImages, lists } = useSearchImageContext();
|
||||
|
||||
const image = () => userImages().find((i) => i.ImageID === imageId);
|
||||
|
||||
createEffect(() => {
|
||||
console.log(userImages());
|
||||
});
|
||||
|
||||
const imageProperties = (): UserImage[] | undefined =>
|
||||
Object.entries(imagesWithProperties()).find(([id]) => id === imageId)?.[1];
|
||||
|
||||
return (
|
||||
<main class="flex flex-col items-center gap-4">
|
||||
<div class="w-full bg-white rounded-xl p-4">
|
||||
<ImageComponent ID={imageId} />
|
||||
<ImageComponentFullHeight ID={imageId} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="font-bold text-xl">Description</h2>
|
||||
<div class="w-full bg-white rounded-xl p-4 flex flex-col gap-4">
|
||||
<h2 class="font-bold text-2xl">Description</h2>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<For each={image()?.Image.ImageLists}>
|
||||
{(imageList) => (
|
||||
<ListCard
|
||||
list={lists().find((l) => l.ID === imageList.ListID)!}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full bg-white rounded-xl p-4">
|
||||
<h2 class="font-bold text-2xl">Description</h2>
|
||||
<SolidjsMarkdown>{image()?.Image.Description}</SolidjsMarkdown>
|
||||
</div>
|
||||
<div class="w-full grid grid-cols-3 gap-2 grid-flow-row-dense p-4 bg-white rounded-xl">
|
||||
<Show when={imageProperties()}>
|
||||
{(image) => (
|
||||
<For each={image()}>
|
||||
{(property) => <SearchCard item={property} />}
|
||||
</For>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
@ -1,8 +1,7 @@
|
||||
export * from "./front";
|
||||
export * from "./gallery";
|
||||
export * from "./image";
|
||||
export * from "./settings";
|
||||
export * from "./login";
|
||||
export * from "./entity";
|
||||
export * from "./search";
|
||||
export * from "./all-images";
|
||||
export * from "./list";
|
||||
|
107
frontend/src/pages/list/index.tsx
Normal file
107
frontend/src/pages/list/index.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import { useSearchImageContext } from "@contexts/SearchImageContext";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { Component, For, Show } from "solid-js";
|
||||
import { base } from "../../network";
|
||||
|
||||
export const List: Component = () => {
|
||||
const { listId } = useParams();
|
||||
|
||||
const { lists } = useSearchImageContext();
|
||||
|
||||
const list = () => lists().find((l) => l.ID === listId);
|
||||
|
||||
return (
|
||||
<Show when={list()} fallback="List could not be found">
|
||||
{(l) => (
|
||||
<div class="w-full h-full bg-white rounded-lg shadow-sm border border-neutral-200 overflow-hidden">
|
||||
<div class="overflow-x-auto overflow-y-auto h-full">
|
||||
<table class="w-full min-w-full">
|
||||
<thead class="bg-neutral-50 border-b border-neutral-200 sticky top-0 z-10">
|
||||
<tr>
|
||||
<th class="px-6 py-4 text-left text-sm font-semibold text-neutral-900 border-r border-neutral-200 min-w-40">
|
||||
Image
|
||||
</th>
|
||||
<For each={l().Schema.SchemaItems}>
|
||||
{(item, index) => (
|
||||
<th
|
||||
class={`px-6 py-4 text-left text-sm font-semibold text-neutral-900 min-w-32 ${
|
||||
index() <
|
||||
l().Schema.SchemaItems
|
||||
.length -
|
||||
1
|
||||
? "border-r border-neutral-200"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{item.Item}
|
||||
</th>
|
||||
)}
|
||||
</For>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-neutral-200">
|
||||
<For each={l().Images}>
|
||||
{(image, rowIndex) => (
|
||||
<tr
|
||||
class={`hover:bg-neutral-50 transition-colors ${
|
||||
rowIndex() % 2 === 0
|
||||
? "bg-white"
|
||||
: "bg-neutral-25"
|
||||
}`}
|
||||
>
|
||||
<td class="px-6 py-4 border-r border-neutral-200">
|
||||
<div class="w-32 h-24 overflow-hidden rounded-lg">
|
||||
<a
|
||||
href={`/image/${image.ImageID}`}
|
||||
class="w-full h-full flex justify-center"
|
||||
>
|
||||
<img
|
||||
class="w-full h-full object-cover rounded-lg"
|
||||
src={`${base}/images/${image.ImageID}`}
|
||||
alt="List item"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<For each={image.Items}>
|
||||
{(item, colIndex) => (
|
||||
<td
|
||||
class={`px-6 py-4 text-sm text-neutral-700 ${
|
||||
colIndex() <
|
||||
image.Items.length -
|
||||
1
|
||||
? "border-r border-neutral-200"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
class="max-w-xs truncate"
|
||||
title={item.Value}
|
||||
>
|
||||
{item.Value}
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</For>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
<Show when={l().Images.length === 0}>
|
||||
<div class="px-6 py-12 text-center text-neutral-500">
|
||||
<p class="text-lg">
|
||||
No images in this list yet
|
||||
</p>
|
||||
<p class="text-sm mt-1">
|
||||
Images will appear here once added to the
|
||||
list
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
);
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
import { isTokenValid } from "@components/protected-route";
|
||||
import { Button } from "@kobalte/core/button";
|
||||
import { TextField } from "@kobalte/core/text-field";
|
||||
import { postCode, postDemoLogin, postLogin } from "@network/index";
|
||||
import { postCode, postLogin } from "@network/index";
|
||||
import { Navigate } from "@solidjs/router";
|
||||
import { type Component, Show, createSignal } from "solid-js";
|
||||
|
||||
@ -18,16 +18,6 @@ export const Login: Component = () => {
|
||||
throw new Error("bruh, no email");
|
||||
}
|
||||
|
||||
if (email.toString() === "demo@email.com") {
|
||||
const { access, refresh } = await postDemoLogin();
|
||||
|
||||
localStorage.setItem("access", access);
|
||||
localStorage.setItem("refresh", refresh);
|
||||
|
||||
window.location.href = "/";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!submitted()) {
|
||||
await postLogin(email.toString());
|
||||
setSubmitted(true);
|
||||
|
@ -2,13 +2,14 @@ import { Component, createSignal, For } from "solid-js";
|
||||
import { Search } from "@kobalte/core/search";
|
||||
import { IconSearch } from "@tabler/icons-solidjs";
|
||||
import { useSearch } from "./search";
|
||||
import { UserImage } from "@network/index";
|
||||
import { SearchCard } from "@components/search-card/SearchCard";
|
||||
import { JustTheImageWhatAreTheseNames } from "@network/index";
|
||||
import { ImageComponent } from "@components/image";
|
||||
|
||||
export const SearchPage: Component = () => {
|
||||
const fuse = useSearch();
|
||||
|
||||
const [searchItems, setSearchItems] = createSignal<UserImage[]>([]);
|
||||
const [searchItems, setSearchItems] =
|
||||
createSignal<JustTheImageWhatAreTheseNames>([]);
|
||||
|
||||
return (
|
||||
<Search
|
||||
@ -36,7 +37,9 @@ export const SearchPage: Component = () => {
|
||||
<Search.Portal>
|
||||
<Search.Content class="container relative w-full rounded-xl bg-white p-4 grid grid-cols-3 gap-4">
|
||||
<Search.Arrow />
|
||||
<For each={searchItems()}>{(item) => <SearchCard item={item} />}</For>
|
||||
<For each={searchItems()}>
|
||||
{(item) => <ImageComponent ID={item.ImageID} />}
|
||||
</For>
|
||||
<Search.NoResult>No result found</Search.NoResult>
|
||||
</Search.Content>
|
||||
</Search.Portal>
|
||||
|
@ -1,46 +1,12 @@
|
||||
import { useSearchImageContext } from "@contexts/SearchImageContext";
|
||||
import { UserImage } from "@network/index";
|
||||
import Fuse from "fuse.js";
|
||||
|
||||
// This language is stupid. `keyof` only returns common keys but this somehow doesnt.
|
||||
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||
|
||||
const weightedTerms: Record<
|
||||
KeysOfUnion<UserImage["data"]>,
|
||||
number | undefined
|
||||
> = {
|
||||
ID: undefined,
|
||||
LocationID: undefined,
|
||||
OrganizerID: undefined,
|
||||
Images: undefined,
|
||||
|
||||
Description: 10,
|
||||
|
||||
Name: 5,
|
||||
Address: 2,
|
||||
|
||||
PhoneNumber: 2,
|
||||
Email: 2,
|
||||
|
||||
CreatedAt: 1,
|
||||
StartDateTime: 1,
|
||||
EndDateTime: 1,
|
||||
};
|
||||
|
||||
export const useSearch = () => {
|
||||
const { images, userImages } = useSearchImageContext();
|
||||
|
||||
const imageDescriptions = () =>
|
||||
userImages().map((i) => ({ data: { Description: i.Image.Description } }));
|
||||
const { userImages } = useSearchImageContext();
|
||||
|
||||
return () =>
|
||||
new Fuse([...images(), ...imageDescriptions()], {
|
||||
new Fuse(userImages(), {
|
||||
shouldSort: true,
|
||||
keys: Object.entries(weightedTerms)
|
||||
.filter(([, w]) => w != null)
|
||||
.map(([name, weight]) => ({
|
||||
name: `data.${name}`,
|
||||
weight,
|
||||
})),
|
||||
keys: ["Image.Description"],
|
||||
});
|
||||
};
|
||||
|
@ -1,16 +0,0 @@
|
||||
import type { getUserImages } from "../network";
|
||||
|
||||
export const groupPropertiesWithImage = ({
|
||||
UserImages,
|
||||
ImageProperties,
|
||||
}: Awaited<ReturnType<typeof getUserImages>>) => {
|
||||
const imageToProperties: Record<string, typeof ImageProperties> = {};
|
||||
|
||||
for (const image of UserImages) {
|
||||
imageToProperties[image.ImageID] = ImageProperties.filter((i) =>
|
||||
i.data.Images.includes(image.ImageID),
|
||||
);
|
||||
}
|
||||
|
||||
return imageToProperties;
|
||||
};
|
Reference in New Issue
Block a user