BIG MASSIVE REFACTOR OMG

Ripped out literally everything to simplify the backend as much as
possible.

Some of the code was so horrifically complicated it's insaneeee
This commit is contained in:
2025-09-21 21:31:44 +01:00
parent f8619d3ef7
commit 221afb599b
40 changed files with 512 additions and 1830 deletions

View File

@ -9,11 +9,15 @@ package model
import ( import (
"github.com/google/uuid" "github.com/google/uuid"
"time"
) )
type Image struct { type Image struct {
ID uuid.UUID `sql:"primary_key"` ID uuid.UUID `sql:"primary_key"`
UserID *uuid.UUID
ImageName string ImageName string
Description string Description string
Status Progress
Image []byte Image []byte
CreatedAt *time.Time
} }

View File

@ -11,8 +11,8 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
type ImageLists struct { type ImageStacks struct {
ID uuid.UUID `sql:"primary_key"` ID uuid.UUID `sql:"primary_key"`
ImageID uuid.UUID ImageID uuid.UUID
ListID uuid.UUID StackID uuid.UUID
} }

View File

@ -1,19 +0,0 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/google/uuid"
"time"
)
type Logs struct {
Log string
ImageID uuid.UUID
CreatedAt *time.Time
}

View File

@ -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 ProcessingLists struct {
ID uuid.UUID `sql:"primary_key"`
UserID uuid.UUID
Title string
Fields string
Status Progress
CreatedAt *time.Time
}

View File

@ -16,5 +16,5 @@ type SchemaItems struct {
Item string Item string
Value string Value string
Description string Description string
SchemaID uuid.UUID StackID uuid.UUID
} }

View File

@ -1,17 +0,0 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/google/uuid"
)
type Schemas struct {
ID uuid.UUID `sql:"primary_key"`
ListID uuid.UUID
}

View File

