1 Commits

300 changed files with 7570 additions and 12922 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -1,38 +0,0 @@
---
description:
globs:
alwaysApply: true
---
You are an expert AI programming assistant focused on producing clean, readable TypeScript and Rust code for modern cross-platform desktop apps.
Use these rules for any code under /frontend folder.
You always use the latest versions of Tauri, Rust, SolidJS, and you're fluent in their latest features, best practices, and patterns.
You give accurate, thoughtful answers and think like a real dev—step-by-step.
Follow the users specs exactly. If a specs folder exists, check it before coding.
Begin with a detailed pseudo-code plan and confirm it with the user before writing actual code.
Write correct, complete, idiomatic, secure, performant, and bug-free code.
Prioritize readability unless performance is explicitly required.
Fully implement all requested features—no TODOs, stubs, or placeholders.
Use TypeScript's type system thoroughly for clarity and safety.
Style with TailwindCSS using utility-first principles.
Use Kobalte components effectively, building with Solids reactive model in mind.
Offload performance-heavy logic to Rust and ensure smooth integration with Tauri.
Guarantee tight coordination between SolidJS, Tauri, and Rust for a polished desktop UX.
When needed, provide bash scripts to generate config files or folder structures.
Be concise—cut the fluff.
If there's no solid answer, say so. If you're unsure, don't guess—own it.

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "haystack"]
path = haystack-arch
url = https://aur.archlinux.org/haystack

View File

