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 (
"github.com/google/uuid"
"time"
)
type Image struct {
ID uuid.UUID `sql:"primary_key"`
UserID *uuid.UUID
ImageName string
Description string
Status Progress
Image []byte
CreatedAt *time.Time
}

View File

@ -11,8 +11,8 @@ import (
"github.com/google/uuid"
)
type ImageLists struct {
type ImageStacks struct {
ID uuid.UUID `sql:"primary_key"`
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
Value 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"
)
type Lists struct {
type Stacks struct {
ID uuid.UUID `sql:"primary_key"`
UserID uuid.UUID
Status Progress
Name string
Description string
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 (
"github.com/google/uuid"
"time"
)
type Users struct {
ID uuid.UUID `sql:"primary_key"`
Email string
CreatedAt *time.Time
}

View File

@ -18,13 +18,15 @@ type imageTable struct {
// Columns
ID postgres.ColumnString
UserID postgres.ColumnString
ImageName postgres.ColumnString
Description postgres.ColumnString
Image postgres.ColumnBytea
Status postgres.ColumnString
Image postgres.ColumnString
CreatedAt postgres.ColumnTimestampz
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type ImageTable struct {
@ -63,12 +65,14 @@ func newImageTable(schemaName, tableName, alias string) *ImageTable {
func newImageTableImpl(schemaName, tableName, alias string) imageTable {
var (
IDColumn = postgres.StringColumn("id")
UserIDColumn = postgres.StringColumn("user_id")
ImageNameColumn = postgres.StringColumn("image_name")
DescriptionColumn = postgres.StringColumn("description")
ImageColumn = postgres.ByteaColumn("image")
allColumns = postgres.ColumnList{IDColumn, ImageNameColumn, DescriptionColumn, ImageColumn}
mutableColumns = postgres.ColumnList{ImageNameColumn, DescriptionColumn, ImageColumn}
defaultColumns = postgres.ColumnList{IDColumn}
StatusColumn = postgres.StringColumn("status")
ImageColumn = postgres.StringColumn("image")
CreatedAtColumn = postgres.TimestampzColumn("created_at")
allColumns = postgres.ColumnList{IDColumn, UserIDColumn, ImageNameColumn, DescriptionColumn, StatusColumn, ImageColumn, CreatedAtColumn}
mutableColumns = postgres.ColumnList{UserIDColumn, ImageNameColumn, DescriptionColumn, StatusColumn, ImageColumn, CreatedAtColumn}
)
return imageTable{
@ -76,12 +80,14 @@ func newImageTableImpl(schemaName, tableName, alias string) imageTable {
//Columns
ID: IDColumn,
UserID: UserIDColumn,
ImageName: ImageNameColumn,
Description: DescriptionColumn,
Status: StatusColumn,
Image: ImageColumn,
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 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
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type ImageSchemaItemsTable struct {
@ -68,7 +67,6 @@ func newImageSchemaItemsTableImpl(schemaName, tableName, alias string) imageSche
ImageIDColumn = postgres.StringColumn("image_id")
allColumns = postgres.ColumnList{IDColumn, ValueColumn, SchemaItemIDColumn, ImageIDColumn}
mutableColumns = postgres.ColumnList{ValueColumn, SchemaItemIDColumn, ImageIDColumn}
defaultColumns = postgres.ColumnList{IDColumn}
)
return imageSchemaItemsTable{
@ -82,6 +80,5 @@ func newImageSchemaItemsTableImpl(schemaName, tableName, alias string) imageSche
AllColumns: allColumns,
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
Value postgres.ColumnString
Description postgres.ColumnString
SchemaID postgres.ColumnString
StackID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type SchemaItemsTable struct {
@ -67,10 +66,9 @@ func newSchemaItemsTableImpl(schemaName, tableName, alias string) schemaItemsTab
ItemColumn = postgres.StringColumn("item")
ValueColumn = postgres.StringColumn("value")
DescriptionColumn = postgres.StringColumn("description")
SchemaIDColumn = postgres.StringColumn("schema_id")
allColumns = postgres.ColumnList{IDColumn, ItemColumn, ValueColumn, DescriptionColumn, SchemaIDColumn}
mutableColumns = postgres.ColumnList{ItemColumn, ValueColumn, DescriptionColumn, SchemaIDColumn}
defaultColumns = postgres.ColumnList{IDColumn}
StackIDColumn = postgres.StringColumn("stack_id")
allColumns = postgres.ColumnList{IDColumn, ItemColumn, ValueColumn, DescriptionColumn, StackIDColumn}
mutableColumns = postgres.ColumnList{ItemColumn, ValueColumn, DescriptionColumn, StackIDColumn}
)
return schemaItemsTable{
@ -81,10 +79,9 @@ func newSchemaItemsTableImpl(schemaName, tableName, alias string) schemaItemsTab
Item: ItemColumn,
Value: ValueColumn,
Description: DescriptionColumn,
SchemaID: SchemaIDColumn,
StackID: StackIDColumn,
AllColumns: allColumns,
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.
func UseSchema(schema string) {
Image = Image.FromSchema(schema)
ImageLists = ImageLists.FromSchema(schema)
ImageSchemaItems = ImageSchemaItems.FromSchema(schema)
Lists = Lists.FromSchema(schema)
Logs = Logs.FromSchema(schema)
ProcessingLists = ProcessingLists.FromSchema(schema)
ImageStacks = ImageStacks.FromSchema(schema)
SchemaItems = SchemaItems.FromSchema(schema)
Schemas = Schemas.FromSchema(schema)
UserImages = UserImages.FromSchema(schema)
UserImagesToProcess = UserImagesToProcess.FromSchema(schema)
Stacks = Stacks.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
ID postgres.ColumnString
Email postgres.ColumnString
CreatedAt postgres.ColumnTimestampz
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type UsersTable struct {
@ -62,9 +62,9 @@ func newUsersTableImpl(schemaName, tableName, alias string) usersTable {
var (
IDColumn = postgres.StringColumn("id")
EmailColumn = postgres.StringColumn("email")
allColumns = postgres.ColumnList{IDColumn, EmailColumn}
mutableColumns = postgres.ColumnList{EmailColumn}
defaultColumns = postgres.ColumnList{IDColumn}
CreatedAtColumn = postgres.TimestampzColumn("created_at")
allColumns = postgres.ColumnList{IDColumn, EmailColumn, CreatedAtColumn}
mutableColumns = postgres.ColumnList{EmailColumn, CreatedAtColumn}
)
return usersTable{
@ -73,9 +73,9 @@ func newUsersTableImpl(schemaName, tableName, alias string) usersTable {
//Columns
ID: IDColumn,
Email: EmailColumn,
CreatedAt: CreatedAtColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}

View File

@ -76,7 +76,7 @@ type createNewListArguments struct {
type CreateListAgent struct {
client client.AgentClient
listModel models.ListModel
listModel models.StackModel
}
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
}
func NewCreateListAgent(log *log.Logger, listModel models.ListModel) CreateListAgent {
func NewCreateListAgent(log *log.Logger, listModel models.StackModel) CreateListAgent {
client := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: createListAgentPrompt,
Log: log,

View File

@ -3,6 +3,7 @@ package agents
import (
"context"
"fmt"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/models"
@ -26,7 +27,7 @@ type DescriptionAgent struct {
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{
Model: "policy/images",
Temperature: 0.3,
@ -51,7 +52,11 @@ func (agent DescriptionAgent) Describe(log *log.Logger, imageId uuid.UUID, image
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 {
return err
}

View File

@ -176,7 +176,7 @@ type addToListArguments struct {
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{
SystemPrompt: listPrompt,
JsonTools: listTools,
@ -206,10 +206,19 @@ func NewListAgent(log *log.Logger, listModel models.ListModel, limitsMethods lim
}
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 {
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
}
@ -229,12 +238,12 @@ func NewListAgent(log *log.Logger, listModel models.ListModel, limitsMethods lim
ctx := context.Background()
listUuid, err := uuid.Parse(args.ListID)
listUUID, err := uuid.Parse(args.ListID)
if err != nil {
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
}

View File

@ -1,23 +1,13 @@
package main
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"net/http"
"os"
"screenmark/screenmark/agents"
"screenmark/screenmark/limits"
"screenmark/screenmark/middleware"
"screenmark/screenmark/models"
"strconv"
"sync"
"time"
"github.com/charmbracelet/log"
"github.com/google/uuid"
"github.com/lib/pq"
)
const (
@ -76,229 +66,6 @@ func (n *Notification) UnmarshalJSON(data []byte) error {
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.
* We never close these channels.

View File

@ -9,7 +9,6 @@ import (
"net/http"
"os"
"path/filepath"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/limits"
"screenmark/screenmark/middleware"
"screenmark/screenmark/models"
@ -27,14 +26,11 @@ type ImageHandler struct {
limitsManager limits.LimitsManagerMethods
processImage func(imageID uuid.UUID)
jwtManager *middleware.JwtManager
}
type ImagesReturn struct {
UserImages []models.UserImageWithImage `json:"userImages"`
ProcessingImages []models.UserProcessingImage `json:"processingImages"`
Lists []models.ListsWithImages `json:"lists"`
}
@ -52,19 +48,19 @@ func (h *ImageHandler) serveImage(w http.ResponseWriter, r *http.Request) {
return
}
isAuthorized := h.imageModel.IsUserAuthorized(ctx, imageID, userID)
if !isAuthorized {
w.WriteHeader(http.StatusUnauthorized)
return
}
image, err := h.imageModel.Get(r.Context(), imageID)
image, exists, err := h.imageModel.Get(r.Context(), imageID)
if err != nil {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "Could not get image")
return
}
// Do not leak that this ID exists.
if !exists || *image.UserID != userID {
w.WriteHeader(http.StatusNotFound)
return
}
// TODO: this could be part of the db table
extension := filepath.Ext(image.ImageName)
if len(extension) == 0 {
@ -89,12 +85,6 @@ func (h *ImageHandler) listImages(w http.ResponseWriter, r *http.Request) {
return
}
processingImages, err := h.imageModel.GetProcessing(r.Context(), userId)
if err != nil {
middleware.WriteErrorInternal(h.logger, "could not get processing images", w)
return
}
listsWithImages, err := h.userModel.ListWithImages(r.Context(), userId)
if err != nil {
middleware.WriteErrorInternal(h.logger, "could not get lists with images", w)
@ -103,7 +93,6 @@ func (h *ImageHandler) listImages(w http.ResponseWriter, r *http.Request) {
imagesReturn := ImagesReturn{
UserImages: images,
ProcessingImages: processingImages,
Lists: listsWithImages,
}
@ -117,7 +106,7 @@ func (h *ImageHandler) uploadImage(w http.ResponseWriter, r *http.Request) {
return
}
userId, err := middleware.GetUserID(r.Context(), h.logger, w)
userID, err := middleware.GetUserID(r.Context(), h.logger, w)
if err != nil {
return
}
@ -151,17 +140,16 @@ func (h *ImageHandler) uploadImage(w http.ResponseWriter, r *http.Request) {
return
}
userImage, err := h.imageModel.Process(r.Context(), userId, model.Image{
Image: image,
ImageName: imageName,
})
ctx := r.Context()
err = h.imageModel.Save(ctx, imageName, image, userID)
if err != nil {
middleware.WriteErrorInternal(h.logger, "could not save image to DB", w)
return
}
middleware.WriteJsonOrError(h.logger, userImage, w)
w.WriteHeader(http.StatusOK)
}
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
}
isAuthorized := h.imageModel.IsUserAuthorized(ctx, imageID, userID)
if !isAuthorized {
w.WriteHeader(http.StatusUnauthorized)
return
}
err = h.imageModel.Delete(ctx, imageID)
exists, err := h.imageModel.Delete(ctx, imageID, userID)
if err != nil {
h.logger.Warn("cannot delete image", "error", err)
w.WriteHeader(http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
}
// 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)
// Don't leak if the image exists or not
if !exists {
w.WriteHeader(http.StatusNotFound)
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)
}
@ -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)
userModel := models.NewUserModel(db)
logger := log.New(os.Stdout).WithPrefix("Images")
@ -276,7 +212,6 @@ func CreateImageHandler(db *sql.DB, limitsManager limits.LimitsManagerMethods, p
imageModel: imageModel,
userModel: userModel,
limitsManager: limitsManager,
processImage: processImage,
jwtManager: jwtManager,
}
}

View File

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

View File

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

View File

@ -4,7 +4,6 @@ import (
"context"
"database/sql"
"fmt"
"screenmark/screenmark/.gen/haystack/haystack/enum"
"screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table"
@ -18,248 +17,50 @@ type ImageModel struct {
dbPool *sql.DB
}
type ImageData struct {
model.UserImages
func (m ImageModel) Save(ctx context.Context, name string, image []byte, userID uuid.UUID) error {
saveImageStmt := Image.INSERT(Image.ImageName, Image.Image, Image.UserID).
VALUES(name, image, userID)
Image model.Image
}
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)
_, err := saveImageStmt.ExecContext(ctx, m.dbPool)
return err
}
func (m ImageModel) StartProcessing(ctx context.Context, processingImageId uuid.UUID) error {
startProcessingStmt := UserImagesToProcess.
UPDATE(UserImagesToProcess.Status).
SET(model.Progress_InProgress).
WHERE(UserImagesToProcess.ID.EQ(UUID(processingImageId)))
_, err := startProcessingStmt.ExecContext(ctx, m.dbPool)
return err
}
func (m ImageModel) Get(ctx context.Context, imageId uuid.UUID) (model.Image, error) {
getImageStmt := Image.SELECT(Image.AllColumns).
WHERE(Image.ID.EQ(UUID(imageId)))
func (m ImageModel) Get(ctx context.Context, imageID uuid.UUID) (model.Image, bool, error) {
getImageStmt := Image.SELECT(Image.AllColumns.Except(Image.Image)).WHERE(Image.ID.EQ(UUID(imageID)))
image := model.Image{}
err := getImageStmt.QueryContext(ctx, m.dbPool, &image)
return image, err
return image, err != qrm.ErrNoRows, err
}
func (m ImageModel) GetProcessing(ctx context.Context, userId uuid.UUID) ([]UserProcessingImage, error) {
getProcessingStmt := SELECT(UserImagesToProcess.AllColumns, Image.ID, Image.ImageName).
FROM(
UserImagesToProcess.INNER_JOIN(
Image, Image.ID.EQ(UserImagesToProcess.ImageID),
),
).WHERE(
UserImagesToProcess.UserID.EQ(UUID(userId)).
AND(UserImagesToProcess.Status.NOT_EQ(enum.Progress.Complete)),
)
func (m ImageModel) Update(ctx context.Context, image model.Image) (model.Image, error) {
updateImageStmt := Image.UPDATE(Image.MutableColumns.Except(Image.Image)).
MODEL(image).
WHERE(Image.ID.EQ(UUID(image.ID))).
RETURNING(Image.AllColumns.Except(Image.Image))
images := []UserProcessingImage{}
err := getProcessingStmt.QueryContext(ctx, m.dbPool, &images)
updatedImage := model.Image{}
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 {
updateImageStmt := Image.UPDATE(Image.Description).
SET(description).
WHERE(Image.ID.EQ(UUID(imageId)))
func (m ImageModel) Delete(ctx context.Context, imageID, userID uuid.UUID) (bool, error) {
deleteImageStmt := Image.DELETE().WHERE(Image.ID.EQ(UUID(imageID)).AND(Image.UserID.EQ(UUID(userID))))
_, err := updateImageStmt.ExecContext(ctx, m.dbPool)
return err
r, err := deleteImageStmt.ExecContext(ctx, m.dbPool)
if err != nil {
return false, fmt.Errorf("deleting image: %w", err)
}
func (m ImageModel) DeleteUserImage(ctx context.Context, imageID uuid.UUID) error {
deleteImageStmt := UserImages.DELETE().
WHERE(UserImages.ImageID.EQ(UUID(imageID)))
_, err := deleteImageStmt.ExecContext(ctx, m.dbPool)
return err
rowsAffected, err := r.RowsAffected()
if err != nil {
return false, fmt.Errorf("unreachable: %w", err)
}
func (m ImageModel) Delete(ctx context.Context, imageID uuid.UUID) error {
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())
return rowsAffected > 0, nil
}
func NewImageModel(db *sql.DB) ImageModel {

View File

@ -3,7 +3,6 @@ package models
import (
"context"
"database/sql"
"fmt"
"screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table"
@ -12,22 +11,18 @@ import (
"github.com/google/uuid"
)
type ListModel struct {
type StackModel struct {
dbPool *sql.DB
}
type ListWithItems struct {
model.Lists
Schema struct {
model.Schemas
type StackWithItems struct {
model.Stacks
SchemaItems []model.SchemaItems
}
}
type ImageWithSchema struct {
model.ImageLists
model.ImageStacks
Items []model.ImageSchemaItems
}
@ -41,35 +36,33 @@ type IDValue struct {
// SELECT for lists
// ========================================
func (m ListModel) List(ctx context.Context, userId uuid.UUID) ([]ListWithItems, error) {
getListsWithItems := SELECT(
Lists.AllColumns,
Schemas.AllColumns,
func (m StackModel) List(ctx context.Context, userId uuid.UUID) ([]StackWithItems, error) {
getStacksWithItems := SELECT(
Stacks.AllColumns,
SchemaItems.AllColumns,
).
FROM(
Lists.
INNER_JOIN(Schemas, Schemas.ListID.EQ(Lists.ID)).
INNER_JOIN(SchemaItems, SchemaItems.SchemaID.EQ(Schemas.ID)),
Stacks.
INNER_JOIN(SchemaItems, SchemaItems.StackID.EQ(Stacks.ID)),
).
WHERE(Lists.UserID.EQ(UUID(userId)))
WHERE(Stacks.UserID.EQ(UUID(userId)))
lists := []ListWithItems{}
err := getListsWithItems.QueryContext(ctx, m.dbPool, &lists)
lists := []StackWithItems{}
err := getStacksWithItems.QueryContext(ctx, m.dbPool, &lists)
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(
ImageLists.AllColumns,
ImageStacks.AllColumns,
ImageSchemaItems.AllColumns,
).
FROM(
ImageLists.
INNER_JOIN(ImageSchemaItems, ImageSchemaItems.ImageID.EQ(ImageLists.ImageID)),
ImageStacks.
INNER_JOIN(ImageSchemaItems, ImageSchemaItems.ImageID.EQ(ImageStacks.ImageID)),
).
WHERE(ImageLists.ListID.EQ(UUID(listID)))
WHERE(ImageStacks.StackID.EQ(UUID(listID)))
listItems := make([]ImageWithSchema, 0)
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
}
// ========================================
// SELECT for specific items
// ========================================
func (m StackModel) Get(ctx context.Context, listID uuid.UUID) (model.Stacks, error) {
getStackStmt := Stacks.SELECT(Stacks.AllColumns).WHERE(Stacks.ID.EQ(UUID(listID)))
func (m ListModel) GetProcessing(ctx context.Context, processingListID uuid.UUID) (model.ProcessingLists, error) {
getProcessingListStmt := ProcessingLists.
SELECT(ProcessingLists.AllColumns).
WHERE(ProcessingLists.ID.EQ(UUID(processingListID)))
stack := model.Stacks{}
err := getStackStmt.QueryContext(ctx, m.dbPool, &stack)
list := model.ProcessingLists{}
err := getProcessingListStmt.QueryContext(ctx, m.dbPool, &list)
return list, err
}
func (m ListModel) GetToProcess(ctx context.Context, listID uuid.UUID) (model.ProcessingLists, error) {
getToProcessStmt := ProcessingLists.
SELECT(ProcessingLists.AllColumns).
WHERE(ProcessingLists.ID.EQ(UUID(listID)))
stack := []model.ProcessingLists{}
err := getToProcessStmt.QueryContext(ctx, m.dbPool, &stack)
if len(stack) != 1 {
return model.ProcessingLists{}, fmt.Errorf("Expected 1, got %d\n", len(stack))
}
return stack[0], err
}
// ========================================
// UPDATE
// ========================================
func (m ListModel) StartProcessing(ctx context.Context, processingListID uuid.UUID) error {
startProcessingStmt := ProcessingLists.
UPDATE(ProcessingLists.Status).
SET(model.Progress_InProgress).
WHERE(ProcessingLists.ID.EQ(UUID(processingListID)))
_, err := startProcessingStmt.ExecContext(ctx, m.dbPool)
return err
}
func (m ListModel) EndProcessing(ctx context.Context, processingListID uuid.UUID) error {
startProcessingStmt := ProcessingLists.
UPDATE(ProcessingLists.Status).
SET(model.Progress_Complete).
WHERE(ProcessingLists.ID.EQ(UUID(processingListID)))
_, err := startProcessingStmt.ExecContext(ctx, m.dbPool)
return err
return stack, err
}
// ========================================
// INSERT methods
// ========================================
func (m ListModel) Save(ctx context.Context, userId uuid.UUID, name string, description string, schemaItems []model.SchemaItems) (ListWithItems, error) {
tx, err := m.dbPool.BeginTx(ctx, nil)
func (m StackModel) Save(ctx context.Context, userID uuid.UUID, name string, description string) (model.Stacks, error) {
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).
VALUES(userId, name, description).
RETURNING(Lists.ID, Lists.Name, Lists.Description)
list := model.Stacks{}
err := saveListStmt.QueryContext(ctx, m.dbPool, &list)
newList := model.Lists{}
err = stmt.QueryContext(ctx, tx, &newList)
if err != nil {
tx.Rollback()
return ListWithItems{}, fmt.Errorf("Could not save new list. %s", err)
return list, err
}
insertSchemaStmt := Schemas.INSERT(Schemas.ListID).
VALUES(newList.ID).
RETURNING(Schemas.ID)
func (m StackModel) SaveItems(ctx context.Context, items []model.SchemaItems) error {
saveItemsStmt := SchemaItems.INSERT(SchemaItems.AllColumns).MODELS(items)
newSchema := model.Schemas{}
err = insertSchemaStmt.QueryContext(ctx, tx, &newSchema)
_, err := saveItemsStmt.ExecContext(ctx, m.dbPool)
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
}
var imageList model.ImageLists
stmt := ImageLists.INSERT(ImageLists.ListID, ImageLists.ImageID).
VALUES(listID, imageID).
RETURNING(ImageLists.ID)
func (m StackModel) SaveImage(ctx context.Context, imageID uuid.UUID, stackID uuid.UUID) error {
saveImageStmt := ImageStacks.
INSERT(ImageStacks.ImageID, ImageStacks.StackID).
VALUES(imageID, stackID)
err = stmt.QueryContext(ctx, m.dbPool, &imageList)
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)
_, err := saveImageStmt.ExecContext(ctx, m.dbPool)
return err
}
@ -253,11 +117,11 @@ func (m ListModel) SaveProcessing(ctx context.Context, userID uuid.UUID, title s
// DELETE methods
// ========================================
func (m ListModel) DeleteImage(ctx context.Context, listID uuid.UUID, imageID uuid.UUID) error {
deleteImageListStmt := ImageLists.DELETE().
func (m StackModel) DeleteImage(ctx context.Context, listID uuid.UUID, imageID uuid.UUID) error {
deleteImageListStmt := ImageStacks.DELETE().
WHERE(
ImageLists.ListID.EQ(UUID(listID)).
AND(ImageLists.ImageID.EQ(UUID(imageID))),
ImageStacks.StackID.EQ(UUID(listID)).
AND(ImageStacks.ImageID.EQ(UUID(imageID))),
)
_, err := deleteImageListStmt.ExecContext(ctx, m.dbPool)
@ -265,60 +129,14 @@ func (m ListModel) DeleteImage(ctx context.Context, listID uuid.UUID, imageID uu
return err
}
func (m ListModel) Delete(ctx context.Context, listID uuid.UUID, userID uuid.UUID) error {
// First verify the list belongs to the user
checkOwnershipStmt := Lists.
SELECT(Lists.ID).
WHERE(Lists.ID.EQ(UUID(listID)).AND(Lists.UserID.EQ(UUID(userID))))
func (m StackModel) Delete(ctx context.Context, listID uuid.UUID, userID uuid.UUID) error {
deleteStackStmt := Stacks.DELETE().WHERE(Stacks.ID.EQ(UUID(listID)).AND(Stacks.UserID.EQ(UUID(userID))))
var existingList model.Lists
err := checkOwnershipStmt.QueryContext(ctx, m.dbPool, &existingList)
if err != nil {
return fmt.Errorf("could not verify list ownership: %w", err)
_, err := deleteStackStmt.ExecContext(ctx, m.dbPool)
return err
}
// Start a transaction to ensure all deletions happen atomically
tx, err := m.dbPool.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("could not start transaction: %w", err)
}
defer tx.Rollback()
// Delete in reverse order of dependencies:
// 1. Delete schema items first
deleteSchemaItemsStmt := SchemaItems.DELETE().
WHERE(SchemaItems.SchemaID.IN(
Schemas.SELECT(Schemas.ID).
WHERE(Schemas.ListID.EQ(UUID(listID))),
))
_, err = deleteSchemaItemsStmt.ExecContext(ctx, tx)
if err != nil {
return fmt.Errorf("could not delete schema items: %w", err)
}
// 2. Delete schemas
deleteSchemasStmt := Schemas.DELETE().WHERE(Schemas.ListID.EQ(UUID(listID)))
_, err = deleteSchemasStmt.ExecContext(ctx, tx)
if err != nil {
return fmt.Errorf("could not delete schemas: %w", err)
}
// 3. Delete the list itself
deleteListStmt := Lists.DELETE().WHERE(Lists.ID.EQ(UUID(listID)))
_, err = deleteListStmt.ExecContext(ctx, tx)
if err != nil {
return fmt.Errorf("could not delete list: %w", err)
}
// Commit the transaction
err = tx.Commit()
if err != nil {
return fmt.Errorf("could not commit transaction: %w", err)
}
return nil
}
func NewListModel(db *sql.DB) ListModel {
return ListModel{dbPool: db}
func NewListModel(db *sql.DB) StackModel {
return StackModel{dbPool: db}
}

View File

@ -49,28 +49,20 @@ func (m UserModel) Save(ctx context.Context, user model.Users) (model.Users, err
}
type UserImageWithImage struct {
model.UserImages
Image struct {
model.Image
ImageLists []model.ImageLists
}
ImageStacks []model.ImageStacks
}
func (m UserModel) GetUserImages(ctx context.Context, userId uuid.UUID) ([]UserImageWithImage, error) {
getUserImagesStmt := SELECT(
UserImages.AllColumns,
Image.ID,
Image.ImageName,
Image.Description,
ImageLists.AllColumns,
Image.AllColumns.Except(Image.Image),
ImageStacks.AllColumns,
).
FROM(
UserImages.
INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID)).
LEFT_JOIN(ImageLists, ImageLists.ImageID.EQ(UserImages.ImageID)),
Image.
LEFT_JOIN(ImageStacks, ImageStacks.ImageID.EQ(ImageStacks.ID)),
).
WHERE(UserImages.UserID.EQ(UUID(userId)))
WHERE(Image.UserID.EQ(UUID(userId)))
userImages := []UserImageWithImage{}
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 {
model.Lists
Schema struct {
model.Schemas
model.Stacks
SchemaItems []model.SchemaItems
}
Images []struct {
model.ImageLists
model.ImageStacks
Items []model.ImageSchemaItems
}
@ -96,20 +84,18 @@ type ListsWithImages struct {
func (m UserModel) ListWithImages(ctx context.Context, userId uuid.UUID) ([]ListsWithImages, error) {
stmt := SELECT(
Lists.AllColumns,
ImageLists.AllColumns,
Schemas.AllColumns,
Stacks.AllColumns,
ImageStacks.AllColumns,
SchemaItems.AllColumns,
ImageSchemaItems.AllColumns,
).
FROM(
Lists.
INNER_JOIN(Schemas, Schemas.ListID.EQ(Lists.ID)).
INNER_JOIN(SchemaItems, SchemaItems.SchemaID.EQ(Schemas.ID)).
LEFT_JOIN(ImageLists, ImageLists.ListID.EQ(Lists.ID)).
LEFT_JOIN(ImageSchemaItems, ImageSchemaItems.ImageID.EQ(ImageLists.ID)),
Stacks.
INNER_JOIN(SchemaItems, SchemaItems.StackID.EQ(Stacks.ID)).
LEFT_JOIN(ImageStacks, ImageStacks.StackID.EQ(Stacks.ID)).
LEFT_JOIN(ImageSchemaItems, ImageSchemaItems.ImageID.EQ(ImageStacks.ID)),
).
WHERE(Lists.UserID.EQ(UUID(userId)))
WHERE(Stacks.UserID.EQ(UUID(userId)))
lists := []ListsWithImages{}
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 (
"database/sql"
"os"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/auth"
"screenmark/screenmark/images"
"screenmark/screenmark/limits"
"screenmark/screenmark/models"
"screenmark/screenmark/stacks"
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 {
imageModel := models.NewImageModel(db)
stackModel := models.NewListModel(db)
limitsManager := limits.CreateLimitsManager(db)
processImageLogger := createLogger("Process Image", os.Stdout)
processImage := ProcessImage(processImageLogger, db)
stackHandler := stacks.CreateStackHandler(db, limitsManager, jwtManager)
authHandler := auth.CreateAuthHandler(db, jwtManager)
imageHandler := images.CreateImageHandler(db, limitsManager, processImage, jwtManager)
imageHandler := images.CreateImageHandler(db, limitsManager, jwtManager)
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.Use(middleware.Logger)
@ -67,11 +46,5 @@ func setupRouter(db *sql.DB, jwtManager *ourmiddleware.JwtManager) chi.Router {
r.Get("/", CreateEventsHandler(&notifier))
})
logWriter := DatabaseWriter{
dbPool: db,
}
r.Route("/logs", createLogHandler(&logWriter))
return r
}

View File

@ -9,72 +9,43 @@ CREATE TYPE haystack.progress AS ENUM('not-started','in-progress', 'complete');
/* -----| Schema tables |----- */
CREATE TABLE haystack.users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
CREATE TABLE haystack.image (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES haystack.users (id),
image_name 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',
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 (
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),
image BYTEA NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
CREATE TABLE haystack.logs (
log TEXT NOT NULL,
image_id UUID NOT NULL REFERENCES haystack.image (id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
CREATE TABLE haystack.lists (
CREATE TABLE haystack.stacks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES haystack.users (id),
status haystack.progress NOT NULL DEFAULT 'not-started',
name TEXT NOT NULL,
description TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
CREATE TABLE haystack.processing_lists (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES haystack.users (id),
title TEXT NOT NULL,
fields TEXT NOT NULL,
status haystack.progress NOT NULL DEFAULT 'not-started',
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
CREATE TABLE haystack.image_lists (
CREATE TABLE haystack.image_stacks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
image_id UUID NOT NULL REFERENCES haystack.image (id) ON DELETE CASCADE,
list_id UUID NOT NULL REFERENCES haystack.lists (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
stack_id UUID NOT NULL REFERENCES haystack.stacks (id) ON DELETE CASCADE
);
CREATE TABLE haystack.schema_items (
@ -84,7 +55,7 @@ CREATE TABLE haystack.schema_items (
value 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 (
@ -92,68 +63,6 @@ CREATE TABLE haystack.image_schema_items (
value TEXT,
schema_item_id UUID NOT NULL REFERENCES haystack.schema_items (id),
image_id UUID NOT NULL REFERENCES haystack.image_lists (id) ON DELETE CASCADE
schema_item_id UUID NOT NULL REFERENCES haystack.schema_items (id) ON DELETE CASCADE,
image_id UUID NOT NULL REFERENCES haystack.image_stacks (id) ON DELETE CASCADE
);
/* -----| 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 (
"database/sql"
"fmt"
"net/http"
"os"
. "screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/limits"
"screenmark/screenmark/middleware"
"screenmark/screenmark/models"
"strings"
"github.com/charmbracelet/log"
"github.com/go-chi/chi/v5"
@ -20,7 +17,7 @@ type StackHandler struct {
logger *log.Logger
imageModel models.ImageModel
stackModel models.ListModel
stackModel models.StackModel
limitsManager limits.LimitsManagerMethods
@ -125,8 +122,13 @@ func (h *StackHandler) deleteImageFromStack(w http.ResponseWriter, r *http.Reque
return
}
isAuthorized := h.imageModel.IsUserAuthorized(ctx, imageID, userID)
if !isAuthorized {
stack, err := h.stackModel.Get(ctx, listID)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
if stack.UserID != userID {
w.WriteHeader(http.StatusUnauthorized)
return
}
@ -143,9 +145,7 @@ func (h *StackHandler) deleteImageFromStack(w http.ResponseWriter, r *http.Reque
type CreateStackBody struct {
Title string `json:"title"`
// We want a regular string because AI will take care of creating these for us.
Fields string `json:"fields"`
Description string `json:"description"`
}
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
}
// Convert fields string to basic schema items
// For now, create a simple schema item for each field
var schemaItems []SchemaItems
if body.Fields != "" {
fields := strings.Split(body.Fields, ",")
for i, field := range fields {
field = strings.TrimSpace(field)
if field != "" {
schemaItems = append(schemaItems, SchemaItems{
Item: field,
Value: "",
Description: fmt.Sprintf("Field %d: %s", i+1, field),
})
}
}
}
// Use empty description for now since the API doesn't provide one
_, err = h.stackModel.Save(ctx, userID, body.Title, "", schemaItems)
_, err = h.stackModel.Save(ctx, userID, body.Title, body.Description)
if err != nil {
h.logger.Warn("could not save stack", "err", err)
w.WriteHeader(http.StatusInternalServerError)

View File

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