@ -12,9 +12,10 @@ import (
"time" "time"
) )
type Lists struct { type Stacks struct {
ID uuid.UUID `sql:"primary_key"` ID uuid.UUID `sql:"primary_key"`
UserID uuid.UUID UserID uuid.UUID
Status Progress
Name string Name string
Description string Description string
CreatedAt *time.Time CreatedAt *time.Time

View File

@ -1,20 +0,0 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/google/uuid"
"time"
)
type UserImages struct {
ID uuid.UUID `sql:"primary_key"`
ImageID uuid.UUID
UserID uuid.UUID
CreatedAt *time.Time
}

View File

@ -1,19 +0,0 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package model
import (
"github.com/google/uuid"
)
type UserImagesToProcess struct {
ID uuid.UUID `sql:"primary_key"`
Status Progress
ImageID uuid.UUID
UserID uuid.UUID
}

View File

@ -9,9 +9,11 @@ package model
import ( import (
"github.com/google/uuid" "github.com/google/uuid"
"time"
) )
type Users struct { type Users struct {
ID uuid.UUID `sql:"primary_key"` ID uuid.UUID `sql:"primary_key"`
Email string Email string
CreatedAt *time.Time
} }

View File

@ -18,13 +18,15 @@ type imageTable struct {
// Columns // Columns
ID postgres.ColumnString ID postgres.ColumnString
UserID postgres.ColumnString
ImageName postgres.ColumnString ImageName postgres.ColumnString
Description postgres.ColumnString Description postgres.ColumnString
Image postgres.ColumnBytea Status postgres.ColumnString
Image postgres.ColumnString
CreatedAt postgres.ColumnTimestampz
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type ImageTable struct { type ImageTable struct {
@ -63,12 +65,14 @@ func newImageTable(schemaName, tableName, alias string) *ImageTable {
func newImageTableImpl(schemaName, tableName, alias string) imageTable { func newImageTableImpl(schemaName, tableName, alias string) imageTable {
var ( var (
IDColumn = postgres.StringColumn("id") IDColumn = postgres.StringColumn("id")
UserIDColumn = postgres.StringColumn("user_id")
ImageNameColumn = postgres.StringColumn("image_name") ImageNameColumn = postgres.StringColumn("image_name")
DescriptionColumn = postgres.StringColumn("description") DescriptionColumn = postgres.StringColumn("description")
ImageColumn = postgres.ByteaColumn("image") StatusColumn = postgres.StringColumn("status")
allColumns = postgres.ColumnList{IDColumn, ImageNameColumn, DescriptionColumn, ImageColumn} ImageColumn = postgres.StringColumn("image")
mutableColumns = postgres.ColumnList{ImageNameColumn, DescriptionColumn, ImageColumn} CreatedAtColumn = postgres.TimestampzColumn("created_at")
defaultColumns = postgres.ColumnList{IDColumn} allColumns = postgres.ColumnList{IDColumn, UserIDColumn, ImageNameColumn, DescriptionColumn, StatusColumn, ImageColumn, CreatedAtColumn}
mutableColumns = postgres.ColumnList{UserIDColumn, ImageNameColumn, DescriptionColumn, StatusColumn, ImageColumn, CreatedAtColumn}
) )
return imageTable{ return imageTable{
@ -76,12 +80,14 @@ func newImageTableImpl(schemaName, tableName, alias string) imageTable {
//Columns //Columns
ID: IDColumn, ID: IDColumn,
UserID: UserIDColumn,
ImageName: ImageNameColumn, ImageName: ImageNameColumn,
Description: DescriptionColumn, Description: DescriptionColumn,
Status: StatusColumn,
Image: ImageColumn, Image: ImageColumn,
CreatedAt: CreatedAtColumn,
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

View File

@ -1,84 +0,0 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var ImageLists = newImageListsTable("haystack", "image_lists", "")
type imageListsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
ImageID postgres.ColumnString
ListID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type ImageListsTable struct {
imageListsTable
EXCLUDED imageListsTable
}
// AS creates new ImageListsTable with assigned alias
func (a ImageListsTable) AS(alias string) *ImageListsTable {
return newImageListsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ImageListsTable with assigned schema name
func (a ImageListsTable) FromSchema(schemaName string) *ImageListsTable {
return newImageListsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ImageListsTable with assigned table prefix
func (a ImageListsTable) WithPrefix(prefix string) *ImageListsTable {
return newImageListsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ImageListsTable with assigned table suffix
func (a ImageListsTable) WithSuffix(suffix string) *ImageListsTable {
return newImageListsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newImageListsTable(schemaName, tableName, alias string) *ImageListsTable {
return &ImageListsTable{
imageListsTable: newImageListsTableImpl(schemaName, tableName, alias),
EXCLUDED: newImageListsTableImpl("", "excluded", ""),
}
}
func newImageListsTableImpl(schemaName, tableName, alias string) imageListsTable {
var (
IDColumn = postgres.StringColumn("id")
ImageIDColumn = postgres.StringColumn("image_id")
ListIDColumn = postgres.StringColumn("list_id")
allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, ListIDColumn}
mutableColumns = postgres.ColumnList{ImageIDColumn, ListIDColumn}
defaultColumns = postgres.ColumnList{IDColumn}
)
return imageListsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
ImageID: ImageIDColumn,
ListID: ListIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}

View File

@ -24,7 +24,6 @@ type imageSchemaItemsTable struct {
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type ImageSchemaItemsTable struct { type ImageSchemaItemsTable struct {
@ -68,7 +67,6 @@ func newImageSchemaItemsTableImpl(schemaName, tableName, alias string) imageSche
ImageIDColumn = postgres.StringColumn("image_id") ImageIDColumn = postgres.StringColumn("image_id")
allColumns = postgres.ColumnList{IDColumn, ValueColumn, SchemaItemIDColumn, ImageIDColumn} allColumns = postgres.ColumnList{IDColumn, ValueColumn, SchemaItemIDColumn, ImageIDColumn}
mutableColumns = postgres.ColumnList{ValueColumn, SchemaItemIDColumn, ImageIDColumn} mutableColumns = postgres.ColumnList{ValueColumn, SchemaItemIDColumn, ImageIDColumn}
defaultColumns = postgres.ColumnList{IDColumn}
) )
return imageSchemaItemsTable{ return imageSchemaItemsTable{
@ -82,6 +80,5 @@ func newImageSchemaItemsTableImpl(schemaName, tableName, alias string) imageSche
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

View File

@ -0,0 +1,81 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var ImageStacks = newImageStacksTable("haystack", "image_stacks", "")
type imageStacksTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
ImageID postgres.ColumnString
StackID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ImageStacksTable struct {
imageStacksTable
EXCLUDED imageStacksTable
}
// AS creates new ImageStacksTable with assigned alias
func (a ImageStacksTable) AS(alias string) *ImageStacksTable {
return newImageStacksTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ImageStacksTable with assigned schema name
func (a ImageStacksTable) FromSchema(schemaName string) *ImageStacksTable {
return newImageStacksTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ImageStacksTable with assigned table prefix
func (a ImageStacksTable) WithPrefix(prefix string) *ImageStacksTable {
return newImageStacksTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ImageStacksTable with assigned table suffix
func (a ImageStacksTable) WithSuffix(suffix string) *ImageStacksTable {
return newImageStacksTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newImageStacksTable(schemaName, tableName, alias string) *ImageStacksTable {
return &ImageStacksTable{
imageStacksTable: newImageStacksTableImpl(schemaName, tableName, alias),
EXCLUDED: newImageStacksTableImpl("", "excluded", ""),
}
}
func newImageStacksTableImpl(schemaName, tableName, alias string) imageStacksTable {
var (
IDColumn = postgres.StringColumn("id")
ImageIDColumn = postgres.StringColumn("image_id")
StackIDColumn = postgres.StringColumn("stack_id")
allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, StackIDColumn}
mutableColumns = postgres.ColumnList{ImageIDColumn, StackIDColumn}
)
return imageStacksTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
ImageID: ImageIDColumn,
StackID: StackIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -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 Lists = newListsTable("haystack", "lists", "")
type listsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
UserID postgres.ColumnString
Name postgres.ColumnString
Description postgres.ColumnString
CreatedAt postgres.ColumnTimestampz
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type ListsTable struct {
listsTable
EXCLUDED listsTable
}
// AS creates new ListsTable with assigned alias
func (a ListsTable) AS(alias string) *ListsTable {
return newListsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ListsTable with assigned schema name
func (a ListsTable) FromSchema(schemaName string) *ListsTable {
return newListsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ListsTable with assigned table prefix
func (a ListsTable) WithPrefix(prefix string) *ListsTable {
return newListsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ListsTable with assigned table suffix
func (a ListsTable) WithSuffix(suffix string) *ListsTable {
return newListsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newListsTable(schemaName, tableName, alias string) *ListsTable {
return &ListsTable{
listsTable: newListsTableImpl(schemaName, tableName, alias),
EXCLUDED: newListsTableImpl("", "excluded", ""),
}
}
func newListsTableImpl(schemaName, tableName, alias string) listsTable {
var (
IDColumn = postgres.StringColumn("id")
UserIDColumn = postgres.StringColumn("user_id")
NameColumn = postgres.StringColumn("name")
DescriptionColumn = postgres.StringColumn("description")
CreatedAtColumn = postgres.TimestampzColumn("created_at")
allColumns = postgres.ColumnList{IDColumn, UserIDColumn, NameColumn, DescriptionColumn, CreatedAtColumn}
mutableColumns = postgres.ColumnList{UserIDColumn, NameColumn, DescriptionColumn, CreatedAtColumn}
defaultColumns = postgres.ColumnList{IDColumn, CreatedAtColumn}
)
return listsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
UserID: UserIDColumn,
Name: NameColumn,
Description: DescriptionColumn,
CreatedAt: CreatedAtColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}

View File

@ -1,84 +0,0 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var Logs = newLogsTable("haystack", "logs", "")
type logsTable struct {
postgres.Table
// Columns
Log postgres.ColumnString
ImageID postgres.ColumnString
CreatedAt postgres.ColumnTimestampz
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type LogsTable struct {
logsTable
EXCLUDED logsTable
}
// AS creates new LogsTable with assigned alias
func (a LogsTable) AS(alias string) *LogsTable {
return newLogsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new LogsTable with assigned schema name
func (a LogsTable) FromSchema(schemaName string) *LogsTable {
return newLogsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new LogsTable with assigned table prefix
func (a LogsTable) WithPrefix(prefix string) *LogsTable {
return newLogsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new LogsTable with assigned table suffix
func (a LogsTable) WithSuffix(suffix string) *LogsTable {
return newLogsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newLogsTable(schemaName, tableName, alias string) *LogsTable {
return &LogsTable{
logsTable: newLogsTableImpl(schemaName, tableName, alias),
EXCLUDED: newLogsTableImpl("", "excluded", ""),
}
}
func newLogsTableImpl(schemaName, tableName, alias string) logsTable {
var (
LogColumn = postgres.StringColumn("log")
ImageIDColumn = postgres.StringColumn("image_id")
CreatedAtColumn = postgres.TimestampzColumn("created_at")
allColumns = postgres.ColumnList{LogColumn, ImageIDColumn, CreatedAtColumn}
mutableColumns = postgres.ColumnList{LogColumn, ImageIDColumn, CreatedAtColumn}
defaultColumns = postgres.ColumnList{CreatedAtColumn}
)
return logsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
Log: LogColumn,
ImageID: ImageIDColumn,
CreatedAt: CreatedAtColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}

View File

@ -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 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,
}
}

View File

@ -21,11 +21,10 @@ type schemaItemsTable struct {
Item postgres.ColumnString Item postgres.ColumnString
Value postgres.ColumnString Value postgres.ColumnString
Description postgres.ColumnString Description postgres.ColumnString
SchemaID postgres.ColumnString StackID postgres.ColumnString
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type SchemaItemsTable struct { type SchemaItemsTable struct {
@ -67,10 +66,9 @@ func newSchemaItemsTableImpl(schemaName, tableName, alias string) schemaItemsTab
ItemColumn = postgres.StringColumn("item") ItemColumn = postgres.StringColumn("item")
ValueColumn = postgres.StringColumn("value") ValueColumn = postgres.StringColumn("value")
DescriptionColumn = postgres.StringColumn("description") DescriptionColumn = postgres.StringColumn("description")
SchemaIDColumn = postgres.StringColumn("schema_id") StackIDColumn = postgres.StringColumn("stack_id")
allColumns = postgres.ColumnList{IDColumn, ItemColumn, ValueColumn, DescriptionColumn, SchemaIDColumn} allColumns = postgres.ColumnList{IDColumn, ItemColumn, ValueColumn, DescriptionColumn, StackIDColumn}
mutableColumns = postgres.ColumnList{ItemColumn, ValueColumn, DescriptionColumn, SchemaIDColumn} mutableColumns = postgres.ColumnList{ItemColumn, ValueColumn, DescriptionColumn, StackIDColumn}
defaultColumns = postgres.ColumnList{IDColumn}
) )
return schemaItemsTable{ return schemaItemsTable{
@ -81,10 +79,9 @@ func newSchemaItemsTableImpl(schemaName, tableName, alias string) schemaItemsTab
Item: ItemColumn, Item: ItemColumn,
Value: ValueColumn, Value: ValueColumn,
Description: DescriptionColumn, Description: DescriptionColumn,
SchemaID: SchemaIDColumn, StackID: StackIDColumn,
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

View File

@ -1,81 +0,0 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var 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,
}
}

View File

@ -0,0 +1,90 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var Stacks = newStacksTable("haystack", "stacks", "")
type stacksTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
UserID postgres.ColumnString
Status postgres.ColumnString
Name postgres.ColumnString
Description postgres.ColumnString
CreatedAt postgres.ColumnTimestampz
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type StacksTable struct {
stacksTable
EXCLUDED stacksTable
}
// AS creates new StacksTable with assigned alias
func (a StacksTable) AS(alias string) *StacksTable {
return newStacksTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new StacksTable with assigned schema name
func (a StacksTable) FromSchema(schemaName string) *StacksTable {
return newStacksTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new StacksTable with assigned table prefix
func (a StacksTable) WithPrefix(prefix string) *StacksTable {
return newStacksTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new StacksTable with assigned table suffix
func (a StacksTable) WithSuffix(suffix string) *StacksTable {
return newStacksTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newStacksTable(schemaName, tableName, alias string) *StacksTable {
return &StacksTable{
stacksTable: newStacksTableImpl(schemaName, tableName, alias),
EXCLUDED: newStacksTableImpl("", "excluded", ""),
}
}
func newStacksTableImpl(schemaName, tableName, alias string) stacksTable {
var (
IDColumn = postgres.StringColumn("id")
UserIDColumn = postgres.StringColumn("user_id")
StatusColumn = postgres.StringColumn("status")
NameColumn = postgres.StringColumn("name")
DescriptionColumn = postgres.StringColumn("description")
CreatedAtColumn = postgres.TimestampzColumn("created_at")
allColumns = postgres.ColumnList{IDColumn, UserIDColumn, StatusColumn, NameColumn, DescriptionColumn, CreatedAtColumn}
mutableColumns = postgres.ColumnList{UserIDColumn, StatusColumn, NameColumn, DescriptionColumn, CreatedAtColumn}
)
return stacksTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
UserID: UserIDColumn,
Status: StatusColumn,
Name: NameColumn,
Description: DescriptionColumn,
CreatedAt: CreatedAtColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -11,14 +11,9 @@ package table
// this method only once at the beginning of the program. // this method only once at the beginning of the program.
func UseSchema(schema string) { func UseSchema(schema string) {
Image = Image.FromSchema(schema) Image = Image.FromSchema(schema)
ImageLists = ImageLists.FromSchema(schema)
ImageSchemaItems = ImageSchemaItems.FromSchema(schema) ImageSchemaItems = ImageSchemaItems.FromSchema(schema)
Lists = Lists.FromSchema(schema) ImageStacks = ImageStacks.FromSchema(schema)
Logs = Logs.FromSchema(schema)
ProcessingLists = ProcessingLists.FromSchema(schema)
SchemaItems = SchemaItems.FromSchema(schema) SchemaItems = SchemaItems.FromSchema(schema)
Schemas = Schemas.FromSchema(schema) Stacks = Stacks.FromSchema(schema)
UserImages = UserImages.FromSchema(schema)
UserImagesToProcess = UserImagesToProcess.FromSchema(schema)
Users = Users.FromSchema(schema) Users = Users.FromSchema(schema)
} }

View File

@ -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 UserImages = newUserImagesTable("haystack", "user_images", "")
type userImagesTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
ImageID postgres.ColumnString
UserID postgres.ColumnString
CreatedAt postgres.ColumnTimestampz
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type UserImagesTable struct {
userImagesTable
EXCLUDED userImagesTable
}
// AS creates new UserImagesTable with assigned alias
func (a UserImagesTable) AS(alias string) *UserImagesTable {
return newUserImagesTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new UserImagesTable with assigned schema name
func (a UserImagesTable) FromSchema(schemaName string) *UserImagesTable {
return newUserImagesTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new UserImagesTable with assigned table prefix
func (a UserImagesTable) WithPrefix(prefix string) *UserImagesTable {
return newUserImagesTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new UserImagesTable with assigned table suffix
func (a UserImagesTable) WithSuffix(suffix string) *UserImagesTable {
return newUserImagesTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newUserImagesTable(schemaName, tableName, alias string) *UserImagesTable {
return &UserImagesTable{
userImagesTable: newUserImagesTableImpl(schemaName, tableName, alias),
EXCLUDED: newUserImagesTableImpl("", "excluded", ""),
}
}
func newUserImagesTableImpl(schemaName, tableName, alias string) userImagesTable {
var (
IDColumn = postgres.StringColumn("id")
ImageIDColumn = postgres.StringColumn("image_id")
UserIDColumn = postgres.StringColumn("user_id")
CreatedAtColumn = postgres.TimestampzColumn("created_at")
allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, UserIDColumn, CreatedAtColumn}
mutableColumns = postgres.ColumnList{ImageIDColumn, UserIDColumn, CreatedAtColumn}
defaultColumns = postgres.ColumnList{IDColumn, CreatedAtColumn}
)
return userImagesTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
ImageID: ImageIDColumn,
UserID: UserIDColumn,
CreatedAt: CreatedAtColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}

View File

@ -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 UserImagesToProcess = newUserImagesToProcessTable("haystack", "user_images_to_process", "")
type userImagesToProcessTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
Status postgres.ColumnString
ImageID postgres.ColumnString
UserID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type UserImagesToProcessTable struct {
userImagesToProcessTable
EXCLUDED userImagesToProcessTable
}
// AS creates new UserImagesToProcessTable with assigned alias
func (a UserImagesToProcessTable) AS(alias string) *UserImagesToProcessTable {
return newUserImagesToProcessTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new UserImagesToProcessTable with assigned schema name
func (a UserImagesToProcessTable) FromSchema(schemaName string) *UserImagesToProcessTable {
return newUserImagesToProcessTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new UserImagesToProcessTable with assigned table prefix
func (a UserImagesToProcessTable) WithPrefix(prefix string) *UserImagesToProcessTable {
return newUserImagesToProcessTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new UserImagesToProcessTable with assigned table suffix
func (a UserImagesToProcessTable) WithSuffix(suffix string) *UserImagesToProcessTable {
return newUserImagesToProcessTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newUserImagesToProcessTable(schemaName, tableName, alias string) *UserImagesToProcessTable {
return &UserImagesToProcessTable{
userImagesToProcessTable: newUserImagesToProcessTableImpl(schemaName, tableName, alias),
EXCLUDED: newUserImagesToProcessTableImpl("", "excluded", ""),
}
}
func newUserImagesToProcessTableImpl(schemaName, tableName, alias string) userImagesToProcessTable {
var (
IDColumn = postgres.StringColumn("id")
StatusColumn = postgres.StringColumn("status")
ImageIDColumn = postgres.StringColumn("image_id")
UserIDColumn = postgres.StringColumn("user_id")
allColumns = postgres.ColumnList{IDColumn, StatusColumn, ImageIDColumn, UserIDColumn}
mutableColumns = postgres.ColumnList{StatusColumn, ImageIDColumn, UserIDColumn}
defaultColumns = postgres.ColumnList{IDColumn, StatusColumn}
)
return userImagesToProcessTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Status: StatusColumn,
ImageID: ImageIDColumn,
UserID: UserIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}

View File

@ -19,10 +19,10 @@ type usersTable struct {
// Columns // Columns
ID postgres.ColumnString ID postgres.ColumnString
Email postgres.ColumnString Email postgres.ColumnString
CreatedAt postgres.ColumnTimestampz
AllColumns postgres.ColumnList AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
} }
type UsersTable struct { type UsersTable struct {
@ -62,9 +62,9 @@ func newUsersTableImpl(schemaName, tableName, alias string) usersTable {
var ( var (
IDColumn = postgres.StringColumn("id") IDColumn = postgres.StringColumn("id")
EmailColumn = postgres.StringColumn("email") EmailColumn = postgres.StringColumn("email")
allColumns = postgres.ColumnList{IDColumn, EmailColumn} CreatedAtColumn = postgres.TimestampzColumn("created_at")
mutableColumns = postgres.ColumnList{EmailColumn} allColumns = postgres.ColumnList{IDColumn, EmailColumn, CreatedAtColumn}
defaultColumns = postgres.ColumnList{IDColumn} mutableColumns = postgres.ColumnList{EmailColumn, CreatedAtColumn}
) )
return usersTable{ return usersTable{
@ -73,9 +73,9 @@ func newUsersTableImpl(schemaName, tableName, alias string) usersTable {
//Columns //Columns
ID: IDColumn, ID: IDColumn,
Email: EmailColumn, Email: EmailColumn,
CreatedAt: CreatedAtColumn,
AllColumns: allColumns, AllColumns: allColumns,
MutableColumns: mutableColumns, MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
} }
} }

View File

@ -76,7 +76,7 @@ type createNewListArguments struct {
type CreateListAgent struct { type CreateListAgent struct {
client client.AgentClient client client.AgentClient
listModel models.ListModel listModel models.StackModel
} }
func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, userReq string) error { func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, userReq string) error {
@ -120,12 +120,20 @@ func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, user
}) })
} }
agent.listModel.Save(ctx, userID, createListArgs.Title, createListArgs.Description, schemaItems) _, err = agent.listModel.Save(ctx, userID, createListArgs.Title, createListArgs.Description)
if err != nil {
return fmt.Errorf("creating list agent, saving list: %w", err)
}
err = agent.listModel.SaveItems(ctx, schemaItems)
if err != nil {
return fmt.Errorf("creating list agent, saving items: %w", err)
}
return nil return nil
} }
func NewCreateListAgent(log *log.Logger, listModel models.ListModel) CreateListAgent { func NewCreateListAgent(log *log.Logger, listModel models.StackModel) CreateListAgent {
client := client.CreateAgentClient(client.CreateAgentClientOptions{ client := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: createListAgentPrompt, SystemPrompt: createListAgentPrompt,
Log: log, Log: log,

View File

@ -3,6 +3,7 @@ package agents
import ( import (
"context" "context"
"fmt" "fmt"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents/client" "screenmark/screenmark/agents/client"
"screenmark/screenmark/models" "screenmark/screenmark/models"
@ -26,7 +27,7 @@ type DescriptionAgent struct {
imageModel models.ImageModel imageModel models.ImageModel
} }
func (agent DescriptionAgent) Describe(log *log.Logger, imageId uuid.UUID, imageName string, imageData []byte) error { func (agent DescriptionAgent) Describe(log *log.Logger, imageID uuid.UUID, imageName string, imageData []byte) error {
request := client.AgentRequestBody{ request := client.AgentRequestBody{
Model: "policy/images", Model: "policy/images",
Temperature: 0.3, Temperature: 0.3,
@ -51,7 +52,11 @@ func (agent DescriptionAgent) Describe(log *log.Logger, imageId uuid.UUID, image
markdown := resp.Choices[0].Message.Content markdown := resp.Choices[0].Message.Content
err = agent.imageModel.AddDescription(ctx, imageId, markdown) _, err = agent.imageModel.Update(ctx, model.Image{
ID: imageID,
Description: markdown,
})
if err != nil { if err != nil {
return err return err
} }

View File

@ -176,7 +176,7 @@ type addToListArguments struct {
Schema []models.IDValue Schema []models.IDValue
} }
func NewListAgent(log *log.Logger, listModel models.ListModel, limitsMethods limits.LimitsManagerMethods) client.AgentClient { func NewListAgent(log *log.Logger, listModel models.StackModel, limitsMethods limits.LimitsManagerMethods) client.AgentClient {
agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{ agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: listPrompt, SystemPrompt: listPrompt,
JsonTools: listTools, JsonTools: listTools,
@ -206,10 +206,19 @@ func NewListAgent(log *log.Logger, listModel models.ListModel, limitsMethods lim
} }
ctx := context.Background() ctx := context.Background()
savedList, err := listModel.Save(ctx, info.UserId, args.Name, args.Desription, args.Schema) savedList, err := listModel.Save(ctx, info.UserId, args.Name, args.Desription)
if err != nil { if err != nil {
log.Error(err) log.Error("saving list", "err", err)
return "", err
}
for i := range args.Schema {
args.Schema[i].StackID = savedList.ID
}
err = listModel.SaveItems(ctx, args.Schema)
if err != nil {
log.Error("saving items", "err", err)
return "", err return "", err
} }
@ -229,12 +238,12 @@ func NewListAgent(log *log.Logger, listModel models.ListModel, limitsMethods lim
ctx := context.Background() ctx := context.Background()
listUuid, err := uuid.Parse(args.ListID) listUUID, err := uuid.Parse(args.ListID)
if err != nil { if err != nil {
return "", err return "", err
} }
if err := listModel.SaveInto(ctx, listUuid, info.ImageId, args.Schema); err != nil { if err := listModel.SaveImage(ctx, info.ImageId, listUUID); err != nil {
return "", err return "", err
} }

View File

@ -1,23 +1,13 @@
package main package main
import ( import (
"context"
"database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"os"
"screenmark/screenmark/agents"
"screenmark/screenmark/limits"
"screenmark/screenmark/middleware" "screenmark/screenmark/middleware"
"screenmark/screenmark/models"
"strconv" "strconv"
"sync"
"time"
"github.com/charmbracelet/log"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/lib/pq"
) )
const ( const (
@ -76,229 +66,6 @@ func (n *Notification) UnmarshalJSON(data []byte) error {
return fmt.Errorf("unimplemented") return fmt.Errorf("unimplemented")
} }
func ProcessImage(log *log.Logger, db *sql.DB) func(imageID uuid.UUID) {
imageModel := models.NewImageModel(db)
listModel := models.NewListModel(db)
limits := limits.CreateLimitsManager(db)
ctx := context.Background()
return func(imageID uuid.UUID) {
log.Debug("Starting processing image", "ImageID", imageID)
go func() {
image, err := imageModel.GetToProcessWithData(ctx, imageID)
if err != nil {
log.Error("Failed to GetToProcessWithData", "error", err)
return
}
splitWriter := createDbStdoutWriter(db, image.ImageID)
if err := imageModel.StartProcessing(ctx, image.ID); err != nil {
log.Error("Failed to FinishProcessing", "error", err)
return
}
descriptionAgent := agents.NewDescriptionAgent(createLogger("Description 📝", splitWriter), imageModel)
listAgent := agents.NewListAgent(createLogger("Lists 🖋️", splitWriter), listModel, limits)
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 {
log.Error("Failed to finish processing", "ImageID", imageID, "error", err)
return
}
log.Debug("Finished processing image", "ImageID", imageID)
}()
}
}
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)
}
})
defer listener.Close()
databaseEventLog := createLogger("Database Events 🤖", os.Stdout)
databaseEventLog.SetLevel(log.DebugLevel)
err := listener.Listen("new_image")
if err != nil {
panic(err)
}
for parameters := range listener.Notify {
imageID := uuid.MustParse(parameters.Extra)
ProcessImage(databaseEventLog, db)(imageID)
}
}
func ListenProcessingImageStatus(db *sql.DB, images models.ImageModel, notifier *Notifier[Notification]) {
listener := pq.NewListener(os.Getenv("DB_CONNECTION"), time.Second, time.Second, func(event pq.ListenerEventType, err error) {
if err != nil {
panic(err)
}
})
defer listener.Close()
logger := createLogger("Image Status 📊", os.Stdout)
if err := listener.Listen("new_processing_image_status"); err != nil {
panic(err)
}
for data := range listener.Notify {
imageStringUuid := data.Extra[0:36]
status := data.Extra[36:]
imageUuid, err := uuid.Parse(imageStringUuid)
if err != nil {
logger.Error(err)
continue
}
processingImage, err := images.GetToProcess(context.Background(), imageUuid)
if err != nil {
logger.Error("GetToProcess failed", "err", err)
continue
}
logger.Info("Update", "id", imageStringUuid, "status", status)
notification := 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)
}
}
}
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. * TODO: We have channels open every a user sends an image.
* We never close these channels. * We never close these channels.

View File

@ -9,7 +9,6 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/limits" "screenmark/screenmark/limits"
"screenmark/screenmark/middleware" "screenmark/screenmark/middleware"
"screenmark/screenmark/models" "screenmark/screenmark/models"
@ -27,14 +26,11 @@ type ImageHandler struct {
limitsManager limits.LimitsManagerMethods limitsManager limits.LimitsManagerMethods
processImage func(imageID uuid.UUID)
jwtManager *middleware.JwtManager jwtManager *middleware.JwtManager
} }
type ImagesReturn struct { type ImagesReturn struct {
UserImages []models.UserImageWithImage `json:"userImages"` UserImages []models.UserImageWithImage `json:"userImages"`
ProcessingImages []models.UserProcessingImage `json:"processingImages"`
Lists []models.ListsWithImages `json:"lists"` Lists []models.ListsWithImages `json:"lists"`
} }
@ -52,19 +48,19 @@ func (h *ImageHandler) serveImage(w http.ResponseWriter, r *http.Request) {
return return
} }
isAuthorized := h.imageModel.IsUserAuthorized(ctx, imageID, userID) image, exists, err := h.imageModel.Get(r.Context(), imageID)
if !isAuthorized {
w.WriteHeader(http.StatusUnauthorized)
return
}
image, err := h.imageModel.Get(r.Context(), imageID)
if err != nil { if err != nil {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "Could not get image") fmt.Fprintf(w, "Could not get image")
return return
} }
// Do not leak that this ID exists.
if !exists || *image.UserID != userID {
w.WriteHeader(http.StatusNotFound)
return
}
// TODO: this could be part of the db table // TODO: this could be part of the db table
extension := filepath.Ext(image.ImageName) extension := filepath.Ext(image.ImageName)
if len(extension) == 0 { if len(extension) == 0 {
@ -89,12 +85,6 @@ func (h *ImageHandler) listImages(w http.ResponseWriter, r *http.Request) {
return 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) listsWithImages, err := h.userModel.ListWithImages(r.Context(), userId)
if err != nil { if err != nil {
middleware.WriteErrorInternal(h.logger, "could not get lists with images", w) middleware.WriteErrorInternal(h.logger, "could not get lists with images", w)
@ -103,7 +93,6 @@ func (h *ImageHandler) listImages(w http.ResponseWriter, r *http.Request) {
imagesReturn := ImagesReturn{ imagesReturn := ImagesReturn{
UserImages: images, UserImages: images,
ProcessingImages: processingImages,
Lists: listsWithImages, Lists: listsWithImages,
} }
@ -117,7 +106,7 @@ func (h *ImageHandler) uploadImage(w http.ResponseWriter, r *http.Request) {
return return
} }
userId, err := middleware.GetUserID(r.Context(), h.logger, w) userID, err := middleware.GetUserID(r.Context(), h.logger, w)
if err != nil { if err != nil {
return return
} }
@ -151,17 +140,16 @@ func (h *ImageHandler) uploadImage(w http.ResponseWriter, r *http.Request) {
return return
} }
userImage, err := h.imageModel.Process(r.Context(), userId, model.Image{ ctx := r.Context()
Image: image,
ImageName: imageName, err = h.imageModel.Save(ctx, imageName, image, userID)
})
if err != nil { if err != nil {
middleware.WriteErrorInternal(h.logger, "could not save image to DB", w) middleware.WriteErrorInternal(h.logger, "could not save image to DB", w)
return return
} }
middleware.WriteJsonOrError(h.logger, userImage, w) w.WriteHeader(http.StatusOK)
} }
func (h *ImageHandler) deleteImage(w http.ResponseWriter, r *http.Request) { func (h *ImageHandler) deleteImage(w http.ResponseWriter, r *http.Request) {
@ -180,71 +168,19 @@ func (h *ImageHandler) deleteImage(w http.ResponseWriter, r *http.Request) {
return return
} }
isAuthorized := h.imageModel.IsUserAuthorized(ctx, imageID, userID) exists, err := h.imageModel.Delete(ctx, imageID, userID)
if !isAuthorized {
w.WriteHeader(http.StatusUnauthorized)
return
}
err = h.imageModel.Delete(ctx, imageID)
if err != nil { if err != nil {
h.logger.Warn("cannot delete image", "error", err) h.logger.Warn("cannot delete image", "error", err)
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
return return
} }
w.WriteHeader(http.StatusOK) // Don't leak if the image exists or not
} if !exists {
w.WriteHeader(http.StatusNotFound)
// This feature is actually stupid
func (h *ImageHandler) reprocessImage(w http.ResponseWriter, r *http.Request) {
stringImageID := chi.URLParam(r, "image-id")
imageID, err := uuid.Parse(stringImageID)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return return
} }
ctx := r.Context()
userID, err := middleware.GetUserID(ctx, h.logger, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
isAuthorized := h.imageModel.IsUserAuthorized(ctx, imageID, userID)
if !isAuthorized {
w.WriteHeader(http.StatusUnauthorized)
return
}
imageToProcessID, err := h.imageModel.GetImageToProcessID(ctx, imageID)
if err != nil {
h.logger.Error("get image to process", "err", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
// The whole way in which I do this event driven stuff is stupid.
// It's so messy now
err = h.imageModel.DeleteUserImage(ctx, imageID)
if err != nil {
h.logger.Error("delete user image", "err", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
err = h.imageModel.SetNotStarted(ctx, imageToProcessID)
if err != nil {
h.logger.Error("set not started", "err", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
h.processImage(imageToProcessID)
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
@ -266,7 +202,7 @@ func (h *ImageHandler) CreateRoutes(r chi.Router) {
}) })
} }
func CreateImageHandler(db *sql.DB, limitsManager limits.LimitsManagerMethods, processImage func(imageID uuid.UUID), jwtManager *middleware.JwtManager) ImageHandler { func CreateImageHandler(db *sql.DB, limitsManager limits.LimitsManagerMethods, jwtManager *middleware.JwtManager) ImageHandler {
imageModel := models.NewImageModel(db) imageModel := models.NewImageModel(db)
userModel := models.NewUserModel(db) userModel := models.NewUserModel(db)
logger := log.New(os.Stdout).WithPrefix("Images") logger := log.New(os.Stdout).WithPrefix("Images")
@ -276,7 +212,6 @@ func CreateImageHandler(db *sql.DB, limitsManager limits.LimitsManagerMethods, p
imageModel: imageModel, imageModel: imageModel,
userModel: userModel, userModel: userModel,
limitsManager: limitsManager, limitsManager: limitsManager,
processImage: processImage,
jwtManager: jwtManager, jwtManager: jwtManager,
} }
} }

View File

@ -12,7 +12,7 @@ import (
const ( const (
LISTS_LIMIT = 10 LISTS_LIMIT = 10
IMAGE_LIMIT = 50 IMAGE_LIMIT = 10
) )
type LimitsManager struct { type LimitsManager struct {
@ -29,9 +29,9 @@ type listCount struct {
} }
func (m *LimitsManager) HasReachedStackLimit(userID uuid.UUID) (bool, error) { func (m *LimitsManager) HasReachedStackLimit(userID uuid.UUID) (bool, error) {
getStacks := Lists. getStacks := Stacks.
SELECT(COUNT(Lists.UserID).AS("listCount.ListCount")). SELECT(COUNT(Stacks.UserID).AS("listCount.ListCount")).
WHERE(Lists.UserID.EQ(UUID(userID))) WHERE(Stacks.UserID.EQ(UUID(userID)))
var count listCount var count listCount
err := getStacks.Query(m.dbPool, &count) err := getStacks.Query(m.dbPool, &count)
@ -44,9 +44,9 @@ type imageCount struct {
} }
func (m *LimitsManager) HasReachedImageLimit(userID uuid.UUID) (bool, error) { func (m *LimitsManager) HasReachedImageLimit(userID uuid.UUID) (bool, error) {
getStacks := UserImages. getStacks := Image.
SELECT(COUNT(UserImages.UserID).AS("imageCount.ImageCount")). SELECT(COUNT(Image.UserID).AS("imageCount.ImageCount")).
WHERE(UserImages.UserID.EQ(UUID(userID))) WHERE(Image.UserID.EQ(UUID(userID)))
var count imageCount var count imageCount
err := getStacks.Query(m.dbPool, &count) err := getStacks.Query(m.dbPool, &count)

View File

@ -1,21 +1,11 @@
package main package main
import ( import (
"context"
"database/sql" "database/sql"
"fmt"
"io" "io"
"net/http"
"os" "os"
"time" "time"
"screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table"
"github.com/go-chi/chi/v5"
. "github.com/go-jet/jet/v2/postgres"
"github.com/robert-nix/ansihtml"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/muesli/termenv" "github.com/muesli/termenv"
@ -31,12 +21,6 @@ func (w *DatabaseWriter) Write(p []byte) (n int, err error) {
return 0, nil return 0, nil
} }
insertLogStmt := Logs.
INSERT(Logs.Log, Logs.ImageID).
VALUES(string(p), w.imageId)
_, err = insertLogStmt.Exec(w.dbPool)
if err != nil { if err != nil {
return 0, err return 0, err
} else { } else {
@ -44,85 +28,6 @@ func (w *DatabaseWriter) Write(p []byte) (n int, err error) {
} }
} }
func (w *DatabaseWriter) GetImageLogs(ctx context.Context, imageId uuid.UUID) ([]string, error) {
getImageLogsStmt := Logs.
SELECT(Logs.Log).
WHERE(Logs.ImageID.EQ(UUID(imageId)))
logs := []model.Logs{}
err := getImageLogsStmt.QueryContext(ctx, w.dbPool, &logs)
if err != nil {
return []string{}, err
}
stringLogs := make([]string, len(logs))
for i, log := range logs {
stringLogs[i] = log.Log
}
return stringLogs, nil
}
func createLogHandler(logWriter *DatabaseWriter) func(r chi.Router) {
return func(r chi.Router) {
r.Get("/{imageId}", func(w http.ResponseWriter, r *http.Request) {
stringImageId := r.PathValue("imageId")
imageId, err := uuid.Parse(stringImageId)
if err != nil {
w.WriteHeader(http.StatusBadGateway)
return
}
logs, err := logWriter.GetImageLogs(r.Context(), imageId)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
html := ""
imageTag := fmt.Sprintf(`<image src="https://haystack.johncosta.tech/image/%s">`, stringImageId)
for _, log := range logs {
html += fmt.Sprintf("<div>%s</div>", string(ansihtml.ConvertToHTML([]byte(log)))+"\n")
}
css := `
<style>
body {
background-color: #1e1e1e;
color: #f0f0f0;
font-family: sans-serif;
line-height: 1.6;
margin: 0;
padding: 20px;
}
/* Basic styling for code blocks often used for logs */
pre {
background-color: #2a2a2a;
padding: 1em;
border-radius: 4px;
overflow-x: auto;
border: 1px solid #444;
}
code {
font-family: monospace;
}
</style>
`
fullHtml := fmt.Sprintf("<html><head><title>Logs</title>%s</head><body>%s%s</body></html>", css, imageTag, html)
w.Header().Add("Content-Type", "text/html")
w.Write([]byte(fullHtml))
w.WriteHeader(http.StatusOK)
})
}
}
func newDatabaseWriter(dbPool *sql.DB, imageId uuid.UUID) *DatabaseWriter { func newDatabaseWriter(dbPool *sql.DB, imageId uuid.UUID) *DatabaseWriter {
return &DatabaseWriter{ return &DatabaseWriter{
dbPool: dbPool, dbPool: dbPool,

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
"screenmark/screenmark/.gen/haystack/haystack/enum"
"screenmark/screenmark/.gen/haystack/haystack/model" "screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table" . "screenmark/screenmark/.gen/haystack/haystack/table"
@ -18,248 +17,50 @@ type ImageModel struct {
dbPool *sql.DB dbPool *sql.DB
} }
type ImageData struct { func (m ImageModel) Save(ctx context.Context, name string, image []byte, userID uuid.UUID) error {
model.UserImages saveImageStmt := Image.INSERT(Image.ImageName, Image.Image, Image.UserID).
VALUES(name, image, userID)
Image model.Image _, err := saveImageStmt.ExecContext(ctx, m.dbPool)
}
type ProcessingImageData struct {
model.UserImagesToProcess
Image model.Image
}
type UserProcessingImage struct {
model.UserImagesToProcess
Image model.Image
}
func (m ImageModel) Process(ctx context.Context, userId uuid.UUID, image model.Image) (model.UserImagesToProcess, error) {
tx, err := m.dbPool.BeginTx(ctx, nil)
if err != nil {
return model.UserImagesToProcess{}, fmt.Errorf("Failed to begin transaction: %w", err)
}
insertImageStmt := Image.
INSERT(Image.ImageName, Image.Image, Image.Description).
VALUES(image.ImageName, image.Image, image.Description).
RETURNING(Image.ID)
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: %w", insertImageStmt.DebugSql(), err)
}
stmt := UserImagesToProcess.
INSERT(UserImagesToProcess.UserID, UserImagesToProcess.ImageID).
VALUES(userId, insertedImage.ID).
RETURNING(UserImagesToProcess.AllColumns)
userImage := model.UserImagesToProcess{}
err = stmt.QueryContext(ctx, tx, &userImage)
if err != nil {
return model.UserImagesToProcess{}, fmt.Errorf("Could not insert user_image: %w", err)
}
err = tx.Commit()
return userImage, err
}
func (m ImageModel) GetToProcess(ctx context.Context, imageId uuid.UUID) (UserProcessingImage, error) {
getToProcessStmt := SELECT(UserImagesToProcess.AllColumns, Image.ID, Image.ImageName).
FROM(
UserImagesToProcess.INNER_JOIN(
Image, Image.ID.EQ(UserImagesToProcess.ImageID),
),
).
WHERE(UserImagesToProcess.ID.EQ(UUID(imageId)))
images := []UserProcessingImage{}
err := getToProcessStmt.QueryContext(ctx, m.dbPool, &images)
if len(images) != 1 {
return UserProcessingImage{}, fmt.Errorf("Expected 1, got %d\n", len(images))
}
return images[0], err
}
func (m ImageModel) GetToProcessWithData(ctx context.Context, imageId uuid.UUID) (ProcessingImageData, error) {
stmt := SELECT(UserImagesToProcess.AllColumns, Image.AllColumns).
FROM(
UserImagesToProcess.INNER_JOIN(
Image, Image.ID.EQ(UserImagesToProcess.ImageID),
),
).WHERE(UserImagesToProcess.ID.EQ(UUID(imageId)))
images := []ProcessingImageData{}
err := stmt.QueryContext(ctx, m.dbPool, &images)
if len(images) != 1 {
return ProcessingImageData{}, fmt.Errorf("Expected 1, got %d\n", len(images))
}
return images[0], err
}
func (m ImageModel) FinishProcessing(ctx context.Context, imageId uuid.UUID) (model.UserImages, error) {
imageToProcess, err := m.GetToProcess(ctx, imageId)
if err != nil {
return model.UserImages{}, err
}
tx, err := m.dbPool.Begin()
if err != nil {
return model.UserImages{}, err
}
insertImageStmt := UserImages.
INSERT(UserImages.UserID, UserImages.ImageID).
VALUES(imageToProcess.UserID, imageToProcess.ImageID).
RETURNING(UserImages.ID, UserImages.UserID, UserImages.ImageID)
userImage := model.UserImages{}
err = insertImageStmt.QueryContext(ctx, tx, &userImage)
if err != nil {
return model.UserImages{}, err
}
// Hacky. Update the status before removing so we can get our regular triggers
// to work.
updateStatusStmt := UserImagesToProcess.
UPDATE(UserImagesToProcess.Status).
SET(model.Progress_Complete).
WHERE(UserImagesToProcess.ID.EQ(UUID(imageToProcess.ID)))
_, err = updateStatusStmt.ExecContext(ctx, tx)
if err != nil {
return model.UserImages{}, err
}
// TODO:
// We cannot delete the image to process because our events rely on it.
// This indicates our DB structure with the two tables might need some adjusting.
// Or re-doing all together perhaps.
// (switching to a one table (user_images) could work)
// But for now, we can just not delete the images to process and set them to complete
// removeProcessingStmt := UserImagesToProcess.
// DELETE().
// WHERE(UserImagesToProcess.ID.EQ(UUID(imageToProcess.ID)))
//
// _, err = removeProcessingStmt.ExecContext(ctx, tx)
// if err != nil {
// return model.UserImages{}, err
// }
err = tx.Commit()
return userImage, err
}
func (m ImageModel) GetImageToProcessID(ctx context.Context, imageID uuid.UUID) (uuid.UUID, error) {
getImageToProcessIDStmt := UserImagesToProcess.
SELECT(UserImagesToProcess.ID).
WHERE(UserImagesToProcess.ImageID.EQ(UUID(imageID)))
imageToProcess := model.UserImagesToProcess{}
err := getImageToProcessIDStmt.QueryContext(ctx, m.dbPool, &imageToProcess)
return imageToProcess.ID, err
}
func (m ImageModel) SetNotStarted(ctx context.Context, processingImageId uuid.UUID) error {
startProcessingStmt := UserImagesToProcess.
UPDATE(UserImagesToProcess.Status).
SET(model.Progress_NotStarted).
WHERE(UserImagesToProcess.ID.EQ(UUID(processingImageId)))
_, err := startProcessingStmt.ExecContext(ctx, m.dbPool)
return err return err
} }
func (m ImageModel) StartProcessing(ctx context.Context, processingImageId uuid.UUID) error { func (m ImageModel) Get(ctx context.Context, imageID uuid.UUID) (model.Image, bool, error) {
startProcessingStmt := UserImagesToProcess. getImageStmt := Image.SELECT(Image.AllColumns.Except(Image.Image)).WHERE(Image.ID.EQ(UUID(imageID)))
UPDATE(UserImagesToProcess.Status).
SET(model.Progress_InProgress).
WHERE(UserImagesToProcess.ID.EQ(UUID(processingImageId)))
_, err := startProcessingStmt.ExecContext(ctx, m.dbPool)
return err
}
func (m ImageModel) Get(ctx context.Context, imageId uuid.UUID) (model.Image, error) {
getImageStmt := Image.SELECT(Image.AllColumns).
WHERE(Image.ID.EQ(UUID(imageId)))
image := model.Image{} image := model.Image{}
err := getImageStmt.QueryContext(ctx, m.dbPool, &image) err := getImageStmt.QueryContext(ctx, m.dbPool, &image)
return image, err return image, err != qrm.ErrNoRows, err
} }
func (m ImageModel) GetProcessing(ctx context.Context, userId uuid.UUID) ([]UserProcessingImage, error) { func (m ImageModel) Update(ctx context.Context, image model.Image) (model.Image, error) {
getProcessingStmt := SELECT(UserImagesToProcess.AllColumns, Image.ID, Image.ImageName). updateImageStmt := Image.UPDATE(Image.MutableColumns.Except(Image.Image)).
FROM( MODEL(image).
UserImagesToProcess.INNER_JOIN( WHERE(Image.ID.EQ(UUID(image.ID))).
Image, Image.ID.EQ(UserImagesToProcess.ImageID), RETURNING(Image.AllColumns.Except(Image.Image))
),
).WHERE(
UserImagesToProcess.UserID.EQ(UUID(userId)).
AND(UserImagesToProcess.Status.NOT_EQ(enum.Progress.Complete)),
)
images := []UserProcessingImage{} updatedImage := model.Image{}
err := getProcessingStmt.QueryContext(ctx, m.dbPool, &images) err := updateImageStmt.QueryContext(ctx, m.dbPool, &updatedImage)
return images, err return updatedImage, err
} }
func (m ImageModel) AddDescription(ctx context.Context, imageId uuid.UUID, description string) error { func (m ImageModel) Delete(ctx context.Context, imageID, userID uuid.UUID) (bool, error) {
updateImageStmt := Image.UPDATE(Image.Description). deleteImageStmt := Image.DELETE().WHERE(Image.ID.EQ(UUID(imageID)).AND(Image.UserID.EQ(UUID(userID))))
SET(description).
WHERE(Image.ID.EQ(UUID(imageId)))
_, err := updateImageStmt.ExecContext(ctx, m.dbPool) r, err := deleteImageStmt.ExecContext(ctx, m.dbPool)
if err != nil {
return err return false, fmt.Errorf("deleting image: %w", err)
} }
func (m ImageModel) DeleteUserImage(ctx context.Context, imageID uuid.UUID) error { rowsAffected, err := r.RowsAffected()
deleteImageStmt := UserImages.DELETE(). if err != nil {
WHERE(UserImages.ImageID.EQ(UUID(imageID))) return false, fmt.Errorf("unreachable: %w", err)
_, err := deleteImageStmt.ExecContext(ctx, m.dbPool)
return err
} }
func (m ImageModel) Delete(ctx context.Context, imageID uuid.UUID) error { return rowsAffected > 0, nil
deleteImageStmt := Image.DELETE().
WHERE(Image.ID.EQ(UUID(imageID)))
_, err := deleteImageStmt.ExecContext(ctx, m.dbPool)
return err
}
func (m ImageModel) IsUserAuthorized(ctx context.Context, imageId uuid.UUID, userId uuid.UUID) bool {
getImageUserId := UserImages.SELECT(UserImages.UserID).WHERE(UserImages.ImageID.EQ(UUID(imageId)))
getProcessingImageUserId := UserImagesToProcess.SELECT(UserImagesToProcess.UserID).WHERE(UserImagesToProcess.ImageID.EQ(UUID(imageId)))
userImage := model.UserImages{}
userProcessingImage := model.UserImagesToProcess{}
err1 := getImageUserId.QueryContext(ctx, m.dbPool, &userImage)
err2 := getProcessingImageUserId.QueryContext(ctx, m.dbPool, &userProcessingImage)
return (err1 == nil || err1 == qrm.ErrNoRows) && (err2 == nil || err2 == qrm.ErrNoRows) && (userImage.UserID.String() == userId.String() || userProcessingImage.UserID.String() == userId.String())
} }
func NewImageModel(db *sql.DB) ImageModel { func NewImageModel(db *sql.DB) ImageModel {

View File

@ -3,7 +3,6 @@ package models
import ( import (
"context" "context"
"database/sql" "database/sql"
"fmt"
"screenmark/screenmark/.gen/haystack/haystack/model" "screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table" . "screenmark/screenmark/.gen/haystack/haystack/table"
@ -12,22 +11,18 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
type ListModel struct { type StackModel struct {
dbPool *sql.DB dbPool *sql.DB
} }
type ListWithItems struct { type StackWithItems struct {
model.Lists model.Stacks
Schema struct {
model.Schemas
SchemaItems []model.SchemaItems SchemaItems []model.SchemaItems
} }
}
type ImageWithSchema struct { type ImageWithSchema struct {
model.ImageLists model.ImageStacks
Items []model.ImageSchemaItems Items []model.ImageSchemaItems
} }
@ -41,35 +36,33 @@ type IDValue struct {
// SELECT for lists // SELECT for lists
// ======================================== // ========================================
func (m ListModel) List(ctx context.Context, userId uuid.UUID) ([]ListWithItems, error) { func (m StackModel) List(ctx context.Context, userId uuid.UUID) ([]StackWithItems, error) {
getListsWithItems := SELECT( getStacksWithItems := SELECT(
Lists.AllColumns, Stacks.AllColumns,
Schemas.AllColumns,
SchemaItems.AllColumns, SchemaItems.AllColumns,
). ).
FROM( FROM(
Lists. Stacks.
INNER_JOIN(Schemas, Schemas.ListID.EQ(Lists.ID)). INNER_JOIN(SchemaItems, SchemaItems.StackID.EQ(Stacks.ID)),
INNER_JOIN(SchemaItems, SchemaItems.SchemaID.EQ(Schemas.ID)),
). ).
WHERE(Lists.UserID.EQ(UUID(userId))) WHERE(Stacks.UserID.EQ(UUID(userId)))
lists := []ListWithItems{} lists := []StackWithItems{}
err := getListsWithItems.QueryContext(ctx, m.dbPool, &lists) err := getStacksWithItems.QueryContext(ctx, m.dbPool, &lists)
return lists, err return lists, err
} }
func (m ListModel) ListItems(ctx context.Context, listID uuid.UUID) ([]ImageWithSchema, error) { func (m StackModel) ListItems(ctx context.Context, listID uuid.UUID) ([]ImageWithSchema, error) {
getListItems := SELECT( getListItems := SELECT(
ImageLists.AllColumns, ImageStacks.AllColumns,
ImageSchemaItems.AllColumns, ImageSchemaItems.AllColumns,
). ).
FROM( FROM(
ImageLists. ImageStacks.
INNER_JOIN(ImageSchemaItems, ImageSchemaItems.ImageID.EQ(ImageLists.ImageID)), INNER_JOIN(ImageSchemaItems, ImageSchemaItems.ImageID.EQ(ImageStacks.ImageID)),
). ).
WHERE(ImageLists.ListID.EQ(UUID(listID))) WHERE(ImageStacks.StackID.EQ(UUID(listID)))
listItems := make([]ImageWithSchema, 0) listItems := make([]ImageWithSchema, 0)
err := getListItems.QueryContext(ctx, m.dbPool, &listItems) err := getListItems.QueryContext(ctx, m.dbPool, &listItems)
@ -77,174 +70,45 @@ func (m ListModel) ListItems(ctx context.Context, listID uuid.UUID) ([]ImageWith
return listItems, err return listItems, err
} }
// ======================================== func (m StackModel) Get(ctx context.Context, listID uuid.UUID) (model.Stacks, error) {
// SELECT for specific items getStackStmt := Stacks.SELECT(Stacks.AllColumns).WHERE(Stacks.ID.EQ(UUID(listID)))
// ========================================
func (m ListModel) GetProcessing(ctx context.Context, processingListID uuid.UUID) (model.ProcessingLists, error) { stack := model.Stacks{}
getProcessingListStmt := ProcessingLists. err := getStackStmt.QueryContext(ctx, m.dbPool, &stack)
SELECT(ProcessingLists.AllColumns).
WHERE(ProcessingLists.ID.EQ(UUID(processingListID)))
list := model.ProcessingLists{} return stack, err
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 // INSERT methods
// ======================================== // ========================================
func (m ListModel) Save(ctx context.Context, userId uuid.UUID, name string, description string, schemaItems []model.SchemaItems) (ListWithItems, error) { func (m StackModel) Save(ctx context.Context, userID uuid.UUID, name string, description string) (model.Stacks, error) {
tx, err := m.dbPool.BeginTx(ctx, nil) saveListStmt := Stacks.
INSERT(Stacks.UserID, Stacks.Name, Stacks.Description).
VALUES(userID, name, description).
RETURNING(Stacks.ID, Stacks.UserID, Stacks.Name, Stacks.Description, Stacks.CreatedAt)
stmt := Lists.INSERT(Lists.UserID, Lists.Name, Lists.Description). list := model.Stacks{}
VALUES(userId, name, description). err := saveListStmt.QueryContext(ctx, m.dbPool, &list)
RETURNING(Lists.ID, Lists.Name, Lists.Description)
newList := model.Lists{} return list, err
err = stmt.QueryContext(ctx, tx, &newList)
if err != nil {
tx.Rollback()
return ListWithItems{}, fmt.Errorf("Could not save new list. %s", err)
} }
insertSchemaStmt := Schemas.INSERT(Schemas.ListID). func (m StackModel) SaveItems(ctx context.Context, items []model.SchemaItems) error {
VALUES(newList.ID). saveItemsStmt := SchemaItems.INSERT(SchemaItems.AllColumns).MODELS(items)
RETURNING(Schemas.ID)
newSchema := model.Schemas{} _, err := saveItemsStmt.ExecContext(ctx, m.dbPool)
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) SaveInto(ctx context.Context, listID uuid.UUID, imageID uuid.UUID, schemaValues []IDValue) error {
tx, err := m.dbPool.BeginTx(ctx, nil)
if err != nil {
return err return err
} }
var imageList model.ImageLists func (m StackModel) SaveImage(ctx context.Context, imageID uuid.UUID, stackID uuid.UUID) error {
stmt := ImageLists.INSERT(ImageLists.ListID, ImageLists.ImageID). saveImageStmt := ImageStacks.
VALUES(listID, imageID). INSERT(ImageStacks.ImageID, ImageStacks.StackID).
RETURNING(ImageLists.ID) VALUES(imageID, stackID)
err = stmt.QueryContext(ctx, m.dbPool, &imageList) _, err := saveImageStmt.ExecContext(ctx, m.dbPool)
if err != nil {
tx.Rollback()
return fmt.Errorf("Could not insert new list. %s", err)
}
imageSchemaItems := make([]model.ImageSchemaItems, len(schemaValues))
for i, v := range schemaValues {
parsedId, err := uuid.Parse(v.ID)
if err != nil {
return err
}
imageSchemaItems[i].SchemaItemID = parsedId
imageSchemaItems[i].ImageID = imageList.ID
imageSchemaItems[i].Value = &v.Value
}
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 return err
} }
@ -253,11 +117,11 @@ func (m ListModel) SaveProcessing(ctx context.Context, userID uuid.UUID, title s
// DELETE methods // DELETE methods
// ======================================== // ========================================
func (m ListModel) DeleteImage(ctx context.Context, listID uuid.UUID, imageID uuid.UUID) error { func (m StackModel) DeleteImage(ctx context.Context, listID uuid.UUID, imageID uuid.UUID) error {
deleteImageListStmt := ImageLists.DELETE(). deleteImageListStmt := ImageStacks.DELETE().
WHERE( WHERE(
ImageLists.ListID.EQ(UUID(listID)). ImageStacks.StackID.EQ(UUID(listID)).
AND(ImageLists.ImageID.EQ(UUID(imageID))), AND(ImageStacks.ImageID.EQ(UUID(imageID))),
) )
_, err := deleteImageListStmt.ExecContext(ctx, m.dbPool) _, err := deleteImageListStmt.ExecContext(ctx, m.dbPool)
@ -265,60 +129,14 @@ func (m ListModel) DeleteImage(ctx context.Context, listID uuid.UUID, imageID uu
return err return err
} }
func (m ListModel) Delete(ctx context.Context, listID uuid.UUID, userID uuid.UUID) error { func (m StackModel) Delete(ctx context.Context, listID uuid.UUID, userID uuid.UUID) error {
// First verify the list belongs to the user deleteStackStmt := Stacks.DELETE().WHERE(Stacks.ID.EQ(UUID(listID)).AND(Stacks.UserID.EQ(UUID(userID))))
checkOwnershipStmt := Lists.
SELECT(Lists.ID).
WHERE(Lists.ID.EQ(UUID(listID)).AND(Lists.UserID.EQ(UUID(userID))))
var existingList model.Lists _, err := deleteStackStmt.ExecContext(ctx, m.dbPool)
err := checkOwnershipStmt.QueryContext(ctx, m.dbPool, &existingList)
if err != nil { return err
return fmt.Errorf("could not verify list ownership: %w", err)
} }
// Start a transaction to ensure all deletions happen atomically func NewListModel(db *sql.DB) StackModel {
tx, err := m.dbPool.BeginTx(ctx, nil) return StackModel{dbPool: db}
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}
} }

View File

@ -49,28 +49,20 @@ func (m UserModel) Save(ctx context.Context, user model.Users) (model.Users, err
} }
type UserImageWithImage struct { type UserImageWithImage struct {
model.UserImages
Image struct {
model.Image model.Image
ImageLists []model.ImageLists ImageStacks []model.ImageStacks
}
} }
func (m UserModel) GetUserImages(ctx context.Context, userId uuid.UUID) ([]UserImageWithImage, error) { func (m UserModel) GetUserImages(ctx context.Context, userId uuid.UUID) ([]UserImageWithImage, error) {
getUserImagesStmt := SELECT( getUserImagesStmt := SELECT(
UserImages.AllColumns, Image.AllColumns.Except(Image.Image),
Image.ID, ImageStacks.AllColumns,
Image.ImageName,
Image.Description,
ImageLists.AllColumns,
). ).
FROM( FROM(
UserImages. Image.
INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID)). LEFT_JOIN(ImageStacks, ImageStacks.ImageID.EQ(ImageStacks.ID)),
LEFT_JOIN(ImageLists, ImageLists.ImageID.EQ(UserImages.ImageID)),
). ).
WHERE(UserImages.UserID.EQ(UUID(userId))) WHERE(Image.UserID.EQ(UUID(userId)))
userImages := []UserImageWithImage{} userImages := []UserImageWithImage{}
err := getUserImagesStmt.QueryContext(ctx, m.dbPool, &userImages) err := getUserImagesStmt.QueryContext(ctx, m.dbPool, &userImages)
@ -79,16 +71,12 @@ func (m UserModel) GetUserImages(ctx context.Context, userId uuid.UUID) ([]UserI
} }
type ListsWithImages struct { type ListsWithImages struct {
model.Lists model.Stacks
Schema struct {
model.Schemas
SchemaItems []model.SchemaItems SchemaItems []model.SchemaItems
}
Images []struct { Images []struct {
model.ImageLists model.ImageStacks
Items []model.ImageSchemaItems Items []model.ImageSchemaItems
} }
@ -96,20 +84,18 @@ type ListsWithImages struct {
func (m UserModel) ListWithImages(ctx context.Context, userId uuid.UUID) ([]ListsWithImages, error) { func (m UserModel) ListWithImages(ctx context.Context, userId uuid.UUID) ([]ListsWithImages, error) {
stmt := SELECT( stmt := SELECT(
Lists.AllColumns, Stacks.AllColumns,
ImageLists.AllColumns, ImageStacks.AllColumns,
Schemas.AllColumns,
SchemaItems.AllColumns, SchemaItems.AllColumns,
ImageSchemaItems.AllColumns, ImageSchemaItems.AllColumns,
). ).
FROM( FROM(
Lists. Stacks.
INNER_JOIN(Schemas, Schemas.ListID.EQ(Lists.ID)). INNER_JOIN(SchemaItems, SchemaItems.StackID.EQ(Stacks.ID)).
INNER_JOIN(SchemaItems, SchemaItems.SchemaID.EQ(Schemas.ID)). LEFT_JOIN(ImageStacks, ImageStacks.StackID.EQ(Stacks.ID)).
LEFT_JOIN(ImageLists, ImageLists.ListID.EQ(Lists.ID)). LEFT_JOIN(ImageSchemaItems, ImageSchemaItems.ImageID.EQ(ImageStacks.ID)),
LEFT_JOIN(ImageSchemaItems, ImageSchemaItems.ImageID.EQ(ImageLists.ID)),
). ).
WHERE(Lists.UserID.EQ(UUID(userId))) WHERE(Stacks.UserID.EQ(UUID(userId)))
lists := []ListsWithImages{} lists := []ListsWithImages{}
err := stmt.QueryContext(ctx, m.dbPool, &lists) err := stmt.QueryContext(ctx, m.dbPool, &lists)

View File

@ -0,0 +1,95 @@
package processor
import (
"context"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/models"
"sync"
"github.com/charmbracelet/log"
)
const IMAGE_PROCESS_AT_A_TIME = 10
type ImageProcessor struct {
imageModel models.ImageModel
logger *log.Logger
descriptionAgent agents.DescriptionAgent
listAgent client.AgentClient
// TODO: add the notifier here
processor *Processor[model.Image]
}
func (p *ImageProcessor) setImageToProcess(ctx context.Context, image model.Image) {
updatedImage := model.Image{
ID: image.ID,
Status: model.Progress_InProgress,
}
_, err := p.imageModel.Update(ctx, updatedImage)
if err != nil {
// TODO: what can we actually do here for the errors?
// We can't stop the work for the others
p.logger.Error("failed to update image", "err", err)
// TODO: we can use context here to actually pass some information through
return
}
}
func (p *ImageProcessor) describe(ctx context.Context, image model.Image) {
descriptionSubLogger := p.logger.With("describe image", image.ID)
err := p.descriptionAgent.Describe(descriptionSubLogger, image.ID, image.ImageName, image.Image)
if err != nil {
// Again, wtf do we do?
// Although i think the agent actually returns an error when it's finished
p.logger.Error("failed to describe image", "err", err)
return
}
}
func (p *ImageProcessor) extractInfo(ctx context.Context, image model.Image) {
err := p.listAgent.RunAgent(*image.UserID, image.ID, image.ImageName, image.Image)
if err != nil {
// Again, wtf do we do?
// Although i think the agent actually returns an error when it's finished
p.logger.Error("failed to process image", "err", err)
return
}
}
func (p *ImageProcessor) processImage(image model.Image) {
ctx := context.Background()
p.setImageToProcess(ctx, image)
var wg sync.WaitGroup
wg.Add(2)
go func() {
p.describe(ctx, image)
wg.Done()
}()
go func() {
p.extractInfo(ctx, image)
wg.Done()
}()
wg.Wait()
}
func CreateImageProcessor(logger *log.Logger, imageModel models.ImageModel) ImageProcessor {
imageProcessor := ImageProcessor{imageModel: imageModel, logger: logger}
imageProcessor.processor = NewProcessor(int(IMAGE_PROCESS_AT_A_TIME), imageProcessor.processImage)
return imageProcessor
}

View File

@ -0,0 +1,19 @@
package processor
type Processor[TMessage any] struct {
queue chan TMessage
process func(message TMessage)
}
func (p *Processor[TMessage]) Work() {
for msg := range p.queue {
p.process(msg)
}
}
func NewProcessor[TMessage any](bufferSize int, process func(message TMessage)) *Processor[TMessage] {
return &Processor[TMessage]{
queue: make(chan TMessage, bufferSize),
process: process,
}
}

View File

@ -2,12 +2,10 @@ package main
import ( import (
"database/sql" "database/sql"
"os"
"screenmark/screenmark/agents/client" "screenmark/screenmark/agents/client"
"screenmark/screenmark/auth" "screenmark/screenmark/auth"
"screenmark/screenmark/images" "screenmark/screenmark/images"
"screenmark/screenmark/limits" "screenmark/screenmark/limits"
"screenmark/screenmark/models"
"screenmark/screenmark/stacks" "screenmark/screenmark/stacks"
ourmiddleware "screenmark/screenmark/middleware" ourmiddleware "screenmark/screenmark/middleware"
@ -25,33 +23,14 @@ func (client TestAiClient) GetImageInfo(imageName string, imageData []byte) (cli
} }
func setupRouter(db *sql.DB, jwtManager *ourmiddleware.JwtManager) chi.Router { func setupRouter(db *sql.DB, jwtManager *ourmiddleware.JwtManager) chi.Router {
imageModel := models.NewImageModel(db)
stackModel := models.NewListModel(db)
limitsManager := limits.CreateLimitsManager(db) limitsManager := limits.CreateLimitsManager(db)
processImageLogger := createLogger("Process Image", os.Stdout)
processImage := ProcessImage(processImageLogger, db)
stackHandler := stacks.CreateStackHandler(db, limitsManager, jwtManager) stackHandler := stacks.CreateStackHandler(db, limitsManager, jwtManager)
authHandler := auth.CreateAuthHandler(db, jwtManager) authHandler := auth.CreateAuthHandler(db, jwtManager)
imageHandler := images.CreateImageHandler(db, limitsManager, processImage, jwtManager) imageHandler := images.CreateImageHandler(db, limitsManager, jwtManager)
notifier := NewNotifier[Notification](10) 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, &notifier)
go ListenNewStackEvents(db)
go ListenProcessingStackStatus(db, stackModel, &notifier)
}
r := chi.NewRouter() r := chi.NewRouter()
r.Use(middleware.Logger) r.Use(middleware.Logger)
@ -67,11 +46,5 @@ func setupRouter(db *sql.DB, jwtManager *ourmiddleware.JwtManager) chi.Router {
r.Get("/", CreateEventsHandler(&notifier)) r.Get("/", CreateEventsHandler(&notifier))
}) })
logWriter := DatabaseWriter{
dbPool: db,
}
r.Route("/logs", createLogHandler(&logWriter))
return r return r
} }

View File

@ -9,72 +9,43 @@ CREATE TYPE haystack.progress AS ENUM('not-started','in-progress', 'complete');
/* -----| Schema tables |----- */ /* -----| Schema tables |----- */
CREATE TABLE haystack.users ( CREATE TABLE haystack.users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL email TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
); );
CREATE TABLE haystack.image ( CREATE TABLE haystack.image (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES haystack.users (id),
image_name TEXT NOT NULL, image_name TEXT NOT NULL,
description TEXT NOT NULL, description TEXT NOT NULL,
image BYTEA NOT NULL
);
CREATE TABLE haystack.user_images_to_process (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
status haystack.progress NOT NULL DEFAULT 'not-started', status haystack.progress NOT NULL DEFAULT 'not-started',
image_id uuid NOT NULL UNIQUE REFERENCES haystack.image (id) ON DELETE CASCADE,
user_id uuid NOT NULL REFERENCES haystack.users (id)
);
CREATE TABLE haystack.user_images ( image BYTEA NOT NULL,
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
image_id uuid NOT NULL UNIQUE REFERENCES haystack.image (id) ON DELETE CASCADE,
user_id uuid NOT NULL REFERENCES haystack.users (id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
); );
CREATE TABLE haystack.logs ( CREATE TABLE haystack.stacks (
log TEXT NOT NULL,
image_id UUID NOT NULL REFERENCES haystack.image (id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
CREATE TABLE haystack.lists (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES haystack.users (id), user_id UUID NOT NULL REFERENCES haystack.users (id),
status haystack.progress NOT NULL DEFAULT 'not-started',
name TEXT NOT NULL, name TEXT NOT NULL,
description TEXT NOT NULL, description TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
); );
CREATE TABLE haystack.processing_lists ( CREATE TABLE haystack.image_stacks (
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(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
image_id UUID NOT NULL REFERENCES haystack.image (id) ON DELETE CASCADE, image_id UUID NOT NULL REFERENCES haystack.image (id) ON DELETE CASCADE,
list_id UUID NOT NULL REFERENCES haystack.lists (id) ON DELETE CASCADE stack_id UUID NOT NULL REFERENCES haystack.stacks (id) ON DELETE CASCADE
);
CREATE TABLE haystack.schemas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
list_id UUID NOT NULL REFERENCES haystack.lists (id) ON DELETE CASCADE
); );
CREATE TABLE haystack.schema_items ( CREATE TABLE haystack.schema_items (
@ -84,7 +55,7 @@ CREATE TABLE haystack.schema_items (
value TEXT NOT NULL, value TEXT NOT NULL,
description TEXT NOT NULL, description TEXT NOT NULL,
schema_id UUID NOT NULL REFERENCES haystack.schemas (id) stack_id UUID NOT NULL REFERENCES haystack.stacks (id) ON DELETE CASCADE
); );
CREATE TABLE haystack.image_schema_items ( CREATE TABLE haystack.image_schema_items (
@ -92,68 +63,6 @@ CREATE TABLE haystack.image_schema_items (
value TEXT, value TEXT,
schema_item_id UUID NOT NULL REFERENCES haystack.schema_items (id), schema_item_id UUID NOT NULL REFERENCES haystack.schema_items (id) ON DELETE CASCADE,
image_id UUID NOT NULL REFERENCES haystack.image_lists (id) ON DELETE CASCADE image_id UUID NOT NULL REFERENCES haystack.image_stacks (id) ON DELETE CASCADE
); );
/* -----| Indexes |----- */
/* -----| Stored Procedures |----- */
CREATE OR REPLACE FUNCTION notify_new_image()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify('new_image', NEW.id::texT);
RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION notify_new_processing_image_status()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.status <> 'not-started' THEN
PERFORM pg_notify('new_processing_image_status', NEW.id::text || NEW.status::text);
END IF;
RETURN NEW;
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
ON haystack.user_images_to_process
FOR EACH ROW
EXECUTE PROCEDURE notify_new_image();
CREATE OR REPLACE TRIGGER on_update_image_progress
AFTER UPDATE OF status
ON haystack.user_images_to_process
FOR EACH ROW
EXECUTE PROCEDURE notify_new_processing_image_status();
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();

View File

@ -2,14 +2,11 @@ package stacks
import ( import (
"database/sql" "database/sql"
"fmt"
"net/http" "net/http"
"os" "os"
. "screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/limits" "screenmark/screenmark/limits"
"screenmark/screenmark/middleware" "screenmark/screenmark/middleware"
"screenmark/screenmark/models" "screenmark/screenmark/models"
"strings"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
@ -20,7 +17,7 @@ type StackHandler struct {
logger *log.Logger logger *log.Logger
imageModel models.ImageModel imageModel models.ImageModel
stackModel models.ListModel stackModel models.StackModel
limitsManager limits.LimitsManagerMethods limitsManager limits.LimitsManagerMethods
@ -125,8 +122,13 @@ func (h *StackHandler) deleteImageFromStack(w http.ResponseWriter, r *http.Reque
return return
} }
isAuthorized := h.imageModel.IsUserAuthorized(ctx, imageID, userID) stack, err := h.stackModel.Get(ctx, listID)
if !isAuthorized { if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
if stack.UserID != userID {
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
return return
} }
@ -143,9 +145,7 @@ func (h *StackHandler) deleteImageFromStack(w http.ResponseWriter, r *http.Reque
type CreateStackBody struct { type CreateStackBody struct {
Title string `json:"title"` Title string `json:"title"`
Description string `json:"description"`
// 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) { func (h *StackHandler) createStack(body CreateStackBody, w http.ResponseWriter, r *http.Request) {
@ -155,25 +155,7 @@ func (h *StackHandler) createStack(body CreateStackBody, w http.ResponseWriter,
return return
} }
// Convert fields string to basic schema items _, err = h.stackModel.Save(ctx, userID, body.Title, body.Description)
// 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 { if err != nil {
h.logger.Warn("could not save stack", "err", err) h.logger.Warn("could not save stack", "err", err)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)

View File

@ -67,7 +67,7 @@ export const ImageComponentFullHeight: Component<ImageComponentProps> = (props)
const [accessToken] = createResource(getAccessToken); const [accessToken] = createResource(getAccessToken);
return ( return (
<> <Suspense>
<div class="relative w-full flex justify-center"> <div class="relative w-full flex justify-center">
<A href={`/image/${props.ID}`} class="flex w-full"> <A href={`/image/${props.ID}`} class="flex w-full">
<img <img
@ -107,6 +107,6 @@ export const ImageComponentFullHeight: Component<ImageComponentProps> = (props)
</Dialog.Content> </Dialog.Content>
</Dialog.Portal> </Dialog.Portal>
</Dialog.Root> </Dialog.Root>
</> </Suspense>
); );
}; };