@ -11,9 +11,7 @@ import (
"github.com/google/uuid"
)
type ImageSchemaItems struct {
ID uuid.UUID `sql:"primary_key"`
Value *string
SchemaItemID uuid.UUID
ImageID uuid.UUID
type Agents struct {
ID uuid.UUID `sql:"primary_key"`
Name string
}

View File

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

View File

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

View File

@ -0,0 +1,78 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var Agents = newAgentsTable("agents", "agents", "")
type agentsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
Name postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type AgentsTable struct {
agentsTable
EXCLUDED agentsTable
}
// AS creates new AgentsTable with assigned alias
func (a AgentsTable) AS(alias string) *AgentsTable {
return newAgentsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new AgentsTable with assigned schema name
func (a AgentsTable) FromSchema(schemaName string) *AgentsTable {
return newAgentsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new AgentsTable with assigned table prefix
func (a AgentsTable) WithPrefix(prefix string) *AgentsTable {
return newAgentsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new AgentsTable with assigned table suffix
func (a AgentsTable) WithSuffix(suffix string) *AgentsTable {
return newAgentsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newAgentsTable(schemaName, tableName, alias string) *AgentsTable {
return &AgentsTable{
agentsTable: newAgentsTableImpl(schemaName, tableName, alias),
EXCLUDED: newAgentsTableImpl("", "excluded", ""),
}
}
func newAgentsTableImpl(schemaName, tableName, alias string) agentsTable {
var (
IDColumn = postgres.StringColumn("id")
NameColumn = postgres.StringColumn("name")
allColumns = postgres.ColumnList{IDColumn, NameColumn}
mutableColumns = postgres.ColumnList{NameColumn}
)
return agentsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Name: NameColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

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 SystemPrompts = newSystemPromptsTable("agents", "system_prompts", "")
type systemPromptsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
Prompt postgres.ColumnString
AgentID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type SystemPromptsTable struct {
systemPromptsTable
EXCLUDED systemPromptsTable
}
// AS creates new SystemPromptsTable with assigned alias
func (a SystemPromptsTable) AS(alias string) *SystemPromptsTable {
return newSystemPromptsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new SystemPromptsTable with assigned schema name
func (a SystemPromptsTable) FromSchema(schemaName string) *SystemPromptsTable {
return newSystemPromptsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new SystemPromptsTable with assigned table prefix
func (a SystemPromptsTable) WithPrefix(prefix string) *SystemPromptsTable {
return newSystemPromptsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new SystemPromptsTable with assigned table suffix
func (a SystemPromptsTable) WithSuffix(suffix string) *SystemPromptsTable {
return newSystemPromptsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newSystemPromptsTable(schemaName, tableName, alias string) *SystemPromptsTable {
return &SystemPromptsTable{
systemPromptsTable: newSystemPromptsTableImpl(schemaName, tableName, alias),
EXCLUDED: newSystemPromptsTableImpl("", "excluded", ""),
}
}
func newSystemPromptsTableImpl(schemaName, tableName, alias string) systemPromptsTable {
var (
IDColumn = postgres.StringColumn("id")
PromptColumn = postgres.StringColumn("prompt")
AgentIDColumn = postgres.StringColumn("agent_id")
allColumns = postgres.ColumnList{IDColumn, PromptColumn, AgentIDColumn}
mutableColumns = postgres.ColumnList{PromptColumn, AgentIDColumn}
)
return systemPromptsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Prompt: PromptColumn,
AgentID: AgentIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -0,0 +1,16 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
// UseSchema sets a new schema name for all generated table SQL builder types. It is recommended to invoke
// this method only once at the beginning of the program.
func UseSchema(schema string) {
Agents = Agents.FromSchema(schema)
SystemPrompts = SystemPrompts.FromSchema(schema)
Tools = Tools.FromSchema(schema)
}

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 Tools = newToolsTable("agents", "tools", "")
type toolsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
Tool postgres.ColumnString
AgentID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ToolsTable struct {
toolsTable
EXCLUDED toolsTable
}
// AS creates new ToolsTable with assigned alias
func (a ToolsTable) AS(alias string) *ToolsTable {
return newToolsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ToolsTable with assigned schema name
func (a ToolsTable) FromSchema(schemaName string) *ToolsTable {
return newToolsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ToolsTable with assigned table prefix
func (a ToolsTable) WithPrefix(prefix string) *ToolsTable {
return newToolsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ToolsTable with assigned table suffix
func (a ToolsTable) WithSuffix(suffix string) *ToolsTable {
return newToolsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newToolsTable(schemaName, tableName, alias string) *ToolsTable {
return &ToolsTable{
toolsTable: newToolsTableImpl(schemaName, tableName, alias),
EXCLUDED: newToolsTableImpl("", "excluded", ""),
}
}
func newToolsTableImpl(schemaName, tableName, alias string) toolsTable {
var (
IDColumn = postgres.StringColumn("id")
ToolColumn = postgres.StringColumn("tool")
AgentIDColumn = postgres.StringColumn("agent_id")
allColumns = postgres.ColumnList{IDColumn, ToolColumn, AgentIDColumn}
mutableColumns = postgres.ColumnList{ToolColumn, AgentIDColumn}
)
return toolsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Tool: ToolColumn,
AgentID: AgentIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -12,9 +12,7 @@ import "github.com/go-jet/jet/v2/postgres"
var Progress = &struct {
NotStarted postgres.StringExpression
InProgress postgres.StringExpression
Complete postgres.StringExpression
}{
NotStarted: postgres.NewEnumValue("not-started"),
InProgress: postgres.NewEnumValue("in-progress"),
Complete: postgres.NewEnumValue("complete"),
}

View File

@ -11,10 +11,10 @@ import (
"github.com/google/uuid"
)
type SchemaItems struct {
type Contacts struct {
ID uuid.UUID `sql:"primary_key"`
Item string
Value string
Description string
StackID uuid.UUID
Name string
Description *string
PhoneNumber *string
Email *string
}

View File

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

View File

@ -9,15 +9,10 @@ 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
ID uuid.UUID `sql:"primary_key"`
ImageName string
Image []byte
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,14 +9,11 @@ package model
import (
"github.com/google/uuid"
"time"
)
type Stacks struct {
type Locations struct {
ID uuid.UUID `sql:"primary_key"`
UserID uuid.UUID
Status Progress
Name string
Description string
CreatedAt *time.Time
Address *string
Description *string
}

View File

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

View File

@ -14,13 +14,11 @@ type Progress string
const (
Progress_NotStarted Progress = "not-started"
Progress_InProgress Progress = "in-progress"
Progress_Complete Progress = "complete"
)
var ProgressAllValues = []Progress{
Progress_NotStarted,
Progress_InProgress,
Progress_Complete,
}
func (e *Progress) Scan(value interface{}) error {
@ -39,8 +37,6 @@ func (e *Progress) Scan(value interface{}) error {
*e = Progress_NotStarted
case "in-progress":
*e = Progress_InProgress
case "complete":
*e = Progress_Complete
default:
return errors.New("jet: Invalid scan value '" + enumValue + "' for Progress enum")
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,87 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var Contacts = newContactsTable("haystack", "contacts", "")
type contactsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
Name postgres.ColumnString
Description postgres.ColumnString
PhoneNumber postgres.ColumnString
Email postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ContactsTable struct {
contactsTable
EXCLUDED contactsTable
}
// AS creates new ContactsTable with assigned alias
func (a ContactsTable) AS(alias string) *ContactsTable {
return newContactsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ContactsTable with assigned schema name
func (a ContactsTable) FromSchema(schemaName string) *ContactsTable {
return newContactsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ContactsTable with assigned table prefix
func (a ContactsTable) WithPrefix(prefix string) *ContactsTable {
return newContactsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ContactsTable with assigned table suffix
func (a ContactsTable) WithSuffix(suffix string) *ContactsTable {
return newContactsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newContactsTable(schemaName, tableName, alias string) *ContactsTable {
return &ContactsTable{
contactsTable: newContactsTableImpl(schemaName, tableName, alias),
EXCLUDED: newContactsTableImpl("", "excluded", ""),
}
}
func newContactsTableImpl(schemaName, tableName, alias string) contactsTable {
var (
IDColumn = postgres.StringColumn("id")
NameColumn = postgres.StringColumn("name")
DescriptionColumn = postgres.StringColumn("description")
PhoneNumberColumn = postgres.StringColumn("phone_number")
EmailColumn = postgres.StringColumn("email")
allColumns = postgres.ColumnList{IDColumn, NameColumn, DescriptionColumn, PhoneNumberColumn, EmailColumn}
mutableColumns = postgres.ColumnList{NameColumn, DescriptionColumn, PhoneNumberColumn, EmailColumn}
)
return contactsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Name: NameColumn,
Description: DescriptionColumn,
PhoneNumber: PhoneNumberColumn,
Email: EmailColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -0,0 +1,93 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var Events = newEventsTable("haystack", "events", "")
type eventsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
Name postgres.ColumnString
Description postgres.ColumnString
StartDateTime postgres.ColumnTimestamp
EndDateTime postgres.ColumnTimestamp
LocationID postgres.ColumnString
OrganizerID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type EventsTable struct {
eventsTable
EXCLUDED eventsTable
}
// AS creates new EventsTable with assigned alias
func (a EventsTable) AS(alias string) *EventsTable {
return newEventsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new EventsTable with assigned schema name
func (a EventsTable) FromSchema(schemaName string) *EventsTable {
return newEventsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new EventsTable with assigned table prefix
func (a EventsTable) WithPrefix(prefix string) *EventsTable {
return newEventsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new EventsTable with assigned table suffix
func (a EventsTable) WithSuffix(suffix string) *EventsTable {
return newEventsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newEventsTable(schemaName, tableName, alias string) *EventsTable {
return &EventsTable{
eventsTable: newEventsTableImpl(schemaName, tableName, alias),
EXCLUDED: newEventsTableImpl("", "excluded", ""),
}
}
func newEventsTableImpl(schemaName, tableName, alias string) eventsTable {
var (
IDColumn = postgres.StringColumn("id")
NameColumn = postgres.StringColumn("name")
DescriptionColumn = postgres.StringColumn("description")
StartDateTimeColumn = postgres.TimestampColumn("start_date_time")
EndDateTimeColumn = postgres.TimestampColumn("end_date_time")
LocationIDColumn = postgres.StringColumn("location_id")
OrganizerIDColumn = postgres.StringColumn("organizer_id")
allColumns = postgres.ColumnList{IDColumn, NameColumn, DescriptionColumn, StartDateTimeColumn, EndDateTimeColumn, LocationIDColumn, OrganizerIDColumn}
mutableColumns = postgres.ColumnList{NameColumn, DescriptionColumn, StartDateTimeColumn, EndDateTimeColumn, LocationIDColumn, OrganizerIDColumn}
)
return eventsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Name: NameColumn,
Description: DescriptionColumn,
StartDateTime: StartDateTimeColumn,
EndDateTime: EndDateTimeColumn,
LocationID: LocationIDColumn,
OrganizerID: OrganizerIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -17,13 +17,9 @@ type imageTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
UserID postgres.ColumnString
ImageName postgres.ColumnString
Description postgres.ColumnString
Status postgres.ColumnString
Image postgres.ColumnString
CreatedAt postgres.ColumnTimestampz
ID postgres.ColumnString
ImageName postgres.ColumnString
Image postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
@ -64,28 +60,20 @@ 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")
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}
IDColumn = postgres.StringColumn("id")
ImageNameColumn = postgres.StringColumn("image_name")
ImageColumn = postgres.StringColumn("image")
allColumns = postgres.ColumnList{IDColumn, ImageNameColumn, ImageColumn}
mutableColumns = postgres.ColumnList{ImageNameColumn, ImageColumn}
)
return imageTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
UserID: UserIDColumn,
ImageName: ImageNameColumn,
Description: DescriptionColumn,
Status: StatusColumn,
Image: ImageColumn,
CreatedAt: CreatedAtColumn,
ID: IDColumn,
ImageName: ImageNameColumn,
Image: ImageColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,

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 ImageContacts = newImageContactsTable("haystack", "image_contacts", "")
type imageContactsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
ImageID postgres.ColumnString
ContactID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ImageContactsTable struct {
imageContactsTable
EXCLUDED imageContactsTable
}
// AS creates new ImageContactsTable with assigned alias
func (a ImageContactsTable) AS(alias string) *ImageContactsTable {
return newImageContactsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ImageContactsTable with assigned schema name
func (a ImageContactsTable) FromSchema(schemaName string) *ImageContactsTable {
return newImageContactsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ImageContactsTable with assigned table prefix
func (a ImageContactsTable) WithPrefix(prefix string) *ImageContactsTable {
return newImageContactsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ImageContactsTable with assigned table suffix
func (a ImageContactsTable) WithSuffix(suffix string) *ImageContactsTable {
return newImageContactsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newImageContactsTable(schemaName, tableName, alias string) *ImageContactsTable {
return &ImageContactsTable{
imageContactsTable: newImageContactsTableImpl(schemaName, tableName, alias),
EXCLUDED: newImageContactsTableImpl("", "excluded", ""),
}
}
func newImageContactsTableImpl(schemaName, tableName, alias string) imageContactsTable {
var (
IDColumn = postgres.StringColumn("id")
ImageIDColumn = postgres.StringColumn("image_id")
ContactIDColumn = postgres.StringColumn("contact_id")
allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, ContactIDColumn}
mutableColumns = postgres.ColumnList{ImageIDColumn, ContactIDColumn}
)
return imageContactsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
ImageID: ImageIDColumn,
ContactID: ContactIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

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 ImageEvents = newImageEventsTable("haystack", "image_events", "")
type imageEventsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
EventID postgres.ColumnString
ImageID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ImageEventsTable struct {
imageEventsTable
EXCLUDED imageEventsTable
}
// AS creates new ImageEventsTable with assigned alias
func (a ImageEventsTable) AS(alias string) *ImageEventsTable {
return newImageEventsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ImageEventsTable with assigned schema name
func (a ImageEventsTable) FromSchema(schemaName string) *ImageEventsTable {
return newImageEventsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ImageEventsTable with assigned table prefix
func (a ImageEventsTable) WithPrefix(prefix string) *ImageEventsTable {
return newImageEventsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ImageEventsTable with assigned table suffix
func (a ImageEventsTable) WithSuffix(suffix string) *ImageEventsTable {
return newImageEventsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newImageEventsTable(schemaName, tableName, alias string) *ImageEventsTable {
return &ImageEventsTable{
imageEventsTable: newImageEventsTableImpl(schemaName, tableName, alias),
EXCLUDED: newImageEventsTableImpl("", "excluded", ""),
}
}
func newImageEventsTableImpl(schemaName, tableName, alias string) imageEventsTable {
var (
IDColumn = postgres.StringColumn("id")
EventIDColumn = postgres.StringColumn("event_id")
ImageIDColumn = postgres.StringColumn("image_id")
allColumns = postgres.ColumnList{IDColumn, EventIDColumn, ImageIDColumn}
mutableColumns = postgres.ColumnList{EventIDColumn, ImageIDColumn}
)
return imageEventsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
EventID: EventIDColumn,
ImageID: ImageIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

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 ImageLinks = newImageLinksTable("haystack", "image_links", "")
type imageLinksTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
Link postgres.ColumnString
ImageID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ImageLinksTable struct {
imageLinksTable
EXCLUDED imageLinksTable
}
// AS creates new ImageLinksTable with assigned alias
func (a ImageLinksTable) AS(alias string) *ImageLinksTable {
return newImageLinksTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ImageLinksTable with assigned schema name
func (a ImageLinksTable) FromSchema(schemaName string) *ImageLinksTable {
return newImageLinksTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ImageLinksTable with assigned table prefix
func (a ImageLinksTable) WithPrefix(prefix string) *ImageLinksTable {
return newImageLinksTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ImageLinksTable with assigned table suffix
func (a ImageLinksTable) WithSuffix(suffix string) *ImageLinksTable {
return newImageLinksTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newImageLinksTable(schemaName, tableName, alias string) *ImageLinksTable {
return &ImageLinksTable{
imageLinksTable: newImageLinksTableImpl(schemaName, tableName, alias),
EXCLUDED: newImageLinksTableImpl("", "excluded", ""),
}
}
func newImageLinksTableImpl(schemaName, tableName, alias string) imageLinksTable {
var (
IDColumn = postgres.StringColumn("id")
LinkColumn = postgres.StringColumn("link")
ImageIDColumn = postgres.StringColumn("image_id")
allColumns = postgres.ColumnList{IDColumn, LinkColumn, ImageIDColumn}
mutableColumns = postgres.ColumnList{LinkColumn, ImageIDColumn}
)
return imageLinksTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Link: LinkColumn,
ImageID: ImageIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

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 ImageLocations = newImageLocationsTable("haystack", "image_locations", "")
type imageLocationsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
LocationID postgres.ColumnString
ImageID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ImageLocationsTable struct {
imageLocationsTable
EXCLUDED imageLocationsTable
}
// AS creates new ImageLocationsTable with assigned alias
func (a ImageLocationsTable) AS(alias string) *ImageLocationsTable {
return newImageLocationsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ImageLocationsTable with assigned schema name
func (a ImageLocationsTable) FromSchema(schemaName string) *ImageLocationsTable {
return newImageLocationsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ImageLocationsTable with assigned table prefix
func (a ImageLocationsTable) WithPrefix(prefix string) *ImageLocationsTable {
return newImageLocationsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ImageLocationsTable with assigned table suffix
func (a ImageLocationsTable) WithSuffix(suffix string) *ImageLocationsTable {
return newImageLocationsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newImageLocationsTable(schemaName, tableName, alias string) *ImageLocationsTable {
return &ImageLocationsTable{
imageLocationsTable: newImageLocationsTableImpl(schemaName, tableName, alias),
EXCLUDED: newImageLocationsTableImpl("", "excluded", ""),
}
}
func newImageLocationsTableImpl(schemaName, tableName, alias string) imageLocationsTable {
var (
IDColumn = postgres.StringColumn("id")
LocationIDColumn = postgres.StringColumn("location_id")
ImageIDColumn = postgres.StringColumn("image_id")
allColumns = postgres.ColumnList{IDColumn, LocationIDColumn, ImageIDColumn}
mutableColumns = postgres.ColumnList{LocationIDColumn, ImageIDColumn}
)
return imageLocationsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
LocationID: LocationIDColumn,
ImageID: ImageIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

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 ImageNotes = newImageNotesTable("haystack", "image_notes", "")
type imageNotesTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
ImageID postgres.ColumnString
NoteID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ImageNotesTable struct {
imageNotesTable
EXCLUDED imageNotesTable
}
// AS creates new ImageNotesTable with assigned alias
func (a ImageNotesTable) AS(alias string) *ImageNotesTable {
return newImageNotesTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ImageNotesTable with assigned schema name
func (a ImageNotesTable) FromSchema(schemaName string) *ImageNotesTable {
return newImageNotesTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ImageNotesTable with assigned table prefix
func (a ImageNotesTable) WithPrefix(prefix string) *ImageNotesTable {
return newImageNotesTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ImageNotesTable with assigned table suffix
func (a ImageNotesTable) WithSuffix(suffix string) *ImageNotesTable {
return newImageNotesTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newImageNotesTable(schemaName, tableName, alias string) *ImageNotesTable {
return &ImageNotesTable{
imageNotesTable: newImageNotesTableImpl(schemaName, tableName, alias),
EXCLUDED: newImageNotesTableImpl("", "excluded", ""),
}
}
func newImageNotesTableImpl(schemaName, tableName, alias string) imageNotesTable {
var (
IDColumn = postgres.StringColumn("id")
ImageIDColumn = postgres.StringColumn("image_id")
NoteIDColumn = postgres.StringColumn("note_id")
allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, NoteIDColumn}
mutableColumns = postgres.ColumnList{ImageIDColumn, NoteIDColumn}
)
return imageNotesTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
ImageID: ImageIDColumn,
NoteID: NoteIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

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 ImageSchemaItems = newImageSchemaItemsTable("haystack", "image_schema_items", "")
type imageSchemaItemsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
Value postgres.ColumnString
SchemaItemID postgres.ColumnString
ImageID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ImageSchemaItemsTable struct {
imageSchemaItemsTable
EXCLUDED imageSchemaItemsTable
}
// AS creates new ImageSchemaItemsTable with assigned alias
func (a ImageSchemaItemsTable) AS(alias string) *ImageSchemaItemsTable {
return newImageSchemaItemsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ImageSchemaItemsTable with assigned schema name
func (a ImageSchemaItemsTable) FromSchema(schemaName string) *ImageSchemaItemsTable {
return newImageSchemaItemsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ImageSchemaItemsTable with assigned table prefix
func (a ImageSchemaItemsTable) WithPrefix(prefix string) *ImageSchemaItemsTable {
return newImageSchemaItemsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ImageSchemaItemsTable with assigned table suffix
func (a ImageSchemaItemsTable) WithSuffix(suffix string) *ImageSchemaItemsTable {
return newImageSchemaItemsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newImageSchemaItemsTable(schemaName, tableName, alias string) *ImageSchemaItemsTable {
return &ImageSchemaItemsTable{
imageSchemaItemsTable: newImageSchemaItemsTableImpl(schemaName, tableName, alias),
EXCLUDED: newImageSchemaItemsTableImpl("", "excluded", ""),
}
}
func newImageSchemaItemsTableImpl(schemaName, tableName, alias string) imageSchemaItemsTable {
var (
IDColumn = postgres.StringColumn("id")
ValueColumn = postgres.StringColumn("value")
SchemaItemIDColumn = postgres.StringColumn("schema_item_id")
ImageIDColumn = postgres.StringColumn("image_id")
allColumns = postgres.ColumnList{IDColumn, ValueColumn, SchemaItemIDColumn, ImageIDColumn}
mutableColumns = postgres.ColumnList{ValueColumn, SchemaItemIDColumn, ImageIDColumn}
)
return imageSchemaItemsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Value: ValueColumn,
SchemaItemID: SchemaItemIDColumn,
ImageID: ImageIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

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 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

@ -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 ImageTags = newImageTagsTable("haystack", "image_tags", "")
type imageTagsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
TagID postgres.ColumnString
ImageID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ImageTagsTable struct {
imageTagsTable
EXCLUDED imageTagsTable
}
// AS creates new ImageTagsTable with assigned alias
func (a ImageTagsTable) AS(alias string) *ImageTagsTable {
return newImageTagsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ImageTagsTable with assigned schema name
func (a ImageTagsTable) FromSchema(schemaName string) *ImageTagsTable {
return newImageTagsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ImageTagsTable with assigned table prefix
func (a ImageTagsTable) WithPrefix(prefix string) *ImageTagsTable {
return newImageTagsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ImageTagsTable with assigned table suffix
func (a ImageTagsTable) WithSuffix(suffix string) *ImageTagsTable {
return newImageTagsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newImageTagsTable(schemaName, tableName, alias string) *ImageTagsTable {
return &ImageTagsTable{
imageTagsTable: newImageTagsTableImpl(schemaName, tableName, alias),
EXCLUDED: newImageTagsTableImpl("", "excluded", ""),
}
}
func newImageTagsTableImpl(schemaName, tableName, alias string) imageTagsTable {
var (
IDColumn = postgres.StringColumn("id")
TagIDColumn = postgres.StringColumn("tag_id")
ImageIDColumn = postgres.StringColumn("image_id")
allColumns = postgres.ColumnList{IDColumn, TagIDColumn, ImageIDColumn}
mutableColumns = postgres.ColumnList{TagIDColumn, ImageIDColumn}
)
return imageTagsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
TagID: TagIDColumn,
ImageID: ImageIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

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 ImageText = newImageTextTable("haystack", "image_text", "")
type imageTextTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
ImageText postgres.ColumnString
ImageID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type ImageTextTable struct {
imageTextTable
EXCLUDED imageTextTable
}
// AS creates new ImageTextTable with assigned alias
func (a ImageTextTable) AS(alias string) *ImageTextTable {
return newImageTextTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new ImageTextTable with assigned schema name
func (a ImageTextTable) FromSchema(schemaName string) *ImageTextTable {
return newImageTextTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new ImageTextTable with assigned table prefix
func (a ImageTextTable) WithPrefix(prefix string) *ImageTextTable {
return newImageTextTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new ImageTextTable with assigned table suffix
func (a ImageTextTable) WithSuffix(suffix string) *ImageTextTable {
return newImageTextTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newImageTextTable(schemaName, tableName, alias string) *ImageTextTable {
return &ImageTextTable{
imageTextTable: newImageTextTableImpl(schemaName, tableName, alias),
EXCLUDED: newImageTextTableImpl("", "excluded", ""),
}
}
func newImageTextTableImpl(schemaName, tableName, alias string) imageTextTable {
var (
IDColumn = postgres.StringColumn("id")
ImageTextColumn = postgres.StringColumn("image_text")
ImageIDColumn = postgres.StringColumn("image_id")
allColumns = postgres.ColumnList{IDColumn, ImageTextColumn, ImageIDColumn}
mutableColumns = postgres.ColumnList{ImageTextColumn, ImageIDColumn}
)
return imageTextTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
ImageText: ImageTextColumn,
ImageID: ImageIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -0,0 +1,84 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var Locations = newLocationsTable("haystack", "locations", "")
type locationsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
Name postgres.ColumnString
Address postgres.ColumnString
Description postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type LocationsTable struct {
locationsTable
EXCLUDED locationsTable
}
// AS creates new LocationsTable with assigned alias
func (a LocationsTable) AS(alias string) *LocationsTable {
return newLocationsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new LocationsTable with assigned schema name
func (a LocationsTable) FromSchema(schemaName string) *LocationsTable {
return newLocationsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new LocationsTable with assigned table prefix
func (a LocationsTable) WithPrefix(prefix string) *LocationsTable {
return newLocationsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new LocationsTable with assigned table suffix
func (a LocationsTable) WithSuffix(suffix string) *LocationsTable {
return newLocationsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newLocationsTable(schemaName, tableName, alias string) *LocationsTable {
return &LocationsTable{
locationsTable: newLocationsTableImpl(schemaName, tableName, alias),
EXCLUDED: newLocationsTableImpl("", "excluded", ""),
}
}
func newLocationsTableImpl(schemaName, tableName, alias string) locationsTable {
var (
IDColumn = postgres.StringColumn("id")
NameColumn = postgres.StringColumn("name")
AddressColumn = postgres.StringColumn("address")
DescriptionColumn = postgres.StringColumn("description")
allColumns = postgres.ColumnList{IDColumn, NameColumn, AddressColumn, DescriptionColumn}
mutableColumns = postgres.ColumnList{NameColumn, AddressColumn, DescriptionColumn}
)
return locationsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Name: NameColumn,
Address: AddressColumn,
Description: DescriptionColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -0,0 +1,84 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var Notes = newNotesTable("haystack", "notes", "")
type notesTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
Name postgres.ColumnString
Description postgres.ColumnString
Content postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type NotesTable struct {
notesTable
EXCLUDED notesTable
}
// AS creates new NotesTable with assigned alias
func (a NotesTable) AS(alias string) *NotesTable {
return newNotesTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new NotesTable with assigned schema name
func (a NotesTable) FromSchema(schemaName string) *NotesTable {
return newNotesTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new NotesTable with assigned table prefix
func (a NotesTable) WithPrefix(prefix string) *NotesTable {
return newNotesTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new NotesTable with assigned table suffix
func (a NotesTable) WithSuffix(suffix string) *NotesTable {
return newNotesTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newNotesTable(schemaName, tableName, alias string) *NotesTable {
return &NotesTable{
notesTable: newNotesTableImpl(schemaName, tableName, alias),
EXCLUDED: newNotesTableImpl("", "excluded", ""),
}
}
func newNotesTableImpl(schemaName, tableName, alias string) notesTable {
var (
IDColumn = postgres.StringColumn("id")
NameColumn = postgres.StringColumn("name")
DescriptionColumn = postgres.StringColumn("description")
ContentColumn = postgres.StringColumn("content")
allColumns = postgres.ColumnList{IDColumn, NameColumn, DescriptionColumn, ContentColumn}
mutableColumns = postgres.ColumnList{NameColumn, DescriptionColumn, ContentColumn}
)
return notesTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Name: NameColumn,
Description: DescriptionColumn,
Content: ContentColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

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 SchemaItems = newSchemaItemsTable("haystack", "schema_items", "")
type schemaItemsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
Item postgres.ColumnString
Value postgres.ColumnString
Description postgres.ColumnString
StackID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type SchemaItemsTable struct {
schemaItemsTable
EXCLUDED schemaItemsTable
}
// AS creates new SchemaItemsTable with assigned alias
func (a SchemaItemsTable) AS(alias string) *SchemaItemsTable {
return newSchemaItemsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new SchemaItemsTable with assigned schema name
func (a SchemaItemsTable) FromSchema(schemaName string) *SchemaItemsTable {
return newSchemaItemsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new SchemaItemsTable with assigned table prefix
func (a SchemaItemsTable) WithPrefix(prefix string) *SchemaItemsTable {
return newSchemaItemsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new SchemaItemsTable with assigned table suffix
func (a SchemaItemsTable) WithSuffix(suffix string) *SchemaItemsTable {
return newSchemaItemsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newSchemaItemsTable(schemaName, tableName, alias string) *SchemaItemsTable {
return &SchemaItemsTable{
schemaItemsTable: newSchemaItemsTableImpl(schemaName, tableName, alias),
EXCLUDED: newSchemaItemsTableImpl("", "excluded", ""),
}
}
func newSchemaItemsTableImpl(schemaName, tableName, alias string) schemaItemsTable {
var (
IDColumn = postgres.StringColumn("id")
ItemColumn = postgres.StringColumn("item")
ValueColumn = postgres.StringColumn("value")
DescriptionColumn = postgres.StringColumn("description")
StackIDColumn = postgres.StringColumn("stack_id")
allColumns = postgres.ColumnList{IDColumn, ItemColumn, ValueColumn, DescriptionColumn, StackIDColumn}
mutableColumns = postgres.ColumnList{ItemColumn, ValueColumn, DescriptionColumn, StackIDColumn}
)
return schemaItemsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Item: ItemColumn,
Value: ValueColumn,
Description: DescriptionColumn,
StackID: StackIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

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 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

@ -10,10 +10,24 @@ package table
// UseSchema sets a new schema name for all generated table SQL builder types. It is recommended to invoke
// this method only once at the beginning of the program.
func UseSchema(schema string) {
Contacts = Contacts.FromSchema(schema)
Events = Events.FromSchema(schema)
Image = Image.FromSchema(schema)
ImageSchemaItems = ImageSchemaItems.FromSchema(schema)
ImageStacks = ImageStacks.FromSchema(schema)
SchemaItems = SchemaItems.FromSchema(schema)
Stacks = Stacks.FromSchema(schema)
ImageContacts = ImageContacts.FromSchema(schema)
ImageEvents = ImageEvents.FromSchema(schema)
ImageLinks = ImageLinks.FromSchema(schema)
ImageLocations = ImageLocations.FromSchema(schema)
ImageNotes = ImageNotes.FromSchema(schema)
ImageTags = ImageTags.FromSchema(schema)
ImageText = ImageText.FromSchema(schema)
Locations = Locations.FromSchema(schema)
Notes = Notes.FromSchema(schema)
UserContacts = UserContacts.FromSchema(schema)
UserEvents = UserEvents.FromSchema(schema)
UserImages = UserImages.FromSchema(schema)
UserImagesToProcess = UserImagesToProcess.FromSchema(schema)
UserLocations = UserLocations.FromSchema(schema)
UserNotes = UserNotes.FromSchema(schema)
UserTags = UserTags.FromSchema(schema)
Users = Users.FromSchema(schema)
}

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 UserContacts = newUserContactsTable("haystack", "user_contacts", "")
type userContactsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
UserID postgres.ColumnString
ContactID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type UserContactsTable struct {
userContactsTable
EXCLUDED userContactsTable
}
// AS creates new UserContactsTable with assigned alias
func (a UserContactsTable) AS(alias string) *UserContactsTable {
return newUserContactsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new UserContactsTable with assigned schema name
func (a UserContactsTable) FromSchema(schemaName string) *UserContactsTable {
return newUserContactsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new UserContactsTable with assigned table prefix
func (a UserContactsTable) WithPrefix(prefix string) *UserContactsTable {
return newUserContactsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new UserContactsTable with assigned table suffix
func (a UserContactsTable) WithSuffix(suffix string) *UserContactsTable {
return newUserContactsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newUserContactsTable(schemaName, tableName, alias string) *UserContactsTable {
return &UserContactsTable{
userContactsTable: newUserContactsTableImpl(schemaName, tableName, alias),
EXCLUDED: newUserContactsTableImpl("", "excluded", ""),
}
}
func newUserContactsTableImpl(schemaName, tableName, alias string) userContactsTable {
var (
IDColumn = postgres.StringColumn("id")
UserIDColumn = postgres.StringColumn("user_id")
ContactIDColumn = postgres.StringColumn("contact_id")
allColumns = postgres.ColumnList{IDColumn, UserIDColumn, ContactIDColumn}
mutableColumns = postgres.ColumnList{UserIDColumn, ContactIDColumn}
)
return userContactsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
UserID: UserIDColumn,
ContactID: ContactIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

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 UserEvents = newUserEventsTable("haystack", "user_events", "")
type userEventsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
EventID postgres.ColumnString
UserID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type UserEventsTable struct {
userEventsTable
EXCLUDED userEventsTable
}
// AS creates new UserEventsTable with assigned alias
func (a UserEventsTable) AS(alias string) *UserEventsTable {
return newUserEventsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new UserEventsTable with assigned schema name
func (a UserEventsTable) FromSchema(schemaName string) *UserEventsTable {
return newUserEventsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new UserEventsTable with assigned table prefix
func (a UserEventsTable) WithPrefix(prefix string) *UserEventsTable {
return newUserEventsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new UserEventsTable with assigned table suffix
func (a UserEventsTable) WithSuffix(suffix string) *UserEventsTable {
return newUserEventsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newUserEventsTable(schemaName, tableName, alias string) *UserEventsTable {
return &UserEventsTable{
userEventsTable: newUserEventsTableImpl(schemaName, tableName, alias),
EXCLUDED: newUserEventsTableImpl("", "excluded", ""),
}
}
func newUserEventsTableImpl(schemaName, tableName, alias string) userEventsTable {
var (
IDColumn = postgres.StringColumn("id")
EventIDColumn = postgres.StringColumn("event_id")
UserIDColumn = postgres.StringColumn("user_id")
allColumns = postgres.ColumnList{IDColumn, EventIDColumn, UserIDColumn}
mutableColumns = postgres.ColumnList{EventIDColumn, UserIDColumn}
)
return userEventsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
EventID: EventIDColumn,
UserID: UserIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

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

View File

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

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 UserLocations = newUserLocationsTable("haystack", "user_locations", "")
type userLocationsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
LocationID postgres.ColumnString
UserID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type UserLocationsTable struct {
userLocationsTable
EXCLUDED userLocationsTable
}
// AS creates new UserLocationsTable with assigned alias
func (a UserLocationsTable) AS(alias string) *UserLocationsTable {
return newUserLocationsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new UserLocationsTable with assigned schema name
func (a UserLocationsTable) FromSchema(schemaName string) *UserLocationsTable {
return newUserLocationsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new UserLocationsTable with assigned table prefix
func (a UserLocationsTable) WithPrefix(prefix string) *UserLocationsTable {
return newUserLocationsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new UserLocationsTable with assigned table suffix
func (a UserLocationsTable) WithSuffix(suffix string) *UserLocationsTable {
return newUserLocationsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newUserLocationsTable(schemaName, tableName, alias string) *UserLocationsTable {
return &UserLocationsTable{
userLocationsTable: newUserLocationsTableImpl(schemaName, tableName, alias),
EXCLUDED: newUserLocationsTableImpl("", "excluded", ""),
}
}
func newUserLocationsTableImpl(schemaName, tableName, alias string) userLocationsTable {
var (
IDColumn = postgres.StringColumn("id")
LocationIDColumn = postgres.StringColumn("location_id")
UserIDColumn = postgres.StringColumn("user_id")
allColumns = postgres.ColumnList{IDColumn, LocationIDColumn, UserIDColumn}
mutableColumns = postgres.ColumnList{LocationIDColumn, UserIDColumn}
)
return userLocationsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
LocationID: LocationIDColumn,
UserID: UserIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

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 UserNotes = newUserNotesTable("haystack", "user_notes", "")
type userNotesTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
UserID postgres.ColumnString
NoteID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type UserNotesTable struct {
userNotesTable
EXCLUDED userNotesTable
}
// AS creates new UserNotesTable with assigned alias
func (a UserNotesTable) AS(alias string) *UserNotesTable {
return newUserNotesTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new UserNotesTable with assigned schema name
func (a UserNotesTable) FromSchema(schemaName string) *UserNotesTable {
return newUserNotesTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new UserNotesTable with assigned table prefix
func (a UserNotesTable) WithPrefix(prefix string) *UserNotesTable {
return newUserNotesTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new UserNotesTable with assigned table suffix
func (a UserNotesTable) WithSuffix(suffix string) *UserNotesTable {
return newUserNotesTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newUserNotesTable(schemaName, tableName, alias string) *UserNotesTable {
return &UserNotesTable{
userNotesTable: newUserNotesTableImpl(schemaName, tableName, alias),
EXCLUDED: newUserNotesTableImpl("", "excluded", ""),
}
}
func newUserNotesTableImpl(schemaName, tableName, alias string) userNotesTable {
var (
IDColumn = postgres.StringColumn("id")
UserIDColumn = postgres.StringColumn("user_id")
NoteIDColumn = postgres.StringColumn("note_id")
allColumns = postgres.ColumnList{IDColumn, UserIDColumn, NoteIDColumn}
mutableColumns = postgres.ColumnList{UserIDColumn, NoteIDColumn}
)
return userNotesTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
UserID: UserIDColumn,
NoteID: NoteIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

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 UserTags = newUserTagsTable("haystack", "user_tags", "")
type userTagsTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
Tag postgres.ColumnString
UserID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
}
type UserTagsTable struct {
userTagsTable
EXCLUDED userTagsTable
}
// AS creates new UserTagsTable with assigned alias
func (a UserTagsTable) AS(alias string) *UserTagsTable {
return newUserTagsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new UserTagsTable with assigned schema name
func (a UserTagsTable) FromSchema(schemaName string) *UserTagsTable {
return newUserTagsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new UserTagsTable with assigned table prefix
func (a UserTagsTable) WithPrefix(prefix string) *UserTagsTable {
return newUserTagsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new UserTagsTable with assigned table suffix
func (a UserTagsTable) WithSuffix(suffix string) *UserTagsTable {
return newUserTagsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newUserTagsTable(schemaName, tableName, alias string) *UserTagsTable {
return &UserTagsTable{
userTagsTable: newUserTagsTableImpl(schemaName, tableName, alias),
EXCLUDED: newUserTagsTableImpl("", "excluded", ""),
}
}
func newUserTagsTableImpl(schemaName, tableName, alias string) userTagsTable {
var (
IDColumn = postgres.StringColumn("id")
TagColumn = postgres.StringColumn("tag")
UserIDColumn = postgres.StringColumn("user_id")
allColumns = postgres.ColumnList{IDColumn, TagColumn, UserIDColumn}
mutableColumns = postgres.ColumnList{TagColumn, UserIDColumn}
)
return userTagsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Tag: TagColumn,
UserID: UserIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

@ -17,9 +17,8 @@ type usersTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
Email postgres.ColumnString
CreatedAt postgres.ColumnTimestampz
ID postgres.ColumnString
Email postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
@ -60,20 +59,18 @@ func newUsersTable(schemaName, tableName, alias string) *UsersTable {
func newUsersTableImpl(schemaName, tableName, alias string) usersTable {
var (
IDColumn = postgres.StringColumn("id")
EmailColumn = postgres.StringColumn("email")
CreatedAtColumn = postgres.TimestampzColumn("created_at")
allColumns = postgres.ColumnList{IDColumn, EmailColumn, CreatedAtColumn}
mutableColumns = postgres.ColumnList{EmailColumn, CreatedAtColumn}
IDColumn = postgres.StringColumn("id")
EmailColumn = postgres.StringColumn("email")
allColumns = postgres.ColumnList{IDColumn, EmailColumn}
mutableColumns = postgres.ColumnList{EmailColumn}
)
return usersTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
Email: EmailColumn,
CreatedAt: CreatedAtColumn,
ID: IDColumn,
Email: EmailColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,

View File

@ -142,12 +142,8 @@ func (m TextMessageContent) IsImageMessage() bool {
}
type ImageMessageContent struct {
ImageType string `json:"type"`
ImageUrl ImageMessageUrl `json:"image_url"`
}
type ImageMessageUrl struct {
Url string `json:"url"`
ImageType string `json:"type"`
ImageUrl string `json:"image_url"`
}
func (m ImageMessageContent) IsImageMessage() bool {
@ -165,7 +161,6 @@ type ImageContentUrl struct {
type ToolCall struct {
Index int `json:"index"`
Id string `json:"id"`
Type string `json:"type,omitzero"`
Function FunctionCall `json:"function"`
}
@ -187,23 +182,11 @@ func (chat *Chat) AddSystem(prompt string) {
})
}
func (chat *Chat) AddUser(msg string) {
chat.Messages = append(chat.Messages, ChatUserMessage{
Role: User,
MessageContent: SingleMessage{
Content: msg,
},
})
}
func (chat *Chat) AddImage(imageName string, image []byte, query *string) error {
extension := filepath.Ext(imageName)
if len(extension) == 0 {
// TODO: could also validate for image types we support.
// return errors.New("Image does not have extension")
// Hacky! It seems apple doesnt add extension.
// BIG TODO: take better metadata from the image.
extension = "png"
return errors.New("Image does not have extension")
}
extension = extension[1:]
@ -230,9 +213,7 @@ func (chat *Chat) AddImage(imageName string, image []byte, query *string) error
messageContent.Content[index] = ImageMessageContent{
ImageType: "image_url",
ImageUrl: ImageMessageUrl{
Url: fmt.Sprintf("data:image/%s;base64,%s", extension, encodedString),
},
ImageUrl: fmt.Sprintf("data:image/%s;base64,%s", extension, encodedString),
}
arrayMessage := ChatUserMessage{Role: User, MessageContent: messageContent}

View File

@ -4,7 +4,6 @@ import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
@ -15,7 +14,7 @@ import (
type ResponseFormat struct {
Type string `json:"type"`
JsonSchema any `json:"json_schema,omitzero"`
JsonSchema any `json:"json_schema"`
}
type AgentRequestBody struct {
@ -26,8 +25,6 @@ type AgentRequestBody struct {
Tools *any `json:"tools,omitempty"`
ToolChoice *string `json:"tool_choice,omitempty"`
RandomSeed *int `json:"random_seed,omitempty"`
EndToolCall string `json:"-"`
Chat *Chat `json:"messages"`
@ -83,7 +80,7 @@ type AgentClient struct {
Options CreateAgentClientOptions
}
const OPENAI_API_KEY = "REAL_OPEN_AI_KEY"
const OPENAI_API_KEY = "OPENAI_API_KEY"
type CreateAgentClientOptions struct {
Log *log.Logger
@ -102,7 +99,7 @@ func CreateAgentClient(options CreateAgentClientOptions) AgentClient {
return AgentClient{
apiKey: apiKey,
url: "https://router.requesty.ai/v1/chat/completions",
url: "https://api.mistral.ai/v1/chat/completions",
Do: func(req *http.Request) (*http.Response, error) {
client := &http.Client{}
return client.Do(req)
@ -133,29 +130,29 @@ func (client AgentClient) getRequest(body []byte) (*http.Request, error) {
func (client AgentClient) Request(req *AgentRequestBody) (AgentResponse, error) {
jsonAiRequest, err := json.Marshal(req)
if err != nil {
return AgentResponse{}, fmt.Errorf("Could not format JSON: %w", err)
return AgentResponse{}, err
}
httpRequest, err := client.getRequest(jsonAiRequest)
if err != nil {
return AgentResponse{}, fmt.Errorf("Could not get request: %w", err)
return AgentResponse{}, err
}
resp, err := client.Do(httpRequest)
if err != nil {
return AgentResponse{}, fmt.Errorf("Could not send request: %w", err)
return AgentResponse{}, err
}
response, err := io.ReadAll(resp.Body)
if err != nil {
return AgentResponse{}, fmt.Errorf("Could not read body: %w", err)
return AgentResponse{}, err
}
agentResponse := AgentResponse{}
err = json.Unmarshal(response, &agentResponse)
if err != nil {
return AgentResponse{}, fmt.Errorf("Could not unmarshal response, response: %s: %w", string(response), err)
return AgentResponse{}, err
}
if len(agentResponse.Choices) != 1 {
@ -240,14 +237,12 @@ func (client *AgentClient) RunAgent(userId uuid.UUID, imageId uuid.UUID, imageNa
panic(err)
}
toolChoice := "auto"
seed := 42
toolChoice := "any"
request := AgentRequestBody{
Tools: &tools,
ToolChoice: &toolChoice,
Model: "policy/images",
RandomSeed: &seed,
Model: "pixtral-12b-2409",
Temperature: 0.3,
EndToolCall: client.Options.EndToolCall,
ResponseFormat: ResponseFormat{
@ -262,7 +257,7 @@ func (client *AgentClient) RunAgent(userId uuid.UUID, imageId uuid.UUID, imageNa
request.Chat.AddImage(imageName, imageData, client.Options.Query)
toolHandlerInfo := ToolHandlerInfo{
ImageID: imageId,
ImageId: imageId,
ImageName: imageName,
UserId: userId,
Image: &imageData,
@ -270,38 +265,3 @@ func (client *AgentClient) RunAgent(userId uuid.UUID, imageId uuid.UUID, imageNa
return client.ToolLoop(toolHandlerInfo, &request)
}
func (client *AgentClient) RunAgentAlone(userID uuid.UUID, userReq string) error {
var tools any
err := json.Unmarshal([]byte(client.Options.JsonTools), &tools)
if err != nil {
return err
}
toolChoice := "auto"
seed := 42
request := AgentRequestBody{
Tools: &tools,
ToolChoice: &toolChoice,
Model: "policy/images",
RandomSeed: &seed,
Temperature: 0.3,
EndToolCall: client.Options.EndToolCall,
ResponseFormat: ResponseFormat{
Type: "text",
},
Chat: &Chat{
Messages: make([]ChatMessage, 0),
},
}
request.Chat.AddSystem(client.Options.SystemPrompt)
request.Chat.AddUser(userReq)
toolHandlerInfo := ToolHandlerInfo{
UserId: userID,
}
return client.ToolLoop(toolHandlerInfo, &request)
}

View File

@ -9,7 +9,7 @@ import (
type ToolHandlerInfo struct {
UserId uuid.UUID
ImageID uuid.UUID
ImageId uuid.UUID
ImageName string
// Pointer because we don't want to copy this around too much.

View File

@ -40,7 +40,7 @@ func (suite *ToolTestSuite) TestSingleToolCall() {
response := suite.handler.Handle(
ToolHandlerInfo{
UserId: uuid.Nil,
ImageID: uuid.Nil,
ImageId: uuid.Nil,
},
ToolCall{
Index: 0,
@ -91,7 +91,7 @@ func (suite *ToolTestSuite) TestMultipleToolCalls() {
err := suite.client.Process(
ToolHandlerInfo{
UserId: uuid.Nil,
ImageID: uuid.Nil,
ImageId: uuid.Nil,
},
&AgentRequestBody{
Chat: &chat,
@ -154,7 +154,7 @@ func (suite *ToolTestSuite) TestMultipleToolCallsWithErrors() {
err := suite.client.Process(
ToolHandlerInfo{
UserId: uuid.Nil,
ImageID: uuid.Nil,
ImageId: uuid.Nil,
},
&AgentRequestBody{
Chat: &chat,

View File

@ -0,0 +1,180 @@
package agents
import (
"context"
"encoding/json"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/models"
"github.com/charmbracelet/log"
"github.com/google/uuid"
)
const contactPrompt = `
**Role:** You are an AI assistant specialized in processing contact information from images. Your primary function is to use the provided tools (listContacts, createContact, stopAgent) to manage contacts based on image analysis and signal when processing is complete.
**Primary Goal:** To accurately identify potential contacts in an image, check against existing contacts using the provided tools, create new contact entries when necessary (meticulously avoiding duplicates), and explicitly stop processing when finished or if no action is needed.
**Input:** You will be given an image that may contain contact information.
**Output Behavior (CRITICAL):**
* **If providing a text response:** Generate only the conversational text intended for the user in the response content. (Note: This should generally not happen in this workflow, as actions are handled by tools).
* **If using a tool:** Generate **only** the structured tool call request in the designated tool call section of the response. **Do NOT include the tool call JSON, parameters, or any description of your intention to call the tool within the main text/content response.** Your output must be strictly one or the other for a given turn: either text content OR a tool call structure.
**Core Workflow:**
1. **Image Analysis:**
* Carefully scan the provided image to identify and extract any visible contact details (Name, Phone Number, Email Address, Physical Address). Extract *all* available information for each potential contact.
* **If NO potential contact information is found in the image, proceed directly to Step 5 (call stopAgent).**
2. **Duplicate Check (Mandatory First Step if contacts found):**
* If potential contact(s) were found in Step 1, you **must** call the listContacts tool first. **Generate only the listContacts tool call structure.**
* Once you receive the list, compare the extracted information against the existing contacts to determine if each identified person is already present.
* **If *all* identified potential contacts already exist in the list, proceed directly to Step 5 (call stopAgent).**
3. **Create New Contact (Conditional):**
* For each potential contact identified in Step 1 that your check in Step 2 confirms is *new*:
* Call the createContact tool with *all* corresponding extracted information (name, phoneNumber, address, email). name is mandatory. **Generate only the createContact tool call structure.**
* Process *one new contact creation per turn*. If multiple new contacts need creating, you will call createContact sequentially (one call per turn).
4. **Handling Multiple Contacts:**
* The workflow intrinsically handles multiple contacts by requiring a listContacts check first, followed by potential sequential createContact calls for each new individual found.
5. **Task Completion / No Action Needed:**
* Call the stopAgent tool **only** when one of the following conditions is met:
* No potential contact information was found in the initial image analysis (Step 1).
* The listContacts check confirmed that *all* potential contacts identified in the image already exist (Step 2).
* You have successfully processed all identified contacts (i.e., performed the listContacts check and called createContact for *all* new individuals found).
* **Generate only the stopAgent tool call structure.**
**Available Tools:**
* **listContacts**: Retrieves the existing contact list. **Must** be called first if potential contacts are found in the image, to enable duplicate checking.
* **createContact**: Adds a *new*, non-duplicate contact. Only call *after* listContacts confirms the person is new. name is mandatory.
* **stopAgent**: Signals that processing for the current image is complete (either action was taken, no action was needed, or all identified contacts already existed). Call this as the final step or when no other action is applicable based on the workflow.
`
const contactTools = `
[
{
"type": "function",
"function": {
"name": "listContacts",
"description": "Retrieves the complete list of the user's currently saved contacts (e.g., names, phone numbers, emails if available in the stored data). This tool is essential and **must** be called *before* attempting to create a new contact if potential contact info is found in the image, to check if the person already exists and prevent duplicate entries.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "createContact",
"description": "Saves a new contact to the user's contact list. Only use this function **after** confirming the contact does not already exist by checking the output of listContacts. Provide all available extracted information for the new contact. Process one new contact per call.",
"parameters": {
"type": "object",
"properties": {
"contactId": {
"type": "string",
"description": "The UUID of the contact. You should only provide this IF you believe the contact already exists, from listContacts."
},
"name": {
"type": "string",
"description": "The full name of the person being added as a contact. This field is mandatory."
},
"phoneNumber": {
"type": "string",
"description": "The contact's primary phone number, including area or country code if available. Provide this if extracted from the image."
},
"address": {
"type": "string",
"description": "The complete physical mailing address of the contact (e.g., street number, street name, city, state/province, postal code, country). Provide this if extracted from the image."
},
"email": {
"type": "string",
"description": "The contact's primary email address. Provide this if extracted from the image."
}
},
"required": ["name"]
}
}
},
{
"type": "function",
"function": {
"name": "stopAgent",
"description": "Use this tool to signal that the contact processing for the current image is complete. Call this *only* when: 1) No contact info was found initially, OR 2) All found contacts were confirmed to already exist after calling listContacts, OR 3) All necessary createContact calls for new individuals have been completed.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
}
]
`
type listContactsArguments struct{}
type createContactsArguments struct {
Name string `json:"name"`
ContactID *string `json:"contactId"`
PhoneNumber *string `json:"phoneNumber"`
Address *string `json:"address"`
Email *string `json:"email"`
}
func NewContactAgent(log *log.Logger, contactModel models.ContactModel) client.AgentClient {
agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: contactPrompt,
JsonTools: contactTools,
Log: log,
EndToolCall: "stopAgent",
})
agentClient.ToolHandler.AddTool("listContacts", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return contactModel.List(context.Background(), info.UserId)
})
agentClient.ToolHandler.AddTool("createContact", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
args := createContactsArguments{}
err := json.Unmarshal([]byte(_args), &args)
if err != nil {
return model.Contacts{}, err
}
ctx := context.Background()
contactId := uuid.Nil
if args.ContactID != nil {
contactUuid, err := uuid.Parse(*args.ContactID)
if err != nil {
return model.Contacts{}, err
}
contactId = contactUuid
}
contact, err := contactModel.Save(ctx, info.UserId, model.Contacts{
ID: contactId,
Name: args.Name,
PhoneNumber: args.PhoneNumber,
Email: args.Email,
})
if err != nil {
return model.Contacts{}, err
}
_, err = contactModel.SaveToImage(ctx, info.ImageId, contact.ID)
if err != nil {
return model.Contacts{}, err
}
return contact, nil
})
return agentClient
}

View File

@ -1,148 +0,0 @@
package agents
import (
"context"
"encoding/json"
"fmt"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/models"
"github.com/charmbracelet/log"
"github.com/google/uuid"
)
const createListAgentPrompt = `
You are an agent who's job is to produce a reasonable output for an unstructured input.
Your job is to create lists for the user, the user will give you a title and some fields they want
as part of the list. Your job is to take these fields, adjust their names so they have good names,
and add a good description for each one.
You can add fields if you think they make a lot of sense.
You can remove fields if they are not correct, but be sure before you do this.
`
const listJsonSchema = `
{
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "the title of the list"
},
"description": {
"type": "string",
"description": "the description of the list"
},
"fields": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the field."
},
"description": {
"type": "string",
"description": "A description of the field."
}
},
"required": [
"name",
"description"
]
},
"description": "An array of field objects."
}
},
"required": [
"fields"
]
}
`
type createNewListArguments struct {
Title string `json:"title"`
Description string `json:"description"`
Fields []struct {
Name string `json:"name"`
Description string `json:"description"`
} `json:"fields"`
}
type CreateListAgent struct {
client client.AgentClient
listModel models.StackModel
}
func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, userReq string) error {
request := client.AgentRequestBody{
Model: "policy/images",
Temperature: 0.3,
ResponseFormat: client.ResponseFormat{
Type: "json_object",
JsonSchema: listJsonSchema,
},
Chat: &client.Chat{
Messages: make([]client.ChatMessage, 0),
},
}
request.Chat.AddSystem(agent.client.Options.SystemPrompt)
request.Chat.AddUser(userReq)
resp, err := agent.client.Request(&request)
if err != nil {
return fmt.Errorf("request: %w", err)
}
ctx := context.Background()
structuredOutput := resp.Choices[0].Message.Content
var createListArgs createNewListArguments
err = json.Unmarshal([]byte(structuredOutput), &createListArgs)
if err != nil {
return err
}
schemaItems := make([]model.SchemaItems, 0)
for _, field := range createListArgs.Fields {
schemaItems = append(schemaItems, model.SchemaItems{
Item: field.Name,
Description: field.Description,
Value: "string", // keep it simple for now.
})
}
_, err = agent.listModel.Save(ctx, userID, createListArgs.Title, createListArgs.Description, model.Progress_Complete)
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.StackModel) CreateListAgent {
client := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: createListAgentPrompt,
Log: log,
})
agent := CreateListAgent{
client,
listModel,
}
return agent
}

View File

@ -1,74 +0,0 @@
package agents
import (
"context"
"fmt"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/models"
"github.com/charmbracelet/log"
"github.com/google/uuid"
)
const noteAgentPrompt = `
You are an AI agent who's job is to describe the image you see.
You should also add any text you see in the image, if no text exists, just add a description.
Be consise and don't add too much extra information or formatting characters, simple text.
You must write this text in Markdown. You can add extra information for the user.
You must organise this text nicely, not be all over the place.
`
type DescriptionAgent struct {
client client.AgentClient
imageModel models.ImageModel
}
func (agent DescriptionAgent) Describe(log *log.Logger, imageID uuid.UUID, imageName string, imageData []byte) error {
request := client.AgentRequestBody{
Model: "policy/images",
Temperature: 0.3,
ResponseFormat: client.ResponseFormat{
Type: "text",
},
Chat: &client.Chat{
Messages: make([]client.ChatMessage, 0),
},
}
request.Chat.AddSystem(noteAgentPrompt)
request.Chat.AddImage(imageName, imageData, nil)
log.Debug("Sending description request")
resp, err := agent.client.Request(&request)
if err != nil {
return fmt.Errorf("Could not request. %s", err)
}
ctx := context.Background()
description := resp.Choices[0].Message.Content
err = agent.imageModel.UpdateDescription(ctx, imageID, description)
if err != nil {
return err
}
return nil
}
func NewDescriptionAgent(log *log.Logger, imageModel models.ImageModel) DescriptionAgent {
client := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: noteAgentPrompt,
Log: log,
})
agent := DescriptionAgent{
client: client,
imageModel: imageModel,
}
return agent
}

View File

@ -0,0 +1,247 @@
package agents
import (
"context"
"encoding/json"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/models"
"time"
"github.com/charmbracelet/log"
"github.com/google/uuid"
)
const eventPrompt = `
**Role:** You are an Event Processing AI Assistant specialized in extracting event information from images, managing event data using provided tools, and ensuring accuracy and avoiding duplicates.
**Primary Goal:** To analyze images, identify potential events (like meetings, appointments, conferences, invitations), extract key details (name, date/time, location description), check against existing events, retrieve location identifiers if applicable, create new event entries when necessary, and signal completion using the 'finish' tool.
**Core Workflow:**
**Duplicate Check (Mandatory if Event Found):**
* If potential event details were found, you **must** call the listEvents tool first to check for duplicates. **Generate only the listEvents tool call structure.**
* Once you receive the list, compare the extracted event details (Name, Start Date/Time primarily) against the existing events.
* **If a matching event already exists, proceed directly to Step 6 (call finish).**
**Location ID Retrieval (Conditional):**
* If the event is identified as *new* AND a *location description* was extracted.
* Call the getEventLocationId tool, providing the extracted location description. **Generate only the getEventLocationId tool call structure.**
**Create Event:**
* If the event was identified as *new*:
* Prepare the parameters for the createEvent tool using the extracted details (Name, Start Date/Time, End Date/Time).
* If you identify the event as *duplicate*, meaning you think an event in listEvents is the same as the event on this image.
* Call the updateEvent tool so this image is also linked to that event. If you find any new information you can update it using this tool too.
**Handling Multiple Events:**
* If the image contains multiple distinct events, ideally process them one by one.
* Do this until there are no more events on this image
**Task Completion / No Action Needed:**
* Call the finish tool **only** when one of the following conditions is met:
* No identifiable event information was found in the initial image analysis.
* The listEvents check confirmed the identified event already exists.
* You have successfully called createEvent for a new event.
**Available Tools:**
* **listEvents**: Retrieves the user's existing events. **Must** be called first if potential event details are found in the image, to enable duplicate checking.
* **getEventLocationId**: Takes a location description (text) and retrieves a unique ID (locationId) for it. Use this *before* createEvent *only* if a new event has a specific location mentioned.
* **createEvent**: Adds a *new*, non-duplicate event to the user's calendar/list. Only call *after* listEvents confirms the event is new. Requires name. Include startDateTime, endDateTime, and locationId (if available and retrieved).
* **stopAgent**: Signals that processing for the current image is complete (either action was taken, no action was needed because the event already existed, or no event was found). Call this as the final step.
`
const eventTools = `
[
{
"type": "function",
"function": {
"name": "listEvents",
"description": "Retrieves the list of the user's currently scheduled events. Essential for checking if an event identified in the image already exists to prevent duplicates. Must be called before potentially creating an event.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "createEvent",
"description": "Creates a new event in the user's calendar or list. Use only after listEvents confirms the event is new. Provide all extracted details.",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name or title of the event. This field is mandatory."
},
"startDateTime": {
"type": "string",
"description": "The event's start date and time in ISO 8601 format (e.g., '2025-04-18T10:00:00Z'). Include if available."
},
"endDateTime": {
"type": "string",
"description": "The event's end date and time in ISO 8601 format. Optional, include if available and different from startDateTime."
},
"locationId": {
"type": "string",
"description": "The unique identifier (UUID or similar) for the event's location. Use this if available, do not invent it."
}
},
"required": ["name"]
}
}
},
{
"type": "function",
"function": {
"name": "updateEvent",
"description": "Updates an existing event record identified by its eventId. Use this tool when listEvents indicates a match for the event details found in the current input.",
"parameters": {
"type": "object",
"properties": {
"eventId": {
"type": "string",
"description": "The UUID of the existing event"
}
},
"required": ["eventId"]
}
}
},
{
"type": "function",
"function": {
"name": "getEventLocationId",
"description": "Retrieves a unique identifier for a location description associated with an event. Use this before createEvent if a new event specifies a location.",
"parameters": {
"type": "object",
"properties": {
"locationDescription": {
"type": "string",
"description": "The text describing the location extracted from the image (e.g., 'Conference Room B', '123 Main St, Anytown', 'Zoom Link details')."
}
},
"required": ["locationDescription"]
}
}
},
{
"type": "function",
"function": {
"name": "stopAgent",
"description": "Call this tool only when event processing for the current image is fully complete. This occurs if: 1) No event info was found, OR 2) The found event already exists, OR 3) A new event has been successfully created.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
}
]`
type listEventArguments struct{}
type createEventArguments struct {
Name string `json:"name"`
StartDateTime *string `json:"startDateTime"`
EndDateTime *string `json:"endDateTime"`
OrganizerName *string `json:"organizerName"`
LocationID *string `json:"locationId"`
}
type updateEventArguments struct {
EventID string `json:"eventId"`
}
func NewEventAgent(log *log.Logger, eventsModel models.EventModel, locationModel models.LocationModel) client.AgentClient {
agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: eventPrompt,
JsonTools: eventTools,
Log: log,
EndToolCall: "stopAgent",
})
locationAgent := NewLocationAgentWithComm(log.WithPrefix("Events 📅 > Locations 📍"), locationModel)
locationQuery := "Can you get me the ID of the location present in this image?"
locationAgent.Options.Query = &locationQuery
agentClient.ToolHandler.AddTool("listEvents", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return eventsModel.List(context.Background(), info.UserId)
})
agentClient.ToolHandler.AddTool("createEvent", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
args := createEventArguments{}
err := json.Unmarshal([]byte(_args), &args)
if err != nil {
return model.Events{}, err
}
ctx := context.Background()
layout := "2006-01-02T15:04:05Z"
startTime, err := time.Parse(layout, *args.StartDateTime)
if err != nil {
return model.Events{}, err
}
endTime, err := time.Parse(layout, *args.EndDateTime)
if err != nil {
return model.Events{}, err
}
locationId, err := uuid.Parse(*args.LocationID)
if err != nil {
return model.Events{}, err
}
events, err := eventsModel.Save(ctx, info.UserId, model.Events{
Name: args.Name,
StartDateTime: &startTime,
EndDateTime: &endTime,
LocationID: &locationId,
})
if err != nil {
return model.Events{}, err
}
_, err = eventsModel.SaveToImage(ctx, info.ImageId, events.ID)
if err != nil {
return model.Events{}, err
}
return events, nil
})
agentClient.ToolHandler.AddTool("updateEvent", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
args := updateEventArguments{}
err := json.Unmarshal([]byte(_args), &args)
if err != nil {
return "", err
}
ctx := context.Background()
contactUuid, err := uuid.Parse(args.EventID)
if err != nil {
return "", err
}
eventsModel.SaveToImage(ctx, info.ImageId, contactUuid)
return "Saved", nil
})
agentClient.ToolHandler.AddTool("getEventLocationId", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
// TODO: reenable this when I'm creating the agent locally instead of getting it from above.
locationAgent.RunAgent(info.UserId, info.ImageId, info.ImageName, *info.Image)
log.Debugf("Reply from location %s\n", locationAgent.Reply)
return locationAgent.Reply, nil
})
return agentClient
}

View File

@ -1,259 +0,0 @@
package agents
import (
"context"
"encoding/json"
"fmt"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/limits"
"screenmark/screenmark/models"
"github.com/charmbracelet/log"
"github.com/google/uuid"
)
const listPrompt = `
**You are an AI used to classify what list a certain image belongs in**
You will need to decide using tool calls, if you must create a new list, or use an existing one.
You must be specific enough so it is useful, but not too specific such that all images belong on seperate lists.
An example of lists are:
- Locations
- Events
- TV Shows
- Movies
- Books
Another one of your tasks is to create a schema for this list. This should contain information that this, and following
pictures contain. Be specific but also generic. You should use the parameters in "createList" to create this schema.
This schema should not be super specific. You must be able to understand the image, and if the content of the image doesnt seem relevant, try
and extract some meaning about what the image is.
You must call "listLists" to see which available lists are already available.
Use "createList" only once, don't create multiple lists for one image.
You can add an image to multiple lists, this is also true if you already created a list. But only add to a list if it makes sense to do so.
**Tools:**
* think: Internal reasoning/planning step.
* listLists: Get existing lists
* createList: Creates a new list with a name and description. Only use this once.
* addToList: Add to an existing list. This will also mean extracting information from this image, and inserting it, fitting the schema.
* stopAgent: Signal task completion.
`
const listTools = `
[
{
"type": "function",
"function": {
"name": "think",
"description": "Use this tool to think through the image, evaluating the event and whether or not it exists in the users listEvents.",
"parameters": {
"type": "object",
"properties": {
"thought": {
"type": "string",
"description": "A singular thought about the image."
}
},
"required": ["thought"]
}
}
},
{
"type": "function",
"function": {
"name": "listLists",
"description": "Retrieves the list of the user's existing lists.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "createList",
"description": "Creates a new list",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of this new list."
},
"description": {
"type": "string",
"description": "A simple description of this list."
},
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": {
"Item": {
"type": "string",
"description": "The name of the key for this specific field. Similar to a column in a database"
},
"Value": {
"type": "string",
"enum": ["string", "number", "boolean"]
},
"Description": {
"type": "string",
"description": "The description for this item"
}
}
}
}
},
"required": ["name", "description", "schema"]
}
}
},
{
"type": "function",
"function": {
"name": "addToList",
"description": "Adds an image to a list, this could be a new one you just created or not.",
"parameters": {
"type": "object",
"properties": {
"listId": {
"type": "string",
"description": "The UUID of the existing list"
},
"schema": {
"type": "array",
"items": {
"type": "object",
"description": "A key-value of ID - value from this image to fit the schema. any of the values can be null",
"properties": {
"id": {
"type": "string",
"description": "The UUID of the schema item."
},
"value": {
"type": "string",
"description": "the concrete value for this field"
}
}
}
}
},
"required": ["listId", "schema"]
}
}
},
{
"type": "function",
"function": {
"name": "stopAgent",
"description": "Use this tool to signal that the contact processing for the current image is complete. Call this *only* when: 1) No contact info was found initially, OR 2) All found contacts were confirmed to already exist after calling listContacts, OR 3) All necessary createContact calls for new individuals have been completed.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
}
]`
type createListArguments struct {
Name string `json:"name"`
Desription string `json:"description"`
Schema []model.SchemaItems
}
type addToListArguments struct {
ListID string `json:"listId"`
Schema []models.IDValue
}
func NewListAgent(log *log.Logger, stackModel models.StackModel, limitsMethods limits.LimitsManagerMethods) client.AgentClient {
agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: listPrompt,
JsonTools: listTools,
Log: log,
EndToolCall: "stopAgent",
})
agentClient.ToolHandler.AddTool("think", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return "Thought", nil
})
agentClient.ToolHandler.AddTool("createList", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
args := createListArguments{}
err := json.Unmarshal([]byte(_args), &args)
if err != nil {
return "", err
}
hasReachedLimit, err := limitsMethods.HasReachedStackLimit(info.UserId)
if err != nil {
return "", fmt.Errorf("error checking stack limits: %w", err)
}
if hasReachedLimit {
log.Warn("User has reached limits", "userID", info.UserId)
return "", fmt.Errorf("reached stack limits")
}
ctx := context.Background()
savedList, err := stackModel.Save(ctx, info.UserId, args.Name, args.Desription, model.Progress_Complete)
if err != nil {
log.Error("saving list", "err", err)
return "", err
}
for i := range args.Schema {
args.Schema[i].StackID = savedList.ID
}
err = stackModel.SaveItems(ctx, args.Schema)
if err != nil {
log.Error("saving items", "err", err)
return "", err
}
return savedList, nil
})
agentClient.ToolHandler.AddTool("listLists", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return stackModel.List(context.Background(), info.UserId)
})
agentClient.ToolHandler.AddTool("addToList", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
args := addToListArguments{}
err := json.Unmarshal([]byte(_args), &args)
if err != nil {
return "", err
}
ctx := context.Background()
listUUID, err := uuid.Parse(args.ListID)
if err != nil {
return "", err
}
imageStack, err := stackModel.SaveImage(ctx, info.ImageID, listUUID)
if err != nil {
return "", err
}
if err := stackModel.SaveSchemaItems(ctx, imageStack.ID, args.Schema); err != nil {
return "", err
}
return "Saved", nil
})
return agentClient
}

View File

@ -0,0 +1,196 @@
package agents
import (
"context"
"encoding/json"
"fmt"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/models"
"github.com/charmbracelet/log"
"github.com/google/uuid"
)
const locationPrompt = `
Role: Location AI Assistant
Objective: Identify locations from images/text, manage a saved list (create, update), and answer user queries about saved locations using the provided tools.
Core Logic:
**Extract Location Details:** Attempt to extract location details (like InputName, InputAddress) from the user's input (image or text).
* If no details can be extracted, inform the user and use stopAgent.
**Check for Existing Location:** If details *were* extracted:
* Use listLocations with the extracted InputName and/or InputAddress to search for potentially matching locations already saved in the list.
**Decide Action based on Search Results:**
* **If listLocations returns one or more likely matches:**
* Identify the *best* match (based on name, address similarity).
* **Crucially:** Call upsertLocation, providing the locationId of that best match. Include the newly extracted InputName (required) and any other extracted details (InputAddress, etc.) to potentially *update* the existing record or simply link the current input to it.
* **If listLocations returns no matches OR no returned location is a confident match:**
* Call upsertLocation providing *only* the newly extracted InputName (required) and any other extracted details (InputAddress, etc.). **Do NOT provide a locationId in this case.** This will create a *new* location entry.
4. **Finalize:** After successfully calling upsertLocation (or determining no action could be taken), use stopAgent.
Tool Usage:
* **listLocations**: Searches the saved locations list based on provided criteria (like name or address). Used specifically to check if a location potentially already exists before using upsertLocation. Returns a list of matching locations, *each including its locationId*.
* **upsertLocation**: Creates or updates a location in the saved list. Requires name. Can include address, etc.
* **To UPDATE:** If you identified an existing location using listLocations, provide its locationId along with any new/updated details (name, address, etc.).
* **To CREATE:** If no existing location was found (or you are creating intentionally), provide the location details (name, address, etc.) but **omit the locationId**.
* **stopAgent**: Signals the end of the agent's processing for the current turn. Call this *after* completing the location task (create/update/failed extraction).
`
const replyTool = `
{
"type": "function",
"function": {
"name": "reply",
"description": "Signals intent to provide information about a specific known location in response to a user's query. Use only if the user asked a question and the location's ID was found via listLocations.",
"parameters": {
"type": "object",
"properties": {
"locationId": {
"type": "string",
"description": "The unique identifier of the saved location that the user is asking about."
}
},
"required": ["locationId"]
}
}
},`
const locationTools = `
[
{
"type": "function",
"function": {
"name": "listLocations",
"description": "Retrieves the list of the user's currently saved locations (names, addresses, IDs). Use this first to check if a location from an image already exists, or to find the ID of a location the user is asking about.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "upsertLocation",
"description": "Upserts a location. This is used for both creating new locations, and updating existing ones. Providing locationId from an existing ID from listLocations, will make this an update function. Not providing one will create a new location. You must provide a locationId if you think the input is a location that already exists.",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The primary name of the location (e.g., 'Eiffel Tower', 'Mom's House', 'Acme Corp HQ'). This field is mandatory."
},
"locationId": {
"type": "string",
"description": "The UUID of the location. You should only provide this IF you believe the location already exists, from listLocation."
},
"address": {
"type": "string",
"description": "The full street address of the location, if available (e.g., 'Champ de Mars, 5 Av. Anatole France, 75007 Paris, France'). Include if extracted."
}
},
"required": ["name"]
}
}
},
%s
{
"type": "function",
"function": {
"name": "stopAgent",
"description": "Use this tool to signal that the contact processing for the current image is complete.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
}
]`
func getLocationAgentTools(allowReply bool) string {
if allowReply {
return fmt.Sprintf(locationTools, replyTool)
} else {
return fmt.Sprintf(locationTools, "")
}
}
type listLocationArguments struct{}
type upsertLocationArguments struct {
Name string `json:"name"`
LocationID *string `json:"locationId"`
Address *string `json:"address"`
}
func NewLocationAgentWithComm(log *log.Logger, locationModel models.LocationModel) client.AgentClient {
client := NewLocationAgent(log, locationModel)
client.Options.JsonTools = getLocationAgentTools(true)
return client
}
func NewLocationAgent(log *log.Logger, locationModel models.LocationModel) client.AgentClient {
agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: locationPrompt,
JsonTools: getLocationAgentTools(false),
Log: log,
EndToolCall: "stopAgent",
})
agentClient.ToolHandler.AddTool("listLocations", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return locationModel.List(context.Background(), info.UserId)
})
agentClient.ToolHandler.AddTool("upsertLocation", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
args := upsertLocationArguments{}
err := json.Unmarshal([]byte(_args), &args)
if err != nil {
return model.Locations{}, err
}
ctx := context.Background()
locationId := uuid.Nil
if args.LocationID != nil {
locationUuid, err := uuid.Parse(*args.LocationID)
if err != nil {
return model.Locations{}, err
}
locationId = locationUuid
}
location, err := locationModel.Save(ctx, info.UserId, model.Locations{
ID: locationId,
Name: args.Name,
Address: args.Address,
})
if err != nil {
return model.Locations{}, err
}
_, err = locationModel.SaveToImage(ctx, info.ImageId, location.ID)
if err != nil {
return model.Locations{}, err
}
return location, nil
})
agentClient.ToolHandler.AddTool("reply", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return "ok", nil
})
return agentClient
}

View File

@ -0,0 +1,83 @@
package agents
import (
"context"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/models"
"github.com/charmbracelet/log"
"github.com/google/uuid"
)
const noteAgentPrompt = `
You are a helpful agent, who's job is to extract notes from images.
Not all images contain notes, in such cases there's not need to create them.
An image can have more than one note.
You must return markdown, and adapt the text to best fit markdown.
Do not return anything except markdown.
If the image contains code, add this inside code blocks. You must try and correctly guess the language too.
`
type NoteAgent struct {
client client.AgentClient
noteModel models.NoteModel
}
func (agent NoteAgent) GetNotes(userId uuid.UUID, imageId uuid.UUID, imageName string, imageData []byte) error {
request := client.AgentRequestBody{
Model: "pixtral-12b-2409",
Temperature: 0.3,
ResponseFormat: client.ResponseFormat{
Type: "text",
},
Chat: &client.Chat{
Messages: make([]client.ChatMessage, 0),
},
}
request.Chat.AddSystem(noteAgentPrompt)
request.Chat.AddImage(imageName, imageData, nil)
resp, err := agent.client.Request(&request)
if err != nil {
return err
}
ctx := context.Background()
markdown := resp.Choices[0].Message.Content
note, err := agent.noteModel.Save(ctx, userId, model.Notes{
Name: "the note", // TODO: add some json schema
Content: markdown,
})
if err != nil {
return err
}
_, err = agent.noteModel.SaveToImage(ctx, imageId, note.ID)
if err != nil {
return err
}
return nil
}
func NewNoteAgent(log *log.Logger, noteModel models.NoteModel) NoteAgent {
client := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: noteAgentPrompt,
Log: log,
})
agent := NoteAgent{
client: client,
noteModel: noteModel,
}
return agent
}

View File

@ -0,0 +1,145 @@
package agents
import (
"screenmark/screenmark/agents/client"
"github.com/charmbracelet/log"
)
const orchestratorPrompt = `
**Role:** You are an Orchestrator AI responsible for analyzing images provided by the user.
**Primary Task:** Examine the input image and determine which specialized AI agent(s), available as tool calls, should be invoked to process the relevant information within the image, or if no specialized processing is needed. Your goal is to either extract and structure useful information for the user by selecting the most appropriate tool(s) or explicitly indicate that no specific action is required.
**Analysis Process & Decision Logic:**
1. **Analyze Image Content:** Scrutinize the image for distinct types of information:
* General text/writing (including code, formulas)
* Information about a person or contact details
* Information about a place, location, or address
* Information about an event
* Content that doesn't fit any specific category or lacks actionable information.
2. **Agent Selection - Determine ALL that apply:**
* **contactAgent:** Is there information specifically related to a person or their contact details (e.g., business card, name/email/phone)? If YES, select contactAgent.
* **locationAgent:** Is there information specifically identifying a place, location, or address (e.g., map, street sign, address text)? If YES, select locationAgent.
* **eventAgent:** Is there information specifically related to an event (e.g., invitation, poster with date/time, schedule)? If YES, select eventAgent.
* **noteAgent** Does the image contain *any* text/writing (including code, formulas)?
* **noAgent**: Call this when you are done working on this image.
* Call all applicable specialized agents (noteAgent, contactAgent, locationAgent, eventAgent) simultaneously (in parallel).
`
const orchestratorTools = `
[
{
"type": "function",
"function": {
"name": "noteAgent",
"description": "Extracts general textual content like handwritten notes, paragraphs in documents, presentation slides, code snippets, or mathematical formulas. Use this for significant text that isn't primarily contact details, an address, or specific event information.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "contactAgent",
"description": "Extracts personal contact information. Use when the image clearly shows details like names, phone numbers, email addresses, job titles, or company names, especially from sources like business cards, email signatures, or contact lists.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "locationAgent",
"description": "Identifies and extracts specific geographic locations or addresses. Use for content like street addresses on mail or signs, place names (e.g., restaurant, shop), map snippets, or recognizable landmarks.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "eventAgent",
"description": "Extracts details related to scheduled events, appointments, or specific occasions. Use when the image contains information like event titles, dates, times, venues, agendas, or descriptions, typically found on invitations, posters, calendar entries, or schedules.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "noAgent",
"description": "Extracts details related to scheduled events, appointments, or specific occasions. Use when the image contains information like event titles, dates, times, venues, agendas, or descriptions, typically found on invitations, posters, calendar entries, or schedules.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
}
]
`
type OrchestratorAgent struct {
Client client.AgentClient
log log.Logger
}
type Status struct {
Ok bool `json:"ok"`
}
func NewOrchestratorAgent(log *log.Logger, noteAgent NoteAgent, contactAgent client.AgentClient, locationAgent client.AgentClient, eventAgent client.AgentClient, imageName string, imageData []byte) client.AgentClient {
agent := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: orchestratorPrompt,
JsonTools: orchestratorTools,
Log: log,
EndToolCall: "noAgent",
})
agent.ToolHandler.AddTool("noteAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
// go noteAgent.GetNotes(info.UserId, info.ImageId, imageName, imageData)
return "noteAgent called successfully", nil
})
agent.ToolHandler.AddTool("contactAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
go contactAgent.RunAgent(info.UserId, info.ImageId, imageName, imageData)
return "contactAgent called successfully", nil
})
agent.ToolHandler.AddTool("locationAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
// go locationAgent.RunAgent(info.UserId, info.ImageId, imageName, imageData)
return "locationAgent called successfully", nil
})
agent.ToolHandler.AddTool("eventAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
// go eventAgent.RunAgent(info.UserId, info.ImageId, imageName, imageData)
return "eventAgent called successfully", nil
})
agent.ToolHandler.AddTool("noAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return "ok", nil
})
return agent
}

View File

@ -1,4 +1,4 @@
package auth
package main
import (
"errors"

View File

@ -1,134 +0,0 @@
package auth
import (
"database/sql"
"net/http"
"os"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/middleware"
"screenmark/screenmark/models"
"github.com/charmbracelet/log"
"github.com/go-chi/chi/v5"
)
type AuthHandler struct {
logger *log.Logger
user models.UserModel
auth Auth
jwtManager *middleware.JwtManager
}
type loginBody struct {
Email string `json:"email"`
}
type codeBody struct {
Email string `json:"email"`
Code string `json:"code"`
}
type codeReturn struct {
Access string `json:"access"`
Refresh string `json:"refresh"`
}
type refreshBody struct {
Refresh string `json:"refresh"`
}
type refreshReturn struct {
Access string `json:"access"`
}
func (h *AuthHandler) login(body loginBody, w http.ResponseWriter, r *http.Request) {
err := h.auth.CreateCode(body.Email)
if err != nil {
middleware.WriteErrorInternal(h.logger, "could not create a code", w)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *AuthHandler) code(body codeBody, w http.ResponseWriter, r *http.Request) {
if err := h.auth.UseCode(body.Email, body.Code); err != nil {
middleware.WriteErrorBadRequest(h.logger, "email or code are incorrect", w)
return
}
// TODO: we should only keep emails around for a little bit.
// Time to first login should be less than 10 minutes.
// So actually, they shouldn't be written to our database.
if exists := h.user.DoesUserExist(r.Context(), body.Email); !exists {
h.user.Save(r.Context(), model.Users{
Email: body.Email,
})
}
uuid, err := h.user.GetUserIdFromEmail(r.Context(), body.Email)
if err != nil {
middleware.WriteErrorBadRequest(h.logger, "failed to get user", w)
return
}
refresh := h.jwtManager.CreateRefreshToken(uuid)
access := h.jwtManager.CreateAccessToken(uuid)
codeReturn := codeReturn{
Access: access,
Refresh: refresh,
}
middleware.WriteJsonOrError(h.logger, codeReturn, w)
}
func (h *AuthHandler) refresh(body refreshBody, w http.ResponseWriter, r *http.Request) {
userId, err := h.jwtManager.GetUserIdFromRefresh(body.Refresh)
if err != nil {
middleware.WriteErrorBadRequest(h.logger, "invalid refresh token: "+err.Error(), w)
return
}
access := h.jwtManager.CreateAccessToken(userId)
refreshReturn := refreshReturn{
Access: access,
}
middleware.WriteJsonOrError(h.logger, refreshReturn, w)
}
func (h *AuthHandler) CreateRoutes(r chi.Router) {
h.logger.Info("Mounting auth router")
r.Group(func(r chi.Router) {
r.Use(middleware.SetJson)
r.Post("/login", middleware.WithValidatedPost(h.login))
r.Post("/code", middleware.WithValidatedPost(h.code))
r.Post("/refresh", middleware.WithValidatedPost(h.refresh))
})
}
func CreateAuthHandler(db *sql.DB, jwtManager *middleware.JwtManager) AuthHandler {
userModel := models.NewUserModel(db)
logger := log.New(os.Stdout).WithPrefix("Auth")
mailer, err := CreateMailClient(logger)
if err != nil {
panic(err)
}
auth := CreateAuth(mailer)
return AuthHandler{
logger: logger,
user: userModel,
auth: auth,
jwtManager: jwtManager,
}
}

View File

@ -1,4 +1,4 @@
package auth
package main
import (
"testing"

23
backend/builder/agents.go Normal file
View File

@ -0,0 +1,23 @@
package builder
import (
"context"
"database/sql"
"screenmark/screenmark/.gen/haystack/agents/model"
. "screenmark/screenmark/.gen/haystack/agents/table"
. "github.com/go-jet/jet/v2/postgres"
"github.com/google/uuid"
)
type AgentModel struct {
dbPool *sql.DB
}
func (m AgentModel) SaveTool(ctx context.Context, agentId uuid.UUID, tool string) (model.Tools, error) {
insertToolStmt := Tools.
INSERT(Tools.Tool).
VALUES(Json(tool))
}

View File

@ -1,9 +1,9 @@
package auth
package main
import (
"fmt"
"os"
"github.com/charmbracelet/log"
"github.com/wneessen/go-mail"
)
@ -11,9 +11,7 @@ type MailClient struct {
client *mail.Client
}
type TestMailClient struct {
logger *log.Logger
}
type TestMailClient struct{}
type Mailer interface {
SendCode(to string, code string) error
@ -45,17 +43,15 @@ func (m MailClient) SendCode(to string, code string) error {
}
func (m TestMailClient) SendCode(to string, code string) error {
m.logger.Info("Auth Code", "email", to, "code", code)
fmt.Printf("Email: %s | Code %s\n", to, code)
return nil
}
func CreateMailClient(log *log.Logger) (Mailer, error) {
func CreateMailClient() (Mailer, error) {
mode := os.Getenv("MODE")
if mode == "DEV" {
return TestMailClient{
log,
}, nil
return TestMailClient{}, nil
}
client, err := mail.NewClient(

View File

@ -1,81 +1,136 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"screenmark/screenmark/middleware"
"screenmark/screenmark/notifications"
"strconv"
"context"
"database/sql"
"os"
"screenmark/screenmark/agents"
"screenmark/screenmark/models"
"time"
"github.com/charmbracelet/log"
"github.com/google/uuid"
"github.com/lib/pq"
)
/*
* TODO: We have channels open every a user sends an image.
* We never close these channels.
*
* What is a reasonable default? Close the channel after 1 minute of inactivity?
*/
func CreateEventsHandler(notifier *notifications.Notifier[notifications.Notification]) http.HandlerFunc {
counter := 0
func createLogger(prefix string) *log.Logger {
logger := log.NewWithOptions(os.Stdout, log.Options{
ReportTimestamp: true,
TimeFormat: time.Kitchen,
Prefix: prefix,
})
userSplitters := make(map[string]*notifications.ChannelSplitter[notifications.Notification])
logger.SetLevel(log.DebugLevel)
return func(w http.ResponseWriter, r *http.Request) {
_userId := r.Context().Value(middleware.USER_ID).(uuid.UUID)
if _userId == uuid.Nil {
w.WriteHeader(http.StatusUnauthorized)
return
return logger
}
func ListenNewImageEvents(db *sql.DB, eventManager *EventManager) {
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()
userId := _userId.String()
locationModel := models.NewLocationModel(db)
eventModel := models.NewEventModel(db)
noteModel := models.NewNoteModel(db)
imageModel := models.NewImageModel(db)
contactModel := models.NewContactModel(db)
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
// w.(http.Flusher).Flush()
databaseEventLog := createLogger("Database Events 🤖")
databaseEventLog.SetLevel(log.DebugLevel)
if _, exists := notifier.Listeners[userId]; !exists {
notifier.Create(userId)
}
err := listener.Listen("new_image")
if err != nil {
panic(err)
}
userNotifications := notifier.Listeners[userId]
for {
select {
case parameters := <-listener.Notify:
imageId := uuid.MustParse(parameters.Extra)
eventManager.listeners[parameters.Extra] = make(chan string)
if _, exists := userSplitters[userId]; !exists {
splitter := notifications.NewChannelSplitter(userNotifications)
databaseEventLog.Debug("Starting processing image", "ImageID", imageId)
userSplitters[userId] = &splitter
splitter.Listen()
}
ctx := context.Background()
splitter := userSplitters[userId]
go func() {
noteAgent := agents.NewNoteAgent(createLogger("Notes 📝"), noteModel)
contactAgent := agents.NewContactAgent(createLogger("Contacts 👥"), contactModel)
locationAgent := agents.NewLocationAgent(createLogger("Locations 📍"), locationModel)
eventAgent := agents.NewEventAgent(createLogger("Events 📅"), eventModel, locationModel)
id := strconv.Itoa(counter)
counter += 1
notifications := splitter.Add(id)
defer splitter.Remove(id)
for {
select {
case <-r.Context().Done():
fmt.Fprint(w, "event: close\ndata: Connection closed\n\n")
w.(http.Flusher).Flush()
return
case msg := <-notifications:
msgString, err := json.Marshal(msg)
image, err := imageModel.GetToProcessWithData(ctx, imageId)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
databaseEventLog.Error("Failed to GetToProcessWithData", "error", err)
return
}
fmt.Printf("Sending msg %s\n", msgString)
if err := imageModel.StartProcessing(ctx, image.ID); err != nil {
databaseEventLog.Error("Failed to FinishProcessing", "error", err)
return
}
fmt.Fprintf(w, "event: data\ndata: %s\n\n", string(msgString))
w.(http.Flusher).Flush()
}
orchestrator := agents.NewOrchestratorAgent(createLogger("Orchestrator 🎼"), noteAgent, contactAgent, locationAgent, eventAgent, image.Image.ImageName, image.Image.Image)
err = orchestrator.RunAgent(image.UserID, image.ImageID, image.Image.ImageName, image.Image.Image)
if err != nil {
databaseEventLog.Error("Orchestrator failed", "error", err)
return
}
_, err = imageModel.FinishProcessing(ctx, image.ID)
if err != nil {
databaseEventLog.Error("Failed to finish processing", "ImageID", imageId)
return
}
databaseEventLog.Debug("Starting processing image", "ImageID", imageId)
}()
}
}
}
type EventManager struct {
// Maps processing image UUID to a channel
listeners map[string]chan string
}
func NewEventManager() EventManager {
return EventManager{
listeners: make(map[string]chan string),
}
}
func ListenProcessingImageStatus(db *sql.DB, eventManager *EventManager) {
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()
if err := listener.Listen("new_processing_image_status"); err != nil {
panic(err)
}
for {
select {
case data := <-listener.Notify:
stringUuid := data.Extra[0:36]
status := data.Extra[36:]
imageListener, exists := eventManager.listeners[stringUuid]
if !exists {
continue
}
imageListener <- status
close(imageListener)
delete(eventManager.listeners, stringUuid)
}
}
}

View File

@ -9,7 +9,7 @@ require (
github.com/charmbracelet/x/ansi v0.4.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-chi/chi/v5 v5.2.1 // indirect
github.com/go-jet/jet/v2 v2.13.0 // indirect
github.com/go-jet/jet/v2 v2.12.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
@ -20,7 +20,6 @@ require (
github.com/muesli/termenv v0.16.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/robert-nix/ansihtml v1.0.1 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/wneessen/go-mail v0.6.2 // indirect
golang.org/x/crypto v0.33.0 // indirect

View File

@ -6,15 +6,12 @@ github.com/charmbracelet/log v0.4.1 h1:6AYnoHKADkghm/vt4neaNEXkxcXLSV2g1rdyFDOpT
github.com/charmbracelet/log v0.4.1/go.mod h1:pXgyTsqsVu4N9hGdHmQ0xEA4RsXof402LX9ZgiITn2I=
github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk=
github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-jet/jet/v2 v2.12.0 h1:z2JfvBAZgsfxlQz6NXBYdZTXc7ep3jhbszTLtETv1JE=
github.com/go-jet/jet/v2 v2.12.0/go.mod h1:ufQVRQeI1mbcO5R8uCEVcVf3Foej9kReBdwDx7YMWUM=
github.com/go-jet/jet/v2 v2.13.0 h1:DcD2IJRGos+4X40IQRV6S6q9onoOfZY/GPdvU6ImZcQ=
github.com/go-jet/jet/v2 v2.13.0/go.mod h1:YhT75U1FoYAxFOObbQliHmXVYQeffkBKWT7ZilZ3zPc=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
@ -36,10 +33,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robert-nix/ansihtml v1.0.1 h1:VTiyQ6/+AxSJoSSLsMecnkh8i0ZqOEdiRl/odOc64fc=
github.com/robert-nix/ansihtml v1.0.1/go.mod h1:CJwclxYaTPc2RfcxtanEACsYuTksh4yDXcNeHHKZINE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
@ -116,6 +109,5 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,232 +0,0 @@
package images
import (
"bytes"
"database/sql"
"encoding/base64"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/limits"
"screenmark/screenmark/middleware"
"screenmark/screenmark/models"
"screenmark/screenmark/processor"
"github.com/charmbracelet/log"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
)
type ImageHandler struct {
logger *log.Logger
imageModel models.ImageModel
userModel models.UserModel
limitsManager limits.LimitsManagerMethods
jwtManager *middleware.JwtManager
processor *processor.Processor[model.Image]
}
type ImagesReturn struct {
UserImages []models.UserImageWithImage
Stacks []models.ListsWithImages
}
func (h *ImageHandler) serveImage(w http.ResponseWriter, r *http.Request) {
imageID, err := middleware.GetPathParamID(h.logger, "id", w, r)
if err != nil {
return
}
ctx := r.Context()
userID, err := middleware.GetUserID(ctx, h.logger, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
image, exists, err := h.imageModel.Get(r.Context(), imageID)
if err != nil {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "Could not get image")
return
}
// Do not leak that this ID exists.
if !exists || image.UserID != userID {
w.WriteHeader(http.StatusNotFound)
return
}
// TODO: this could be part of the db table
extension := filepath.Ext(image.ImageName)
if len(extension) == 0 {
// Same hack
extension = "png"
}
extension = extension[1:]
w.Header().Add("Content-Type", "image/"+extension)
w.Write(image.Image)
}
func (h *ImageHandler) listImages(w http.ResponseWriter, r *http.Request) {
userId, err := middleware.GetUserID(r.Context(), h.logger, w)
if err != nil {
return
}
images, err := h.userModel.GetUserImages(r.Context(), userId)
if err != nil {
middleware.WriteErrorInternal(h.logger, "could not get user images", w)
return
}
stacksWithImages, err := h.userModel.ListWithImages(r.Context(), userId)
if err != nil {
middleware.WriteErrorInternal(h.logger, "could not get lists with images", w)
return
}
imagesReturn := ImagesReturn{
UserImages: images,
Stacks: stacksWithImages,
}
middleware.WriteJsonOrError(h.logger, imagesReturn, w)
}
func (h *ImageHandler) uploadImage(w http.ResponseWriter, r *http.Request) {
imageName := chi.URLParam(r, "name")
if len(imageName) == 0 {
middleware.WriteErrorBadRequest(h.logger, "you need to provide a name in the path", w)
return
}
userID, err := middleware.GetUserID(r.Context(), h.logger, w)
if err != nil {
return
}
contentType := r.Header.Get("Content-Type")
image := make([]byte, 0)
switch contentType {
case "application/base64":
decoder := base64.NewDecoder(base64.StdEncoding, r.Body)
buf := &bytes.Buffer{}
_, err := io.Copy(buf, decoder)
if err != nil {
middleware.WriteErrorBadRequest(h.logger, "base64 decoding failed", w)
return
}
image = buf.Bytes()
case "application/oclet-stream", "image/png":
bodyData, err := io.ReadAll(r.Body)
if err != nil {
middleware.WriteErrorBadRequest(h.logger, "binary data reading failed", w)
return
}
// TODO: check headers
image = bodyData
default:
middleware.WriteErrorBadRequest(h.logger, "unsupported content type, need octet-stream or base64", w)
return
}
ctx := r.Context()
newImage, err := h.imageModel.Save(ctx, imageName, image, userID)
if err != nil {
middleware.WriteErrorInternal(h.logger, "could not save image to DB: "+err.Error(), w)
return
}
h.logger.Info("About to add image")
h.processor.Add(newImage)
// We nullify the image's data, so we're not transferring all that
// data back to the frontend.
newImage.Image = nil
middleware.WriteJsonOrError(h.logger, newImage, w)
}
func (h *ImageHandler) deleteImage(w http.ResponseWriter, r *http.Request) {
stringImageID := chi.URLParam(r, "image-id")
imageID, err := uuid.Parse(stringImageID)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
ctx := r.Context()
userID, err := middleware.GetUserID(ctx, h.logger, w)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
exists, err := h.imageModel.Delete(ctx, imageID, userID)
if err != nil {
h.logger.Warn("cannot delete image", "error", err)
w.WriteHeader(http.StatusBadRequest)
return
}
// Don't leak if the image exists or not
if !exists {
w.WriteHeader(http.StatusNotFound)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *ImageHandler) CreateRoutes(r chi.Router) {
h.logger.Info("Mounting image router")
r.Group(func(r chi.Router) {
r.Use(middleware.ProtectedRouteURL(h.jwtManager))
r.Get("/{id}", h.serveImage)
})
r.Group(func(r chi.Router) {
r.Use(middleware.ProtectedRoute(h.jwtManager))
r.Use(middleware.SetJson)
r.Get("/", h.listImages)
r.Post("/{name}", middleware.WithLimit(h.logger, h.limitsManager.HasReachedImageLimit, h.uploadImage))
r.Delete("/{image-id}", h.deleteImage)
})
}
func CreateImageHandler(
db *sql.DB,
limitsManager limits.LimitsManagerMethods,
jwtManager *middleware.JwtManager,
processor *processor.Processor[model.Image],
) ImageHandler {
imageModel := models.NewImageModel(db)
userModel := models.NewUserModel(db)
logger := log.New(os.Stdout).WithPrefix("Images")
return ImageHandler{
logger: logger,
imageModel: imageModel,
userModel: userModel,
limitsManager: limitsManager,
jwtManager: jwtManager,
processor: processor,
}
}

View File

@ -1,803 +0,0 @@
// Integration Tests for Haystack Backend
//
// These tests provide comprehensive end-to-end testing of all API endpoints.
//
// Requirements:
// - Docker must be installed and running
// - PostgreSQL Docker image will be automatically pulled and started
//
// To run the integration tests:
//
// 1. Start Docker daemon
// 2. Run: go test -v ./integration_test.go
//
// The tests will:
// - Start a PostgreSQL container on port 5433
// - Set up the database schema
// - Test all auth, stack, and image endpoints
// - Clean up the container after tests complete
//
// Note: These tests require Docker and will be skipped if Docker is not available.
package main
import (
"bytes"
"database/sql"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"strings"
"testing"
"time"
"screenmark/screenmark/middleware"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
)
const (
testDBName = "test_haystack"
testDBUser = "test_user"
testDBPassword = "test_password"
testDBHost = "localhost"
testDBPort = "5433"
testDBSSLMode = "disable"
)
type TestUser struct {
ID uuid.UUID
Email string
Token string
}
type TestContext struct {
db *sql.DB
router chi.Router
server *httptest.Server
users []TestUser
cleanup func()
jwtManager *middleware.JwtManager
}
func setupTestDatabase() (*sql.DB, func(), error) {
// Check if Docker daemon is running
checkCmd := exec.Command("docker", "info")
if err := checkCmd.Run(); err != nil {
return nil, nil, fmt.Errorf("docker daemon is not running: %w", err)
}
// Start PostgreSQL container
containerName := "test_postgres_haystack"
// Clean up any existing container
exec.Command("docker", "rm", "-f", containerName).Run()
// Start new PostgreSQL container
cmd := exec.Command("docker", "run", "-d",
"--name", containerName,
"-e", "POSTGRES_DB="+testDBName,
"-e", "POSTGRES_USER="+testDBUser,
"-e", "POSTGRES_PASSWORD="+testDBPassword,
"-p", testDBPort+":5432",
"postgres:15-alpine",
)
output, err := cmd.CombinedOutput()
if err != nil {
return nil, nil, fmt.Errorf("failed to start postgres container: %w, output: %s", err, string(output))
}
// Wait for database to be ready with retries
maxRetries := 15
for i := range maxRetries {
time.Sleep(2 * time.Second)
// Test connection
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
testDBHost, testDBPort, testDBUser, testDBPassword, testDBName, testDBSSLMode)
testDB, testErr := sql.Open("postgres", connStr)
if testErr == nil {
if pingErr := testDB.Ping(); pingErr == nil {
testDB.Close()
break
}
testDB.Close()
}
if i == maxRetries-1 {
return nil, nil, fmt.Errorf("database failed to become ready after %d retries", maxRetries)
}
}
// Connect to database
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
testDBHost, testDBPort, testDBUser, testDBPassword, testDBName, testDBSSLMode)
db, err := sql.Open("postgres", connStr)
if err != nil {
return nil, nil, fmt.Errorf("failed to connect to test database: %w", err)
}
// Test connection
if err := db.Ping(); err != nil {
return nil, nil, fmt.Errorf("failed to ping test database: %w", err)
}
// Load and execute schema
schema, err := os.ReadFile("schema.sql")
if err != nil {
return nil, nil, fmt.Errorf("failed to read schema file: %w", err)
}
if _, err := db.Exec(string(schema)); err != nil {
return nil, nil, fmt.Errorf("failed to execute schema: %w", err)
}
// Cleanup function
cleanup := func() {
db.Close()
exec.Command("docker", "rm", "-f", containerName).Run()
}
return db, cleanup, nil
}
func setupTestContext(t *testing.T) *TestContext {
// Set environment variables for test environment
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
testDBHost, testDBPort, testDBUser, testDBPassword, testDBName, testDBSSLMode)
originalDBConn := os.Getenv("DB_CONNECTION")
originalTestEnv := os.Getenv("GO_TEST_ENVIRONMENT")
os.Setenv("DB_CONNECTION", connStr)
os.Setenv("GO_TEST_ENVIRONMENT", "true")
defer func() {
if originalDBConn != "" {
os.Setenv("DB_CONNECTION", originalDBConn)
} else {
os.Unsetenv("DB_CONNECTION")
}
if originalTestEnv != "" {
os.Setenv("GO_TEST_ENVIRONMENT", originalTestEnv)
} else {
os.Unsetenv("GO_TEST_ENVIRONMENT")
}
}()
tc := &TestContext{}
db, cleanup, err := setupTestDatabase()
if err != nil {
t.Fatalf("Failed to setup test database: %v", err)
}
jwtManager := middleware.NewJwtManager([]byte("test-jwt-secret"))
router, err := setupRouter(db, jwtManager)
if err != nil {
panic(err)
}
server := httptest.NewServer(router)
tc.db = db
tc.router = router
tc.server = server
tc.jwtManager = jwtManager
tc.cleanup = func() {
server.Close()
cleanup()
}
return tc
}
func (tc *TestContext) createTestUser(email string) TestUser {
// Insert user into database
var userID uuid.UUID
err := tc.db.QueryRow("INSERT INTO haystack.users (email) VALUES ($1) RETURNING id", email).Scan(&userID)
if err != nil {
panic(fmt.Sprintf("Failed to create test user: %v", err))
}
// Create access token for the user
accessToken := tc.jwtManager.CreateAccessToken(userID)
user := TestUser{
ID: userID,
Email: email,
Token: accessToken,
}
tc.users = append(tc.users, user)
return user
}
func (tc *TestContext) makeRequest(t *testing.T, method, path, token string, body io.Reader) *http.Response {
url := tc.server.URL + path
req, err := http.NewRequest(method, url, body)
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to make request: %v", err)
}
return resp
}
func (tc *TestContext) makeJSONRequest(t *testing.T, method, path, token string, data any) *http.Response {
var body io.Reader
if data != nil {
jsonData, err := json.Marshal(data)
if err != nil {
t.Fatalf("Failed to marshal JSON: %v", err)
}
body = bytes.NewReader(jsonData)
}
return tc.makeRequest(t, method, path, token, body)
}
// Comprehensive integration test suite - single database setup for all tests
func TestAllRoutes(t *testing.T) {
tc := setupTestContext(t)
defer tc.cleanup()
// Create test users for different test scenarios
stackUser := tc.createTestUser("stacktest@example.com")
imageUser := tc.createTestUser("imagetest@example.com")
flowUser := tc.createTestUser("flowtest@example.com")
t.Run("Auth Routes", func(t *testing.T) {
t.Run("Login endpoint", func(t *testing.T) {
loginData := map[string]string{
"email": "test@example.com",
}
resp := tc.makeJSONRequest(t, "POST", "/auth/login", "", loginData)
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status 200, got %d", resp.StatusCode)
}
})
t.Run("Code endpoint with valid email", func(t *testing.T) {
// First create a login request to set up the email
loginData := map[string]string{
"email": "test@example.com",
}
tc.makeJSONRequest(t, "POST", "/auth/login", "", loginData)
// Then try to use a code (this will fail with invalid code, but tests the endpoint)
codeData := map[string]string{
"email": "test@example.com",
"code": "invalid",
}
resp := tc.makeJSONRequest(t, "POST", "/auth/code", "", codeData)
defer resp.Body.Close()
// The auth system creates a user for new emails, so this returns 200
// We're testing that the endpoint works, not necessarily the code validation
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status 200 for code endpoint, got %d", resp.StatusCode)
}
})
t.Run("Protected route without token", func(t *testing.T) {
resp := tc.makeRequest(t, "GET", "/images/image", "", nil)
defer resp.Body.Close()
if resp.StatusCode != http.StatusUnauthorized {
t.Errorf("Expected status 401 for protected route without token, got %d", resp.StatusCode)
}
})
})
t.Run("Stack Routes", func(t *testing.T) {
t.Run("Get stacks without authentication", func(t *testing.T) {
resp := tc.makeRequest(t, "GET", "/stacks/", "", nil)
defer resp.Body.Close()
if resp.StatusCode != http.StatusUnauthorized {
t.Errorf("Expected status 401, got %d", resp.StatusCode)
}
})
t.Run("Get stacks with authentication", func(t *testing.T) {
resp := tc.makeRequest(t, "GET", "/stacks/", stackUser.Token, nil)
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status 200, got %d", resp.StatusCode)
}
var stacks []interface{}
if err := json.NewDecoder(resp.Body).Decode(&stacks); err != nil {
t.Errorf("Failed to decode response: %v", err)
}
})
t.Run("Create stack", func(t *testing.T) {
stackData := map[string]string{
"title": "Test Stack",
"fields": "name,description,value",
}
resp := tc.makeJSONRequest(t, "POST", "/stacks/", stackUser.Token, stackData)
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status 200, got %d", resp.StatusCode)
}
})
t.Run("Get stack items with invalid ID", func(t *testing.T) {
resp := tc.makeRequest(t, "GET", "/stacks/invalid-id", stackUser.Token, nil)
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Errorf("Expected status 400 for invalid ID, got %d", resp.StatusCode)
}
})
t.Run("Delete stack without authentication", func(t *testing.T) {
fakeUUID := uuid.New()
resp := tc.makeRequest(t, "DELETE", "/stacks/"+fakeUUID.String(), "", nil)
defer resp.Body.Close()
if resp.StatusCode != http.StatusUnauthorized {
t.Errorf("Expected status 401 for unauthenticated delete, got %d", resp.StatusCode)
}
})
t.Run("Delete stack with invalid ID", func(t *testing.T) {
resp := tc.makeRequest(t, "DELETE", "/stacks/invalid-id", stackUser.Token, nil)
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Errorf("Expected status 400 for invalid ID, got %d", resp.StatusCode)
}
})
t.Run("Delete non-existent stack", func(t *testing.T) {
fakeUUID := uuid.New()
resp := tc.makeRequest(t, "DELETE", "/stacks/"+fakeUUID.String(), stackUser.Token, nil)
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Errorf("Expected status 400 for non-existent stack, got %d", resp.StatusCode)
}
})
t.Run("Create and delete stack successfully", func(t *testing.T) {
// First create a stack
stackData := map[string]string{
"title": "Stack to Delete",
"fields": "name,description,value",
}
resp := tc.makeJSONRequest(t, "POST", "/stacks/", stackUser.Token, stackData)
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("Failed to create stack for deletion test, got %d", resp.StatusCode)
return
}
// Get the list of stacks to find the created stack ID
resp = tc.makeRequest(t, "GET", "/stacks/", stackUser.Token, nil)
var stacks []map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&stacks); err != nil {
t.Errorf("Failed to decode stacks response: %v", err)
resp.Body.Close()
return
}
resp.Body.Close()
if len(stacks) == 0 {
t.Errorf("No stacks found after creation")
return
}
// Find the stack we just created
var stackToDelete map[string]interface{}
for _, stack := range stacks {
if name, ok := stack["Name"].(string); ok && name == "Stack to Delete" {
stackToDelete = stack
break
}
}
if stackToDelete == nil {
t.Errorf("Could not find created stack")
return
}
stackID, ok := stackToDelete["ID"].(string)
if !ok {
t.Errorf("Stack ID not found or not a string")
return
}
// Now delete the stack
resp = tc.makeRequest(t, "DELETE", "/stacks/"+stackID, stackUser.Token, nil)
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status 200 for successful delete, got %d", resp.StatusCode)
}
// Verify the stack is gone by trying to get it again
resp = tc.makeRequest(t, "GET", "/stacks/", stackUser.Token, nil)
defer resp.Body.Close()
var stacksAfterDelete []map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&stacksAfterDelete); err != nil {
t.Errorf("Failed to decode stacks response after delete: %v", err)
return
}
// Check that the deleted stack is no longer in the list
for _, stack := range stacksAfterDelete {
if id, ok := stack["ID"].(string); ok && id == stackID {
t.Errorf("Stack still exists after deletion")
return
}
}
})
t.Run("Delete stack belonging to different user", func(t *testing.T) {
// Create a stack with stackUser
stackData := map[string]string{
"title": "Other User's Stack",
"fields": "name,description,value",
}
resp := tc.makeJSONRequest(t, "POST", "/stacks/", stackUser.Token, stackData)
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("Failed to create stack for ownership test, got %d", resp.StatusCode)
return
}
// Get the stack ID
resp = tc.makeRequest(t, "GET", "/stacks/", stackUser.Token, nil)
var stacks []map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&stacks); err != nil {
t.Errorf("Failed to decode stacks response: %v", err)
resp.Body.Close()
return
}
resp.Body.Close()
var stackID string
for _, stack := range stacks {
if name, ok := stack["Name"].(string); ok && name == "Other User's Stack" {
if id, ok := stack["ID"].(string); ok {
stackID = id
break
}
}
}
if stackID == "" {
t.Errorf("Could not find created stack ID")
return
}
// Try to delete the stack with a different user (imageUser)
resp = tc.makeRequest(t, "DELETE", "/stacks/"+stackID, imageUser.Token, nil)
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Errorf("Expected status 400 when deleting another user's stack, got %d", resp.StatusCode)
}
})
})
t.Run("Image Routes", func(t *testing.T) {
t.Run("Get images without authentication", func(t *testing.T) {
resp := tc.makeRequest(t, "GET", "/images/", "", nil)
defer resp.Body.Close()
if resp.StatusCode != http.StatusUnauthorized {
t.Errorf("Expected status 401, got %d", resp.StatusCode)
}
})
t.Run("Get images with authentication", func(t *testing.T) {
resp := tc.makeRequest(t, "GET", "/images/", imageUser.Token, nil)
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status 200, got %d", resp.StatusCode)
}
var imageData interface{}
if err := json.NewDecoder(resp.Body).Decode(&imageData); err != nil {
t.Errorf("Failed to decode response: %v", err)
}
})
t.Run("Upload image with base64", func(t *testing.T) {
// Create a simple valid base64 string for testing
testImageBase64 := "dGVzdCBkYXRh" // "test data" in base64
req, err := http.NewRequest("POST", tc.server.URL+"/images/test.png", strings.NewReader(testImageBase64))
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
req.Header.Set("Authorization", "Bearer "+imageUser.Token)
req.Header.Set("Content-Type", "application/base64")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to make request: %v", err)
}
defer resp.Body.Close()
// The API might return 200 for successful operations
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
bodyBytes, _ := io.ReadAll(resp.Body)
t.Errorf("Expected status 200 or 201, got %d. Response: %s", resp.StatusCode, string(bodyBytes))
}
})
t.Run("Upload image with binary data", func(t *testing.T) {
// Create a small test image (minimal PNG)
testImageBinary := []byte{
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D,
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00,
0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0x99, 0x01, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x37, 0x6E, 0xF9, 0x5F, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x49,
0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
}
req, err := http.NewRequest("POST", tc.server.URL+"/images/test2.png", bytes.NewReader(testImageBinary))
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
req.Header.Set("Authorization", "Bearer "+imageUser.Token)
req.Header.Set("Content-Type", "image/png")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Failed to make request: %v", err)
}
defer resp.Body.Close()
// The API might return 200 for successful operations
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
bodyBytes, _ := io.ReadAll(resp.Body)
t.Errorf("Expected status 200 or 201, got %d. Response: %s", resp.StatusCode, string(bodyBytes))
}
})
t.Run("Upload image without name", func(t *testing.T) {
resp := tc.makeRequest(t, "POST", "/images/", imageUser.Token, nil)
defer resp.Body.Close()
// Route pattern doesn't match empty names, so returns 404
if resp.StatusCode != http.StatusNotFound {
t.Errorf("Expected status 404 for missing name, got %d", resp.StatusCode)
}
})
t.Run("Serve non-existent image", func(t *testing.T) {
fakeUUID := uuid.New()
resp := tc.makeRequest(t, "GET", "/images/"+fakeUUID.String(), "", nil)
defer resp.Body.Close()
if resp.StatusCode != http.StatusNotFound {
t.Errorf("Expected status 404 for non-existent image, got %d", resp.StatusCode)
}
})
})
t.Run("Complete User Flow", func(t *testing.T) {
// Step 1: Test authentication is working
resp := tc.makeRequest(t, "GET", "/images/", flowUser.Token, nil)
if resp.StatusCode != http.StatusOK {
t.Errorf("Authentication failed, expected 200, got %d", resp.StatusCode)
}
resp.Body.Close()
// Step 2: Upload an image
testImageBinary := []byte{
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D,
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00,
0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0x99, 0x01, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x37, 0x6E, 0xF9, 0x5F, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x49,
0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
}
req, err := http.NewRequest("POST", tc.server.URL+"/images/test_flow.png", bytes.NewReader(testImageBinary))
if err != nil {
t.Fatalf("Failed to create upload request: %v", err)
}
req.Header.Set("Authorization", "Bearer "+flowUser.Token)
req.Header.Set("Content-Type", "image/png")
client := &http.Client{Timeout: 10 * time.Second}
resp, err = client.Do(req)
if err != nil {
t.Fatalf("Failed to upload image: %v", err)
}
// The API returns 200 for successful image uploads
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
t.Errorf("Image upload failed, expected 200, got %d. Response: %s", resp.StatusCode, string(bodyBytes))
}
resp.Body.Close()
// Step 3: Verify image appears in user's image list
resp = tc.makeRequest(t, "GET", "/images/", flowUser.Token, nil)
if resp.StatusCode != http.StatusOK {
t.Errorf("Failed to get user images, expected 200, got %d", resp.StatusCode)
}
var imageData map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&imageData); err != nil {
t.Errorf("Failed to decode image list: %v", err)
}
resp.Body.Close()
// Check that we have user images
if userImages, ok := imageData["userImages"].([]interface{}); ok {
if len(userImages) == 0 {
t.Log("Warning: No user images found, but upload succeeded")
} else {
t.Logf("Found %d user images", len(userImages))
}
}
// Step 4: Test stack creation
stackData := map[string]string{
"title": "Integration Test Stack",
"fields": "name,description,value",
}
resp = tc.makeJSONRequest(t, "POST", "/stacks/", flowUser.Token, stackData)
if resp.StatusCode != http.StatusOK {
t.Errorf("Stack creation failed, expected 200, got %d", resp.StatusCode)
}
resp.Body.Close()
// Step 5: Verify stack appears in user's stack list
resp = tc.makeRequest(t, "GET", "/stacks/", flowUser.Token, nil)
if resp.StatusCode != http.StatusOK {
t.Errorf("Failed to get user stacks, expected 200, got %d", resp.StatusCode)
}
var stacks []interface{}
if err := json.NewDecoder(resp.Body).Decode(&stacks); err != nil {
t.Errorf("Failed to decode stack list: %v", err)
}
resp.Body.Close()
if len(stacks) == 0 {
t.Log("Warning: No stacks found, but creation succeeded")
} else {
t.Logf("Found %d stacks", len(stacks))
}
t.Log("Complete user flow test passed!")
})
}
// Simple test that doesn't require Docker
func TestIntegrationTestSetup(t *testing.T) {
// This test verifies that the test structure is correct
// It doesn't require Docker to be running
t.Run("Test structure validation", func(t *testing.T) {
// This test verifies that the test structure is correct
// It doesn't require Docker to be running
// Verify that our test types are properly defined
var _ TestUser
var _ TestContext
// Verify that our constants are defined
if testDBName == "" {
t.Error("testDBName constant is not defined")
}
if testDBPort == "" {
t.Error("testDBPort constant is not defined")
}
t.Log("Test structure is valid")
})
t.Run("Database and router setup", func(t *testing.T) {
// This test verifies that the database and router can be set up without SSL errors
tc := setupTestContext(t)
defer tc.cleanup()
// Verify that the router was created successfully
if tc.router == nil {
t.Error("Router was not created successfully")
}
// Verify that the server was created successfully
if tc.server == nil {
t.Error("Server was not created successfully")
}
// Verify that the database connection is working
if err := tc.db.Ping(); err != nil {
t.Errorf("Database connection failed: %v", err)
}
t.Log("Database and router setup successful - no SSL errors!")
})
t.Run("Docker availability check", func(t *testing.T) {
// Check if Docker is available but don't fail the test
if _, err := exec.LookPath("docker"); err != nil {
t.Skip("Docker not found, skipping Docker-dependent tests")
}
// Check if Docker daemon is running
checkCmd := exec.Command("docker", "info")
if err := checkCmd.Run(); err != nil {
t.Skip("Docker daemon is not running, skipping Docker-dependent tests")
}
t.Log("Docker is available and running")
})
}
func TestMain(m *testing.M) {
// Check if Docker is available
if _, err := exec.LookPath("docker"); err != nil {
fmt.Println("Docker not found, skipping integration tests")
os.Exit(0)
}
// Check if Docker daemon is running
checkCmd := exec.Command("docker", "info")
if err := checkCmd.Run(); err != nil {
fmt.Println("Docker daemon is not running, skipping integration tests")
fmt.Println("To run integration tests, start Docker daemon and try again")
os.Exit(0)
}
// Run tests
code := m.Run()
os.Exit(code)
}

96
backend/jwt.go Normal file
View File

@ -0,0 +1,96 @@
package main
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
type JwtType string
const (
Access JwtType = "access"
Refresh JwtType = "refresh"
)
type JwtClaims struct {
UserID string
Type JwtType
Expire time.Time
}
// obviously this is very not secure. TODO: extract to env
var JWT_SECRET = []byte("very secret")
func createToken(claims JwtClaims) *jwt.Token {
return jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"UserID": claims.UserID,
"Type": claims.Type,
"Expire": claims.Expire,
})
}
func CreateRefreshToken(userId uuid.UUID) string {
token := createToken(JwtClaims{
UserID: userId.String(),
Type: Refresh,
Expire: time.Now().Add(time.Hour * 24 * 7),
})
// TODO: bruh what is this
tokenString, err := token.SignedString(JWT_SECRET)
if err != nil {
panic(err)
}
return tokenString
}
func CreateAccessToken(userId uuid.UUID) string {
token := createToken(JwtClaims{
UserID: userId.String(),
Type: Access,
Expire: time.Now().Add(time.Hour),
})
// TODO: bruh what is this
tokenString, err := token.SignedString(JWT_SECRET)
if err != nil {
panic(err)
}
return tokenString
}
var NotValidToken = errors.New("Not a valid token")
func GetUserIdFromAccess(accessToken string) (uuid.UUID, error) {
token, err := jwt.Parse(accessToken, func(token *jwt.Token) (any, error) {
return JWT_SECRET, nil
}, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))
if err != nil {
return uuid.Nil, err
}
// Blah blah, check expiry and stuff
// this function is stupid
if claims, ok := token.Claims.(jwt.MapClaims); ok {
tokenType, ok := claims["Type"]
if !ok || tokenType.(string) != "access" {
return uuid.Nil, NotValidToken
}
userId, err := uuid.Parse(claims["UserID"].(string))
if err != nil {
return uuid.Nil, NotValidToken
}
return userId, nil
} else {
return uuid.Nil, NotValidToken
}
}

View File

@ -1,61 +0,0 @@
package limits
import (
"database/sql"
. "screenmark/screenmark/.gen/haystack/haystack/table"
. "github.com/go-jet/jet/v2/postgres"
"github.com/google/uuid"
)
const (
LISTS_LIMIT = 10
IMAGE_LIMIT = 10
)
type LimitsManager struct {
dbPool *sql.DB
}
type LimitsManagerMethods interface {
HasReachedStackLimit(userID uuid.UUID) (bool, error)
HasReachedImageLimit(userID uuid.UUID) (bool, error)
}
type listCount struct {
ListCount int `alias:"list_count"`
}
func (m *LimitsManager) HasReachedStackLimit(userID uuid.UUID) (bool, error) {
getStacks := Stacks.
SELECT(COUNT(Stacks.UserID).AS("listCount.ListCount")).
WHERE(Stacks.UserID.EQ(UUID(userID)))
var count listCount
err := getStacks.Query(m.dbPool, &count)
return count.ListCount >= LISTS_LIMIT, err
}
type imageCount struct {
ImageCount int `alias:"image_count"`
}
func (m *LimitsManager) HasReachedImageLimit(userID uuid.UUID) (bool, error) {
getStacks := Image.
SELECT(COUNT(Image.UserID).AS("imageCount.ImageCount")).
WHERE(Image.UserID.EQ(UUID(userID)))
var count imageCount
err := getStacks.Query(m.dbPool, &count)
return count.ImageCount >= IMAGE_LIMIT, err
}
func CreateLimitsManager(db *sql.DB) *LimitsManager {
return &LimitsManager{
db,
}
}

View File

@ -1,54 +0,0 @@
package main
import (
"database/sql"
"io"
"os"
"time"
"github.com/charmbracelet/log"
"github.com/google/uuid"
"github.com/muesli/termenv"
)
type DatabaseWriter struct {
dbPool *sql.DB
imageId uuid.UUID
}
func (w *DatabaseWriter) Write(p []byte) (n int, err error) {
if len(p) == 0 {
return 0, nil
}
if err != nil {
return 0, err
} else {
return len(p), nil
}
}
func newDatabaseWriter(dbPool *sql.DB, imageId uuid.UUID) *DatabaseWriter {
return &DatabaseWriter{
dbPool: dbPool,
imageId: imageId,
}
}
func createDbStdoutWriter(dbPool *sql.DB, imageId uuid.UUID) io.Writer {
return io.MultiWriter(os.Stdout, newDatabaseWriter(dbPool, imageId))
}
func createLogger(prefix string, writer io.Writer) *log.Logger {
logger := log.NewWithOptions(writer, log.Options{
ReportTimestamp: true,
TimeFormat: time.Kitchen,
Prefix: prefix,
Formatter: log.TextFormatter,
})
logger.SetColorProfile(termenv.TrueColor)
logger.SetLevel(log.DebugLevel)
return logger
}

View File

@ -1,50 +1,401 @@
package main
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"screenmark/screenmark/middleware"
"path/filepath"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/models"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/google/uuid"
"github.com/joho/godotenv"
)
type TestAiClient struct {
ImageInfo client.ImageMessageContent
}
func (client TestAiClient) GetImageInfo(imageName string, imageData []byte) (client.ImageMessageContent, error) {
return client.ImageInfo, nil
}
func main() {
err := godotenv.Load()
if err != nil {
panic(err)
}
jwtSecret := os.Getenv("JWT_SECRET")
if jwtSecret == "" {
panic("JWT_SECRET environment variable not set")
}
jwtManager := middleware.NewJwtManager([]byte(jwtSecret))
db, err := models.InitDatabase()
if err != nil {
panic(err)
}
router, err := setupRouter(db, jwtManager)
imageModel := models.NewImageModel(db)
userModel := models.NewUserModel(db)
mail, err := CreateMailClient()
if err != nil {
panic(err)
}
port, exists := os.LookupEnv("PORT")
if !exists {
panic("no port can be found")
}
auth := CreateAuth(mail)
portWithColon := fmt.Sprintf(":%s", port)
eventManager := NewEventManager()
logger := createLogger("Main", os.Stdout)
go ListenNewImageEvents(db, &eventManager)
go ListenProcessingImageStatus(db, &eventManager)
logger.Info("Serving router", "port", portWithColon)
err = http.ListenAndServe(portWithColon, router)
if err != nil {
panic(err)
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(CorsMiddleware)
r.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
next.ServeHTTP(w, r)
})
})
r.Options("/*", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
r.Group(func(r chi.Router) {
r.Use(ProtectedRoute)
r.Get("/image", func(w http.ResponseWriter, r *http.Request) {
userId := r.Context().Value(USER_ID).(uuid.UUID)
images, err := userModel.ListWithProperties(r.Context(), userId)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "Something went wrong")
return
}
type DataType struct {
Type string `json:"type"`
Data any `json:"data"`
}
dataTypes := make([]DataType, 0)
// lord
// forgive me
idMap := make(map[uuid.UUID]bool)
for _, image := range images {
for _, location := range image.Locations {
_, exists := idMap[location.ID]
if exists {
continue
}
dataTypes = append(dataTypes, DataType{
Type: "location",
Data: location,
})
idMap[location.ID] = true
}
for _, event := range image.Events {
_, exists := idMap[event.ID]
if exists {
continue
}
dataTypes = append(dataTypes, DataType{
Type: "event",
Data: event,
})
idMap[event.ID] = true
}
for _, note := range image.Notes {
dataTypes = append(dataTypes, DataType{
Type: "note",
Data: note,
})
idMap[note.ID] = true
}
for _, contact := range image.Contacts {
_, exists := idMap[contact.ID]
if exists {
continue
}
dataTypes = append(dataTypes, DataType{
Type: "contact",
Data: contact,
})
idMap[contact.ID] = true
}
}
jsonImages, err := json.Marshal(dataTypes)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Could not create JSON response for this image")
return
}
w.Write(jsonImages)
})
r.Get("/image/{id}", func(w http.ResponseWriter, r *http.Request) {
stringImageId := r.PathValue("id")
userId := r.Context().Value(USER_ID).(uuid.UUID)
imageId, err := uuid.Parse(stringImageId)
if err != nil {
w.WriteHeader(http.StatusForbidden)
fmt.Fprintf(w, "You cannot read this")
return
}
if authorized := imageModel.IsUserAuthorized(r.Context(), imageId, userId); !authorized {
w.WriteHeader(http.StatusForbidden)
fmt.Fprintf(w, "You cannot read this")
return
}
// TODO: really need authorization here!
image, err := imageModel.Get(r.Context(), imageId)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "Could not get image")
return
}
// TODO: this could be part of the db table
extension := filepath.Ext(image.Image.ImageName)
extension = extension[1:]
w.Header().Add("Content-Type", "image/"+extension)
w.Write(image.Image.Image)
})
r.Post("/image/{name}", func(w http.ResponseWriter, r *http.Request) {
imageName := r.PathValue("name")
userId := r.Context().Value(USER_ID).(uuid.UUID)
if len(imageName) == 0 {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "You need to provide a name in the path")
return
}
contentType := r.Header.Get("Content-Type")
// TODO: length checks on body
// TODO: extract this shit out
image := make([]byte, 0)
if contentType == "application/base64" {
decoder := base64.NewDecoder(base64.StdEncoding, r.Body)
buf := &bytes.Buffer{}
decodedIamge, err := io.Copy(buf, decoder)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "bruh, base64 aint decoding")
return
}
fmt.Println(string(image))
fmt.Println(decodedIamge)
image = buf.Bytes()
} else if contentType == "application/oclet-stream" {
bodyData, err := io.ReadAll(r.Body)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "bruh, binary aint binaring")
return
}
// TODO: check headers
image = bodyData
} else {
log.Println("bad stuff?")
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Bruh, you need oclet stream or base64")
return
}
if err != nil {
log.Println("First case")
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Couldnt read the image from the request body")
return
}
userImage, err := imageModel.Process(r.Context(), userId, model.Image{
Image: image,
ImageName: imageName,
})
if err != nil {
log.Println("Second case")
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Could not save image to DB")
return
}
jsonUserImage, err := json.Marshal(userImage)
if err != nil {
log.Println("Third case")
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Could not create JSON response for this image")
return
}
w.WriteHeader(http.StatusCreated)
fmt.Fprint(w, string(jsonUserImage))
w.Header().Add("Content-Type", "application/json")
})
})
r.Get("/image-events/{id}", func(w http.ResponseWriter, r *http.Request) {
// TODO: authentication :)
id := r.PathValue("id")
imageNotifier, exists := eventManager.listeners[id]
if !exists {
fmt.Println("Not found!")
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.(http.Flusher).Flush()
ctx, cancel := context.WithCancel(r.Context())
for {
select {
case <-ctx.Done():
fmt.Fprint(w, "event: close\ndata: Connection closed\n\n")
w.(http.Flusher).Flush()
cancel()
return
case data := <-imageNotifier:
fmt.Printf("Status received: %s\n", data)
fmt.Fprintf(w, "data: %s-%s\n", data, time.Now().String())
w.(http.Flusher).Flush()
cancel()
}
}
})
r.Post("/login", func(w http.ResponseWriter, r *http.Request) {
type LoginBody struct {
Email string `json:"email"`
}
loginBody := LoginBody{}
err := json.NewDecoder(r.Body).Decode(&loginBody)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Request body was not correct")
return
}
// TODO: validate it's an email
auth.CreateCode(loginBody.Email)
w.WriteHeader(http.StatusOK)
})
r.Post("/code", func(w http.ResponseWriter, r *http.Request) {
type CodeBody struct {
Email string `json:"email"`
Code string `json:"code"`
}
type CodeReturn struct {
Access string `json:"access"`
Refresh string `json:"refresh"`
}
codeBody := CodeBody{}
if err := json.NewDecoder(r.Body).Decode(&codeBody); err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "Request body was not correct")
return
}
if err := auth.UseCode(codeBody.Email, codeBody.Code); err != nil {
log.Println(err)
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "email or code are incorrect")
return
}
if exists := userModel.DoesUserExist(r.Context(), codeBody.Email); !exists {
userModel.Save(r.Context(), model.Users{
Email: codeBody.Email,
})
}
uuid, err := userModel.GetUserIdFromEmail(r.Context(), codeBody.Email)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Something went wrong.")
return
}
refresh := CreateRefreshToken(uuid)
access := CreateAccessToken(uuid)
codeReturn := CodeReturn{
Access: access,
Refresh: refresh,
}
json, err := json.Marshal(codeReturn)
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Something went wrong.")
return
}
w.WriteHeader(http.StatusOK)
w.Header().Add("Content-Type", "application/json")
fmt.Fprint(w, string(json))
})
log.Println("Listening and serving on port 3040.")
if err := http.ListenAndServe(":3040", r); err != nil {
log.Println(err)
return
}
}

39
backend/middleware.go Normal file
View File

@ -0,0 +1,39 @@
package main
import (
"context"
"net/http"
)
func CorsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Access-Control-Allow-Origin", "*")
w.Header().Add("Access-Control-Allow-Credentials", "*")
w.Header().Add("Access-Control-Allow-Headers", "*")
next.ServeHTTP(w, r)
})
}
const USER_ID = "UserID"
func ProtectedRoute(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if len(token) < len("Bearer ") {
w.WriteHeader(http.StatusUnauthorized)
return
}
userId, err := GetUserIdFromAccess(token[len("Bearer "):])
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
contextWithUserId := context.WithValue(r.Context(), USER_ID, userId)
newR := r.WithContext(contextWithUserId)
next.ServeHTTP(w, newR)
})
}

View File

@ -1,29 +0,0 @@
package middleware
import (
"encoding/json"
"io"
"net/http"
)
func WithValidatedPost[K any](
fn func(request K, w http.ResponseWriter, r *http.Request),
) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
request := new(K)
body, err := io.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
err = json.Unmarshal(body, request)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
fn(*request, w, r)
}
}

View File

@ -1,11 +0,0 @@
package middleware
import "net/http"
func SetJson(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
next.ServeHTTP(w, r)
})
}

View File

@ -1,136 +0,0 @@
package middleware
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
type JwtType string
const (
Access JwtType = "access"
Refresh JwtType = "refresh"
)
type JwtClaims struct {
UserID string
Type JwtType
Expiry time.Time
}
type JwtManager struct {
secret []byte
}
func NewJwtManager(secret []byte) *JwtManager {
return &JwtManager{secret: secret}
}
func (jm *JwtManager) createToken(claims JwtClaims) *jwt.Token {
return jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"UserID": claims.UserID,
"Type": claims.Type,
"exp": claims.Expiry.Unix(),
})
}
func (jm *JwtManager) CreateRefreshToken(userId uuid.UUID) string {
token := jm.createToken(JwtClaims{
UserID: userId.String(),
Type: Refresh,
Expiry: time.Now().Add(time.Hour * 24 * 30),
})
tokenString, err := token.SignedString(jm.secret)
if err != nil {
panic(err)
}
return tokenString
}
func (jm *JwtManager) CreateAccessToken(userId uuid.UUID) string {
token := jm.createToken(JwtClaims{
UserID: userId.String(),
Type: Access,
Expiry: time.Now().Add(time.Minute),
})
tokenString, err := token.SignedString(jm.secret)
if err != nil {
panic(err)
}
return tokenString
}
var NotValidToken = errors.New("Not a valid token")
func (jm *JwtManager) GetUserIdFromAccess(accessToken string) (uuid.UUID, error) {
token, err := jwt.Parse(accessToken, func(token *jwt.Token) (any, error) {
return jm.secret, nil
}, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))
if err != nil {
return uuid.Nil, err
}
// Check if token is valid (JWT library validates exp claim automatically)
if !token.Valid {
return uuid.Nil, NotValidToken
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
tokenType, ok := claims["Type"]
if !ok || tokenType.(string) != "access" {
return uuid.Nil, NotValidToken
}
userId, err := uuid.Parse(claims["UserID"].(string))
if err != nil {
return uuid.Nil, NotValidToken
}
return userId, nil
} else {
return uuid.Nil, NotValidToken
}
}
func (jm *JwtManager) GetUserIdFromRefresh(refreshToken string) (uuid.UUID, error) {
token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (any, error) {
return jm.secret, nil
}, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))
if err != nil {
return uuid.Nil, err
}
// Check if token is valid (JWT library validates exp claim automatically)
if !token.Valid {
return uuid.Nil, NotValidToken
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
tokenType, ok := claims["Type"]
if !ok || tokenType.(string) != "refresh" {
return uuid.Nil, NotValidToken
}
userId, err := uuid.Parse(claims["UserID"].(string))
if err != nil {
return uuid.Nil, NotValidToken
}
return userId, nil
} else {
return uuid.Nil, NotValidToken
}
}
func GetUserIdFromAccess(jm *JwtManager, accessToken string) (uuid.UUID, error) {
return jm.GetUserIdFromAccess(accessToken)
}

View File

@ -1,36 +0,0 @@
package middleware
import (
"net/http"
"github.com/charmbracelet/log"
"github.com/google/uuid"
)
func WithLimit(logger *log.Logger, getLimit func(userID uuid.UUID) (bool, error), next func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
userID, err := GetUserID(ctx, logger, w)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
hasReachedLimit, err := getLimit(userID)
if err != nil {
logger.Error("failed to image limit", "err", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
logger.Info("Limits", "hasReachedLimit", hasReachedLimit)
if hasReachedLimit {
w.WriteHeader(http.StatusTooManyRequests)
return
}
next(w, r)
}
}

View File

@ -1,140 +0,0 @@
package middleware
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/charmbracelet/log"
"github.com/google/uuid"
)
func CorsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Access-Control-Allow-Origin", "*")
w.Header().Add("Access-Control-Allow-Headers", "*")
// Access-Control-Allow-Methods is often needed for preflight OPTIONS requests
w.Header().Add("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
// The client makes an OPTIONS preflight request before a complex request.
// We must handle this and respond with the appropriate headers.
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
const USER_ID = "UserID"
func GetUserID(ctx context.Context, logger *log.Logger, w http.ResponseWriter) (uuid.UUID, error) {
userId := ctx.Value(USER_ID)
if userId == nil {
w.WriteHeader(http.StatusUnauthorized)
logger.Warn("UserID not present in request")
return uuid.Nil, errors.New("context does not contain a user id")
}
userIdUuid, ok := userId.(uuid.UUID)
if !ok {
w.WriteHeader(http.StatusUnauthorized)
logger.Warn("UserID not of correct type")
return uuid.Nil, fmt.Errorf("context user id is not of type uuid, got: %t", userId)
}
return userIdUuid, nil
}
func ProtectedRouteURL(jm *JwtManager) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
userId, err := GetUserIdFromAccess(jm, token)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
contextWithUserId := context.WithValue(r.Context(), USER_ID, userId)
newR := r.WithContext(contextWithUserId)
next.ServeHTTP(w, newR)
})
}
}
func ProtectedRoute(jm *JwtManager) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if len(token) < len("Bearer ") {
w.WriteHeader(http.StatusUnauthorized)
return
}
userId, err := GetUserIdFromAccess(jm, token[len("Bearer "):])
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
contextWithUserId := context.WithValue(r.Context(), USER_ID, userId)
newR := r.WithContext(contextWithUserId)
next.ServeHTTP(w, newR)
})
}
}
func GetUserIdFromUrl(jm *JwtManager) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
if len(token) == 0 {
w.WriteHeader(http.StatusUnauthorized)
return
}
userId, err := GetUserIdFromAccess(jm, token)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
contextWithUserId := context.WithValue(r.Context(), USER_ID, userId)
newR := r.WithContext(contextWithUserId)
next.ServeHTTP(w, newR)
})
}
}
func GetPathParamID(logger *log.Logger, param string, w http.ResponseWriter, r *http.Request) (uuid.UUID, error) {
pathParam := r.PathValue(param)
if len(pathParam) == 0 {
w.WriteHeader(http.StatusBadRequest)
err := fmt.Errorf("%s was not present", param)
logger.Warn(err)
return uuid.Nil, err
}
uuidParam, err := uuid.Parse(pathParam)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
err := fmt.Errorf("could not parse param: %w", err)
logger.Warn(err)
return uuid.Nil, err
}
return uuidParam, nil
}

View File

@ -1,49 +0,0 @@
package middleware
import (
"encoding/json"
"net/http"
"github.com/charmbracelet/log"
)
func WriteJsonOrError[K any](logger *log.Logger, object K, w http.ResponseWriter) {
jsonObject, err := json.Marshal(object)
if err != nil {
logger.Warn("could not marshal json object", "err", err)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Write(jsonObject)
w.WriteHeader(http.StatusOK)
}
type ErrorObject struct {
Error string `json:"error"`
}
func writeError(logger *log.Logger, error string, w http.ResponseWriter, code int) {
e := ErrorObject{
error,
}
jsonObject, err := json.Marshal(e)
if err != nil {
logger.Warn("could not marshal json object", "err", err)
w.WriteHeader(http.StatusBadRequest)
return
}
logger.Error("writing error", "error", error)
w.Write(jsonObject)
w.WriteHeader(code)
}
func WriteErrorBadRequest(logger *log.Logger, error string, w http.ResponseWriter) {
writeError(logger, error, w, http.StatusBadRequest)
}
func WriteErrorInternal(logger *log.Logger, error string, w http.ResponseWriter) {
writeError(logger, error, w, http.StatusInternalServerError)
}

117
backend/models/contacts.go Normal file
View File

@ -0,0 +1,117 @@
package models
import (
"context"
"database/sql"
"screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table"
. "github.com/go-jet/jet/v2/postgres"
"github.com/google/uuid"
)
type ContactModel struct {
dbPool *sql.DB
}
func (m ContactModel) List(ctx context.Context, userId uuid.UUID) ([]model.Contacts, error) {
listContactsStmt := SELECT(Contacts.AllColumns).
FROM(
Contacts.
INNER_JOIN(UserContacts, UserContacts.ContactID.EQ(Contacts.ID)),
).
WHERE(UserContacts.UserID.EQ(UUID(userId)))
locations := []model.Contacts{}
err := listContactsStmt.QueryContext(ctx, m.dbPool, &locations)
return locations, err
}
func (m ContactModel) Get(ctx context.Context, contactId uuid.UUID) (model.Contacts, error) {
getContactStmt := Contacts.
SELECT(Contacts.AllColumns).
WHERE(Contacts.ID.EQ(UUID(contactId)))
contact := model.Contacts{}
err := getContactStmt.QueryContext(ctx, m.dbPool, &contact)
return contact, err
}
func (m ContactModel) Update(ctx context.Context, contact model.Contacts) (model.Contacts, error) {
existingContact, err := m.Get(ctx, contact.ID)
if err != nil {
return model.Contacts{}, err
}
existingContact.Name = contact.Name
if contact.Description != nil {
existingContact.Description = contact.Description
}
if contact.PhoneNumber != nil {
existingContact.PhoneNumber = contact.PhoneNumber
}
if contact.Email != nil {
existingContact.Email = contact.Email
}
updateContactStmt := Contacts.
UPDATE(Contacts.MutableColumns).
MODEL(existingContact).
WHERE(Contacts.ID.EQ(UUID(contact.ID))).
RETURNING(Contacts.AllColumns)
updatedContact := model.Contacts{}
err = updateContactStmt.QueryContext(ctx, m.dbPool, &updatedContact)
return updatedContact, err
}
func (m ContactModel) Save(ctx context.Context, userId uuid.UUID, contact model.Contacts) (model.Contacts, error) {
// TODO: make this a transaction
if contact.ID != uuid.Nil {
return m.Update(ctx, contact)
}
insertContactStmt := Contacts.
INSERT(Contacts.Name, Contacts.Description, Contacts.PhoneNumber, Contacts.Email).
VALUES(contact.Name, contact.Description, contact.PhoneNumber, contact.Email).
RETURNING(Contacts.AllColumns)
insertedContact := model.Contacts{}
err := insertContactStmt.QueryContext(ctx, m.dbPool, &insertedContact)
if err != nil {
return insertedContact, err
}
insertUserContactStmt := UserContacts.
INSERT(UserContacts.UserID, UserContacts.ContactID).
VALUES(userId, insertedContact.ID)
_, err = insertUserContactStmt.ExecContext(ctx, m.dbPool)
return insertedContact, err
}
func (m ContactModel) SaveToImage(ctx context.Context, imageId uuid.UUID, contactId uuid.UUID) (model.ImageContacts, error) {
insertImageContactStmt := ImageContacts.
INSERT(ImageContacts.ImageID, ImageContacts.ContactID).
VALUES(imageId, contactId).
RETURNING(ImageContacts.AllColumns)
imageContact := model.ImageContacts{}
err := insertImageContactStmt.QueryContext(ctx, m.dbPool, &imageContact)
return imageContact, err
}
func NewContactModel(db *sql.DB) ContactModel {
return ContactModel{dbPool: db}
}

94
backend/models/events.go Normal file
View File

@ -0,0 +1,94 @@
package models
import (
"context"
"database/sql"
. "github.com/go-jet/jet/v2/postgres"
"screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table"
"github.com/google/uuid"
)
type EventModel struct {
dbPool *sql.DB
}
func (m EventModel) List(ctx context.Context, userId uuid.UUID) ([]model.Events, error) {
listEventsStmt := SELECT(Events.AllColumns).
FROM(
Events.
INNER_JOIN(UserEvents, UserEvents.EventID.EQ(Events.ID)),
).
WHERE(UserEvents.UserID.EQ(UUID(userId)))
events := []model.Events{}
err := listEventsStmt.QueryContext(ctx, m.dbPool, &events)
return events, err
}
func (m EventModel) Save(ctx context.Context, userId uuid.UUID, event model.Events) (model.Events, error) {
// TODO tx here
insertEventStmt := Events.
INSERT(Events.MutableColumns).
MODEL(event).
RETURNING(Events.AllColumns)
insertedEvent := model.Events{}
err := insertEventStmt.QueryContext(ctx, m.dbPool, &insertedEvent)
if err != nil {
return insertedEvent, err
}
insertUserEventStmt := UserEvents.
INSERT(UserEvents.UserID, UserEvents.EventID).
VALUES(userId, insertedEvent.ID)
_, err = insertUserEventStmt.ExecContext(ctx, m.dbPool)
return insertedEvent, err
}
func (m EventModel) SaveToImage(ctx context.Context, imageId uuid.UUID, eventId uuid.UUID) (model.ImageEvents, error) {
insertImageEventStmt := ImageEvents.
INSERT(ImageEvents.ImageID, ImageEvents.EventID).
VALUES(imageId, eventId).
RETURNING(ImageEvents.AllColumns)
imageEvent := model.ImageEvents{}
err := insertImageEventStmt.QueryContext(ctx, m.dbPool, &imageEvent)
return imageEvent, err
}
func (m EventModel) UpdateLocation(ctx context.Context, eventId uuid.UUID, locationId uuid.UUID) (model.Events, error) {
updateEventLocationStmt := Events.
UPDATE(Events.LocationID).
SET(locationId).
WHERE(Events.ID.EQ(UUID(eventId))).
RETURNING(Events.AllColumns)
updatedEvent := model.Events{}
err := updateEventLocationStmt.QueryContext(ctx, m.dbPool, &updatedEvent)
return updatedEvent, err
}
func (m EventModel) UpdateOrganizer(ctx context.Context, eventId uuid.UUID, organizerId uuid.UUID) (model.Events, error) {
updateEventContactStmt := Events.
UPDATE(Events.OrganizerID).
SET(organizerId).
WHERE(Events.ID.EQ(UUID(eventId))).
RETURNING(Events.AllColumns)
updatedEvent := model.Events{}
err := updateEventContactStmt.QueryContext(ctx, m.dbPool, &updatedEvent)
return updatedEvent, err
}
func NewEventModel(db *sql.DB) EventModel {
return EventModel{dbPool: db}
}

View File

@ -3,12 +3,12 @@ package models
import (
"context"
"database/sql"
"errors"
"fmt"
"screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table"
. "github.com/go-jet/jet/v2/postgres"
"github.com/go-jet/jet/v2/qrm"
"github.com/google/uuid"
)
@ -17,72 +17,154 @@ type ImageModel struct {
dbPool *sql.DB
}
func (m ImageModel) Save(ctx context.Context, name string, image []byte, userID uuid.UUID) (model.Image, error) {
saveImageStmt := Image.INSERT(Image.ImageName, Image.Image, Image.Description, Image.UserID).
VALUES(name, image, "", userID).
RETURNING(Image.AllColumns)
type ImageData struct {
model.UserImages
newImage := model.Image{}
err := saveImageStmt.QueryContext(ctx, m.dbPool, &newImage)
return newImage, err
Image model.Image
}
func (m ImageModel) Get(ctx context.Context, imageID uuid.UUID) (model.Image, bool, error) {
getImageStmt := Image.SELECT(Image.AllColumns).WHERE(Image.ID.EQ(UUID(imageID)))
type ProcessingImageData struct {
model.UserImagesToProcess
image := model.Image{}
err := getImageStmt.QueryContext(ctx, m.dbPool, &image)
return image, err != qrm.ErrNoRows, err
Image model.Image
}
func (m ImageModel) UpdateDescription(ctx context.Context, imageID uuid.UUID, description string) error {
updateImageDescriptionStmt := Image.UPDATE(Image.Description).
SET(Image.Description.SET(String(description))).
WHERE(Image.ID.EQ(UUID(imageID)))
func (m ImageModel) Process(ctx context.Context, userId uuid.UUID, image model.Image) (model.UserImagesToProcess, error) {
tx, err := m.dbPool.BeginTx(ctx, nil)
if err != nil {
return model.UserImagesToProcess{}, err
}
_, err := updateImageDescriptionStmt.ExecContext(ctx, m.dbPool)
insertImageStmt := Image.
INSERT(Image.ImageName, Image.Image).
VALUES(image.ImageName, image.Image).
RETURNING(Image.ID)
insertedImage := model.Image{}
err = insertImageStmt.QueryContext(ctx, tx, &insertedImage)
if err != nil {
return model.UserImagesToProcess{}, err
}
stmt := UserImagesToProcess.
INSERT(UserImagesToProcess.UserID, UserImagesToProcess.ImageID).
VALUES(userId, insertedImage.ID).
RETURNING(UserImagesToProcess.AllColumns)
userImage := model.UserImagesToProcess{}
err = stmt.QueryContext(ctx, tx, &userImage)
if err != nil {
return model.UserImagesToProcess{}, err
}
err = tx.Commit()
return userImage, err
}
func (m ImageModel) GetToProcess(ctx context.Context, imageId uuid.UUID) (model.UserImagesToProcess, error) {
getToProcessStmt := UserImagesToProcess.
SELECT(UserImagesToProcess.AllColumns).
WHERE(UserImagesToProcess.ID.EQ(UUID(imageId)))
images := []model.UserImagesToProcess{}
err := getToProcessStmt.QueryContext(ctx, m.dbPool, &images)
if len(images) != 1 {
return model.UserImagesToProcess{}, errors.New(fmt.Sprintf("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{}, errors.New(fmt.Sprintf("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
}
removeProcessingStmt := UserImagesToProcess.
DELETE().
WHERE(UserImagesToProcess.ID.EQ(UUID(imageToProcess.ID)))
_, err = removeProcessingStmt.ExecContext(ctx, tx)
if err != nil {
return model.UserImages{}, err
}
err = tx.Commit()
return userImage, err
}
func (m ImageModel) StartProcessing(ctx context.Context, processingImageId uuid.UUID) error {
startProcessingStmt := UserImagesToProcess.
UPDATE(UserImagesToProcess.Status).
SET(model.Progress_InProgress).
WHERE(UserImagesToProcess.ID.EQ(UUID(processingImageId)))
_, err := startProcessingStmt.ExecContext(ctx, m.dbPool)
return err
}
func (m ImageModel) UpdateProcess(ctx context.Context, imageID uuid.UUID, process model.Progress) error {
updateImageDescriptionStmt := Image.UPDATE(Image.Status).
SET(process).
WHERE(Image.ID.EQ(UUID(imageID)))
func (m ImageModel) Get(ctx context.Context, imageId uuid.UUID) (ImageData, error) {
getImageStmt := SELECT(UserImages.AllColumns, Image.AllColumns).
FROM(
UserImages.INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID)),
).
WHERE(UserImages.ID.EQ(UUID(imageId)))
_, err := updateImageDescriptionStmt.ExecContext(ctx, m.dbPool)
images := []ImageData{}
err := getImageStmt.QueryContext(ctx, m.dbPool, &images)
return err
}
func (m ImageModel) Update(ctx context.Context, image model.Image) (model.Image, error) {
updateImageStmt := Image.UPDATE(Image.MutableColumns.Except(Image.Image)).
MODEL(image).
WHERE(Image.ID.EQ(UUID(image.ID))).
RETURNING(Image.AllColumns.Except(Image.Image))
updatedImage := model.Image{}
err := updateImageStmt.QueryContext(ctx, m.dbPool, &updatedImage)
return updatedImage, err
}
func (m ImageModel) Delete(ctx context.Context, imageID, userID uuid.UUID) (bool, error) {
deleteImageStmt := Image.DELETE().WHERE(Image.ID.EQ(UUID(imageID)).AND(Image.UserID.EQ(UUID(userID))))
r, err := deleteImageStmt.ExecContext(ctx, m.dbPool)
if err != nil {
return false, fmt.Errorf("deleting image: %w", err)
if len(images) != 1 {
return ImageData{}, errors.New(fmt.Sprintf("Expected 1, got %d\n", len(images)))
}
rowsAffected, err := r.RowsAffected()
if err != nil {
return false, fmt.Errorf("unreachable: %w", err)
}
return images[0], err
}
return rowsAffected > 0, nil
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)))
userImage := model.UserImages{}
err := getImageUserId.QueryContext(ctx, m.dbPool, &userImage)
return err != nil && userImage.UserID.String() == userId.String()
}
func NewImageModel(db *sql.DB) ImageModel {

33
backend/models/links.go Normal file
View File

@ -0,0 +1,33 @@
package models
import (
"context"
"database/sql"
. "screenmark/screenmark/.gen/haystack/haystack/table"
"github.com/google/uuid"
)
type LinkModel struct {
dbPool *sql.DB
}
func (m LinkModel) Save(ctx context.Context, imageId uuid.UUID, links []string) error {
if len(links) == 0 {
return nil
}
stmt := ImageLinks.INSERT(ImageLinks.ImageID, ImageLinks.Link)
for _, link := range links {
stmt = stmt.VALUES(imageId, link)
}
_, err := stmt.ExecContext(ctx, m.dbPool)
return err
}
func NewLinkModel(db *sql.DB) LinkModel {
return LinkModel{dbPool: db}
}

130
backend/models/locations.go Normal file
View File

@ -0,0 +1,130 @@
package models
import (
"context"
"database/sql"
"screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table"
. "github.com/go-jet/jet/v2/postgres"
"github.com/go-jet/jet/v2/qrm"
"github.com/google/uuid"
)
type LocationModel struct {
dbPool *sql.DB
}
func (m LocationModel) List(ctx context.Context, userId uuid.UUID) ([]model.Locations, error) {
listLocationsStmt := SELECT(Locations.AllColumns).
FROM(
Locations.
INNER_JOIN(UserLocations, UserLocations.LocationID.EQ(Locations.ID)),
).
WHERE(UserLocations.UserID.EQ(UUID(userId)))
locations := []model.Locations{}
err := listLocationsStmt.QueryContext(ctx, m.dbPool, &locations)
return locations, err
}
func (m LocationModel) Get(ctx context.Context, locationId uuid.UUID) (model.Locations, error) {
getLocationStmt := Locations.
SELECT(Locations.AllColumns).
WHERE(Locations.ID.EQ(UUID(locationId)))
location := model.Locations{}
err := getLocationStmt.QueryContext(ctx, m.dbPool, &location)
return location, err
}
func (m LocationModel) Update(ctx context.Context, location model.Locations) (model.Locations, error) {
existingLocation, err := m.Get(ctx, location.ID)
if err != nil {
return model.Locations{}, err
}
existingLocation.Name = location.Name
if location.Description != nil {
existingLocation.Description = location.Description
}
if location.Address != nil {
existingLocation.Address = location.Address
}
updateLocationStmt := Locations.
UPDATE(Locations.MutableColumns).
MODEL(existingLocation).
WHERE(Locations.ID.EQ(UUID(location.ID))).
RETURNING(Locations.AllColumns)
updatedLocation := model.Locations{}
err = updateLocationStmt.QueryContext(ctx, m.dbPool, &updatedLocation)
return updatedLocation, err
}
func (m LocationModel) Save(ctx context.Context, userId uuid.UUID, location model.Locations) (model.Locations, error) {
if location.ID != uuid.Nil {
return m.Update(ctx, location)
}
insertLocationStmt := Locations.
INSERT(Locations.Name, Locations.Address, Locations.Description).
VALUES(location.Name, location.Address, location.Description).
RETURNING(Locations.AllColumns)
insertedLocation := model.Locations{}
err := insertLocationStmt.QueryContext(ctx, m.dbPool, &insertedLocation)
if err != nil {
return model.Locations{}, err
}
insertUserLocationStmt := UserLocations.
INSERT(UserLocations.UserID, UserLocations.LocationID).
VALUES(userId, insertedLocation.ID)
_, err = insertUserLocationStmt.ExecContext(ctx, m.dbPool)
return insertedLocation, err
}
func (m LocationModel) SaveToImage(ctx context.Context, imageId uuid.UUID, locationId uuid.UUID) (model.ImageLocations, error) {
imageLocation := model.ImageLocations{}
checkExistingStmt := ImageLocations.
SELECT(ImageLocations.AllColumns).
WHERE(
ImageLocations.ImageID.EQ(UUID(imageId)).
AND(ImageLocations.LocationID.EQ(UUID(locationId))),
)
err := checkExistingStmt.QueryContext(ctx, m.dbPool, &imageLocation)
if err != nil && err != qrm.ErrNoRows {
// A real error
return model.ImageLocations{}, err
}
if err == nil {
// Already exists.
return imageLocation, nil
}
insertImageLocationStmt := ImageLocations.
INSERT(ImageLocations.ImageID, ImageLocations.LocationID).
VALUES(imageId, locationId).
RETURNING(ImageLocations.AllColumns)
err = insertImageLocationStmt.QueryContext(ctx, m.dbPool, &imageLocation)
return imageLocation, err
}
func NewLocationModel(db *sql.DB) LocationModel {
return LocationModel{dbPool: db}
}

67
backend/models/notes.go Normal file
View File

@ -0,0 +1,67 @@
package models
import (
"context"
"database/sql"
"screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table"
. "github.com/go-jet/jet/v2/postgres"
"github.com/google/uuid"
)
type NoteModel struct {
dbPool *sql.DB
}
func (m NoteModel) List(ctx context.Context, userId uuid.UUID) ([]model.Notes, error) {
listNotesStmt := SELECT(Notes.AllColumns).
FROM(
Notes.
INNER_JOIN(UserNotes, UserNotes.NoteID.EQ(Notes.ID)),
).
WHERE(UserNotes.UserID.EQ(UUID(userId)))
locations := []model.Notes{}
err := listNotesStmt.QueryContext(ctx, m.dbPool, &locations)
return locations, err
}
func (m NoteModel) Save(ctx context.Context, userId uuid.UUID, note model.Notes) (model.Notes, error) {
insertNoteStmt := Notes.
INSERT(Notes.Name, Notes.Description, Notes.Content).
VALUES(note.Name, note.Description, note.Content).
RETURNING(Notes.AllColumns)
insertedNote := model.Notes{}
err := insertNoteStmt.QueryContext(ctx, m.dbPool, &insertedNote)
if err != nil {
return model.Notes{}, err
}
insertUserNoteStmt := UserNotes.
INSERT(UserNotes.UserID, UserNotes.NoteID).
VALUES(userId, insertedNote.ID)
_, err = insertUserNoteStmt.ExecContext(ctx, m.dbPool)
return insertedNote, err
}
func (m NoteModel) SaveToImage(ctx context.Context, imageId uuid.UUID, noteId uuid.UUID) (model.ImageNotes, error) {
insertImageNoteStmt := ImageNotes.
INSERT(ImageNotes.ImageID, ImageNotes.NoteID).
VALUES(imageId, noteId).
RETURNING(ImageNotes.AllColumns)
imageNote := model.ImageNotes{}
err := insertImageNoteStmt.QueryContext(ctx, m.dbPool, &imageNote)
return imageNote, err
}
func NewNoteModel(db *sql.DB) NoteModel {
return NoteModel{dbPool: db}
}

View File

@ -1,186 +0,0 @@
package models
import (
"context"
"database/sql"
"fmt"
"screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table"
. "github.com/go-jet/jet/v2/postgres"
"github.com/google/uuid"
)
type StackModel struct {
dbPool *sql.DB
}
type StackWithItems struct {
model.Stacks
SchemaItems []model.SchemaItems
}
type ImageWithSchema struct {
model.ImageStacks
Items []model.ImageSchemaItems
}
type IDValue struct {
ID string `json:"id"`
Value string `json:"value"`
}
// ========================================
// SELECT for lists
// ========================================
func (m StackModel) List(ctx context.Context, userId uuid.UUID) ([]StackWithItems, error) {
getStacksWithItems := SELECT(
Stacks.AllColumns,
SchemaItems.AllColumns,
).
FROM(
Stacks.
INNER_JOIN(SchemaItems, SchemaItems.StackID.EQ(Stacks.ID)),
).
WHERE(Stacks.UserID.EQ(UUID(userId)))
lists := []StackWithItems{}
err := getStacksWithItems.QueryContext(ctx, m.dbPool, &lists)
return lists, err
}
func (m StackModel) ListItems(ctx context.Context, stackID uuid.UUID) ([]ImageWithSchema, error) {
getListItems := SELECT(
ImageStacks.AllColumns,
ImageSchemaItems.AllColumns,
).
FROM(
ImageStacks.
INNER_JOIN(ImageSchemaItems, ImageSchemaItems.ImageID.EQ(ImageStacks.ImageID)),
).
WHERE(ImageStacks.StackID.EQ(UUID(stackID)))
listItems := make([]ImageWithSchema, 0)
err := getListItems.QueryContext(ctx, m.dbPool, &listItems)
return listItems, err
}
func (m StackModel) Get(ctx context.Context, stackID uuid.UUID) (model.Stacks, error) {
getStackStmt := Stacks.SELECT(Stacks.AllColumns).WHERE(Stacks.ID.EQ(UUID(stackID)))
stack := model.Stacks{}
err := getStackStmt.QueryContext(ctx, m.dbPool, &stack)
return stack, err
}
// ========================================
// INSERT methods
// ========================================
func (m StackModel) Save(ctx context.Context, userID uuid.UUID, name string, description string, status model.Progress) (model.Stacks, error) {
saveListStmt := Stacks.
INSERT(Stacks.UserID, Stacks.Name, Stacks.Description, Stacks.Status).
VALUES(userID, name, description, status).
RETURNING(Stacks.ID, Stacks.UserID, Stacks.Name, Stacks.Description, Stacks.Status, Stacks.CreatedAt)
list := model.Stacks{}
err := saveListStmt.QueryContext(ctx, m.dbPool, &list)
return list, err
}
func (m StackModel) SaveItems(ctx context.Context, items []model.SchemaItems) error {
saveItemsStmt := SchemaItems.INSERT(SchemaItems.MutableColumns).MODELS(items)
_, err := saveItemsStmt.ExecContext(ctx, m.dbPool)
return err
}
func (m StackModel) SaveImage(ctx context.Context, imageID uuid.UUID, stackID uuid.UUID) (model.ImageStacks, error) {
saveImageStmt := ImageStacks.
INSERT(ImageStacks.ImageID, ImageStacks.StackID).
VALUES(imageID, stackID).
RETURNING(ImageStacks.AllColumns)
imageStack := model.ImageStacks{}
err := saveImageStmt.QueryContext(ctx, m.dbPool, &imageStack)
return imageStack, err
}
func (m StackModel) SaveSchemaItems(ctx context.Context, imageID uuid.UUID, items []IDValue) error {
if len(items) == 0 {
return fmt.Errorf("items cannot be empty")
}
saveSchemaItemStmt := ImageSchemaItems.
INSERT(
ImageSchemaItems.ImageID,
ImageSchemaItems.SchemaItemID,
ImageSchemaItems.Value,
)
for _, item := range items {
saveSchemaItemStmt = saveSchemaItemStmt.VALUES(
imageID,
item.ID,
item.Value,
)
}
_, err := saveSchemaItemStmt.ExecContext(ctx, m.dbPool)
return err
}
// ========================================
// DELETE methods
// ========================================
func (m StackModel) DeleteSchemaItem(ctx context.Context, stackID uuid.UUID, schemaItemID uuid.UUID) error {
deleteImageListStmt := SchemaItems.DELETE().
WHERE(
SchemaItems.ID.EQ(UUID(schemaItemID)).
// The StackID check is a sanity check.
// We don't technically need it, but it adds extra protection
// in case we make a mistake later on
AND(SchemaItems.StackID.EQ(UUID(stackID))),
)
_, err := deleteImageListStmt.ExecContext(ctx, m.dbPool)
return err
}
func (m StackModel) DeleteImage(ctx context.Context, stackID uuid.UUID, imageID uuid.UUID) error {
deleteImageListStmt := ImageStacks.DELETE().
WHERE(
ImageStacks.StackID.EQ(UUID(stackID)).
AND(ImageStacks.ImageID.EQ(UUID(imageID))),
)
_, err := deleteImageListStmt.ExecContext(ctx, m.dbPool)
return err
}
func (m StackModel) Delete(ctx context.Context, stackID uuid.UUID, userID uuid.UUID) error {
deleteStackStmt := Stacks.DELETE().WHERE(Stacks.ID.EQ(UUID(stackID)).AND(Stacks.UserID.EQ(UUID(userID))))
_, err := deleteStackStmt.ExecContext(ctx, m.dbPool)
return err
}
func NewStackModel(db *sql.DB) StackModel {
return StackModel{dbPool: db}
}

156
backend/models/tags.go Normal file
View File

@ -0,0 +1,156 @@
package models
import (
"context"
"database/sql"
"fmt"
"screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table"
. "github.com/go-jet/jet/v2/postgres"
"github.com/google/uuid"
)
type TagModel struct {
dbPool *sql.DB
}
// Raw dogging SQL is kinda based though?
//
// | nO, usE OrM!!
//
// | RAW - RAW
// | SQL | \ SQL
// | GOOD | \ GOOD
// | - -
// | -- --
// | -- --
// | ---- IQ ----
func (m TagModel) getNonExistantTags(ctx context.Context, userId uuid.UUID, tags []string) ([]string, error) {
if len(tags) == 0 {
return tags, nil
}
values := ""
counter := 1
// big big SQL injection problem here?
for counter = 1; counter <= len(tags); counter++ {
values += fmt.Sprintf("($%d),", counter)
}
values = values[0 : len(values)-1]
getNonExistingTags := fmt.Sprintf(`WITH given_tags
AS (SELECT given_tags.tag FROM (VALUES `+values+`) AS given_tags (tag)),
this_user_tags AS
(SELECT id, tag FROM haystack.user_tags WHERE user_tags.user_id = $%d)
SELECT given_tags.tag
FROM given_tags
LEFT OUTER JOIN haystack.user_tags ON haystack.user_tags.tag = given_tags.tag
where user_tags.tag is null`, counter)
getNonExistingTagsStmt, err := m.dbPool.PrepareContext(ctx, getNonExistingTags)
defer getNonExistingTagsStmt.Close()
if err != nil {
return []string{}, err
}
args := make([]any, counter)
for i, v := range tags {
args[i] = v
}
args[counter-1] = userId.String()
rows, err := getNonExistingTagsStmt.QueryContext(ctx, args...)
if err != nil {
return []string{}, err
}
nonExistantTags := make([]string, 0)
for rows.Next() {
var tag string
rows.Scan(&tag)
nonExistantTags = append(nonExistantTags, tag)
}
return nonExistantTags, nil
}
func (m TagModel) Save(ctx context.Context, userId uuid.UUID, tags []string) error {
tagsToInsert, err := m.getNonExistantTags(ctx, userId, tags)
if err != nil {
return err
}
if len(tagsToInsert) == 0 {
return nil
}
stmt := UserTags.INSERT(UserTags.UserID, UserTags.Tag)
for _, tag := range tagsToInsert {
stmt = stmt.VALUES(UUID(userId), tag)
}
_, err = stmt.ExecContext(ctx, m.dbPool)
return err
}
func (m TagModel) List(ctx context.Context, userId uuid.UUID) ([]model.UserTags, error) {
listTagsStmt := UserTags.SELECT(UserTags.AllColumns).WHERE(UserTags.UserID.EQ(UUID(userId)))
userTags := []model.UserTags{}
err := listTagsStmt.QueryContext(ctx, m.dbPool, &userTags)
return userTags, err
}
func (m TagModel) SaveToImage(ctx context.Context, imageId uuid.UUID, tags []string) error {
if len(tags) == 0 {
return nil
}
userId, err := getUserIdFromImage(ctx, m.dbPool, imageId)
if err != nil {
return err
}
err = m.Save(ctx, userId, tags)
if err != nil {
return err
}
userTagsExpression := make([]Expression, 0)
for _, tag := range tags {
userTagsExpression = append(userTagsExpression, String(tag))
}
userTags := make([]model.UserTags, 0)
getTagsStmt := UserTags.SELECT(
UserTags.ID, UserTags.Tag,
).WHERE(UserTags.Tag.IN(userTagsExpression...))
err = getTagsStmt.Query(m.dbPool, &userTags)
if err != nil {
return err
}
stmt := ImageTags.INSERT(ImageTags.ImageID, ImageTags.TagID)
for _, t := range userTags {
stmt = stmt.VALUES(imageId, t.ID)
}
_, err = stmt.ExecContext(ctx, m.dbPool)
return err
}
func NewTagModel(db *sql.DB) TagModel {
return TagModel{dbPool: db}
}

35
backend/models/text.go Normal file
View File

@ -0,0 +1,35 @@
package models
import (
"context"
"database/sql"
. "screenmark/screenmark/.gen/haystack/haystack/table"
"github.com/google/uuid"
)
type TextModel struct {
dbPool *sql.DB
}
func (m TextModel) Save(ctx context.Context, imageId uuid.UUID, texts []string) error {
if len(texts) == 0 {
return nil
}
saveImageTextStmt := ImageText.INSERT(ImageText.ImageID, ImageText.ImageText)
for _, t := range texts {
saveImageTextStmt = saveImageTextStmt.VALUES(imageId, t)
}
saveImageTextStmt.RETURNING(ImageText.AllColumns)
_, err := saveImageTextStmt.ExecContext(ctx, m.dbPool)
return err
}
func NewTextModel(db *sql.DB) TextModel {
return TextModel{dbPool: db}
}

View File

@ -3,6 +3,8 @@ package models
import (
"context"
"database/sql"
"errors"
"log"
"screenmark/screenmark/.gen/haystack/haystack/model"
. "screenmark/screenmark/.gen/haystack/haystack/table"
@ -19,6 +21,79 @@ type ImageWithProperties struct {
ID uuid.UUID
Image model.Image
Tags []struct {
model.ImageTags
Tag model.UserTags
}
Links []model.ImageLinks
Text []model.ImageText
Locations []model.Locations
Events []model.Events
Notes []model.Notes
Contacts []model.Contacts
}
func getUserIdFromImage(ctx context.Context, dbPool *sql.DB, imageId uuid.UUID) (uuid.UUID, error) {
getUserIdStmt := UserImages.SELECT(UserImages.UserID).WHERE(UserImages.ImageID.EQ(UUID(imageId)))
log.Println(getUserIdStmt.DebugSql())
userImages := []model.UserImages{}
err := getUserIdStmt.QueryContext(ctx, dbPool, &userImages)
if err != nil {
return uuid.Nil, err
}
if len(userImages) != 1 {
return uuid.Nil, errors.New("Expected exactly one choice.")
}
return userImages[0].UserID, nil
}
func (m UserModel) ListWithProperties(ctx context.Context, userId uuid.UUID) ([]ImageWithProperties, error) {
listWithPropertiesStmt := SELECT(
UserImages.ID.AS("ImageWithProperties.ID"),
Image.ID,
Image.ImageName,
ImageTags.AllColumns,
UserTags.AllColumns,
ImageText.AllColumns,
ImageLinks.AllColumns,
ImageLocations.AllColumns,
Locations.AllColumns,
ImageEvents.AllColumns,
Events.AllColumns,
ImageContacts.AllColumns,
Contacts.AllColumns,
ImageNotes.AllColumns,
Notes.AllColumns,
).
FROM(
UserImages.INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID)).
LEFT_JOIN(ImageTags, ImageTags.ImageID.EQ(Image.ID)).
LEFT_JOIN(UserTags, UserTags.ID.EQ(ImageTags.TagID)).
LEFT_JOIN(ImageText, ImageText.ImageID.EQ(Image.ID)).
LEFT_JOIN(ImageLinks, ImageLinks.ImageID.EQ(Image.ID)).
LEFT_JOIN(ImageLocations, ImageLocations.ImageID.EQ(UserImages.ImageID)).
LEFT_JOIN(Locations, Locations.ID.EQ(ImageLocations.LocationID)).
LEFT_JOIN(ImageEvents, ImageEvents.ImageID.EQ(UserImages.ImageID)).
LEFT_JOIN(Events, Events.ID.EQ(ImageEvents.EventID)).
LEFT_JOIN(ImageContacts, ImageContacts.ImageID.EQ(UserImages.ImageID)).
LEFT_JOIN(Contacts, Contacts.ID.EQ(ImageContacts.ContactID)).
LEFT_JOIN(ImageNotes, ImageNotes.ImageID.EQ(UserImages.ImageID)).
LEFT_JOIN(Notes, Notes.ID.EQ(ImageNotes.NoteID))).
WHERE(UserImages.UserID.EQ(UUID(userId)))
images := []ImageWithProperties{}
err := listWithPropertiesStmt.QueryContext(ctx, m.dbPool, &images)
if err != nil {
return images, err
}
return images, err
}
func (m UserModel) GetUserIdFromEmail(ctx context.Context, email string) (uuid.UUID, error) {
@ -48,61 +123,6 @@ func (m UserModel) Save(ctx context.Context, user model.Users) (model.Users, err
return insertedUser, err
}
type UserImageWithImage struct {
model.Image
ImageStacks []model.ImageStacks
}
func (m UserModel) GetUserImages(ctx context.Context, userId uuid.UUID) ([]UserImageWithImage, error) {
getUserImagesStmt := SELECT(
Image.AllColumns.Except(Image.Image),
ImageStacks.AllColumns,
).
FROM(
Image.
LEFT_JOIN(ImageStacks, ImageStacks.ImageID.EQ(ImageStacks.ID)),
).
WHERE(Image.UserID.EQ(UUID(userId)))
userImages := []UserImageWithImage{}
err := getUserImagesStmt.QueryContext(ctx, m.dbPool, &userImages)
return userImages, err
}
type ListsWithImages struct {
model.Stacks
SchemaItems []model.SchemaItems
Images []struct {
model.ImageStacks
Items []model.ImageSchemaItems
}
}
func (m UserModel) ListWithImages(ctx context.Context, userId uuid.UUID) ([]ListsWithImages, error) {
stmt := SELECT(
Stacks.AllColumns,
ImageStacks.AllColumns,
SchemaItems.AllColumns,
ImageSchemaItems.AllColumns,
).
FROM(
Stacks.
INNER_JOIN(SchemaItems, SchemaItems.StackID.EQ(Stacks.ID)).
LEFT_JOIN(ImageStacks, ImageStacks.StackID.EQ(Stacks.ID)).
LEFT_JOIN(ImageSchemaItems, ImageSchemaItems.ImageID.EQ(ImageStacks.ID)),
).
WHERE(Stacks.UserID.EQ(UUID(userId)))
lists := []ListsWithImages{}
err := stmt.QueryContext(ctx, m.dbPool, &lists)
return lists, err
}
func NewUserModel(db *sql.DB) UserModel {
return UserModel{dbPool: db}
}

Some files were not shown because too many files have changed in this diff Show More