Compare commits
301 Commits
feat/split
...
95330c163b
Author | SHA1 | Date | |
---|---|---|---|
95330c163b | |||
84a0996be9 | |||
48579267b5 | |||
8b54d502f2 | |||
e45688d57e | |||
f7c9c97f0a | |||
76924a0332 | |||
d97593d487 | |||
de96f12b55 | |||
70161da3ed | |||
3a182fc49b | |||
ec7bd469f9 | |||
6523b10699 | |||
61d2b81e8c | |||
fe0968716d | |||
769f3981cd | |||
a78f766122 | |||
10cea769bf | |||
f5e65524aa | |||
390a216260 | |||
3e57d10360 | |||
28a4b37dde | |||
4de4431390 | |||
5ff7788a7b | |||
13170a33e8 | |||
5024933852 | |||
706d562e3e | |||
fda09ae07a | |||
5de5e0b56e | |||
a0bf27dd16 | |||
3d05ff708e | |||
ee109f05a0 | |||
f4d8c9f083 | |||
a1af3feb1d | |||
8597584cf0 | |||
88d033314e | |||
9cae780431 | |||
fa71f68de5 | |||
0058cdce40 | |||
37f966e508 | |||
59bf884f5d | |||
2ac996db73 | |||
e19e6562bb | |||
a283bc1bcd | |||
1b816e512a | |||
4d0dcccf94 | |||
bc4e4ab36c | |||
a663f27fb2 | |||
744b300d00 | |||
818a163235 | |||
2b1eb2b948 | |||
018f0e96d4 | |||
d5594c6e32 | |||
7d9845737e | |||
68010503ab | |||
251e2bc553 | |||
5d0fa51e01 | |||
ad4967a97d | |||
eb0914c9ca | |||
5c8e0094f4 | |||
4b85cae22c | |||
75b2cc53a4 | |||
bb3ae507ea | |||
0d5e6146f2 | |||
ec18cb0ee0 | |||
510cb3012b | |||
6e2c6acd9d | |||
5130691ab9 | |||
1a9731c4bb | |||
300a4925df | |||
4870a8b1b1 | |||
275ae4598f | |||
eeb6d2bb3b | |||
a89c6dc658 | |||
c6ad67345e | |||
459c8e1c4e | |||
ec4e8b7e2a | |||
5a1f3bb75b | |||
d4b14605c1 | |||
00e530df4b | |||
16b43ec561 | |||
6d235eea36 | |||
27ad03b1c1 | |||
1a845c7846 | |||
3c3a25bcfc | |||
e508f03abb | |||
e74975a52a | |||
a94c7255c6 | |||
a65ef5f548 | |||
be302b77d4 | |||
a4a8c191b6 | |||
6482a76a51 | |||
c62378c20a | |||
5cf0b66688 | |||
357927e2a0 | |||
c632487d7e | |||
cb4a03015a | |||
7f8b345e77 | |||
df9c42136e | |||
019c4c6b0c | |||
ad2a70aaf3 | |||
076e230a01 | |||
636bd9df0e | |||
3eab20049e | |||
64879ac9d6 | |||
e674043daa | |||
71049a7f26 | |||
e8a51ecc52 | |||
a6a6026a11 | |||
a9749f062e | |||
5a2b990c0c | |||
b97eae10a3 | |||
cc07ef983f | |||
b4a0383be7 | |||
a4b94fc6c2 | |||
71dfe5647e | |||
90b863b6cf | |||
64439d9041 | |||
2f3d643278 | |||
8e6424aa63 | |||
c69ca7da5c | |||
7b0c84e88e | |||
ff7960e2dd | |||
d08fd452f9 | |||
0d64e90bbf | |||
a7119dfda4 | |||
c0f6af7a05 | |||
ac92f80dc6 | |||
52cb50b168 | |||
eaa029cce1 | |||
9a7654ae2d | |||
a9ab92b7b3 | |||
378900d1b1 | |||
4fa8bfb7bd | |||
ac4fd30b0a | |||
7d1498c3eb | |||
ce32291437 | |||
33b8d51f89 | |||
a2ba328097 | |||
23d91890f5 | |||
07b83aa728 | |||
9c325c7799 | |||
7970e8670c | |||
2deba39907 | |||
0a766e1ebb | |||
6119938e52 | |||
a8d12b5d53 | |||
ce8d546447 | |||
b57a703812 | |||
6952aa16da | |||
63e3081a69 | |||
b046a928b0 | |||
9860dd2dc5 | |||
94920c01fb | |||
bb280f52fe | |||
92e346578a | |||
e9617f86ec | |||
875d1d778c | |||
372a891f97 | |||
9ea466610b | |||
9fb926db03 | |||
50b8645897 | |||
4541b366e5 | |||
4ed42678f1 | |||
a93fd7500a | |||
b50ca077e0 | |||
12cd338967 | |||
2a838c81f2 | |||
cd39559834 | |||
4c5f3d92e6 | |||
54bb75956c | |||
0a2d27c150 | |||
a05a625516 | |||
7e9b33f625 | |||
9f3a2a473a | |||
61e9258538 | |||
c8d9ae7aff | |||
2eda77827a | |||
afd2e03234 | |||
3a3acc4a1c | |||
d102ab3f6e | |||
9b006836c6 | |||
385a0cd186 | |||
365ef387dd | |||
4922df6682 | |||
a9ecd5818a | |||
84d66a1c3b | |||
d34805030f | |||
78a28dee8d | |||
7f7a2975af | |||
151142fa9b | |||
fa187b3a79 | |||
b27e191e5c | |||
e2a4b85d15 | |||
f1500837e0 | |||
e6c027aca7 | |||
495cd742b0 | |||
8cdb4367c7 | |||
1388383909 | |||
3cd60d4dfc | |||
526044d1e3 | |||
90ea845521 | |||
dcd3bbb4fb | |||
7aef91c5e0 | |||
9245187056 | |||
e84655a181 | |||
9a25d2e839 | |||
f02b22f2fa | |||
6e9dc81e2b | |||
08b4175b73 | |||
fa5d38d796 | |||
fdb607caea | |||
169b95c450 | |||
191ed3db40 | |||
88bb2fafe2 | |||
a859abfc17 | |||
8cad29a661 | |||
a5d74a97a6 | |||
cf71d26f14 | |||
7e31af27f1 | |||
78fe25497b | |||
dc83bdb3fb | |||
f6f31540af | |||
2eb346bb6a | |||
2b022c31cb | |||
c3f4403145 | |||
1d07fa271d | |||
839a1af51b | |||
0324216753 | |||
335d4403f1 | |||
89ba950c5b | |||
2b8e0695c6 | |||
d448a41a9f | |||
a69d4e4d55 | |||
6edc1e2915 | |||
57f1e70c98 | |||
1b1f957e01 | |||
49969b0608 | |||
9b95ffb59e | |||
c9560f6881 | |||
c5535a5b3b | |||
5ab0d13b21 | |||
15289e4965 | |||
181da1f09d | |||
90b90a8185 | |||
fb30eb4ad6 | |||
5454a1cfaf | |||
3716d22eca | |||
6d2f0c6108 | |||
61c158d5b6 | |||
82331c0833 | |||
e42aa75639 | |||
fa486153b4 | |||
aacecfffac | |||
e89a342751 | |||
e16b6f4529 | |||
6ddae3426d | |||
67468bddb6 | |||
10bc0a04a2 | |||
8a57236f04 | |||
b138661991 | |||
6db9bb2ab3 | |||
6ae2458186 | |||
51d36bf15b | |||
ecc2da5f86 | |||
d7ab3f56dc | |||
55aa1e67ba | |||
1f83b721a6 | |||
0596ea2b1e | |||
3c1f6ba40f | |||
0eff145f02 | |||
1fa1db7d1b | |||
a1369719d7 | |||
40ddf737c8 | |||
ad14254ecb | |||
e8d996cec5 | |||
0ed6b4c123 | |||
0bc556f47c | |||
5a530b2e39 | |||
868c8e6409 | |||
30143019d6 | |||
cd5dd347d3 | |||
ab09378fcd | |||
18f85a8929 | |||
55614b34c7 | |||
664918f431 | |||
048fc38032 | |||
2f26b5dfd9 | |||
4f6c198307 | |||
c99d6e4e6b | |||
b97cf63484 | |||
7af536bd9c | |||
5406e79fc8 | |||
0e88f77474 | |||
878a47ffd1 | |||
eba4268718 | |||
4d903f40bf | |||
24bed2aafb | |||
349dcc2275 | |||
dcfed6a746 | |||
91b9e5402e |
38
.cursor/rules/frontend-rules.mdc
Normal file
38
.cursor/rules/frontend-rules.mdc
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
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 user’s 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 Solid’s 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
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "haystack"]
|
||||
path = haystack-arch
|
||||
url = https://aur.archlinux.org/haystack
|
@ -12,7 +12,9 @@ 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"),
|
||||
}
|
||||
|
@ -1,23 +0,0 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Events struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
Name string
|
||||
Description *string
|
||||
StartDateTime *time.Time
|
||||
EndDateTime *time.Time
|
||||
LocationID *uuid.UUID
|
||||
OrganizerID *uuid.UUID
|
||||
}
|
@ -12,7 +12,8 @@ import (
|
||||
)
|
||||
|
||||
type Image struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
ImageName string
|
||||
Image []byte
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
ImageName string
|
||||
Description string
|
||||
Image []byte
|
||||
}
|
||||
|
@ -1,18 +0,0 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ImageContacts struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
ImageID uuid.UUID
|
||||
ContactID uuid.UUID
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ImageEvents struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
EventID uuid.UUID
|
||||
ImageID uuid.UUID
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ImageLinks struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
Link string
|
||||
ImageID uuid.UUID
|
||||
}
|
@ -11,8 +11,8 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ImageNotes struct {
|
||||
type ImageLists struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
ImageID uuid.UUID
|
||||
NoteID uuid.UUID
|
||||
ListID uuid.UUID
|
||||
}
|
@ -11,8 +11,9 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ImageLocations struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
LocationID uuid.UUID
|
||||
ImageID uuid.UUID
|
||||
type ImageSchemaItems struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
Value *string
|
||||
SchemaItemID uuid.UUID
|
||||
ImageID uuid.UUID
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ImageTags struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
TagID uuid.UUID
|
||||
ImageID uuid.UUID
|
||||
}
|
@ -9,12 +9,13 @@ package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Contacts struct {
|
||||
type Lists struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
UserID uuid.UUID
|
||||
Name string
|
||||
Description *string
|
||||
PhoneNumber *string
|
||||
Email *string
|
||||
Description string
|
||||
CreatedAt *time.Time
|
||||
}
|
@ -9,10 +9,11 @@ package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ImageText struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
ImageText string
|
||||
type Logs struct {
|
||||
Log string
|
||||
ImageID uuid.UUID
|
||||
CreatedAt *time.Time
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Notes struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
Name string
|
||||
Description *string
|
||||
Content string
|
||||
}
|
@ -9,10 +9,14 @@ package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserContacts struct {
|
||||
type ProcessingLists struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
UserID uuid.UUID
|
||||
ContactID uuid.UUID
|
||||
Title string
|
||||
Fields string
|
||||
Status Progress
|
||||
CreatedAt *time.Time
|
||||
}
|
@ -14,11 +14,13 @@ 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 {
|
||||
@ -37,6 +39,8 @@ 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")
|
||||
}
|
||||
|
@ -11,9 +11,10 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Locations struct {
|
||||
type SchemaItems struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
Name string
|
||||
Address *string
|
||||
Description *string
|
||||
Item string
|
||||
Value string
|
||||
Description string
|
||||
SchemaID uuid.UUID
|
||||
}
|
@ -11,8 +11,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UserTags struct {
|
||||
type Schemas struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
Tag string
|
||||
UserID uuid.UUID
|
||||
ListID uuid.UUID
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UserEvents struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
EventID uuid.UUID
|
||||
UserID uuid.UUID
|
||||
}
|
@ -9,10 +9,12 @@ package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserImages struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
ImageID uuid.UUID
|
||||
UserID uuid.UUID
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
ImageID uuid.UUID
|
||||
UserID uuid.UUID
|
||||
CreatedAt *time.Time
|
||||
}
|
||||
|
@ -1,18 +0,0 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UserLocations struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
LocationID uuid.UUID
|
||||
UserID uuid.UUID
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UserNotes struct {
|
||||
ID uuid.UUID `sql:"primary_key"`
|
||||
UserID uuid.UUID
|
||||
NoteID uuid.UUID
|
||||
}
|
@ -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 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,
|
||||
}
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var 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,
|
||||
}
|
||||
}
|
@ -17,12 +17,14 @@ type imageTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
ImageName postgres.ColumnString
|
||||
Image postgres.ColumnString
|
||||
ID postgres.ColumnString
|
||||
ImageName postgres.ColumnString
|
||||
Description postgres.ColumnString
|
||||
Image postgres.ColumnBytea
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type ImageTable struct {
|
||||
@ -60,22 +62,26 @@ func newImageTable(schemaName, tableName, alias string) *ImageTable {
|
||||
|
||||
func newImageTableImpl(schemaName, tableName, alias string) imageTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
ImageNameColumn = postgres.StringColumn("image_name")
|
||||
ImageColumn = postgres.StringColumn("image")
|
||||
allColumns = postgres.ColumnList{IDColumn, ImageNameColumn, ImageColumn}
|
||||
mutableColumns = postgres.ColumnList{ImageNameColumn, ImageColumn}
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
ImageNameColumn = postgres.StringColumn("image_name")
|
||||
DescriptionColumn = postgres.StringColumn("description")
|
||||
ImageColumn = postgres.ByteaColumn("image")
|
||||
allColumns = postgres.ColumnList{IDColumn, ImageNameColumn, DescriptionColumn, ImageColumn}
|
||||
mutableColumns = postgres.ColumnList{ImageNameColumn, DescriptionColumn, ImageColumn}
|
||||
defaultColumns = postgres.ColumnList{IDColumn}
|
||||
)
|
||||
|
||||
return imageTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
ImageName: ImageNameColumn,
|
||||
Image: ImageColumn,
|
||||
ID: IDColumn,
|
||||
ImageName: ImageNameColumn,
|
||||
Description: DescriptionColumn,
|
||||
Image: ImageColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
||||
|
@ -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 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,
|
||||
}
|
||||
}
|
@ -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 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,
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var ImageLinks = newImageLinksTable("haystack", "image_links", "")
|
||||
|
||||
type imageLinksTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
Link postgres.ColumnString
|
||||
ImageID postgres.ColumnString
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type ImageLinksTable struct {
|
||||
imageLinksTable
|
||||
|
||||
EXCLUDED imageLinksTable
|
||||
}
|
||||
|
||||
// AS creates new ImageLinksTable with assigned alias
|
||||
func (a ImageLinksTable) AS(alias string) *ImageLinksTable {
|
||||
return newImageLinksTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new ImageLinksTable with assigned schema name
|
||||
func (a ImageLinksTable) FromSchema(schemaName string) *ImageLinksTable {
|
||||
return newImageLinksTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new ImageLinksTable with assigned table prefix
|
||||
func (a ImageLinksTable) WithPrefix(prefix string) *ImageLinksTable {
|
||||
return newImageLinksTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new ImageLinksTable with assigned table suffix
|
||||
func (a ImageLinksTable) WithSuffix(suffix string) *ImageLinksTable {
|
||||
return newImageLinksTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newImageLinksTable(schemaName, tableName, alias string) *ImageLinksTable {
|
||||
return &ImageLinksTable{
|
||||
imageLinksTable: newImageLinksTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newImageLinksTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newImageLinksTableImpl(schemaName, tableName, alias string) imageLinksTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
LinkColumn = postgres.StringColumn("link")
|
||||
ImageIDColumn = postgres.StringColumn("image_id")
|
||||
allColumns = postgres.ColumnList{IDColumn, LinkColumn, ImageIDColumn}
|
||||
mutableColumns = postgres.ColumnList{LinkColumn, ImageIDColumn}
|
||||
)
|
||||
|
||||
return imageLinksTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
Link: LinkColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
84
backend/.gen/haystack/haystack/table/image_lists.go
Normal file
84
backend/.gen/haystack/haystack/table/image_lists.go
Normal file
@ -0,0 +1,84 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var ImageLists = newImageListsTable("haystack", "image_lists", "")
|
||||
|
||||
type imageListsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
ImageID postgres.ColumnString
|
||||
ListID postgres.ColumnString
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type ImageListsTable struct {
|
||||
imageListsTable
|
||||
|
||||
EXCLUDED imageListsTable
|
||||
}
|
||||
|
||||
// AS creates new ImageListsTable with assigned alias
|
||||
func (a ImageListsTable) AS(alias string) *ImageListsTable {
|
||||
return newImageListsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new ImageListsTable with assigned schema name
|
||||
func (a ImageListsTable) FromSchema(schemaName string) *ImageListsTable {
|
||||
return newImageListsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new ImageListsTable with assigned table prefix
|
||||
func (a ImageListsTable) WithPrefix(prefix string) *ImageListsTable {
|
||||
return newImageListsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new ImageListsTable with assigned table suffix
|
||||
func (a ImageListsTable) WithSuffix(suffix string) *ImageListsTable {
|
||||
return newImageListsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newImageListsTable(schemaName, tableName, alias string) *ImageListsTable {
|
||||
return &ImageListsTable{
|
||||
imageListsTable: newImageListsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newImageListsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newImageListsTableImpl(schemaName, tableName, alias string) imageListsTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
ImageIDColumn = postgres.StringColumn("image_id")
|
||||
ListIDColumn = postgres.StringColumn("list_id")
|
||||
allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, ListIDColumn}
|
||||
mutableColumns = postgres.ColumnList{ImageIDColumn, ListIDColumn}
|
||||
defaultColumns = postgres.ColumnList{IDColumn}
|
||||
)
|
||||
|
||||
return imageListsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
ListID: ListIDColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
@ -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 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,
|
||||
}
|
||||
}
|
@ -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 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,
|
||||
}
|
||||
}
|
87
backend/.gen/haystack/haystack/table/image_schema_items.go
Normal file
87
backend/.gen/haystack/haystack/table/image_schema_items.go
Normal file
@ -0,0 +1,87 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var ImageSchemaItems = newImageSchemaItemsTable("haystack", "image_schema_items", "")
|
||||
|
||||
type imageSchemaItemsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
Value postgres.ColumnString
|
||||
SchemaItemID postgres.ColumnString
|
||||
ImageID postgres.ColumnString
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type ImageSchemaItemsTable struct {
|
||||
imageSchemaItemsTable
|
||||
|
||||
EXCLUDED imageSchemaItemsTable
|
||||
}
|
||||
|
||||
// AS creates new ImageSchemaItemsTable with assigned alias
|
||||
func (a ImageSchemaItemsTable) AS(alias string) *ImageSchemaItemsTable {
|
||||
return newImageSchemaItemsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new ImageSchemaItemsTable with assigned schema name
|
||||
func (a ImageSchemaItemsTable) FromSchema(schemaName string) *ImageSchemaItemsTable {
|
||||
return newImageSchemaItemsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new ImageSchemaItemsTable with assigned table prefix
|
||||
func (a ImageSchemaItemsTable) WithPrefix(prefix string) *ImageSchemaItemsTable {
|
||||
return newImageSchemaItemsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new ImageSchemaItemsTable with assigned table suffix
|
||||
func (a ImageSchemaItemsTable) WithSuffix(suffix string) *ImageSchemaItemsTable {
|
||||
return newImageSchemaItemsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newImageSchemaItemsTable(schemaName, tableName, alias string) *ImageSchemaItemsTable {
|
||||
return &ImageSchemaItemsTable{
|
||||
imageSchemaItemsTable: newImageSchemaItemsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newImageSchemaItemsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newImageSchemaItemsTableImpl(schemaName, tableName, alias string) imageSchemaItemsTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
ValueColumn = postgres.StringColumn("value")
|
||||
SchemaItemIDColumn = postgres.StringColumn("schema_item_id")
|
||||
ImageIDColumn = postgres.StringColumn("image_id")
|
||||
allColumns = postgres.ColumnList{IDColumn, ValueColumn, SchemaItemIDColumn, ImageIDColumn}
|
||||
mutableColumns = postgres.ColumnList{ValueColumn, SchemaItemIDColumn, ImageIDColumn}
|
||||
defaultColumns = postgres.ColumnList{IDColumn}
|
||||
)
|
||||
|
||||
return imageSchemaItemsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
Value: ValueColumn,
|
||||
SchemaItemID: SchemaItemIDColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var ImageTags = newImageTagsTable("haystack", "image_tags", "")
|
||||
|
||||
type imageTagsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
TagID postgres.ColumnString
|
||||
ImageID postgres.ColumnString
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type ImageTagsTable struct {
|
||||
imageTagsTable
|
||||
|
||||
EXCLUDED imageTagsTable
|
||||
}
|
||||
|
||||
// AS creates new ImageTagsTable with assigned alias
|
||||
func (a ImageTagsTable) AS(alias string) *ImageTagsTable {
|
||||
return newImageTagsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new ImageTagsTable with assigned schema name
|
||||
func (a ImageTagsTable) FromSchema(schemaName string) *ImageTagsTable {
|
||||
return newImageTagsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new ImageTagsTable with assigned table prefix
|
||||
func (a ImageTagsTable) WithPrefix(prefix string) *ImageTagsTable {
|
||||
return newImageTagsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new ImageTagsTable with assigned table suffix
|
||||
func (a ImageTagsTable) WithSuffix(suffix string) *ImageTagsTable {
|
||||
return newImageTagsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newImageTagsTable(schemaName, tableName, alias string) *ImageTagsTable {
|
||||
return &ImageTagsTable{
|
||||
imageTagsTable: newImageTagsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newImageTagsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newImageTagsTableImpl(schemaName, tableName, alias string) imageTagsTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
TagIDColumn = postgres.StringColumn("tag_id")
|
||||
ImageIDColumn = postgres.StringColumn("image_id")
|
||||
allColumns = postgres.ColumnList{IDColumn, TagIDColumn, ImageIDColumn}
|
||||
mutableColumns = postgres.ColumnList{TagIDColumn, ImageIDColumn}
|
||||
)
|
||||
|
||||
return imageTagsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
TagID: TagIDColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var ImageText = newImageTextTable("haystack", "image_text", "")
|
||||
|
||||
type imageTextTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
ImageText postgres.ColumnString
|
||||
ImageID postgres.ColumnString
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type ImageTextTable struct {
|
||||
imageTextTable
|
||||
|
||||
EXCLUDED imageTextTable
|
||||
}
|
||||
|
||||
// AS creates new ImageTextTable with assigned alias
|
||||
func (a ImageTextTable) AS(alias string) *ImageTextTable {
|
||||
return newImageTextTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new ImageTextTable with assigned schema name
|
||||
func (a ImageTextTable) FromSchema(schemaName string) *ImageTextTable {
|
||||
return newImageTextTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new ImageTextTable with assigned table prefix
|
||||
func (a ImageTextTable) WithPrefix(prefix string) *ImageTextTable {
|
||||
return newImageTextTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new ImageTextTable with assigned table suffix
|
||||
func (a ImageTextTable) WithSuffix(suffix string) *ImageTextTable {
|
||||
return newImageTextTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newImageTextTable(schemaName, tableName, alias string) *ImageTextTable {
|
||||
return &ImageTextTable{
|
||||
imageTextTable: newImageTextTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newImageTextTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newImageTextTableImpl(schemaName, tableName, alias string) imageTextTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
ImageTextColumn = postgres.StringColumn("image_text")
|
||||
ImageIDColumn = postgres.StringColumn("image_id")
|
||||
allColumns = postgres.ColumnList{IDColumn, ImageTextColumn, ImageIDColumn}
|
||||
mutableColumns = postgres.ColumnList{ImageTextColumn, ImageIDColumn}
|
||||
)
|
||||
|
||||
return imageTextTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
ImageText: ImageTextColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
90
backend/.gen/haystack/haystack/table/lists.go
Normal file
90
backend/.gen/haystack/haystack/table/lists.go
Normal file
@ -0,0 +1,90 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var Lists = newListsTable("haystack", "lists", "")
|
||||
|
||||
type listsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
UserID postgres.ColumnString
|
||||
Name postgres.ColumnString
|
||||
Description postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type ListsTable struct {
|
||||
listsTable
|
||||
|
||||
EXCLUDED listsTable
|
||||
}
|
||||
|
||||
// AS creates new ListsTable with assigned alias
|
||||
func (a ListsTable) AS(alias string) *ListsTable {
|
||||
return newListsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new ListsTable with assigned schema name
|
||||
func (a ListsTable) FromSchema(schemaName string) *ListsTable {
|
||||
return newListsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new ListsTable with assigned table prefix
|
||||
func (a ListsTable) WithPrefix(prefix string) *ListsTable {
|
||||
return newListsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new ListsTable with assigned table suffix
|
||||
func (a ListsTable) WithSuffix(suffix string) *ListsTable {
|
||||
return newListsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newListsTable(schemaName, tableName, alias string) *ListsTable {
|
||||
return &ListsTable{
|
||||
listsTable: newListsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newListsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newListsTableImpl(schemaName, tableName, alias string) listsTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
UserIDColumn = postgres.StringColumn("user_id")
|
||||
NameColumn = postgres.StringColumn("name")
|
||||
DescriptionColumn = postgres.StringColumn("description")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, UserIDColumn, NameColumn, DescriptionColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{UserIDColumn, NameColumn, DescriptionColumn, CreatedAtColumn}
|
||||
defaultColumns = postgres.ColumnList{IDColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return listsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
UserID: UserIDColumn,
|
||||
Name: NameColumn,
|
||||
Description: DescriptionColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
@ -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 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,
|
||||
}
|
||||
}
|
84
backend/.gen/haystack/haystack/table/logs.go
Normal file
84
backend/.gen/haystack/haystack/table/logs.go
Normal file
@ -0,0 +1,84 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var Logs = newLogsTable("haystack", "logs", "")
|
||||
|
||||
type logsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
Log postgres.ColumnString
|
||||
ImageID postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type LogsTable struct {
|
||||
logsTable
|
||||
|
||||
EXCLUDED logsTable
|
||||
}
|
||||
|
||||
// AS creates new LogsTable with assigned alias
|
||||
func (a LogsTable) AS(alias string) *LogsTable {
|
||||
return newLogsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new LogsTable with assigned schema name
|
||||
func (a LogsTable) FromSchema(schemaName string) *LogsTable {
|
||||
return newLogsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new LogsTable with assigned table prefix
|
||||
func (a LogsTable) WithPrefix(prefix string) *LogsTable {
|
||||
return newLogsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new LogsTable with assigned table suffix
|
||||
func (a LogsTable) WithSuffix(suffix string) *LogsTable {
|
||||
return newLogsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newLogsTable(schemaName, tableName, alias string) *LogsTable {
|
||||
return &LogsTable{
|
||||
logsTable: newLogsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newLogsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newLogsTableImpl(schemaName, tableName, alias string) logsTable {
|
||||
var (
|
||||
LogColumn = postgres.StringColumn("log")
|
||||
ImageIDColumn = postgres.StringColumn("image_id")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{LogColumn, ImageIDColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{LogColumn, ImageIDColumn, CreatedAtColumn}
|
||||
defaultColumns = postgres.ColumnList{CreatedAtColumn}
|
||||
)
|
||||
|
||||
return logsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
Log: LogColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
@ -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 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,
|
||||
}
|
||||
}
|
93
backend/.gen/haystack/haystack/table/processing_lists.go
Normal file
93
backend/.gen/haystack/haystack/table/processing_lists.go
Normal file
@ -0,0 +1,93 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var ProcessingLists = newProcessingListsTable("haystack", "processing_lists", "")
|
||||
|
||||
type processingListsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
UserID postgres.ColumnString
|
||||
Title postgres.ColumnString
|
||||
Fields postgres.ColumnString
|
||||
Status postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type ProcessingListsTable struct {
|
||||
processingListsTable
|
||||
|
||||
EXCLUDED processingListsTable
|
||||
}
|
||||
|
||||
// AS creates new ProcessingListsTable with assigned alias
|
||||
func (a ProcessingListsTable) AS(alias string) *ProcessingListsTable {
|
||||
return newProcessingListsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new ProcessingListsTable with assigned schema name
|
||||
func (a ProcessingListsTable) FromSchema(schemaName string) *ProcessingListsTable {
|
||||
return newProcessingListsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new ProcessingListsTable with assigned table prefix
|
||||
func (a ProcessingListsTable) WithPrefix(prefix string) *ProcessingListsTable {
|
||||
return newProcessingListsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new ProcessingListsTable with assigned table suffix
|
||||
func (a ProcessingListsTable) WithSuffix(suffix string) *ProcessingListsTable {
|
||||
return newProcessingListsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newProcessingListsTable(schemaName, tableName, alias string) *ProcessingListsTable {
|
||||
return &ProcessingListsTable{
|
||||
processingListsTable: newProcessingListsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newProcessingListsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newProcessingListsTableImpl(schemaName, tableName, alias string) processingListsTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
UserIDColumn = postgres.StringColumn("user_id")
|
||||
TitleColumn = postgres.StringColumn("title")
|
||||
FieldsColumn = postgres.StringColumn("fields")
|
||||
StatusColumn = postgres.StringColumn("status")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, UserIDColumn, TitleColumn, FieldsColumn, StatusColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{UserIDColumn, TitleColumn, FieldsColumn, StatusColumn, CreatedAtColumn}
|
||||
defaultColumns = postgres.ColumnList{IDColumn, StatusColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return processingListsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
UserID: UserIDColumn,
|
||||
Title: TitleColumn,
|
||||
Fields: FieldsColumn,
|
||||
Status: StatusColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
90
backend/.gen/haystack/haystack/table/schema_items.go
Normal file
90
backend/.gen/haystack/haystack/table/schema_items.go
Normal file
@ -0,0 +1,90 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var SchemaItems = newSchemaItemsTable("haystack", "schema_items", "")
|
||||
|
||||
type schemaItemsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
Item postgres.ColumnString
|
||||
Value postgres.ColumnString
|
||||
Description postgres.ColumnString
|
||||
SchemaID postgres.ColumnString
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type SchemaItemsTable struct {
|
||||
schemaItemsTable
|
||||
|
||||
EXCLUDED schemaItemsTable
|
||||
}
|
||||
|
||||
// AS creates new SchemaItemsTable with assigned alias
|
||||
func (a SchemaItemsTable) AS(alias string) *SchemaItemsTable {
|
||||
return newSchemaItemsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new SchemaItemsTable with assigned schema name
|
||||
func (a SchemaItemsTable) FromSchema(schemaName string) *SchemaItemsTable {
|
||||
return newSchemaItemsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new SchemaItemsTable with assigned table prefix
|
||||
func (a SchemaItemsTable) WithPrefix(prefix string) *SchemaItemsTable {
|
||||
return newSchemaItemsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new SchemaItemsTable with assigned table suffix
|
||||
func (a SchemaItemsTable) WithSuffix(suffix string) *SchemaItemsTable {
|
||||
return newSchemaItemsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newSchemaItemsTable(schemaName, tableName, alias string) *SchemaItemsTable {
|
||||
return &SchemaItemsTable{
|
||||
schemaItemsTable: newSchemaItemsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newSchemaItemsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newSchemaItemsTableImpl(schemaName, tableName, alias string) schemaItemsTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
ItemColumn = postgres.StringColumn("item")
|
||||
ValueColumn = postgres.StringColumn("value")
|
||||
DescriptionColumn = postgres.StringColumn("description")
|
||||
SchemaIDColumn = postgres.StringColumn("schema_id")
|
||||
allColumns = postgres.ColumnList{IDColumn, ItemColumn, ValueColumn, DescriptionColumn, SchemaIDColumn}
|
||||
mutableColumns = postgres.ColumnList{ItemColumn, ValueColumn, DescriptionColumn, SchemaIDColumn}
|
||||
defaultColumns = postgres.ColumnList{IDColumn}
|
||||
)
|
||||
|
||||
return schemaItemsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
Item: ItemColumn,
|
||||
Value: ValueColumn,
|
||||
Description: DescriptionColumn,
|
||||
SchemaID: SchemaIDColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
81
backend/.gen/haystack/haystack/table/schemas.go
Normal file
81
backend/.gen/haystack/haystack/table/schemas.go
Normal file
@ -0,0 +1,81 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var Schemas = newSchemasTable("haystack", "schemas", "")
|
||||
|
||||
type schemasTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
ListID postgres.ColumnString
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type SchemasTable struct {
|
||||
schemasTable
|
||||
|
||||
EXCLUDED schemasTable
|
||||
}
|
||||
|
||||
// AS creates new SchemasTable with assigned alias
|
||||
func (a SchemasTable) AS(alias string) *SchemasTable {
|
||||
return newSchemasTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new SchemasTable with assigned schema name
|
||||
func (a SchemasTable) FromSchema(schemaName string) *SchemasTable {
|
||||
return newSchemasTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new SchemasTable with assigned table prefix
|
||||
func (a SchemasTable) WithPrefix(prefix string) *SchemasTable {
|
||||
return newSchemasTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new SchemasTable with assigned table suffix
|
||||
func (a SchemasTable) WithSuffix(suffix string) *SchemasTable {
|
||||
return newSchemasTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newSchemasTable(schemaName, tableName, alias string) *SchemasTable {
|
||||
return &SchemasTable{
|
||||
schemasTable: newSchemasTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newSchemasTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newSchemasTableImpl(schemaName, tableName, alias string) schemasTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
ListIDColumn = postgres.StringColumn("list_id")
|
||||
allColumns = postgres.ColumnList{IDColumn, ListIDColumn}
|
||||
mutableColumns = postgres.ColumnList{ListIDColumn}
|
||||
defaultColumns = postgres.ColumnList{IDColumn}
|
||||
)
|
||||
|
||||
return schemasTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
ListID: ListIDColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
@ -10,24 +10,15 @@ package table
|
||||
// UseSchema sets a new schema name for all generated table SQL builder types. It is recommended to invoke
|
||||
// this method only once at the beginning of the program.
|
||||
func UseSchema(schema string) {
|
||||
Contacts = Contacts.FromSchema(schema)
|
||||
Events = Events.FromSchema(schema)
|
||||
Image = Image.FromSchema(schema)
|
||||
ImageContacts = ImageContacts.FromSchema(schema)
|
||||
ImageEvents = ImageEvents.FromSchema(schema)
|
||||
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)
|
||||
ImageLists = ImageLists.FromSchema(schema)
|
||||
ImageSchemaItems = ImageSchemaItems.FromSchema(schema)
|
||||
Lists = Lists.FromSchema(schema)
|
||||
Logs = Logs.FromSchema(schema)
|
||||
ProcessingLists = ProcessingLists.FromSchema(schema)
|
||||
SchemaItems = SchemaItems.FromSchema(schema)
|
||||
Schemas = Schemas.FromSchema(schema)
|
||||
UserImages = UserImages.FromSchema(schema)
|
||||
UserImagesToProcess = UserImagesToProcess.FromSchema(schema)
|
||||
UserLocations = UserLocations.FromSchema(schema)
|
||||
UserNotes = UserNotes.FromSchema(schema)
|
||||
UserTags = UserTags.FromSchema(schema)
|
||||
Users = Users.FromSchema(schema)
|
||||
}
|
||||
|
@ -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 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,
|
||||
}
|
||||
}
|
@ -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 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,
|
||||
}
|
||||
}
|
@ -17,12 +17,14 @@ type userImagesTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
ImageID postgres.ColumnString
|
||||
UserID postgres.ColumnString
|
||||
ID postgres.ColumnString
|
||||
ImageID postgres.ColumnString
|
||||
UserID postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type UserImagesTable struct {
|
||||
@ -60,22 +62,26 @@ func newUserImagesTable(schemaName, tableName, alias string) *UserImagesTable {
|
||||
|
||||
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}
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
ImageIDColumn = postgres.StringColumn("image_id")
|
||||
UserIDColumn = postgres.StringColumn("user_id")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, UserIDColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{ImageIDColumn, UserIDColumn, CreatedAtColumn}
|
||||
defaultColumns = postgres.ColumnList{IDColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return userImagesTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
UserID: UserIDColumn,
|
||||
ID: IDColumn,
|
||||
ImageID: ImageIDColumn,
|
||||
UserID: UserIDColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ type userImagesToProcessTable struct {
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type UserImagesToProcessTable struct {
|
||||
@ -67,6 +68,7 @@ func newUserImagesToProcessTableImpl(schemaName, tableName, alias string) userIm
|
||||
UserIDColumn = postgres.StringColumn("user_id")
|
||||
allColumns = postgres.ColumnList{IDColumn, StatusColumn, ImageIDColumn, UserIDColumn}
|
||||
mutableColumns = postgres.ColumnList{StatusColumn, ImageIDColumn, UserIDColumn}
|
||||
defaultColumns = postgres.ColumnList{IDColumn, StatusColumn}
|
||||
)
|
||||
|
||||
return userImagesToProcessTable{
|
||||
@ -80,5 +82,6 @@ func newUserImagesToProcessTableImpl(schemaName, tableName, alias string) userIm
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
||||
|
@ -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 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,
|
||||
}
|
||||
}
|
@ -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 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,
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var UserTags = newUserTagsTable("haystack", "user_tags", "")
|
||||
|
||||
type userTagsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ID postgres.ColumnString
|
||||
Tag postgres.ColumnString
|
||||
UserID postgres.ColumnString
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type UserTagsTable struct {
|
||||
userTagsTable
|
||||
|
||||
EXCLUDED userTagsTable
|
||||
}
|
||||
|
||||
// AS creates new UserTagsTable with assigned alias
|
||||
func (a UserTagsTable) AS(alias string) *UserTagsTable {
|
||||
return newUserTagsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new UserTagsTable with assigned schema name
|
||||
func (a UserTagsTable) FromSchema(schemaName string) *UserTagsTable {
|
||||
return newUserTagsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new UserTagsTable with assigned table prefix
|
||||
func (a UserTagsTable) WithPrefix(prefix string) *UserTagsTable {
|
||||
return newUserTagsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new UserTagsTable with assigned table suffix
|
||||
func (a UserTagsTable) WithSuffix(suffix string) *UserTagsTable {
|
||||
return newUserTagsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newUserTagsTable(schemaName, tableName, alias string) *UserTagsTable {
|
||||
return &UserTagsTable{
|
||||
userTagsTable: newUserTagsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newUserTagsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newUserTagsTableImpl(schemaName, tableName, alias string) userTagsTable {
|
||||
var (
|
||||
IDColumn = postgres.StringColumn("id")
|
||||
TagColumn = postgres.StringColumn("tag")
|
||||
UserIDColumn = postgres.StringColumn("user_id")
|
||||
allColumns = postgres.ColumnList{IDColumn, TagColumn, UserIDColumn}
|
||||
mutableColumns = postgres.ColumnList{TagColumn, UserIDColumn}
|
||||
)
|
||||
|
||||
return userTagsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ID: IDColumn,
|
||||
Tag: TagColumn,
|
||||
UserID: UserIDColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@ type usersTable struct {
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type UsersTable struct {
|
||||
@ -63,6 +64,7 @@ func newUsersTableImpl(schemaName, tableName, alias string) usersTable {
|
||||
EmailColumn = postgres.StringColumn("email")
|
||||
allColumns = postgres.ColumnList{IDColumn, EmailColumn}
|
||||
mutableColumns = postgres.ColumnList{EmailColumn}
|
||||
defaultColumns = postgres.ColumnList{IDColumn}
|
||||
)
|
||||
|
||||
return usersTable{
|
||||
@ -74,5 +76,6 @@ func newUsersTableImpl(schemaName, tableName, alias string) usersTable {
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
||||
|
@ -65,8 +65,8 @@ func (m ChatUserMessage) MarshalJSON() ([]byte, error) {
|
||||
})
|
||||
case ArrayMessage:
|
||||
return json.Marshal(&struct {
|
||||
Role UserRole `json:"role"`
|
||||
Content []ImageMessageContent `json:"content"`
|
||||
Role UserRole `json:"role"`
|
||||
Content []MessageContentMessage `json:"content"`
|
||||
}{
|
||||
Role: User,
|
||||
Content: t.Content,
|
||||
@ -121,16 +121,37 @@ func (m SingleMessage) IsSingleMessage() bool {
|
||||
}
|
||||
|
||||
type ArrayMessage struct {
|
||||
Content []ImageMessageContent `json:"content"`
|
||||
Content []MessageContentMessage `json:"content"`
|
||||
}
|
||||
|
||||
func (m ArrayMessage) IsSingleMessage() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type MessageContentMessage interface {
|
||||
IsImageMessage() bool
|
||||
}
|
||||
|
||||
type TextMessageContent struct {
|
||||
TextType string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func (m TextMessageContent) IsImageMessage() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type ImageMessageContent struct {
|
||||
ImageType string `json:"type"`
|
||||
ImageUrl string `json:"image_url"`
|
||||
ImageType string `json:"type"`
|
||||
ImageUrl ImageMessageUrl `json:"image_url"`
|
||||
}
|
||||
|
||||
type ImageMessageUrl struct {
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
func (m ImageMessageContent) IsImageMessage() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type ImageContentUrl struct {
|
||||
@ -144,6 +165,7 @@ type ImageContentUrl struct {
|
||||
type ToolCall struct {
|
||||
Index int `json:"index"`
|
||||
Id string `json:"id"`
|
||||
Type string `json:"type,omitzero"`
|
||||
Function FunctionCall `json:"function"`
|
||||
}
|
||||
|
||||
@ -165,24 +187,52 @@ func (chat *Chat) AddSystem(prompt string) {
|
||||
})
|
||||
}
|
||||
|
||||
func (chat *Chat) AddImage(imageName string, image []byte) error {
|
||||
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")
|
||||
// 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"
|
||||
}
|
||||
|
||||
extension = extension[1:]
|
||||
|
||||
encodedString := base64.StdEncoding.EncodeToString(image)
|
||||
|
||||
messageContent := ArrayMessage{
|
||||
Content: make([]ImageMessageContent, 1),
|
||||
contentLength := 1
|
||||
if query != nil {
|
||||
contentLength += 1
|
||||
}
|
||||
|
||||
messageContent.Content[0] = ImageMessageContent{
|
||||
messageContent := ArrayMessage{
|
||||
Content: make([]MessageContentMessage, contentLength),
|
||||
}
|
||||
|
||||
index := 0
|
||||
|
||||
if query != nil {
|
||||
messageContent.Content[index] = TextMessageContent{
|
||||
TextType: "text",
|
||||
Text: *query,
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
|
||||
messageContent.Content[index] = ImageMessageContent{
|
||||
ImageType: "image_url",
|
||||
ImageUrl: fmt.Sprintf("data:image/%s;base64,%s", extension, encodedString),
|
||||
ImageUrl: ImageMessageUrl{
|
||||
Url: fmt.Sprintf("data:image/%s;base64,%s", extension, encodedString),
|
||||
},
|
||||
}
|
||||
|
||||
arrayMessage := ChatUserMessage{Role: User, MessageContent: messageContent}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -14,7 +15,7 @@ import (
|
||||
|
||||
type ResponseFormat struct {
|
||||
Type string `json:"type"`
|
||||
JsonSchema any `json:"json_schema"`
|
||||
JsonSchema any `json:"json_schema,omitzero"`
|
||||
}
|
||||
|
||||
type AgentRequestBody struct {
|
||||
@ -25,6 +26,8 @@ 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"`
|
||||
@ -73,32 +76,46 @@ type AgentClient struct {
|
||||
|
||||
Log *log.Logger
|
||||
|
||||
Reply string
|
||||
|
||||
Do func(req *http.Request) (*http.Response, error)
|
||||
|
||||
Options CreateAgentClientOptions
|
||||
}
|
||||
|
||||
const OPENAI_API_KEY = "OPENAI_API_KEY"
|
||||
const OPENAI_API_KEY = "REAL_OPEN_AI_KEY"
|
||||
|
||||
func CreateAgentClient(log *log.Logger) (AgentClient, error) {
|
||||
type CreateAgentClientOptions struct {
|
||||
Log *log.Logger
|
||||
SystemPrompt string
|
||||
JsonTools string
|
||||
EndToolCall string
|
||||
Query *string
|
||||
}
|
||||
|
||||
func CreateAgentClient(options CreateAgentClientOptions) AgentClient {
|
||||
apiKey := os.Getenv(OPENAI_API_KEY)
|
||||
|
||||
if len(apiKey) == 0 {
|
||||
return AgentClient{}, errors.New(OPENAI_API_KEY + " was not found.")
|
||||
panic("No api key")
|
||||
}
|
||||
|
||||
return AgentClient{
|
||||
apiKey: apiKey,
|
||||
url: "https://api.mistral.ai/v1/chat/completions",
|
||||
url: "https://router.requesty.ai/v1/chat/completions",
|
||||
Do: func(req *http.Request) (*http.Response, error) {
|
||||
client := &http.Client{}
|
||||
return client.Do(req)
|
||||
},
|
||||
|
||||
Log: log,
|
||||
Log: options.Log,
|
||||
|
||||
ToolHandler: ToolsHandlers{
|
||||
handlers: map[string]ToolHandler{},
|
||||
},
|
||||
}, nil
|
||||
|
||||
Options: options,
|
||||
}
|
||||
}
|
||||
|
||||
func (client AgentClient) getRequest(body []byte) (*http.Request, error) {
|
||||
@ -116,29 +133,29 @@ func (client AgentClient) getRequest(body []byte) (*http.Request, error) {
|
||||
func (client AgentClient) Request(req *AgentRequestBody) (AgentResponse, error) {
|
||||
jsonAiRequest, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return AgentResponse{}, err
|
||||
return AgentResponse{}, fmt.Errorf("Could not format JSON: %w", err)
|
||||
}
|
||||
|
||||
httpRequest, err := client.getRequest(jsonAiRequest)
|
||||
if err != nil {
|
||||
return AgentResponse{}, err
|
||||
return AgentResponse{}, fmt.Errorf("Could not get request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return AgentResponse{}, err
|
||||
return AgentResponse{}, fmt.Errorf("Could not send request: %w", err)
|
||||
}
|
||||
|
||||
response, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return AgentResponse{}, err
|
||||
return AgentResponse{}, fmt.Errorf("Could not read body: %w", err)
|
||||
}
|
||||
|
||||
agentResponse := AgentResponse{}
|
||||
err = json.Unmarshal(response, &agentResponse)
|
||||
|
||||
if err != nil {
|
||||
return AgentResponse{}, err
|
||||
return AgentResponse{}, fmt.Errorf("Could not unmarshal response, response: %s: %w", string(response), err)
|
||||
}
|
||||
|
||||
if len(agentResponse.Choices) != 1 {
|
||||
@ -146,39 +163,32 @@ func (client AgentClient) Request(req *AgentRequestBody) (AgentResponse, error)
|
||||
return AgentResponse{}, errors.New("Unsupported. We currently only accept 1 choice from AI.")
|
||||
}
|
||||
|
||||
client.Log.SetLevel(log.DebugLevel)
|
||||
|
||||
msg := agentResponse.Choices[0].Message
|
||||
|
||||
if len(msg.Content) > 0 {
|
||||
client.Log.Debugf("Content: %s", msg.Content)
|
||||
}
|
||||
|
||||
if msg.ToolCalls != nil && len(*msg.ToolCalls) > 0 {
|
||||
client.Log.Debugf("Tool Call: %s", (*msg.ToolCalls)[0].Function.Name)
|
||||
|
||||
prettyJson, err := json.MarshalIndent((*msg.ToolCalls)[0].Function.Arguments, "", " ")
|
||||
if err != nil {
|
||||
return AgentResponse{}, err
|
||||
}
|
||||
|
||||
client.Log.Debugf("Arguments: %s", string(prettyJson))
|
||||
}
|
||||
|
||||
req.Chat.AddAiResponse(agentResponse.Choices[0].Message)
|
||||
req.Chat.AddAiResponse(msg)
|
||||
|
||||
return agentResponse, nil
|
||||
}
|
||||
|
||||
func (client AgentClient) ToolLoop(info ToolHandlerInfo, req *AgentRequestBody) error {
|
||||
func (client *AgentClient) ToolLoop(info ToolHandlerInfo, req *AgentRequestBody) error {
|
||||
for {
|
||||
err := client.Process(info, req)
|
||||
response, err := client.Request(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = client.Request(req)
|
||||
if response.Choices[0].FinishReason == "stop" {
|
||||
client.Log.Debug("Agent is finished")
|
||||
return nil
|
||||
}
|
||||
|
||||
err = client.Process(info, req)
|
||||
|
||||
if err != nil {
|
||||
|
||||
if err == FinishedCall {
|
||||
client.Log.Debug("Agent is finished")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -186,7 +196,7 @@ func (client AgentClient) ToolLoop(info ToolHandlerInfo, req *AgentRequestBody)
|
||||
|
||||
var FinishedCall = errors.New("Last tool tool was called")
|
||||
|
||||
func (client AgentClient) Process(info ToolHandlerInfo, req *AgentRequestBody) error {
|
||||
func (client *AgentClient) Process(info ToolHandlerInfo, req *AgentRequestBody) error {
|
||||
var err error
|
||||
|
||||
message, err := req.Chat.GetLatest()
|
||||
@ -211,8 +221,11 @@ func (client AgentClient) Process(info ToolHandlerInfo, req *AgentRequestBody) e
|
||||
|
||||
toolResponse := client.ToolHandler.Handle(info, toolCall)
|
||||
|
||||
client.Log.SetLevel(log.DebugLevel)
|
||||
client.Log.Debugf("Response: %s", toolResponse.Content)
|
||||
if toolCall.Function.Name == "reply" {
|
||||
client.Reply = toolCall.Function.Arguments
|
||||
}
|
||||
|
||||
client.Log.Debug("Tool call", "name", toolCall.Function.Name, "arguments", toolCall.Function.Arguments, "response", toolResponse.Content)
|
||||
|
||||
req.Chat.AddToolResponse(toolResponse)
|
||||
}
|
||||
@ -220,18 +233,23 @@ func (client AgentClient) Process(info ToolHandlerInfo, req *AgentRequestBody) e
|
||||
return err
|
||||
}
|
||||
|
||||
func (client AgentClient) RunAgent(systemPrompt string, jsonTools string, endToolCall string, userId uuid.UUID, imageId uuid.UUID, imageName string, imageData []byte) error {
|
||||
func (client *AgentClient) RunAgent(userId uuid.UUID, imageId uuid.UUID, imageName string, imageData []byte) error {
|
||||
var tools any
|
||||
err := json.Unmarshal([]byte(jsonTools), &tools)
|
||||
err := json.Unmarshal([]byte(client.Options.JsonTools), &tools)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
toolChoice := "any"
|
||||
toolChoice := "auto"
|
||||
seed := 42
|
||||
|
||||
request := AgentRequestBody{
|
||||
Tools: &tools,
|
||||
ToolChoice: &toolChoice,
|
||||
Model: "pixtral-12b-2409",
|
||||
Model: "policy/images",
|
||||
RandomSeed: &seed,
|
||||
Temperature: 0.3,
|
||||
EndToolCall: endToolCall,
|
||||
EndToolCall: client.Options.EndToolCall,
|
||||
ResponseFormat: ResponseFormat{
|
||||
Type: "text",
|
||||
},
|
||||
@ -240,17 +258,49 @@ func (client AgentClient) RunAgent(systemPrompt string, jsonTools string, endToo
|
||||
},
|
||||
}
|
||||
|
||||
request.Chat.AddSystem(systemPrompt)
|
||||
request.Chat.AddImage(imageName, imageData)
|
||||
|
||||
_, err = client.Request(&request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Chat.AddSystem(client.Options.SystemPrompt)
|
||||
request.Chat.AddImage(imageName, imageData, client.Options.Query)
|
||||
|
||||
toolHandlerInfo := ToolHandlerInfo{
|
||||
ImageId: imageId,
|
||||
UserId: userId,
|
||||
ImageId: imageId,
|
||||
ImageName: imageName,
|
||||
UserId: userId,
|
||||
Image: &imageData,
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -8,8 +8,12 @@ import (
|
||||
)
|
||||
|
||||
type ToolHandlerInfo struct {
|
||||
UserId uuid.UUID
|
||||
ImageId uuid.UUID
|
||||
UserId uuid.UUID
|
||||
ImageId uuid.UUID
|
||||
ImageName string
|
||||
|
||||
// Pointer because we don't want to copy this around too much.
|
||||
Image *[]byte
|
||||
}
|
||||
|
||||
type ToolHandler struct {
|
||||
|
@ -1,188 +0,0 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/models"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const contactPrompt = `
|
||||
You are an agent that performs actions on contacts and people you find on an image.
|
||||
|
||||
You can use tools to achieve your task.
|
||||
|
||||
You should use listContacts to make sure that you don't create duplicate contacts.
|
||||
|
||||
Call createContact when you see there is a new contact on this image. Do not create duplicate contacts.
|
||||
Or call linkContact when you think this image contains an existing contact.
|
||||
|
||||
Call finish if you dont think theres anything else to do.
|
||||
`
|
||||
|
||||
const contactTools = `
|
||||
[
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "listContacts",
|
||||
"description": "List the users existing contacts",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "createContact",
|
||||
"description": "Creates a new contact",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "the name of the person"
|
||||
},
|
||||
"phoneNumber": {
|
||||
"type": "string"
|
||||
},
|
||||
"address": {
|
||||
"type": "string",
|
||||
"description": "their physical address"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "linkContact",
|
||||
"description": "Links an existing contact with this image",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"contactId": {
|
||||
"type": "string",
|
||||
"description": "The UUID of the existing contact"
|
||||
}
|
||||
},
|
||||
"required": ["contactId"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "finish",
|
||||
"description": "Call when you dont think theres anything to do",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
`
|
||||
|
||||
type ContactAgent struct {
|
||||
client client.AgentClient
|
||||
|
||||
contactModel models.ContactModel
|
||||
}
|
||||
|
||||
type listContactsArguments struct{}
|
||||
type createContactsArguments struct {
|
||||
Name string `json:"name"`
|
||||
PhoneNumber *string `json:"phoneNumber"`
|
||||
Address *string `json:"address"`
|
||||
Email *string `json:"email"`
|
||||
}
|
||||
type linkContactArguments struct {
|
||||
ContactID string `json:"contactId"`
|
||||
}
|
||||
|
||||
func NewContactAgent(contactModel models.ContactModel) (ContactAgent, error) {
|
||||
agentClient, err := client.CreateAgentClient(log.NewWithOptions(os.Stdout, log.Options{
|
||||
ReportTimestamp: true,
|
||||
TimeFormat: time.Kitchen,
|
||||
Prefix: "Contacts 👥",
|
||||
}))
|
||||
if err != nil {
|
||||
return ContactAgent{}, err
|
||||
}
|
||||
|
||||
agent := ContactAgent{
|
||||
client: agentClient,
|
||||
contactModel: contactModel,
|
||||
}
|
||||
|
||||
agentClient.ToolHandler.AddTool("listContacts", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
return agent.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()
|
||||
|
||||
contact, err := agent.contactModel.Save(ctx, info.UserId, model.Contacts{
|
||||
Name: args.Name,
|
||||
PhoneNumber: args.PhoneNumber,
|
||||
Email: args.Email,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return model.Contacts{}, err
|
||||
}
|
||||
|
||||
_, err = agent.contactModel.SaveToImage(ctx, info.ImageId, contact.ID)
|
||||
if err != nil {
|
||||
return model.Contacts{}, err
|
||||
}
|
||||
|
||||
return contact, nil
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("linkContact", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
|
||||
args := linkContactArguments{}
|
||||
err := json.Unmarshal([]byte(_args), &args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
contactUuid, err := uuid.Parse(args.ContactID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = agent.contactModel.SaveToImage(ctx, info.ImageId, contactUuid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "Saved", nil
|
||||
})
|
||||
|
||||
return agent, nil
|
||||
}
|
140
backend/agents/create_list_agent.go
Normal file
140
backend/agents/create_list_agent.go
Normal file
@ -0,0 +1,140 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/models"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const createListAgentPrompt = `
|
||||
You are an agent who's job is to produce a reasonable output for an unstructured input.
|
||||
|
||||
Your job is to create lists for the user, the user will give you a title and some fields they want
|
||||
as part of the list. Your job is to take these fields, adjust their names so they have good names,
|
||||
and add a good description for each one.
|
||||
|
||||
You can add fields if you think they make a lot of sense.
|
||||
You can remove fields if they are not correct, but be sure before you do this.
|
||||
`
|
||||
|
||||
const listJsonSchema = `
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "the title of the list"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "the description of the list"
|
||||
},
|
||||
"fields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the field."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "A description of the field."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"description"
|
||||
]
|
||||
},
|
||||
"description": "An array of field objects."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fields"
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
type createNewListArguments struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
|
||||
Fields []struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
} `json:"fields"`
|
||||
}
|
||||
|
||||
type CreateListAgent struct {
|
||||
client client.AgentClient
|
||||
|
||||
listModel models.ListModel
|
||||
}
|
||||
|
||||
func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, userReq string) error {
|
||||
request := client.AgentRequestBody{
|
||||
Model: "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.
|
||||
})
|
||||
}
|
||||
|
||||
agent.listModel.Save(ctx, userID, createListArgs.Title, createListArgs.Description, schemaItems)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewCreateListAgent(log *log.Logger, listModel models.ListModel) CreateListAgent {
|
||||
client := client.CreateAgentClient(client.CreateAgentClientOptions{
|
||||
SystemPrompt: createListAgentPrompt,
|
||||
Log: log,
|
||||
})
|
||||
|
||||
agent := CreateListAgent{
|
||||
client,
|
||||
listModel,
|
||||
}
|
||||
|
||||
return agent
|
||||
}
|
74
backend/agents/description_agent.go
Normal file
74
backend/agents/description_agent.go
Normal file
@ -0,0 +1,74 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/models"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const noteAgentPrompt = `
|
||||
You are an AI agent who's job is to describe the image you see.
|
||||
|
||||
You should also add any text you see in the image, if no text exists, just add a description.
|
||||
Be consise and don't add too much extra information or formatting characters, simple text.
|
||||
|
||||
You must write this text in Markdown. You can add extra information for the user.
|
||||
You must organise this text nicely, not be all over the place.
|
||||
`
|
||||
|
||||
type DescriptionAgent struct {
|
||||
client client.AgentClient
|
||||
|
||||
imageModel models.ImageModel
|
||||
}
|
||||
|
||||
func (agent DescriptionAgent) Describe(log *log.Logger, imageId uuid.UUID, imageName string, imageData []byte) error {
|
||||
request := client.AgentRequestBody{
|
||||
Model: "policy/images",
|
||||
Temperature: 0.3,
|
||||
ResponseFormat: client.ResponseFormat{
|
||||
Type: "text",
|
||||
},
|
||||
Chat: &client.Chat{
|
||||
Messages: make([]client.ChatMessage, 0),
|
||||
},
|
||||
}
|
||||
|
||||
request.Chat.AddSystem(noteAgentPrompt)
|
||||
request.Chat.AddImage(imageName, imageData, nil)
|
||||
|
||||
log.Debug("Sending description request")
|
||||
resp, err := agent.client.Request(&request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not request. %s", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
markdown := resp.Choices[0].Message.Content
|
||||
|
||||
err = agent.imageModel.AddDescription(ctx, imageId, markdown)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewDescriptionAgent(log *log.Logger, imageModel models.ImageModel) DescriptionAgent {
|
||||
client := client.CreateAgentClient(client.CreateAgentClientOptions{
|
||||
SystemPrompt: noteAgentPrompt,
|
||||
Log: log,
|
||||
})
|
||||
|
||||
agent := DescriptionAgent{
|
||||
client: client,
|
||||
imageModel: imageModel,
|
||||
}
|
||||
|
||||
return agent
|
||||
}
|
@ -1,200 +0,0 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/models"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const eventPrompt = `
|
||||
You are an agent.
|
||||
|
||||
The user will send you images and you have to identify if they have any events or a place.
|
||||
This could be a friend suggesting to meet, a conference, or anything that looks like an event.
|
||||
|
||||
There are various tools you can use to perform this task.
|
||||
|
||||
listEvents
|
||||
Lists the users already existing events, you should do this before using createEvents to avoid creating duplicates.
|
||||
|
||||
createEvent
|
||||
Use this to create a new events.
|
||||
|
||||
linkEvent
|
||||
Links an image to a events.
|
||||
|
||||
finish
|
||||
Call when there is nothing else to do.
|
||||
`
|
||||
|
||||
const eventTools = `
|
||||
[
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "listEvents",
|
||||
"description": "List the events the user already has.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "createEvent",
|
||||
"description": "Use to create a new events",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"startDateTime": {
|
||||
"type": "string",
|
||||
"description": "The start time as an ISO string"
|
||||
},
|
||||
"endDateTime": {
|
||||
"type": "string",
|
||||
"description": "The end time as an ISO string"
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "linkEvent",
|
||||
"description": "Use to link an already existing events to the image you were sent",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"eventId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["eventsId"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "finish",
|
||||
"description": "Call this when there is nothing left to do.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]`
|
||||
|
||||
type EventAgent struct {
|
||||
client client.AgentClient
|
||||
|
||||
eventsModel models.EventModel
|
||||
}
|
||||
|
||||
type listEventArguments struct{}
|
||||
type createEventArguments struct {
|
||||
Name string `json:"name"`
|
||||
StartDateTime *string `json:"startDateTime"`
|
||||
EndDateTime *string `json:"endDateTime"`
|
||||
OrganizerName *string `json:"organizerName"`
|
||||
}
|
||||
type linkEventArguments struct {
|
||||
EventID string `json:"eventId"`
|
||||
}
|
||||
|
||||
func NewEventAgent(eventsModel models.EventModel) (EventAgent, error) {
|
||||
agentClient, err := client.CreateAgentClient(log.NewWithOptions(os.Stdout, log.Options{
|
||||
ReportTimestamp: true,
|
||||
TimeFormat: time.Kitchen,
|
||||
Prefix: "Events 📍",
|
||||
}))
|
||||
|
||||
if err != nil {
|
||||
return EventAgent{}, err
|
||||
}
|
||||
|
||||
agent := EventAgent{
|
||||
client: agentClient,
|
||||
eventsModel: eventsModel,
|
||||
}
|
||||
|
||||
agentClient.ToolHandler.AddTool("listEvents", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
return agent.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
|
||||
}
|
||||
|
||||
events, err := agent.eventsModel.Save(ctx, info.UserId, model.Events{
|
||||
Name: args.Name,
|
||||
StartDateTime: &startTime,
|
||||
EndDateTime: &endTime,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return model.Events{}, err
|
||||
}
|
||||
|
||||
_, err = agent.eventsModel.SaveToImage(ctx, info.ImageId, events.ID)
|
||||
if err != nil {
|
||||
return model.Events{}, err
|
||||
}
|
||||
|
||||
return events, nil
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("linkEvent", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
|
||||
args := linkEventArguments{}
|
||||
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
|
||||
}
|
||||
|
||||
agent.eventsModel.SaveToImage(ctx, info.ImageId, contactUuid)
|
||||
return "Saved", nil
|
||||
})
|
||||
|
||||
return agent, nil
|
||||
}
|
245
backend/agents/list_agent.go
Normal file
245
backend/agents/list_agent.go
Normal file
@ -0,0 +1,245 @@
|
||||
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, listModel models.ListModel, 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 := listModel.Save(ctx, info.UserId, args.Name, args.Desription, args.Schema)
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return savedList, nil
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("listLists", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
return listModel.List(context.Background(), info.UserId)
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("addToList", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
|
||||
args := addToListArguments{}
|
||||
err := json.Unmarshal([]byte(_args), &args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
listUuid, err := uuid.Parse(args.ListID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := listModel.SaveInto(ctx, listUuid, info.ImageId, args.Schema); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "Saved", nil
|
||||
})
|
||||
|
||||
return agentClient
|
||||
}
|
@ -1,179 +0,0 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/models"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const locationPrompt = `
|
||||
You are an agent.
|
||||
|
||||
The user will send you images and you have to identify if they have any location or a place. This could a picture of a real place, an address, or it's name.
|
||||
|
||||
There are various tools you can use to perform this task.
|
||||
|
||||
listLocations
|
||||
Lists the users already existing locations, you should do this before using createLocation to avoid creating duplicates.
|
||||
|
||||
createLocation
|
||||
Use this to create a new location. Avoid making duplicates and only create a new location if listLocations doesnt contain the location on the image.
|
||||
|
||||
linkLocation
|
||||
Links an image to a location.
|
||||
|
||||
finish
|
||||
Call when there is nothing else to do.
|
||||
`
|
||||
|
||||
const locationTools = `
|
||||
[
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "listLocations",
|
||||
"description": "List the locations the user already has.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "createLocation",
|
||||
"description": "Use to create a new location",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"address": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "linkLocation",
|
||||
"description": "Use to link an already existing location to the image you were sent",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"locationId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["locationId"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "finish",
|
||||
"description": "Call this when there is nothing left to do.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]`
|
||||
|
||||
type LocationAgent struct {
|
||||
client client.AgentClient
|
||||
|
||||
locationModel models.LocationModel
|
||||
}
|
||||
|
||||
type listLocationArguments struct{}
|
||||
type createLocationArguments struct {
|
||||
Name string `json:"name"`
|
||||
Address *string `json:"address"`
|
||||
}
|
||||
type linkLocationArguments struct {
|
||||
LocationID string `json:"locationId"`
|
||||
}
|
||||
|
||||
func NewLocationAgent(locationModel models.LocationModel) (LocationAgent, error) {
|
||||
agentClient, err := client.CreateAgentClient(log.NewWithOptions(os.Stdout, log.Options{
|
||||
ReportTimestamp: true,
|
||||
TimeFormat: time.Kitchen,
|
||||
Prefix: "Locations 📍",
|
||||
}))
|
||||
|
||||
if err != nil {
|
||||
return LocationAgent{}, err
|
||||
}
|
||||
|
||||
agent := LocationAgent{
|
||||
client: agentClient,
|
||||
locationModel: locationModel,
|
||||
}
|
||||
|
||||
agentClient.ToolHandler.AddTool("listLocations", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
return agent.locationModel.List(context.Background(), info.UserId)
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("createLocation", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
|
||||
args := createLocationArguments{}
|
||||
err := json.Unmarshal([]byte(_args), &args)
|
||||
if err != nil {
|
||||
return model.Locations{}, err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
location, err := agent.locationModel.Save(ctx, info.UserId, model.Locations{
|
||||
Name: args.Name,
|
||||
Address: args.Address,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return model.Locations{}, err
|
||||
}
|
||||
|
||||
_, err = agent.locationModel.SaveToImage(ctx, info.ImageId, location.ID)
|
||||
if err != nil {
|
||||
return model.Locations{}, err
|
||||
}
|
||||
|
||||
return location, nil
|
||||
})
|
||||
|
||||
agentClient.ToolHandler.AddTool("linkLocation", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
|
||||
args := linkLocationArguments{}
|
||||
err := json.Unmarshal([]byte(_args), &args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
contactUuid, err := uuid.Parse(args.LocationID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
agent.locationModel.SaveToImage(ctx, info.ImageId, contactUuid)
|
||||
return "Saved", nil
|
||||
})
|
||||
|
||||
return agent, nil
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/models"
|
||||
"time"
|
||||
|
||||
"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)
|
||||
|
||||
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(noteModel models.NoteModel) (NoteAgent, error) {
|
||||
client, err := client.CreateAgentClient(log.NewWithOptions(os.Stdout, log.Options{
|
||||
ReportTimestamp: true,
|
||||
TimeFormat: time.Kitchen,
|
||||
Prefix: "Notes 📝",
|
||||
}))
|
||||
if err != nil {
|
||||
return NoteAgent{}, err
|
||||
}
|
||||
|
||||
agent := NoteAgent{
|
||||
client: client,
|
||||
noteModel: noteModel,
|
||||
}
|
||||
|
||||
return agent, nil
|
||||
}
|
@ -1,171 +0,0 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
const OrchestratorPrompt = `
|
||||
You are an Orchestrator for various AI agents.
|
||||
|
||||
The user will send you images and you have to determine which agents you have to call, in order to best help the user.
|
||||
|
||||
You might decide no agent needs to be called.
|
||||
|
||||
The agents are available as tool calls.
|
||||
|
||||
Agents available:
|
||||
|
||||
noteAgent
|
||||
Use when there is ANY text on the image.
|
||||
|
||||
contactAgent
|
||||
Use it when the image contains information relating a person.
|
||||
|
||||
locationAgent
|
||||
Use it when the image contains some address or a place.
|
||||
|
||||
eventAgent
|
||||
Use it when the image contains an event, this can be a date, a message suggesting an event.
|
||||
|
||||
noAction
|
||||
When you think there is no more information to extract from the image.
|
||||
|
||||
Always call agents in parallel if you need to call more than 1.
|
||||
|
||||
Do not call the agent if you do not think it is relevant for the image.
|
||||
`
|
||||
|
||||
const OrchestratorTools = `
|
||||
[
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "noteAgent",
|
||||
"description": "Use when there is any text on the image, this can be code/text/formulas any writing",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "contactAgent",
|
||||
"description": "Use when then image contains some person or contact",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "locationAgent",
|
||||
"description": "Use when then image contains some place, location or address",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "eventAgent",
|
||||
"description": "Use when then image contains some event",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "noAction",
|
||||
"description": "Use when you are sure nothing can be done about this image anymore",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
}
|
||||
]`
|
||||
|
||||
type OrchestratorAgent struct {
|
||||
Client client.AgentClient
|
||||
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
Ok bool `json:"ok"`
|
||||
}
|
||||
|
||||
func NewOrchestratorAgent(noteAgent NoteAgent, contactAgent ContactAgent, locationAgent LocationAgent, eventAgent EventAgent, imageName string, imageData []byte) (OrchestratorAgent, error) {
|
||||
agent, err := client.CreateAgentClient(log.NewWithOptions(os.Stdout, log.Options{
|
||||
ReportTimestamp: true,
|
||||
TimeFormat: time.Kitchen,
|
||||
Prefix: "Orchestrator 🎼",
|
||||
}))
|
||||
|
||||
if err != nil {
|
||||
return OrchestratorAgent{}, err
|
||||
}
|
||||
|
||||
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 Status{
|
||||
Ok: true,
|
||||
}, nil
|
||||
})
|
||||
|
||||
agent.ToolHandler.AddTool("contactAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
go contactAgent.client.RunAgent(contactPrompt, contactTools, "finish", info.UserId, info.ImageId, imageName, imageData)
|
||||
|
||||
return Status{
|
||||
Ok: true,
|
||||
}, nil
|
||||
})
|
||||
|
||||
agent.ToolHandler.AddTool("locationAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
go locationAgent.client.RunAgent(locationPrompt, locationTools, "finish", info.UserId, info.ImageId, imageName, imageData)
|
||||
|
||||
return Status{
|
||||
Ok: true,
|
||||
}, nil
|
||||
})
|
||||
|
||||
agent.ToolHandler.AddTool("eventAgent", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
go eventAgent.client.RunAgent(eventPrompt, eventTools, "finish", info.UserId, info.ImageId, imageName, imageData)
|
||||
|
||||
return Status{
|
||||
Ok: true,
|
||||
}, nil
|
||||
})
|
||||
|
||||
agent.ToolHandler.AddTool("noAction", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||
// To nothing
|
||||
|
||||
return Status{
|
||||
Ok: true,
|
||||
}, errors.New("Finished! Kinda bad return type but...")
|
||||
})
|
||||
|
||||
return OrchestratorAgent{
|
||||
Client: agent,
|
||||
}, nil
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
package main
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
@ -18,7 +17,7 @@ type Auth struct {
|
||||
mailer Mailer
|
||||
}
|
||||
|
||||
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz")
|
||||
var letterRunes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
|
||||
func randString(n int) string {
|
||||
b := make([]rune, n)
|
||||
@ -44,7 +43,6 @@ func (a *Auth) CreateCode(email string) error {
|
||||
}
|
||||
|
||||
func (a *Auth) IsCodeValid(email string, code string) bool {
|
||||
fmt.Println(a.codes)
|
||||
existingCode, exists := a.codes[email]
|
||||
if !exists {
|
||||
return false
|
||||
@ -55,7 +53,6 @@ func (a *Auth) IsCodeValid(email string, code string) bool {
|
||||
|
||||
func (a *Auth) UseCode(email string, code string) error {
|
||||
if valid := a.IsCodeValid(email, code); !valid {
|
||||
fmt.Println("returning error?")
|
||||
return errors.New("This code is invalid.")
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
@ -1,9 +1,9 @@
|
||||
package main
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/wneessen/go-mail"
|
||||
)
|
||||
|
||||
@ -11,7 +11,9 @@ type MailClient struct {
|
||||
client *mail.Client
|
||||
}
|
||||
|
||||
type TestMailClient struct{}
|
||||
type TestMailClient struct {
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
type Mailer interface {
|
||||
SendCode(to string, code string) error
|
||||
@ -43,15 +45,17 @@ func (m MailClient) SendCode(to string, code string) error {
|
||||
}
|
||||
|
||||
func (m TestMailClient) SendCode(to string, code string) error {
|
||||
fmt.Printf("Email: %s | Code %s\n", to, code)
|
||||
m.logger.Info("Auth Code", "email", to, "code", code)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateMailClient() (Mailer, error) {
|
||||
func CreateMailClient(log *log.Logger) (Mailer, error) {
|
||||
mode := os.Getenv("MODE")
|
||||
if mode == "DEV" {
|
||||
return TestMailClient{}, nil
|
||||
return TestMailClient{
|
||||
log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
client, err := mail.NewClient(
|
106
backend/auth/handler.go
Normal file
106
backend/auth/handler.go
Normal file
@ -0,0 +1,106 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"os"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/middleware"
|
||||
"screenmark/screenmark/models"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
logger *log.Logger
|
||||
|
||||
user models.UserModel
|
||||
|
||||
auth Auth
|
||||
}
|
||||
|
||||
type loginBody struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
type codeBody struct {
|
||||
Email string `json:"email"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
type codeReturn struct {
|
||||
Access string `json:"access"`
|
||||
Refresh string `json:"refresh"`
|
||||
}
|
||||
|
||||
func (h *AuthHandler) login(body loginBody, w http.ResponseWriter, r *http.Request) {
|
||||
err := h.auth.CreateCode(body.Email)
|
||||
if err != nil {
|
||||
middleware.WriteErrorInternal(h.logger, "could not create a code", w)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) code(body codeBody, w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.auth.UseCode(body.Email, body.Code); err != nil {
|
||||
middleware.WriteErrorBadRequest(h.logger, "email or code are incorrect", w)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: we should only keep emails around for a little bit.
|
||||
// Time to first login should be less than 10 minutes.
|
||||
// So actually, they shouldn't be written to our database.
|
||||
if exists := h.user.DoesUserExist(r.Context(), body.Email); !exists {
|
||||
h.user.Save(r.Context(), model.Users{
|
||||
Email: body.Email,
|
||||
})
|
||||
}
|
||||
|
||||
uuid, err := h.user.GetUserIdFromEmail(r.Context(), body.Email)
|
||||
if err != nil {
|
||||
middleware.WriteErrorBadRequest(h.logger, "failed to get user", w)
|
||||
return
|
||||
}
|
||||
|
||||
refresh := middleware.CreateRefreshToken(uuid)
|
||||
access := middleware.CreateAccessToken(uuid)
|
||||
|
||||
codeReturn := codeReturn{
|
||||
Access: access,
|
||||
Refresh: refresh,
|
||||
}
|
||||
|
||||
middleware.WriteJsonOrError(h.logger, codeReturn, w)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) CreateRoutes(r chi.Router) {
|
||||
h.logger.Info("Mounting auth router")
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.SetJson)
|
||||
|
||||
r.Post("/login", middleware.WithValidatedPost(h.login))
|
||||
r.Post("/code", middleware.WithValidatedPost(h.code))
|
||||
})
|
||||
}
|
||||
|
||||
func CreateAuthHandler(db *sql.DB) AuthHandler {
|
||||
userModel := models.NewUserModel(db)
|
||||
logger := log.New(os.Stdout).WithPrefix("Auth")
|
||||
|
||||
mailer, err := CreateMailClient(logger)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
auth := CreateAuth(mailer)
|
||||
|
||||
return AuthHandler{
|
||||
logger,
|
||||
userModel,
|
||||
auth,
|
||||
}
|
||||
}
|
@ -3,18 +3,80 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"screenmark/screenmark/agents"
|
||||
"screenmark/screenmark/limits"
|
||||
"screenmark/screenmark/middleware"
|
||||
"screenmark/screenmark/models"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
func ListenNewImageEvents(db *sql.DB, eventManager *EventManager) {
|
||||
const (
|
||||
IMAGE_TYPE = "image"
|
||||
LIST_TYPE = "list"
|
||||
)
|
||||
|
||||
type imageNotification struct {
|
||||
Type string
|
||||
|
||||
ImageID uuid.UUID
|
||||
ImageName string
|
||||
|
||||
Status string
|
||||
}
|
||||
|
||||
type listNotification struct {
|
||||
Type string
|
||||
|
||||
ListID uuid.UUID
|
||||
Name string
|
||||
|
||||
Status string
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
image *imageNotification
|
||||
list *listNotification
|
||||
}
|
||||
|
||||
func getImageNotification(image imageNotification) Notification {
|
||||
return Notification{
|
||||
image: &image,
|
||||
}
|
||||
}
|
||||
|
||||
func getListNotification(list listNotification) Notification {
|
||||
return Notification{
|
||||
list: &list,
|
||||
}
|
||||
}
|
||||
|
||||
func (n Notification) MarshalJSON() ([]byte, error) {
|
||||
if n.image != nil {
|
||||
return json.Marshal(n.image)
|
||||
}
|
||||
|
||||
if n.list != nil {
|
||||
return json.Marshal(n.list)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no image or list present")
|
||||
}
|
||||
|
||||
func (n *Notification) UnmarshalJSON(data []byte) error {
|
||||
return fmt.Errorf("unimplemented")
|
||||
}
|
||||
|
||||
func ListenNewImageEvents(db *sql.DB) {
|
||||
listener := pq.NewListener(os.Getenv("DB_CONNECTION"), time.Second, time.Second, func(event pq.ListenerEventType, err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -22,89 +84,72 @@ func ListenNewImageEvents(db *sql.DB, eventManager *EventManager) {
|
||||
})
|
||||
defer listener.Close()
|
||||
|
||||
locationModel := models.NewLocationModel(db)
|
||||
eventModel := models.NewEventModel(db)
|
||||
noteModel := models.NewNoteModel(db)
|
||||
imageModel := models.NewImageModel(db)
|
||||
contactModel := models.NewContactModel(db)
|
||||
listModel := models.NewListModel(db)
|
||||
|
||||
limits := limits.CreateLimitsManager(db)
|
||||
|
||||
databaseEventLog := createLogger("Database Events 🤖", os.Stdout)
|
||||
databaseEventLog.SetLevel(log.DebugLevel)
|
||||
|
||||
err := listener.Listen("new_image")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case parameters := <-listener.Notify:
|
||||
imageId := uuid.MustParse(parameters.Extra)
|
||||
eventManager.listeners[parameters.Extra] = make(chan string)
|
||||
for parameters := range listener.Notify {
|
||||
imageId := uuid.MustParse(parameters.Extra)
|
||||
|
||||
ctx := context.Background()
|
||||
databaseEventLog.Debug("Starting processing image", "ImageID", imageId)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
go func() {
|
||||
image, err := imageModel.GetToProcessWithData(ctx, imageId)
|
||||
if err != nil {
|
||||
databaseEventLog.Error("Failed to GetToProcessWithData", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
splitWriter := createDbStdoutWriter(db, image.ImageID)
|
||||
|
||||
if err := imageModel.StartProcessing(ctx, image.ID); err != nil {
|
||||
databaseEventLog.Error("Failed to FinishProcessing", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
descriptionAgent := agents.NewDescriptionAgent(createLogger("Description 📝", splitWriter), imageModel)
|
||||
listAgent := agents.NewListAgent(createLogger("Lists 🖋️", splitWriter), listModel, limits)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
go func() {
|
||||
noteAgent, err := agents.NewNoteAgent(noteModel)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer wg.Done()
|
||||
|
||||
contactAgent, err := agents.NewContactAgent(contactModel)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
locationAgent, err := agents.NewLocationAgent(locationModel)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
eventAgent, err := agents.NewEventAgent(eventModel)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
image, err := imageModel.GetToProcessWithData(ctx, imageId)
|
||||
if err != nil {
|
||||
log.Println("Failed to GetToProcessWithData")
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := imageModel.StartProcessing(ctx, image.ID); err != nil {
|
||||
log.Println("Failed to FinishProcessing")
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
orchestrator, err := agents.NewOrchestratorAgent(noteAgent, contactAgent, locationAgent, eventAgent, image.Image.ImageName, image.Image.Image)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Still need to find some way to hide this complexity away.
|
||||
// I don't think wrapping agents in structs actually works too well.
|
||||
err = orchestrator.Client.RunAgent(agents.OrchestratorPrompt, agents.OrchestratorTools, "noAction", image.UserID, image.ImageID, image.Image.ImageName, image.Image.Image)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
imageModel.FinishProcessing(ctx, image.ID)
|
||||
descriptionAgent.Describe(createLogger("Description 📓", splitWriter), image.Image.ID, image.Image.ImageName, image.Image.Image)
|
||||
}()
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
listAgent.RunAgent(image.UserID, image.ImageID, image.Image.ImageName, image.Image.Image)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
_, err = imageModel.FinishProcessing(ctx, image.ID)
|
||||
if err != nil {
|
||||
databaseEventLog.Error("Failed to finish processing", "ImageID", imageId, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
databaseEventLog.Debug("Finished 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) {
|
||||
func ListenProcessingImageStatus(db *sql.DB, images models.ImageModel, notifier *Notifier[Notification]) {
|
||||
listener := pq.NewListener(os.Getenv("DB_CONNECTION"), time.Second, time.Second, func(event pq.ListenerEventType, err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -112,28 +157,209 @@ func ListenProcessingImageStatus(db *sql.DB, eventManager *EventManager) {
|
||||
})
|
||||
defer listener.Close()
|
||||
|
||||
logger := createLogger("Image Status 📊", os.Stdout)
|
||||
|
||||
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:]
|
||||
for data := range listener.Notify {
|
||||
imageStringUuid := data.Extra[0:36]
|
||||
status := data.Extra[36:]
|
||||
|
||||
fmt.Printf("UUID: %s\n", stringUuid)
|
||||
fmt.Printf("Receiving :s\n", data.Extra)
|
||||
imageUuid, err := uuid.Parse(imageStringUuid)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
imageListener, exists := eventManager.listeners[stringUuid]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
processingImage, err := images.GetToProcess(context.Background(), imageUuid)
|
||||
if err != nil {
|
||||
logger.Error("GetToProcess failed", "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
imageListener <- status
|
||||
logger.Info("Update", "id", imageStringUuid, "status", status)
|
||||
|
||||
close(imageListener)
|
||||
delete(eventManager.listeners, stringUuid)
|
||||
notification := getImageNotification(imageNotification{
|
||||
Type: IMAGE_TYPE,
|
||||
ImageID: processingImage.ImageID,
|
||||
ImageName: processingImage.Image.ImageName,
|
||||
Status: status,
|
||||
})
|
||||
|
||||
if err := notifier.SendAndCreate(processingImage.UserID.String(), notification); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ListenNewStackEvents(db *sql.DB) {
|
||||
listener := pq.NewListener(os.Getenv("DB_CONNECTION"), time.Second, time.Second, func(event pq.ListenerEventType, err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
defer listener.Close()
|
||||
|
||||
stackModel := models.NewListModel(db)
|
||||
|
||||
newStacksLogger := createLogger("New Stacks 🤖", os.Stdout)
|
||||
newStacksLogger.SetLevel(log.DebugLevel)
|
||||
|
||||
err := listener.Listen("new_stack")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for parameters := range listener.Notify {
|
||||
stackID := uuid.MustParse(parameters.Extra)
|
||||
|
||||
newStacksLogger.Debug("Starting processing stack", "StackID", stackID)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
go func() {
|
||||
stack, err := stackModel.GetProcessing(ctx, stackID)
|
||||
if err != nil {
|
||||
newStacksLogger.Error("failed to get processing", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := stackModel.StartProcessing(ctx, stackID); err != nil {
|
||||
newStacksLogger.Error("failed to start processing", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
listAgent := agents.NewCreateListAgent(newStacksLogger, stackModel)
|
||||
userListRequest := fmt.Sprintf("title=%s,fields=%s", stack.Title, stack.Fields)
|
||||
|
||||
err = listAgent.CreateList(newStacksLogger, stack.UserID, userListRequest)
|
||||
if err != nil {
|
||||
newStacksLogger.Error("running agent", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := stackModel.EndProcessing(ctx, stackID); err != nil {
|
||||
newStacksLogger.Error("failed to finish processing", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
newStacksLogger.Debug("Finished processing stack", "StackID", stackID)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func ListenProcessingStackStatus(db *sql.DB, stacks models.ListModel, notifier *Notifier[Notification]) {
|
||||
listener := pq.NewListener(os.Getenv("DB_CONNECTION"), time.Second, time.Second, func(event pq.ListenerEventType, err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
defer listener.Close()
|
||||
|
||||
logger := createLogger("Stack Status 📊", os.Stdout)
|
||||
|
||||
if err := listener.Listen("new_processing_stack_status"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for data := range listener.Notify {
|
||||
stackStringUUID := data.Extra[0:36]
|
||||
status := data.Extra[36:]
|
||||
|
||||
stackUUID, err := uuid.Parse(stackStringUUID)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
processingStack, err := stacks.GetToProcess(context.Background(), stackUUID)
|
||||
if err != nil {
|
||||
logger.Error("GetToProcess failed", "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info("Update", "id", stackStringUUID, "status", status)
|
||||
|
||||
notification := getListNotification(listNotification{
|
||||
Type: LIST_TYPE,
|
||||
Name: processingStack.Title,
|
||||
ListID: stackUUID,
|
||||
Status: status,
|
||||
})
|
||||
|
||||
if err := notifier.SendAndCreate(processingStack.UserID.String(), notification); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO: We have channels open every a user sends an image.
|
||||
* We never close these channels.
|
||||
*
|
||||
* What is a reasonable default? Close the channel after 1 minute of inactivity?
|
||||
*/
|
||||
func CreateEventsHandler(notifier *Notifier[Notification]) http.HandlerFunc {
|
||||
counter := 0
|
||||
|
||||
userSplitters := make(map[string]*ChannelSplitter[Notification])
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
userId := _userId.String()
|
||||
|
||||
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()
|
||||
|
||||
if _, exists := notifier.Listeners[userId]; !exists {
|
||||
notifier.Create(userId)
|
||||
}
|
||||
|
||||
userNotifications := notifier.Listeners[userId]
|
||||
|
||||
if _, exists := userSplitters[userId]; !exists {
|
||||
splitter := NewChannelSplitter(userNotifications)
|
||||
|
||||
userSplitters[userId] = &splitter
|
||||
splitter.Listen()
|
||||
}
|
||||
|
||||
splitter := userSplitters[userId]
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("Sending msg %s\n", msgString)
|
||||
|
||||
fmt.Fprintf(w, "event: data\ndata: %s\n\n", string(msgString))
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ require (
|
||||
github.com/charmbracelet/x/ansi v0.4.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.1 // indirect
|
||||
github.com/go-jet/jet/v2 v2.12.0 // indirect
|
||||
github.com/go-jet/jet/v2 v2.13.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
@ -20,6 +20,7 @@ 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
|
||||
|
@ -6,12 +6,15 @@ 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=
|
||||
@ -33,6 +36,10 @@ 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=
|
||||
@ -109,5 +116,6 @@ 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=
|
||||
|
173
backend/images/handler.go
Normal file
173
backend/images/handler.go
Normal file
@ -0,0 +1,173 @@
|
||||
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"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type ImageHandler struct {
|
||||
logger *log.Logger
|
||||
imageModel models.ImageModel
|
||||
userModel models.UserModel
|
||||
limitsManager limits.LimitsManagerMethods
|
||||
}
|
||||
|
||||
type ImagesReturn struct {
|
||||
UserImages []models.UserImageWithImage `json:"userImages"`
|
||||
ProcessingImages []models.UserProcessingImage `json:"processingImages"`
|
||||
Lists []models.ListsWithImages `json:"lists"`
|
||||
}
|
||||
|
||||
func (h *ImageHandler) serveImage(w http.ResponseWriter, r *http.Request) {
|
||||
imageId, err := middleware.GetPathParamID(h.logger, "id", w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
image, err := h.imageModel.Get(r.Context(), imageId)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprintf(w, "Could not get image")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: this could be part of the db table
|
||||
extension := filepath.Ext(image.ImageName)
|
||||
if len(extension) == 0 {
|
||||
// Same hack
|
||||
extension = "png"
|
||||
}
|
||||
extension = extension[1:]
|
||||
|
||||
w.Header().Add("Content-Type", "image/"+extension)
|
||||
w.Write(image.Image)
|
||||
}
|
||||
|
||||
func (h *ImageHandler) listImages(w http.ResponseWriter, r *http.Request) {
|
||||
userId, err := middleware.GetUserID(r.Context(), h.logger, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
images, err := h.userModel.GetUserImages(r.Context(), userId)
|
||||
if err != nil {
|
||||
middleware.WriteErrorInternal(h.logger, "could not get user images", w)
|
||||
return
|
||||
}
|
||||
|
||||
processingImages, err := h.imageModel.GetProcessing(r.Context(), userId)
|
||||
if err != nil {
|
||||
middleware.WriteErrorInternal(h.logger, "could not get processing images", w)
|
||||
return
|
||||
}
|
||||
|
||||
listsWithImages, err := h.userModel.ListWithImages(r.Context(), userId)
|
||||
if err != nil {
|
||||
middleware.WriteErrorInternal(h.logger, "could not get lists with images", w)
|
||||
return
|
||||
}
|
||||
|
||||
imagesReturn := ImagesReturn{
|
||||
UserImages: images,
|
||||
ProcessingImages: processingImages,
|
||||
Lists: listsWithImages,
|
||||
}
|
||||
|
||||
middleware.WriteJsonOrError(h.logger, imagesReturn, w)
|
||||
}
|
||||
|
||||
func (h *ImageHandler) uploadImage(w http.ResponseWriter, r *http.Request) {
|
||||
imageName := chi.URLParam(r, "name")
|
||||
if len(imageName) == 0 {
|
||||
middleware.WriteErrorBadRequest(h.logger, "you need to provide a name in the path", w)
|
||||
return
|
||||
}
|
||||
|
||||
userId, err := middleware.GetUserID(r.Context(), h.logger, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
|
||||
image := make([]byte, 0)
|
||||
switch contentType {
|
||||
case "application/base64":
|
||||
decoder := base64.NewDecoder(base64.StdEncoding, r.Body)
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
_, err := io.Copy(buf, decoder)
|
||||
if err != nil {
|
||||
middleware.WriteErrorBadRequest(h.logger, "base64 decoding failed", w)
|
||||
return
|
||||
}
|
||||
|
||||
image = buf.Bytes()
|
||||
case "application/oclet-stream", "image/png":
|
||||
bodyData, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
middleware.WriteErrorBadRequest(h.logger, "binary data reading failed", w)
|
||||
return
|
||||
}
|
||||
// TODO: check headers
|
||||
|
||||
image = bodyData
|
||||
default:
|
||||
middleware.WriteErrorBadRequest(h.logger, "unsupported content type, need octet-stream or base64", w)
|
||||
return
|
||||
}
|
||||
|
||||
userImage, err := h.imageModel.Process(r.Context(), userId, model.Image{
|
||||
Image: image,
|
||||
ImageName: imageName,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
middleware.WriteErrorInternal(h.logger, "could not save image to DB", w)
|
||||
return
|
||||
}
|
||||
|
||||
middleware.WriteJsonOrError(h.logger, userImage, w)
|
||||
}
|
||||
|
||||
func (h *ImageHandler) CreateRoutes(r chi.Router) {
|
||||
h.logger.Info("Mounting image router")
|
||||
|
||||
// Public route for serving images (not protected)
|
||||
r.Get("/{id}", h.serveImage)
|
||||
|
||||
// Protected routes
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.ProtectedRoute)
|
||||
r.Use(middleware.SetJson)
|
||||
|
||||
r.Get("/", h.listImages)
|
||||
r.Post("/{name}", middleware.WithLimit(h.logger, h.limitsManager.HasReachedImageLimit, h.uploadImage))
|
||||
})
|
||||
}
|
||||
|
||||
func CreateImageHandler(db *sql.DB, limitsManager limits.LimitsManagerMethods) 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,
|
||||
}
|
||||
}
|
796
backend/integration_test.go
Normal file
796
backend/integration_test.go
Normal file
@ -0,0 +1,796 @@
|
||||
// Integration Tests for Haystack Backend
|
||||
//
|
||||
// These tests provide comprehensive end-to-end testing of all API endpoints.
|
||||
//
|
||||
// Requirements:
|
||||
// - Docker must be installed and running
|
||||
// - PostgreSQL Docker image will be automatically pulled and started
|
||||
//
|
||||
// To run the integration tests:
|
||||
//
|
||||
// 1. Start Docker daemon
|
||||
// 2. Run: go test -v ./integration_test.go
|
||||
//
|
||||
// The tests will:
|
||||
// - Start a PostgreSQL container on port 5433
|
||||
// - Set up the database schema
|
||||
// - Test all auth, stack, and image endpoints
|
||||
// - Clean up the container after tests complete
|
||||
//
|
||||
// Note: These tests require Docker and will be skipped if Docker is not available.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"screenmark/screenmark/middleware"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
testDBName = "test_haystack"
|
||||
testDBUser = "test_user"
|
||||
testDBPassword = "test_password"
|
||||
testDBHost = "localhost"
|
||||
testDBPort = "5433"
|
||||
testDBSSLMode = "disable"
|
||||
)
|
||||
|
||||
type TestUser struct {
|
||||
ID uuid.UUID
|
||||
Email string
|
||||
Token string
|
||||
}
|
||||
|
||||
type TestContext struct {
|
||||
db *sql.DB
|
||||
router chi.Router
|
||||
server *httptest.Server
|
||||
users []TestUser
|
||||
cleanup func()
|
||||
}
|
||||
|
||||
func setupTestDatabase() (*sql.DB, func(), error) {
|
||||
// Check if Docker daemon is running
|
||||
checkCmd := exec.Command("docker", "info")
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
return nil, nil, fmt.Errorf("docker daemon is not running: %w", err)
|
||||
}
|
||||
|
||||
// Start PostgreSQL container
|
||||
containerName := "test_postgres_haystack"
|
||||
|
||||
// Clean up any existing container
|
||||
exec.Command("docker", "rm", "-f", containerName).Run()
|
||||
|
||||
// Start new PostgreSQL container
|
||||
cmd := exec.Command("docker", "run", "-d",
|
||||
"--name", containerName,
|
||||
"-e", "POSTGRES_DB="+testDBName,
|
||||
"-e", "POSTGRES_USER="+testDBUser,
|
||||
"-e", "POSTGRES_PASSWORD="+testDBPassword,
|
||||
"-p", testDBPort+":5432",
|
||||
"postgres:15-alpine",
|
||||
)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to start postgres container: %w, output: %s", err, string(output))
|
||||
}
|
||||
|
||||
// Wait for database to be ready with retries
|
||||
maxRetries := 15
|
||||
for i := range maxRetries {
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// Test connection
|
||||
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
|
||||
testDBHost, testDBPort, testDBUser, testDBPassword, testDBName, testDBSSLMode)
|
||||
|
||||
testDB, testErr := sql.Open("postgres", connStr)
|
||||
if testErr == nil {
|
||||
if pingErr := testDB.Ping(); pingErr == nil {
|
||||
testDB.Close()
|
||||
break
|
||||
}
|
||||
testDB.Close()
|
||||
}
|
||||
|
||||
if i == maxRetries-1 {
|
||||
return nil, nil, fmt.Errorf("database failed to become ready after %d retries", maxRetries)
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to database
|
||||
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
|
||||
testDBHost, testDBPort, testDBUser, testDBPassword, testDBName, testDBSSLMode)
|
||||
|
||||
db, err := sql.Open("postgres", connStr)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to connect to test database: %w", err)
|
||||
}
|
||||
|
||||
// Test connection
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to ping test database: %w", err)
|
||||
}
|
||||
|
||||
// Load and execute schema
|
||||
schema, err := os.ReadFile("schema.sql")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read schema file: %w", err)
|
||||
}
|
||||
|
||||
if _, err := db.Exec(string(schema)); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to execute schema: %w", err)
|
||||
}
|
||||
|
||||
// Cleanup function
|
||||
cleanup := func() {
|
||||
db.Close()
|
||||
exec.Command("docker", "rm", "-f", containerName).Run()
|
||||
}
|
||||
|
||||
return db, cleanup, nil
|
||||
}
|
||||
|
||||
func setupTestContext(t *testing.T) *TestContext {
|
||||
// Set environment variables for test environment
|
||||
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
|
||||
testDBHost, testDBPort, testDBUser, testDBPassword, testDBName, testDBSSLMode)
|
||||
|
||||
originalDBConn := os.Getenv("DB_CONNECTION")
|
||||
originalTestEnv := os.Getenv("GO_TEST_ENVIRONMENT")
|
||||
|
||||
os.Setenv("DB_CONNECTION", connStr)
|
||||
os.Setenv("GO_TEST_ENVIRONMENT", "true")
|
||||
|
||||
defer func() {
|
||||
if originalDBConn != "" {
|
||||
os.Setenv("DB_CONNECTION", originalDBConn)
|
||||
} else {
|
||||
os.Unsetenv("DB_CONNECTION")
|
||||
}
|
||||
if originalTestEnv != "" {
|
||||
os.Setenv("GO_TEST_ENVIRONMENT", originalTestEnv)
|
||||
} else {
|
||||
os.Unsetenv("GO_TEST_ENVIRONMENT")
|
||||
}
|
||||
}()
|
||||
|
||||
tc := &TestContext{}
|
||||
|
||||
db, cleanup, err := setupTestDatabase()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to setup test database: %v", err)
|
||||
}
|
||||
|
||||
router := setupRouter(db)
|
||||
server := httptest.NewServer(router)
|
||||
|
||||
tc.db = db
|
||||
tc.router = router
|
||||
tc.server = server
|
||||
tc.cleanup = func() {
|
||||
server.Close()
|
||||
cleanup()
|
||||
}
|
||||
|
||||
return tc
|
||||
}
|
||||
|
||||
func (tc *TestContext) createTestUser(email string) TestUser {
|
||||
// Insert user into database
|
||||
var userID uuid.UUID
|
||||
err := tc.db.QueryRow("INSERT INTO haystack.users (email) VALUES ($1) RETURNING id", email).Scan(&userID)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to create test user: %v", err))
|
||||
}
|
||||
|
||||
// Create access token for the user
|
||||
accessToken := middleware.CreateAccessToken(userID)
|
||||
|
||||
user := TestUser{
|
||||
ID: userID,
|
||||
Email: email,
|
||||
Token: accessToken,
|
||||
}
|
||||
|
||||
tc.users = append(tc.users, user)
|
||||
return user
|
||||
}
|
||||
|
||||
func (tc *TestContext) makeRequest(t *testing.T, method, path, token string, body io.Reader) *http.Response {
|
||||
url := tc.server.URL + path
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (tc *TestContext) makeJSONRequest(t *testing.T, method, path, token string, data any) *http.Response {
|
||||
var body io.Reader
|
||||
if data != nil {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal JSON: %v", err)
|
||||
}
|
||||
body = bytes.NewReader(jsonData)
|
||||
}
|
||||
|
||||
return tc.makeRequest(t, method, path, token, body)
|
||||
}
|
||||
|
||||
// Comprehensive integration test suite - single database setup for all tests
|
||||
func TestAllRoutes(t *testing.T) {
|
||||
tc := setupTestContext(t)
|
||||
defer tc.cleanup()
|
||||
|
||||
// Create test users for different test scenarios
|
||||
stackUser := tc.createTestUser("stacktest@example.com")
|
||||
imageUser := tc.createTestUser("imagetest@example.com")
|
||||
flowUser := tc.createTestUser("flowtest@example.com")
|
||||
|
||||
t.Run("Auth Routes", func(t *testing.T) {
|
||||
t.Run("Login endpoint", func(t *testing.T) {
|
||||
loginData := map[string]string{
|
||||
"email": "test@example.com",
|
||||
}
|
||||
|
||||
resp := tc.makeJSONRequest(t, "POST", "/auth/login", "", loginData)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Code endpoint with valid email", func(t *testing.T) {
|
||||
// First create a login request to set up the email
|
||||
loginData := map[string]string{
|
||||
"email": "test@example.com",
|
||||
}
|
||||
tc.makeJSONRequest(t, "POST", "/auth/login", "", loginData)
|
||||
|
||||
// Then try to use a code (this will fail with invalid code, but tests the endpoint)
|
||||
codeData := map[string]string{
|
||||
"email": "test@example.com",
|
||||
"code": "invalid",
|
||||
}
|
||||
|
||||
resp := tc.makeJSONRequest(t, "POST", "/auth/code", "", codeData)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// The auth system creates a user for new emails, so this returns 200
|
||||
// We're testing that the endpoint works, not necessarily the code validation
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200 for code endpoint, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Protected route without token", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "GET", "/images/image", "", nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected status 401 for protected route without token, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Stack Routes", func(t *testing.T) {
|
||||
t.Run("Get stacks without authentication", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "GET", "/stacks/", "", nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected status 401, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Get stacks with authentication", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "GET", "/stacks/", stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var stacks []interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&stacks); err != nil {
|
||||
t.Errorf("Failed to decode response: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Create stack", func(t *testing.T) {
|
||||
stackData := map[string]string{
|
||||
"title": "Test Stack",
|
||||
"fields": "name,description,value",
|
||||
}
|
||||
|
||||
resp := tc.makeJSONRequest(t, "POST", "/stacks/", stackUser.Token, stackData)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Get stack items with invalid ID", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "GET", "/stacks/invalid-id", stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400 for invalid ID, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete stack without authentication", func(t *testing.T) {
|
||||
fakeUUID := uuid.New()
|
||||
resp := tc.makeRequest(t, "DELETE", "/stacks/"+fakeUUID.String(), "", nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected status 401 for unauthenticated delete, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete stack with invalid ID", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "DELETE", "/stacks/invalid-id", stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400 for invalid ID, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete non-existent stack", func(t *testing.T) {
|
||||
fakeUUID := uuid.New()
|
||||
resp := tc.makeRequest(t, "DELETE", "/stacks/"+fakeUUID.String(), stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400 for non-existent stack, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Create and delete stack successfully", func(t *testing.T) {
|
||||
// First create a stack
|
||||
stackData := map[string]string{
|
||||
"title": "Stack to Delete",
|
||||
"fields": "name,description,value",
|
||||
}
|
||||
|
||||
resp := tc.makeJSONRequest(t, "POST", "/stacks/", stackUser.Token, stackData)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Failed to create stack for deletion test, got %d", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the list of stacks to find the created stack ID
|
||||
resp = tc.makeRequest(t, "GET", "/stacks/", stackUser.Token, nil)
|
||||
|
||||
var stacks []map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&stacks); err != nil {
|
||||
t.Errorf("Failed to decode stacks response: %v", err)
|
||||
resp.Body.Close()
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if len(stacks) == 0 {
|
||||
t.Errorf("No stacks found after creation")
|
||||
return
|
||||
}
|
||||
|
||||
// Find the stack we just created
|
||||
var stackToDelete map[string]interface{}
|
||||
for _, stack := range stacks {
|
||||
if name, ok := stack["Name"].(string); ok && name == "Stack to Delete" {
|
||||
stackToDelete = stack
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if stackToDelete == nil {
|
||||
t.Errorf("Could not find created stack")
|
||||
return
|
||||
}
|
||||
|
||||
stackID, ok := stackToDelete["ID"].(string)
|
||||
if !ok {
|
||||
t.Errorf("Stack ID not found or not a string")
|
||||
return
|
||||
}
|
||||
|
||||
// Now delete the stack
|
||||
resp = tc.makeRequest(t, "DELETE", "/stacks/"+stackID, stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200 for successful delete, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Verify the stack is gone by trying to get it again
|
||||
resp = tc.makeRequest(t, "GET", "/stacks/", stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var stacksAfterDelete []map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&stacksAfterDelete); err != nil {
|
||||
t.Errorf("Failed to decode stacks response after delete: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check that the deleted stack is no longer in the list
|
||||
for _, stack := range stacksAfterDelete {
|
||||
if id, ok := stack["ID"].(string); ok && id == stackID {
|
||||
t.Errorf("Stack still exists after deletion")
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete stack belonging to different user", func(t *testing.T) {
|
||||
// Create a stack with stackUser
|
||||
stackData := map[string]string{
|
||||
"title": "Other User's Stack",
|
||||
"fields": "name,description,value",
|
||||
}
|
||||
|
||||
resp := tc.makeJSONRequest(t, "POST", "/stacks/", stackUser.Token, stackData)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Failed to create stack for ownership test, got %d", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the stack ID
|
||||
resp = tc.makeRequest(t, "GET", "/stacks/", stackUser.Token, nil)
|
||||
|
||||
var stacks []map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&stacks); err != nil {
|
||||
t.Errorf("Failed to decode stacks response: %v", err)
|
||||
resp.Body.Close()
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
var stackID string
|
||||
for _, stack := range stacks {
|
||||
if name, ok := stack["Name"].(string); ok && name == "Other User's Stack" {
|
||||
if id, ok := stack["ID"].(string); ok {
|
||||
stackID = id
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if stackID == "" {
|
||||
t.Errorf("Could not find created stack ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Try to delete the stack with a different user (imageUser)
|
||||
resp = tc.makeRequest(t, "DELETE", "/stacks/"+stackID, imageUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400 when deleting another user's stack, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Image Routes", func(t *testing.T) {
|
||||
t.Run("Get images without authentication", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "GET", "/images/", "", nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected status 401, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Get images with authentication", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "GET", "/images/", imageUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var imageData interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&imageData); err != nil {
|
||||
t.Errorf("Failed to decode response: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Upload image with base64", func(t *testing.T) {
|
||||
// Create a simple valid base64 string for testing
|
||||
testImageBase64 := "dGVzdCBkYXRh" // "test data" in base64
|
||||
|
||||
req, err := http.NewRequest("POST", tc.server.URL+"/images/test.png", strings.NewReader(testImageBase64))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+imageUser.Token)
|
||||
req.Header.Set("Content-Type", "application/base64")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// The API might return 200 for successful operations
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Errorf("Expected status 200 or 201, got %d. Response: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Upload image with binary data", func(t *testing.T) {
|
||||
// Create a small test image (minimal PNG)
|
||||
testImageBinary := []byte{
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D,
|
||||
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
||||
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00,
|
||||
0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0x99, 0x01, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x37, 0x6E, 0xF9, 0x5F, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x49,
|
||||
0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", tc.server.URL+"/images/test2.png", bytes.NewReader(testImageBinary))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+imageUser.Token)
|
||||
req.Header.Set("Content-Type", "image/png")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to make request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// The API might return 200 for successful operations
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Errorf("Expected status 200 or 201, got %d. Response: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Upload image without name", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "POST", "/images/", imageUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Route pattern doesn't match empty names, so returns 404
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
t.Errorf("Expected status 404 for missing name, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Serve non-existent image", func(t *testing.T) {
|
||||
fakeUUID := uuid.New()
|
||||
resp := tc.makeRequest(t, "GET", "/images/"+fakeUUID.String(), "", nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
t.Errorf("Expected status 404 for non-existent image, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Complete User Flow", func(t *testing.T) {
|
||||
// Step 1: Test authentication is working
|
||||
resp := tc.makeRequest(t, "GET", "/images/", flowUser.Token, nil)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Authentication failed, expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Step 2: Upload an image
|
||||
testImageBinary := []byte{
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D,
|
||||
0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
||||
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00,
|
||||
0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0x99, 0x01, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x37, 0x6E, 0xF9, 0x5F, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x49,
|
||||
0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", tc.server.URL+"/images/test_flow.png", bytes.NewReader(testImageBinary))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create upload request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+flowUser.Token)
|
||||
req.Header.Set("Content-Type", "image/png")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to upload image: %v", err)
|
||||
}
|
||||
|
||||
// The API returns 200 for successful image uploads
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Errorf("Image upload failed, expected 200, got %d. Response: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Step 3: Verify image appears in user's image list
|
||||
resp = tc.makeRequest(t, "GET", "/images/", flowUser.Token, nil)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Failed to get user images, expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var imageData map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&imageData); err != nil {
|
||||
t.Errorf("Failed to decode image list: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Check that we have user images
|
||||
if userImages, ok := imageData["userImages"].([]interface{}); ok {
|
||||
if len(userImages) == 0 {
|
||||
t.Log("Warning: No user images found, but upload succeeded")
|
||||
} else {
|
||||
t.Logf("Found %d user images", len(userImages))
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Test stack creation
|
||||
stackData := map[string]string{
|
||||
"title": "Integration Test Stack",
|
||||
"fields": "name,description,value",
|
||||
}
|
||||
|
||||
resp = tc.makeJSONRequest(t, "POST", "/stacks/", flowUser.Token, stackData)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Stack creation failed, expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Step 5: Verify stack appears in user's stack list
|
||||
resp = tc.makeRequest(t, "GET", "/stacks/", flowUser.Token, nil)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Failed to get user stacks, expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var stacks []interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&stacks); err != nil {
|
||||
t.Errorf("Failed to decode stack list: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if len(stacks) == 0 {
|
||||
t.Log("Warning: No stacks found, but creation succeeded")
|
||||
} else {
|
||||
t.Logf("Found %d stacks", len(stacks))
|
||||
}
|
||||
|
||||
t.Log("Complete user flow test passed!")
|
||||
})
|
||||
}
|
||||
|
||||
// Simple test that doesn't require Docker
|
||||
func TestIntegrationTestSetup(t *testing.T) {
|
||||
// This test verifies that the test structure is correct
|
||||
// It doesn't require Docker to be running
|
||||
|
||||
t.Run("Test structure validation", func(t *testing.T) {
|
||||
// This test verifies that the test structure is correct
|
||||
// It doesn't require Docker to be running
|
||||
|
||||
// Verify that our test types are properly defined
|
||||
var _ TestUser
|
||||
var _ TestContext
|
||||
|
||||
// Verify that our constants are defined
|
||||
if testDBName == "" {
|
||||
t.Error("testDBName constant is not defined")
|
||||
}
|
||||
|
||||
if testDBPort == "" {
|
||||
t.Error("testDBPort constant is not defined")
|
||||
}
|
||||
|
||||
t.Log("Test structure is valid")
|
||||
})
|
||||
|
||||
t.Run("Database and router setup", func(t *testing.T) {
|
||||
// This test verifies that the database and router can be set up without SSL errors
|
||||
tc := setupTestContext(t)
|
||||
defer tc.cleanup()
|
||||
|
||||
// Verify that the router was created successfully
|
||||
if tc.router == nil {
|
||||
t.Error("Router was not created successfully")
|
||||
}
|
||||
|
||||
// Verify that the server was created successfully
|
||||
if tc.server == nil {
|
||||
t.Error("Server was not created successfully")
|
||||
}
|
||||
|
||||
// Verify that the database connection is working
|
||||
if err := tc.db.Ping(); err != nil {
|
||||
t.Errorf("Database connection failed: %v", err)
|
||||
}
|
||||
|
||||
t.Log("Database and router setup successful - no SSL errors!")
|
||||
})
|
||||
|
||||
t.Run("Docker availability check", func(t *testing.T) {
|
||||
// Check if Docker is available but don't fail the test
|
||||
if _, err := exec.LookPath("docker"); err != nil {
|
||||
t.Skip("Docker not found, skipping Docker-dependent tests")
|
||||
}
|
||||
|
||||
// Check if Docker daemon is running
|
||||
checkCmd := exec.Command("docker", "info")
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
t.Skip("Docker daemon is not running, skipping Docker-dependent tests")
|
||||
}
|
||||
|
||||
t.Log("Docker is available and running")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// Check if Docker is available
|
||||
if _, err := exec.LookPath("docker"); err != nil {
|
||||
fmt.Println("Docker not found, skipping integration tests")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Check if Docker daemon is running
|
||||
checkCmd := exec.Command("docker", "info")
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
fmt.Println("Docker daemon is not running, skipping integration tests")
|
||||
fmt.Println("To run integration tests, start Docker daemon and try again")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Run tests
|
||||
code := m.Run()
|
||||
os.Exit(code)
|
||||
}
|
61
backend/limits/limits.go
Normal file
61
backend/limits/limits.go
Normal file
@ -0,0 +1,61 @@
|
||||
package limits
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
LISTS_LIMIT = 10
|
||||
IMAGE_LIMIT = 50
|
||||
)
|
||||
|
||||
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 := Lists.
|
||||
SELECT(COUNT(Lists.UserID).AS("listCount.ListCount")).
|
||||
WHERE(Lists.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 := UserImages.
|
||||
SELECT(COUNT(UserImages.UserID).AS("imageCount.ImageCount")).
|
||||
WHERE(UserImages.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,
|
||||
}
|
||||
}
|
149
backend/logs.go
Normal file
149
backend/logs.go
Normal file
@ -0,0 +1,149 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
"github.com/robert-nix/ansihtml"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
"github.com/muesli/termenv"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
insertLogStmt := Logs.
|
||||
INSERT(Logs.Log, Logs.ImageID).
|
||||
VALUES(string(p), w.imageId)
|
||||
|
||||
_, err = insertLogStmt.Exec(w.dbPool)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else {
|
||||
return len(p), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (w *DatabaseWriter) GetImageLogs(ctx context.Context, imageId uuid.UUID) ([]string, error) {
|
||||
getImageLogsStmt := Logs.
|
||||
SELECT(Logs.Log).
|
||||
WHERE(Logs.ImageID.EQ(UUID(imageId)))
|
||||
|
||||
logs := []model.Logs{}
|
||||
err := getImageLogsStmt.QueryContext(ctx, w.dbPool, &logs)
|
||||
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
stringLogs := make([]string, len(logs))
|
||||
for i, log := range logs {
|
||||
stringLogs[i] = log.Log
|
||||
}
|
||||
|
||||
return stringLogs, nil
|
||||
}
|
||||
|
||||
func createLogHandler(logWriter *DatabaseWriter) func(r chi.Router) {
|
||||
return func(r chi.Router) {
|
||||
r.Get("/{imageId}", func(w http.ResponseWriter, r *http.Request) {
|
||||
stringImageId := r.PathValue("imageId")
|
||||
imageId, err := uuid.Parse(stringImageId)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
logs, err := logWriter.GetImageLogs(r.Context(), imageId)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
html := ""
|
||||
|
||||
imageTag := fmt.Sprintf(`<image src="https://haystack.johncosta.tech/image/%s">`, stringImageId)
|
||||
|
||||
for _, log := range logs {
|
||||
html += fmt.Sprintf("<div>%s</div>", string(ansihtml.ConvertToHTML([]byte(log)))+"\n")
|
||||
}
|
||||
|
||||
css := `
|
||||
<style>
|
||||
body {
|
||||
background-color: #1e1e1e;
|
||||
color: #f0f0f0;
|
||||
font-family: sans-serif;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Basic styling for code blocks often used for logs */
|
||||
pre {
|
||||
background-color: #2a2a2a;
|
||||
padding: 1em;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
`
|
||||
|
||||
fullHtml := fmt.Sprintf("<html><head><title>Logs</title>%s</head><body>%s%s</body></html>", css, imageTag, html)
|
||||
|
||||
w.Header().Add("Content-Type", "text/html")
|
||||
w.Write([]byte(fullHtml))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newDatabaseWriter(dbPool *sql.DB, imageId uuid.UUID) *DatabaseWriter {
|
||||
return &DatabaseWriter{
|
||||
dbPool: dbPool,
|
||||
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
|
||||
}
|
358
backend/main.go
358
backend/main.go
@ -1,34 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"os"
|
||||
"screenmark/screenmark/models"
|
||||
"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 {
|
||||
@ -40,332 +20,20 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
imageModel := models.NewImageModel(db)
|
||||
userModel := models.NewUserModel(db)
|
||||
router := setupRouter(db)
|
||||
|
||||
mail, err := CreateMailClient()
|
||||
port, exists := os.LookupEnv("PORT")
|
||||
if !exists {
|
||||
panic("no port can be found")
|
||||
}
|
||||
|
||||
portWithColon := fmt.Sprintf(":%s", port)
|
||||
|
||||
logger := createLogger("Main", os.Stdout)
|
||||
|
||||
logger.Info("Serving router", "port", portWithColon)
|
||||
err = http.ListenAndServe(portWithColon, router)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
auth := CreateAuth(mail)
|
||||
|
||||
eventManager := NewEventManager()
|
||||
|
||||
go ListenNewImageEvents(db, &eventManager)
|
||||
go ListenProcessingImageStatus(db, &eventManager)
|
||||
|
||||
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)
|
||||
for _, image := range images {
|
||||
for _, location := range image.Locations {
|
||||
dataTypes = append(dataTypes, DataType{
|
||||
Type: "location",
|
||||
Data: location,
|
||||
})
|
||||
}
|
||||
|
||||
for _, event := range image.Events {
|
||||
dataTypes = append(dataTypes, DataType{
|
||||
Type: "event",
|
||||
Data: event,
|
||||
})
|
||||
}
|
||||
|
||||
for _, note := range image.Notes {
|
||||
dataTypes = append(dataTypes, DataType{
|
||||
Type: "note",
|
||||
Data: note,
|
||||
})
|
||||
}
|
||||
|
||||
for _, contact := range image.Contacts {
|
||||
dataTypes = append(dataTypes, DataType{
|
||||
Type: "contact",
|
||||
Data: contact,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -1,39 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func CorsMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Add("Access-Control-Allow-Credentials", "*")
|
||||
w.Header().Add("Access-Control-Allow-Headers", "*")
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
const USER_ID = "UserID"
|
||||
|
||||
func ProtectedRoute(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get("Authorization")
|
||||
if len(token) < len("Bearer ") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
userId, err := GetUserIdFromAccess(token[len("Bearer "):])
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
contextWithUserId := context.WithValue(r.Context(), USER_ID, userId)
|
||||
|
||||
newR := r.WithContext(contextWithUserId)
|
||||
next.ServeHTTP(w, newR)
|
||||
})
|
||||
}
|
29
backend/middleware/body.go
Normal file
29
backend/middleware/body.go
Normal file
@ -0,0 +1,29 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func WithValidatedPost[K any](
|
||||
fn func(request K, w http.ResponseWriter, r *http.Request),
|
||||
) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
request := new(K)
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, request)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
fn(*request, w, r)
|
||||
}
|
||||
}
|
11
backend/middleware/json.go
Normal file
11
backend/middleware/json.go
Normal file
@ -0,0 +1,11 @@
|
||||
package middleware
|
||||
|
||||
import "net/http"
|
||||
|
||||
func SetJson(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
36
backend/middleware/limits.go
Normal file
36
backend/middleware/limits.go
Normal file
@ -0,0 +1,36 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func WithLimit(logger *log.Logger, getLimit func(userID uuid.UUID) (bool, error), next func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
userID, err := GetUserID(ctx, logger, w)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
hasReachedLimit, err := getLimit(userID)
|
||||
if err != nil {
|
||||
logger.Error("failed to image limit", "err", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("Limits", "hasReachedLimit", hasReachedLimit)
|
||||
|
||||
if hasReachedLimit {
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
116
backend/middleware/middleware.go
Normal file
116
backend/middleware/middleware.go
Normal file
@ -0,0 +1,116 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func CorsMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Add("Access-Control-Allow-Headers", "*")
|
||||
|
||||
// Access-Control-Allow-Methods is often needed for preflight OPTIONS requests
|
||||
w.Header().Add("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||||
|
||||
// The client makes an OPTIONS preflight request before a complex request.
|
||||
// We must handle this and respond with the appropriate headers.
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
const USER_ID = "UserID"
|
||||
|
||||
func GetUserID(ctx context.Context, logger *log.Logger, w http.ResponseWriter) (uuid.UUID, error) {
|
||||
userId := ctx.Value(USER_ID)
|
||||
|
||||
if userId == nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
logger.Warn("UserID not present in request")
|
||||
return uuid.Nil, errors.New("context does not contain a user id")
|
||||
}
|
||||
|
||||
userIdUuid, ok := userId.(uuid.UUID)
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
logger.Warn("UserID not of correct type")
|
||||
return uuid.Nil, fmt.Errorf("context user id is not of type uuid, got: %t", userId)
|
||||
}
|
||||
|
||||
return userIdUuid, nil
|
||||
}
|
||||
|
||||
func ProtectedRoute(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get("Authorization")
|
||||
if len(token) < len("Bearer ") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
userId, err := GetUserIdFromAccess(token[len("Bearer "):])
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
contextWithUserId := context.WithValue(r.Context(), USER_ID, userId)
|
||||
|
||||
newR := r.WithContext(contextWithUserId)
|
||||
next.ServeHTTP(w, newR)
|
||||
})
|
||||
}
|
||||
|
||||
func GetUserIdFromUrl(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.URL.Query().Get("token")
|
||||
|
||||
if len(token) == 0 {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
userId, err := GetUserIdFromAccess(token)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
contextWithUserId := context.WithValue(r.Context(), USER_ID, userId)
|
||||
|
||||
newR := r.WithContext(contextWithUserId)
|
||||
next.ServeHTTP(w, newR)
|
||||
})
|
||||
}
|
||||
|
||||
func GetPathParamID(logger *log.Logger, param string, w http.ResponseWriter, r *http.Request) (uuid.UUID, error) {
|
||||
pathParam := r.PathValue(param)
|
||||
if len(pathParam) == 0 {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
err := fmt.Errorf("%s was not present", param)
|
||||
logger.Warn(err)
|
||||
return uuid.Nil, err
|
||||
}
|
||||
|
||||
uuidParam, err := uuid.Parse(pathParam)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
err := fmt.Errorf("could not parse param: %w", err)
|
||||
logger.Warn(err)
|
||||
return uuid.Nil, err
|
||||
}
|
||||
|
||||
return uuidParam, nil
|
||||
}
|
48
backend/middleware/util.go
Normal file
48
backend/middleware/util.go
Normal file
@ -0,0 +1,48 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
func WriteJsonOrError[K any](logger *log.Logger, object K, w http.ResponseWriter) {
|
||||
jsonObject, err := json.Marshal(object)
|
||||
if err != nil {
|
||||
logger.Warn("could not marshal json object", "err", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(jsonObject)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
type ErrorObject struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func writeError(logger *log.Logger, error string, w http.ResponseWriter, code int) {
|
||||
e := ErrorObject{
|
||||
error,
|
||||
}
|
||||
|
||||
jsonObject, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
logger.Warn("could not marshal json object", "err", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(jsonObject)
|
||||
w.WriteHeader(code)
|
||||
}
|
||||
|
||||
func WriteErrorBadRequest(logger *log.Logger, error string, w http.ResponseWriter) {
|
||||
writeError(logger, error, w, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func WriteErrorInternal(logger *log.Logger, error string, w http.ResponseWriter) {
|
||||
writeError(logger, error, w, http.StatusInternalServerError)
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ContactModel struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
func (m ContactModel) List(ctx context.Context, userId uuid.UUID) ([]model.Contacts, error) {
|
||||
listContactsStmt := SELECT(Contacts.AllColumns).
|
||||
FROM(
|
||||
Contacts.
|
||||
INNER_JOIN(UserContacts, UserContacts.ContactID.EQ(Contacts.ID)),
|
||||
).
|
||||
WHERE(UserContacts.UserID.EQ(UUID(userId)))
|
||||
|
||||
locations := []model.Contacts{}
|
||||
|
||||
err := listContactsStmt.QueryContext(ctx, m.dbPool, &locations)
|
||||
return locations, err
|
||||
}
|
||||
|
||||
func (m ContactModel) Save(ctx context.Context, userId uuid.UUID, contact model.Contacts) (model.Contacts, error) {
|
||||
// TODO: make this a transaction
|
||||
|
||||
insertContactStmt := Contacts.
|
||||
INSERT(Contacts.Name, Contacts.Description, Contacts.PhoneNumber, Contacts.Email).
|
||||
VALUES(contact.Name, contact.Description, contact.PhoneNumber, contact.Email).
|
||||
RETURNING(Contacts.AllColumns)
|
||||
|
||||
insertedContact := model.Contacts{}
|
||||
err := insertContactStmt.QueryContext(ctx, m.dbPool, &insertedContact)
|
||||
|
||||
if err != nil {
|
||||
return insertedContact, err
|
||||
}
|
||||
|
||||
insertUserContactStmt := UserContacts.
|
||||
INSERT(UserContacts.UserID, UserContacts.ContactID).
|
||||
VALUES(userId, insertedContact.ID)
|
||||
|
||||
_, err = insertUserContactStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return insertedContact, err
|
||||
}
|
||||
|
||||
func (m ContactModel) SaveToImage(ctx context.Context, imageId uuid.UUID, contactId uuid.UUID) (model.ImageContacts, error) {
|
||||
insertImageContactStmt := ImageContacts.
|
||||
INSERT(ImageContacts.ImageID, ImageContacts.ContactID).
|
||||
VALUES(imageId, contactId).
|
||||
RETURNING(ImageContacts.AllColumns)
|
||||
|
||||
imageContact := model.ImageContacts{}
|
||||
err := insertImageContactStmt.QueryContext(ctx, m.dbPool, &imageContact)
|
||||
|
||||
return imageContact, err
|
||||
}
|
||||
|
||||
func NewContactModel(db *sql.DB) ContactModel {
|
||||
return ContactModel{dbPool: db}
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type EventModel struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
func (m EventModel) List(ctx context.Context, userId uuid.UUID) ([]model.Events, error) {
|
||||
listEventsStmt := SELECT(Events.AllColumns).
|
||||
FROM(
|
||||
Events.
|
||||
INNER_JOIN(UserEvents, UserEvents.EventID.EQ(Events.ID)),
|
||||
).
|
||||
WHERE(UserEvents.UserID.EQ(UUID(userId)))
|
||||
|
||||
events := []model.Events{}
|
||||
|
||||
err := listEventsStmt.QueryContext(ctx, m.dbPool, &events)
|
||||
return events, err
|
||||
}
|
||||
|
||||
func (m EventModel) Save(ctx context.Context, userId uuid.UUID, event model.Events) (model.Events, error) {
|
||||
// TODO tx here
|
||||
insertEventStmt := Events.
|
||||
INSERT(Events.Name, Events.Description, Events.StartDateTime, Events.EndDateTime).
|
||||
VALUES(event.Name, event.Description, event.StartDateTime, event.EndDateTime).
|
||||
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}
|
||||
}
|
@ -3,8 +3,8 @@ package models
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/enum"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
@ -29,21 +29,27 @@ type ProcessingImageData struct {
|
||||
Image model.Image
|
||||
}
|
||||
|
||||
type UserProcessingImage struct {
|
||||
model.UserImagesToProcess
|
||||
|
||||
Image model.Image
|
||||
}
|
||||
|
||||
func (m ImageModel) Process(ctx context.Context, userId uuid.UUID, image model.Image) (model.UserImagesToProcess, error) {
|
||||
tx, err := m.dbPool.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return model.UserImagesToProcess{}, err
|
||||
return model.UserImagesToProcess{}, fmt.Errorf("Failed to begin transaction: %w", err)
|
||||
}
|
||||
|
||||
insertImageStmt := Image.
|
||||
INSERT(Image.ImageName, Image.Image).
|
||||
VALUES(image.ImageName, image.Image).
|
||||
INSERT(Image.ImageName, Image.Image, Image.Description).
|
||||
VALUES(image.ImageName, image.Image, image.Description).
|
||||
RETURNING(Image.ID)
|
||||
|
||||
insertedImage := model.Image{}
|
||||
err = insertImageStmt.QueryContext(ctx, tx, &insertedImage)
|
||||
if err != nil {
|
||||
return model.UserImagesToProcess{}, err
|
||||
return model.UserImagesToProcess{}, fmt.Errorf("Could not insert/query new image. SQL %s: %w", insertImageStmt.DebugSql(), err)
|
||||
}
|
||||
|
||||
stmt := UserImagesToProcess.
|
||||
@ -54,7 +60,7 @@ func (m ImageModel) Process(ctx context.Context, userId uuid.UUID, image model.I
|
||||
userImage := model.UserImagesToProcess{}
|
||||
err = stmt.QueryContext(ctx, tx, &userImage)
|
||||
if err != nil {
|
||||
return model.UserImagesToProcess{}, err
|
||||
return model.UserImagesToProcess{}, fmt.Errorf("Could not insert user_image: %w", err)
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
@ -62,16 +68,20 @@ func (m ImageModel) Process(ctx context.Context, userId uuid.UUID, image model.I
|
||||
return userImage, err
|
||||
}
|
||||
|
||||
func (m ImageModel) GetToProcess(ctx context.Context, imageId uuid.UUID) (model.UserImagesToProcess, error) {
|
||||
getToProcessStmt := UserImagesToProcess.
|
||||
SELECT(UserImagesToProcess.AllColumns).
|
||||
func (m ImageModel) GetToProcess(ctx context.Context, imageId uuid.UUID) (UserProcessingImage, error) {
|
||||
getToProcessStmt := SELECT(UserImagesToProcess.AllColumns, Image.ID, Image.ImageName).
|
||||
FROM(
|
||||
UserImagesToProcess.INNER_JOIN(
|
||||
Image, Image.ID.EQ(UserImagesToProcess.ImageID),
|
||||
),
|
||||
).
|
||||
WHERE(UserImagesToProcess.ID.EQ(UUID(imageId)))
|
||||
|
||||
images := []model.UserImagesToProcess{}
|
||||
images := []UserProcessingImage{}
|
||||
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 UserProcessingImage{}, fmt.Errorf("Expected 1, got %d\n", len(images))
|
||||
}
|
||||
|
||||
return images[0], err
|
||||
@ -89,7 +99,7 @@ func (m ImageModel) GetToProcessWithData(ctx context.Context, imageId uuid.UUID)
|
||||
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 ProcessingImageData{}, fmt.Errorf("Expected 1, got %d\n", len(images))
|
||||
}
|
||||
|
||||
return images[0], err
|
||||
@ -117,15 +127,35 @@ func (m ImageModel) FinishProcessing(ctx context.Context, imageId uuid.UUID) (mo
|
||||
return model.UserImages{}, err
|
||||
}
|
||||
|
||||
removeProcessingStmt := UserImagesToProcess.
|
||||
DELETE().
|
||||
// Hacky. Update the status before removing so we can get our regular triggers
|
||||
// to work.
|
||||
|
||||
updateStatusStmt := UserImagesToProcess.
|
||||
UPDATE(UserImagesToProcess.Status).
|
||||
SET(model.Progress_Complete).
|
||||
WHERE(UserImagesToProcess.ID.EQ(UUID(imageToProcess.ID)))
|
||||
|
||||
_, err = removeProcessingStmt.ExecContext(ctx, tx)
|
||||
_, err = updateStatusStmt.ExecContext(ctx, tx)
|
||||
if err != nil {
|
||||
return model.UserImages{}, err
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// We cannot delete the image to process because our events rely on it.
|
||||
// This indicates our DB structure with the two tables might need some adjusting.
|
||||
// Or re-doing all together perhaps.
|
||||
// (switching to a one table (user_images) could work)
|
||||
// But for now, we can just not delete the images to process and set them to complete
|
||||
|
||||
// removeProcessingStmt := UserImagesToProcess.
|
||||
// DELETE().
|
||||
// WHERE(UserImagesToProcess.ID.EQ(UUID(imageToProcess.ID)))
|
||||
//
|
||||
// _, err = removeProcessingStmt.ExecContext(ctx, tx)
|
||||
// if err != nil {
|
||||
// return model.UserImages{}, err
|
||||
// }
|
||||
|
||||
err = tx.Commit()
|
||||
return userImage, err
|
||||
}
|
||||
@ -141,21 +171,41 @@ func (m ImageModel) StartProcessing(ctx context.Context, processingImageId uuid.
|
||||
return err
|
||||
}
|
||||
|
||||
func (m ImageModel) Get(ctx context.Context, imageId uuid.UUID) (ImageData, error) {
|
||||
getImageStmt := SELECT(UserImages.AllColumns, Image.AllColumns).
|
||||
func (m ImageModel) Get(ctx context.Context, imageId uuid.UUID) (model.Image, error) {
|
||||
getImageStmt := Image.SELECT(Image.AllColumns).
|
||||
WHERE(Image.ID.EQ(UUID(imageId)))
|
||||
|
||||
image := model.Image{}
|
||||
err := getImageStmt.QueryContext(ctx, m.dbPool, &image)
|
||||
|
||||
return image, err
|
||||
}
|
||||
|
||||
func (m ImageModel) GetProcessing(ctx context.Context, userId uuid.UUID) ([]UserProcessingImage, error) {
|
||||
getProcessingStmt := SELECT(UserImagesToProcess.AllColumns, Image.ID, Image.ImageName).
|
||||
FROM(
|
||||
UserImages.INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID)),
|
||||
).
|
||||
WHERE(UserImages.ID.EQ(UUID(imageId)))
|
||||
UserImagesToProcess.INNER_JOIN(
|
||||
Image, Image.ID.EQ(UserImagesToProcess.ImageID),
|
||||
),
|
||||
).WHERE(
|
||||
UserImagesToProcess.UserID.EQ(UUID(userId)).
|
||||
AND(UserImagesToProcess.Status.NOT_EQ(enum.Progress.Complete)),
|
||||
)
|
||||
|
||||
images := []ImageData{}
|
||||
err := getImageStmt.QueryContext(ctx, m.dbPool, &images)
|
||||
images := []UserProcessingImage{}
|
||||
err := getProcessingStmt.QueryContext(ctx, m.dbPool, &images)
|
||||
|
||||
if len(images) != 1 {
|
||||
return ImageData{}, errors.New(fmt.Sprintf("Expected 1, got %d\n", len(images)))
|
||||
}
|
||||
return images, err
|
||||
}
|
||||
|
||||
return images[0], err
|
||||
func (m ImageModel) AddDescription(ctx context.Context, imageId uuid.UUID, description string) error {
|
||||
updateImageStmt := Image.UPDATE(Image.Description).
|
||||
SET(description).
|
||||
WHERE(Image.ID.EQ(UUID(imageId)))
|
||||
|
||||
_, err := updateImageStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m ImageModel) IsUserAuthorized(ctx context.Context, imageId uuid.UUID, userId uuid.UUID) bool {
|
||||
|
@ -1,33 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type LinkModel struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
func (m LinkModel) Save(ctx context.Context, imageId uuid.UUID, links []string) error {
|
||||
if len(links) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
stmt := ImageLinks.INSERT(ImageLinks.ImageID, ImageLinks.Link)
|
||||
|
||||
for _, link := range links {
|
||||
stmt = stmt.VALUES(imageId, link)
|
||||
}
|
||||
|
||||
_, err := stmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func NewLinkModel(db *sql.DB) LinkModel {
|
||||
return LinkModel{dbPool: db}
|
||||
}
|
310
backend/models/lists.go
Normal file
310
backend/models/lists.go
Normal file
@ -0,0 +1,310 @@
|
||||
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 ListModel struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
type ListWithItems struct {
|
||||
model.Lists
|
||||
|
||||
Schema struct {
|
||||
model.Schemas
|
||||
|
||||
SchemaItems []model.SchemaItems
|
||||
}
|
||||
}
|
||||
|
||||
type ImageWithSchema struct {
|
||||
model.ImageLists
|
||||
|
||||
Items []model.ImageSchemaItems
|
||||
}
|
||||
|
||||
type IDValue struct {
|
||||
ID string `json:"id"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SELECT for lists
|
||||
// ========================================
|
||||
|
||||
func (m ListModel) List(ctx context.Context, userId uuid.UUID) ([]ListWithItems, error) {
|
||||
getListsWithItems := SELECT(
|
||||
Lists.AllColumns,
|
||||
Schemas.AllColumns,
|
||||
SchemaItems.AllColumns,
|
||||
).
|
||||
FROM(
|
||||
Lists.
|
||||
INNER_JOIN(Schemas, Schemas.ListID.EQ(Lists.ID)).
|
||||
INNER_JOIN(SchemaItems, SchemaItems.SchemaID.EQ(Schemas.ID)),
|
||||
).
|
||||
WHERE(Lists.UserID.EQ(UUID(userId)))
|
||||
|
||||
lists := []ListWithItems{}
|
||||
err := getListsWithItems.QueryContext(ctx, m.dbPool, &lists)
|
||||
|
||||
return lists, err
|
||||
}
|
||||
|
||||
func (m ListModel) ListItems(ctx context.Context, listID uuid.UUID) ([]ImageWithSchema, error) {
|
||||
getListItems := SELECT(
|
||||
ImageLists.AllColumns,
|
||||
ImageSchemaItems.AllColumns,
|
||||
).
|
||||
FROM(
|
||||
ImageLists.
|
||||
INNER_JOIN(ImageSchemaItems, ImageSchemaItems.ImageID.EQ(ImageLists.ImageID)),
|
||||
).
|
||||
WHERE(ImageLists.ListID.EQ(UUID(listID)))
|
||||
|
||||
listItems := make([]ImageWithSchema, 0)
|
||||
err := getListItems.QueryContext(ctx, m.dbPool, &listItems)
|
||||
|
||||
return listItems, err
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// SELECT for specific items
|
||||
// ========================================
|
||||
|
||||
func (m ListModel) GetProcessing(ctx context.Context, processingListID uuid.UUID) (model.ProcessingLists, error) {
|
||||
getProcessingListStmt := ProcessingLists.
|
||||
SELECT(ProcessingLists.AllColumns).
|
||||
WHERE(ProcessingLists.ID.EQ(UUID(processingListID)))
|
||||
|
||||
list := model.ProcessingLists{}
|
||||
err := getProcessingListStmt.QueryContext(ctx, m.dbPool, &list)
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (m ListModel) GetToProcess(ctx context.Context, listID uuid.UUID) (model.ProcessingLists, error) {
|
||||
getToProcessStmt := ProcessingLists.
|
||||
SELECT(ProcessingLists.AllColumns).
|
||||
WHERE(ProcessingLists.ID.EQ(UUID(listID)))
|
||||
|
||||
stack := []model.ProcessingLists{}
|
||||
err := getToProcessStmt.QueryContext(ctx, m.dbPool, &stack)
|
||||
|
||||
if len(stack) != 1 {
|
||||
return model.ProcessingLists{}, fmt.Errorf("Expected 1, got %d\n", len(stack))
|
||||
}
|
||||
|
||||
return stack[0], err
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// UPDATE
|
||||
// ========================================
|
||||
|
||||
func (m ListModel) StartProcessing(ctx context.Context, processingListID uuid.UUID) error {
|
||||
startProcessingStmt := ProcessingLists.
|
||||
UPDATE(ProcessingLists.Status).
|
||||
SET(model.Progress_InProgress).
|
||||
WHERE(ProcessingLists.ID.EQ(UUID(processingListID)))
|
||||
|
||||
_, err := startProcessingStmt.ExecContext(ctx, m.dbPool)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m ListModel) EndProcessing(ctx context.Context, processingListID uuid.UUID) error {
|
||||
startProcessingStmt := ProcessingLists.
|
||||
UPDATE(ProcessingLists.Status).
|
||||
SET(model.Progress_Complete).
|
||||
WHERE(ProcessingLists.ID.EQ(UUID(processingListID)))
|
||||
|
||||
_, err := startProcessingStmt.ExecContext(ctx, m.dbPool)
|
||||
return err
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// INSERT methods
|
||||
// ========================================
|
||||
|
||||
func (m ListModel) Save(ctx context.Context, userId uuid.UUID, name string, description string, schemaItems []model.SchemaItems) (ListWithItems, error) {
|
||||
tx, err := m.dbPool.BeginTx(ctx, nil)
|
||||
|
||||
stmt := Lists.INSERT(Lists.UserID, Lists.Name, Lists.Description).
|
||||
VALUES(userId, name, description).
|
||||
RETURNING(Lists.ID, Lists.Name, Lists.Description)
|
||||
|
||||
newList := model.Lists{}
|
||||
err = stmt.QueryContext(ctx, tx, &newList)
|
||||
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return ListWithItems{}, fmt.Errorf("Could not save new list. %s", err)
|
||||
}
|
||||
|
||||
insertSchemaStmt := Schemas.INSERT(Schemas.ListID).
|
||||
VALUES(newList.ID).
|
||||
RETURNING(Schemas.ID)
|
||||
|
||||
newSchema := model.Schemas{}
|
||||
err = insertSchemaStmt.QueryContext(ctx, tx, &newSchema)
|
||||
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return ListWithItems{}, fmt.Errorf("Could not save new schema. %s", err)
|
||||
}
|
||||
|
||||
// This is very interesting...
|
||||
for i := range schemaItems {
|
||||
schemaItems[i].SchemaID = newSchema.ID
|
||||
}
|
||||
|
||||
insertSchemaItemsStmt := SchemaItems.INSERT(SchemaItems.Item, SchemaItems.Value, SchemaItems.Description, SchemaItems.SchemaID).
|
||||
MODELS(schemaItems)
|
||||
_, err = insertSchemaItemsStmt.ExecContext(ctx, tx)
|
||||
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return ListWithItems{}, fmt.Errorf("Could not save schema items. %s", err)
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return ListWithItems{}, fmt.Errorf("Could not commit transaction. %s", err)
|
||||
}
|
||||
|
||||
getListAndItems := SELECT(Lists.AllColumns, Schemas.AllColumns, SchemaItems.AllColumns).
|
||||
FROM(
|
||||
Lists.
|
||||
INNER_JOIN(Schemas, Schemas.ListID.EQ(Lists.ID)).
|
||||
INNER_JOIN(SchemaItems, SchemaItems.SchemaID.EQ(Schemas.ID)),
|
||||
).
|
||||
WHERE(Lists.ID.EQ(UUID(newList.ID)))
|
||||
|
||||
listWithItems := ListWithItems{}
|
||||
err = getListAndItems.QueryContext(ctx, m.dbPool, &listWithItems)
|
||||
|
||||
return listWithItems, err
|
||||
}
|
||||
|
||||
func (m ListModel) SaveInto(ctx context.Context, listId uuid.UUID, imageId uuid.UUID, schemaValues []IDValue) error {
|
||||
imageSchemaItems := make([]model.ImageSchemaItems, len(schemaValues))
|
||||
|
||||
for i, v := range schemaValues {
|
||||
parsedId, err := uuid.Parse(v.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imageSchemaItems[i].SchemaItemID = parsedId
|
||||
imageSchemaItems[i].ImageID = imageId
|
||||
imageSchemaItems[i].Value = &v.Value
|
||||
}
|
||||
|
||||
tx, err := m.dbPool.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stmt := ImageLists.INSERT(ImageLists.ListID, ImageLists.ImageID).
|
||||
VALUES(listId, imageId)
|
||||
|
||||
_, err = stmt.ExecContext(ctx, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("Could not insert new list. %s", err)
|
||||
}
|
||||
|
||||
insertSchemaItemsStmt := ImageSchemaItems.
|
||||
INSERT(ImageSchemaItems.Value, ImageSchemaItems.SchemaItemID, ImageSchemaItems.ImageID).
|
||||
MODELS(imageSchemaItems)
|
||||
|
||||
_, err = insertSchemaItemsStmt.ExecContext(ctx, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return fmt.Errorf("Could not insert schema items. %s", err)
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
return err
|
||||
}
|
||||
|
||||
func (m ListModel) SaveProcessing(ctx context.Context, userID uuid.UUID, title string, fields string) error {
|
||||
insertListToProcess := ProcessingLists.
|
||||
INSERT(ProcessingLists.UserID, ProcessingLists.Title, ProcessingLists.Fields).
|
||||
VALUES(userID, title, fields)
|
||||
|
||||
_, err := insertListToProcess.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// DELETE methods
|
||||
// ========================================
|
||||
|
||||
func (m ListModel) Delete(ctx context.Context, listID uuid.UUID, userID uuid.UUID) error {
|
||||
// First verify the list belongs to the user
|
||||
checkOwnershipStmt := Lists.
|
||||
SELECT(Lists.ID).
|
||||
WHERE(Lists.ID.EQ(UUID(listID)).AND(Lists.UserID.EQ(UUID(userID))))
|
||||
|
||||
var existingList model.Lists
|
||||
err := checkOwnershipStmt.QueryContext(ctx, m.dbPool, &existingList)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not verify list ownership: %w", err)
|
||||
}
|
||||
|
||||
// Start a transaction to ensure all deletions happen atomically
|
||||
tx, err := m.dbPool.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not start transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Delete in reverse order of dependencies:
|
||||
// 1. Delete schema items first
|
||||
deleteSchemaItemsStmt := SchemaItems.DELETE().
|
||||
WHERE(SchemaItems.SchemaID.IN(
|
||||
Schemas.SELECT(Schemas.ID).
|
||||
WHERE(Schemas.ListID.EQ(UUID(listID))),
|
||||
))
|
||||
_, err = deleteSchemaItemsStmt.ExecContext(ctx, tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not delete schema items: %w", err)
|
||||
}
|
||||
|
||||
// 2. Delete schemas
|
||||
deleteSchemasStmt := Schemas.DELETE().WHERE(Schemas.ListID.EQ(UUID(listID)))
|
||||
_, err = deleteSchemasStmt.ExecContext(ctx, tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not delete schemas: %w", err)
|
||||
}
|
||||
|
||||
// 3. Delete the list itself
|
||||
deleteListStmt := Lists.DELETE().WHERE(Lists.ID.EQ(UUID(listID)))
|
||||
_, err = deleteListStmt.ExecContext(ctx, tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not delete list: %w", err)
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not commit transaction: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewListModel(db *sql.DB) ListModel {
|
||||
return ListModel{dbPool: db}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
"github.com/go-jet/jet/v2/qrm"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type LocationModel struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
func (m LocationModel) List(ctx context.Context, userId uuid.UUID) ([]model.Locations, error) {
|
||||
listLocationsStmt := SELECT(Locations.AllColumns).
|
||||
FROM(
|
||||
Locations.
|
||||
INNER_JOIN(UserLocations, UserLocations.LocationID.EQ(Locations.ID)),
|
||||
).
|
||||
WHERE(UserLocations.UserID.EQ(UUID(userId)))
|
||||
|
||||
locations := []model.Locations{}
|
||||
|
||||
err := listLocationsStmt.QueryContext(ctx, m.dbPool, &locations)
|
||||
return locations, err
|
||||
}
|
||||
|
||||
func (m LocationModel) Save(ctx context.Context, userId uuid.UUID, location model.Locations) (model.Locations, error) {
|
||||
insertLocationStmt := Locations.
|
||||
INSERT(Locations.Name, Locations.Address, Locations.Description).
|
||||
VALUES(location.Name, location.Address, location.Description).
|
||||
RETURNING(Locations.AllColumns)
|
||||
|
||||
insertedLocation := model.Locations{}
|
||||
err := insertLocationStmt.QueryContext(ctx, m.dbPool, &insertedLocation)
|
||||
if err != nil {
|
||||
return model.Locations{}, err
|
||||
}
|
||||
|
||||
insertUserLocationStmt := UserLocations.
|
||||
INSERT(UserLocations.UserID, UserLocations.LocationID).
|
||||
VALUES(userId, insertedLocation.ID)
|
||||
|
||||
_, err = insertUserLocationStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return insertedLocation, err
|
||||
}
|
||||
|
||||
func (m LocationModel) SaveToImage(ctx context.Context, imageId uuid.UUID, locationId uuid.UUID) (model.ImageLocations, error) {
|
||||
imageLocation := model.ImageLocations{}
|
||||
|
||||
checkExistingStmt := ImageLocations.
|
||||
SELECT(ImageLocations.AllColumns).
|
||||
WHERE(
|
||||
ImageLocations.ImageID.EQ(UUID(imageId)).
|
||||
AND(ImageLocations.LocationID.EQ(UUID(locationId))),
|
||||
)
|
||||
|
||||
err := checkExistingStmt.QueryContext(ctx, m.dbPool, &imageLocation)
|
||||
if err != nil && err != qrm.ErrNoRows {
|
||||
// A real error
|
||||
return model.ImageLocations{}, err
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
// Already exists.
|
||||
return imageLocation, nil
|
||||
}
|
||||
|
||||
insertImageLocationStmt := ImageLocations.
|
||||
INSERT(ImageLocations.ImageID, ImageLocations.LocationID).
|
||||
VALUES(imageId, locationId).
|
||||
RETURNING(ImageLocations.AllColumns)
|
||||
|
||||
err = insertImageLocationStmt.QueryContext(ctx, m.dbPool, &imageLocation)
|
||||
|
||||
return imageLocation, err
|
||||
}
|
||||
|
||||
func NewLocationModel(db *sql.DB) LocationModel {
|
||||
return LocationModel{dbPool: db}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type NoteModel struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
func (m NoteModel) List(ctx context.Context, userId uuid.UUID) ([]model.Notes, error) {
|
||||
listNotesStmt := SELECT(Notes.AllColumns).
|
||||
FROM(
|
||||
Notes.
|
||||
INNER_JOIN(UserNotes, UserNotes.NoteID.EQ(Notes.ID)),
|
||||
).
|
||||
WHERE(UserNotes.UserID.EQ(UUID(userId)))
|
||||
|
||||
locations := []model.Notes{}
|
||||
|
||||
err := listNotesStmt.QueryContext(ctx, m.dbPool, &locations)
|
||||
return locations, err
|
||||
}
|
||||
|
||||
func (m NoteModel) Save(ctx context.Context, userId uuid.UUID, note model.Notes) (model.Notes, error) {
|
||||
insertNoteStmt := Notes.
|
||||
INSERT(Notes.Name, Notes.Description, Notes.Content).
|
||||
VALUES(note.Name, note.Description, note.Content).
|
||||
RETURNING(Notes.AllColumns)
|
||||
|
||||
insertedNote := model.Notes{}
|
||||
err := insertNoteStmt.QueryContext(ctx, m.dbPool, &insertedNote)
|
||||
if err != nil {
|
||||
return model.Notes{}, err
|
||||
}
|
||||
|
||||
insertUserNoteStmt := UserNotes.
|
||||
INSERT(UserNotes.UserID, UserNotes.NoteID).
|
||||
VALUES(userId, insertedNote.ID)
|
||||
|
||||
_, err = insertUserNoteStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return insertedNote, err
|
||||
}
|
||||
|
||||
func (m NoteModel) SaveToImage(ctx context.Context, imageId uuid.UUID, noteId uuid.UUID) (model.ImageNotes, error) {
|
||||
insertImageNoteStmt := ImageNotes.
|
||||
INSERT(ImageNotes.ImageID, ImageNotes.NoteID).
|
||||
VALUES(imageId, noteId).
|
||||
RETURNING(ImageNotes.AllColumns)
|
||||
|
||||
imageNote := model.ImageNotes{}
|
||||
err := insertImageNoteStmt.QueryContext(ctx, m.dbPool, &imageNote)
|
||||
|
||||
return imageNote, err
|
||||
}
|
||||
|
||||
func NewNoteModel(db *sql.DB) NoteModel {
|
||||
return NoteModel{dbPool: db}
|
||||
}
|
@ -1,156 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
. "github.com/go-jet/jet/v2/postgres"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type TagModel struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
// Raw dogging SQL is kinda based though?
|
||||
//
|
||||
// | nO, usE OrM!!
|
||||
//
|
||||
// | RAW - RAW
|
||||
// | SQL | \ SQL
|
||||
// | GOOD | \ GOOD
|
||||
// | - -
|
||||
// | -- --
|
||||
// | -- --
|
||||
// | ---- IQ ----
|
||||
func (m TagModel) getNonExistantTags(ctx context.Context, userId uuid.UUID, tags []string) ([]string, error) {
|
||||
if len(tags) == 0 {
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
values := ""
|
||||
counter := 1
|
||||
// big big SQL injection problem here?
|
||||
for counter = 1; counter <= len(tags); counter++ {
|
||||
values += fmt.Sprintf("($%d),", counter)
|
||||
}
|
||||
values = values[0 : len(values)-1]
|
||||
|
||||
getNonExistingTags := fmt.Sprintf(`WITH given_tags
|
||||
AS (SELECT given_tags.tag FROM (VALUES `+values+`) AS given_tags (tag)),
|
||||
this_user_tags AS
|
||||
(SELECT id, tag FROM haystack.user_tags WHERE user_tags.user_id = $%d)
|
||||
SELECT given_tags.tag
|
||||
FROM given_tags
|
||||
LEFT OUTER JOIN haystack.user_tags ON haystack.user_tags.tag = given_tags.tag
|
||||
where user_tags.tag is null`, counter)
|
||||
|
||||
getNonExistingTagsStmt, err := m.dbPool.PrepareContext(ctx, getNonExistingTags)
|
||||
defer getNonExistingTagsStmt.Close()
|
||||
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
args := make([]any, counter)
|
||||
for i, v := range tags {
|
||||
args[i] = v
|
||||
}
|
||||
args[counter-1] = userId.String()
|
||||
|
||||
rows, err := getNonExistingTagsStmt.QueryContext(ctx, args...)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
nonExistantTags := make([]string, 0)
|
||||
|
||||
for rows.Next() {
|
||||
var tag string
|
||||
rows.Scan(&tag)
|
||||
|
||||
nonExistantTags = append(nonExistantTags, tag)
|
||||
}
|
||||
|
||||
return nonExistantTags, nil
|
||||
}
|
||||
|
||||
func (m TagModel) Save(ctx context.Context, userId uuid.UUID, tags []string) error {
|
||||
tagsToInsert, err := m.getNonExistantTags(ctx, userId, tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(tagsToInsert) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
stmt := UserTags.INSERT(UserTags.UserID, UserTags.Tag)
|
||||
|
||||
for _, tag := range tagsToInsert {
|
||||
stmt = stmt.VALUES(UUID(userId), tag)
|
||||
}
|
||||
|
||||
_, err = stmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m TagModel) List(ctx context.Context, userId uuid.UUID) ([]model.UserTags, error) {
|
||||
listTagsStmt := UserTags.SELECT(UserTags.AllColumns).WHERE(UserTags.UserID.EQ(UUID(userId)))
|
||||
|
||||
userTags := []model.UserTags{}
|
||||
|
||||
err := listTagsStmt.QueryContext(ctx, m.dbPool, &userTags)
|
||||
|
||||
return userTags, err
|
||||
}
|
||||
|
||||
func (m TagModel) SaveToImage(ctx context.Context, imageId uuid.UUID, tags []string) error {
|
||||
if len(tags) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
userId, err := getUserIdFromImage(ctx, m.dbPool, imageId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.Save(ctx, userId, tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userTagsExpression := make([]Expression, 0)
|
||||
for _, tag := range tags {
|
||||
userTagsExpression = append(userTagsExpression, String(tag))
|
||||
}
|
||||
|
||||
userTags := make([]model.UserTags, 0)
|
||||
|
||||
getTagsStmt := UserTags.SELECT(
|
||||
UserTags.ID, UserTags.Tag,
|
||||
).WHERE(UserTags.Tag.IN(userTagsExpression...))
|
||||
err = getTagsStmt.Query(m.dbPool, &userTags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stmt := ImageTags.INSERT(ImageTags.ImageID, ImageTags.TagID)
|
||||
|
||||
for _, t := range userTags {
|
||||
stmt = stmt.VALUES(imageId, t.ID)
|
||||
}
|
||||
|
||||
_, err = stmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func NewTagModel(db *sql.DB) TagModel {
|
||||
return TagModel{dbPool: db}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type TextModel struct {
|
||||
dbPool *sql.DB
|
||||
}
|
||||
|
||||
func (m TextModel) Save(ctx context.Context, imageId uuid.UUID, texts []string) error {
|
||||
if len(texts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
saveImageTextStmt := ImageText.INSERT(ImageText.ImageID, ImageText.ImageText)
|
||||
|
||||
for _, t := range texts {
|
||||
saveImageTextStmt = saveImageTextStmt.VALUES(imageId, t)
|
||||
}
|
||||
|
||||
saveImageTextStmt.RETURNING(ImageText.AllColumns)
|
||||
|
||||
_, err := saveImageTextStmt.ExecContext(ctx, m.dbPool)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func NewTextModel(db *sql.DB) TextModel {
|
||||
return TextModel{dbPool: db}
|
||||
}
|
@ -3,13 +3,11 @@ package models
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"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"
|
||||
)
|
||||
|
||||
@ -21,89 +19,6 @@ 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 []struct {
|
||||
model.Events
|
||||
|
||||
Location *model.Locations
|
||||
Organizer *model.Contacts
|
||||
}
|
||||
|
||||
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)))
|
||||
|
||||
fmt.Println(listWithPropertiesStmt.DebugSql())
|
||||
|
||||
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) {
|
||||
@ -115,6 +30,93 @@ func (m UserModel) GetUserIdFromEmail(ctx context.Context, email string) (uuid.U
|
||||
return user.ID, err
|
||||
}
|
||||
|
||||
func (m UserModel) DoesUserExist(ctx context.Context, email string) bool {
|
||||
getUserIdStmt := Users.SELECT(Users.ID).WHERE(Users.Email.EQ(String(email)))
|
||||
|
||||
user := model.Users{}
|
||||
err := getUserIdStmt.QueryContext(ctx, m.dbPool, &user)
|
||||
|
||||
return err != qrm.ErrNoRows
|
||||
}
|
||||
|
||||
func (m UserModel) Save(ctx context.Context, user model.Users) (model.Users, error) {
|
||||
insertUserStmt := Users.INSERT(Users.Email).VALUES(user.Email).RETURNING(Users.AllColumns)
|
||||
|
||||
insertedUser := model.Users{}
|
||||
err := insertUserStmt.QueryContext(ctx, m.dbPool, &insertedUser)
|
||||
|
||||
return insertedUser, err
|
||||
}
|
||||
|
||||
type UserImageWithImage struct {
|
||||
model.UserImages
|
||||
|
||||
Image struct {
|
||||
model.Image
|
||||
ImageLists []model.ImageLists
|
||||
}
|
||||
}
|
||||
|
||||
func (m UserModel) GetUserImages(ctx context.Context, userId uuid.UUID) ([]UserImageWithImage, error) {
|
||||
getUserImagesStmt := SELECT(
|
||||
UserImages.AllColumns,
|
||||
Image.ID,
|
||||
Image.ImageName,
|
||||
Image.Description,
|
||||
ImageLists.AllColumns,
|
||||
).
|
||||
FROM(
|
||||
UserImages.
|
||||
INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID)).
|
||||
LEFT_JOIN(ImageLists, ImageLists.ImageID.EQ(UserImages.ImageID)),
|
||||
).
|
||||
WHERE(UserImages.UserID.EQ(UUID(userId)))
|
||||
|
||||
userImages := []UserImageWithImage{}
|
||||
err := getUserImagesStmt.QueryContext(ctx, m.dbPool, &userImages)
|
||||
|
||||
return userImages, err
|
||||
}
|
||||
|
||||
type ListsWithImages struct {
|
||||
model.Lists
|
||||
|
||||
Schema struct {
|
||||
model.Schemas
|
||||
|
||||
SchemaItems []model.SchemaItems
|
||||
}
|
||||
|
||||
Images []struct {
|
||||
model.ImageLists
|
||||
|
||||
Items []model.ImageSchemaItems
|
||||
}
|
||||
}
|
||||
|
||||
func (m UserModel) ListWithImages(ctx context.Context, userId uuid.UUID) ([]ListsWithImages, error) {
|
||||
stmt := SELECT(
|
||||
Lists.AllColumns,
|
||||
ImageLists.AllColumns,
|
||||
Schemas.AllColumns,
|
||||
SchemaItems.AllColumns,
|
||||
ImageSchemaItems.AllColumns,
|
||||
).
|
||||
FROM(
|
||||
Lists.
|
||||
INNER_JOIN(Schemas, Schemas.ListID.EQ(Lists.ID)).
|
||||
INNER_JOIN(SchemaItems, SchemaItems.SchemaID.EQ(Schemas.ID)).
|
||||
LEFT_JOIN(ImageLists, ImageLists.ListID.EQ(Lists.ID)).
|
||||
LEFT_JOIN(ImageSchemaItems, ImageSchemaItems.ImageID.EQ(ImageLists.ImageID)),
|
||||
).
|
||||
WHERE(Lists.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}
|
||||
}
|
||||
|
97
backend/notifications.go
Normal file
97
backend/notifications.go
Normal file
@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
type Notifier[TNotification any] struct {
|
||||
bufferSize int
|
||||
|
||||
Listeners map[string]chan TNotification
|
||||
}
|
||||
|
||||
func (n *Notifier[TNotification]) Create(id string) error {
|
||||
if _, exists := n.Listeners[id]; exists {
|
||||
return errors.New("This listener already exists")
|
||||
}
|
||||
|
||||
n.Listeners[id] = make(chan TNotification, n.bufferSize)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var ChannelFullErr = errors.New("Channel is full")
|
||||
|
||||
// Ensures the listener exists before sending
|
||||
func (n *Notifier[TNotification]) SendAndCreate(id string, notification TNotification) error {
|
||||
if _, exists := n.Listeners[id]; !exists {
|
||||
if err := n.Create(id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ch := n.Listeners[id]
|
||||
|
||||
select {
|
||||
case ch <- notification:
|
||||
return nil
|
||||
default:
|
||||
return ChannelFullErr
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Notifier[TNotification]) Delete(id string) error {
|
||||
if _, exists := n.Listeners[id]; !exists {
|
||||
return errors.New("This listener does not exists")
|
||||
}
|
||||
|
||||
delete(n.Listeners, id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewNotifier[TNotification any](bufferSize int) Notifier[TNotification] {
|
||||
return Notifier[TNotification]{
|
||||
bufferSize: bufferSize,
|
||||
Listeners: make(map[string]chan TNotification),
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
|
||||
type ChannelSplitter[TNotification any] struct {
|
||||
ch chan TNotification
|
||||
|
||||
Listeners map[string]chan TNotification
|
||||
}
|
||||
|
||||
func (s *ChannelSplitter[TNotification]) Listen() {
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case msg := <-s.ch:
|
||||
for _, v := range s.Listeners {
|
||||
v <- msg
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *ChannelSplitter[TNotification]) Add(id string) chan TNotification {
|
||||
ch := make(chan TNotification)
|
||||
s.Listeners[id] = ch
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
func (s *ChannelSplitter[TNotification]) Remove(id string) {
|
||||
delete(s.Listeners, id)
|
||||
}
|
||||
|
||||
func NewChannelSplitter[TNotification any](ch chan TNotification) ChannelSplitter[TNotification] {
|
||||
return ChannelSplitter[TNotification]{
|
||||
ch: ch,
|
||||
Listeners: make(map[string]chan TNotification),
|
||||
}
|
||||
}
|
48
backend/notifications_test.go
Normal file
48
backend/notifications_test.go
Normal file
@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSendingNotifications(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
notifier := NewNotifier[string](3)
|
||||
|
||||
err := notifier.SendAndCreate("1", "a")
|
||||
require.NoError(err)
|
||||
|
||||
err = notifier.SendAndCreate("1", "b")
|
||||
require.NoError(err)
|
||||
|
||||
err = notifier.SendAndCreate("1", "c")
|
||||
require.NoError(err)
|
||||
|
||||
ch := notifier.Listeners["1"]
|
||||
|
||||
a := <-ch
|
||||
b := <-ch
|
||||
c := <-ch
|
||||
|
||||
assert.Equal(a, "a")
|
||||
assert.Equal(b, "b")
|
||||
assert.Equal(c, "c")
|
||||
}
|
||||
|
||||
func TestFullBuffer(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
notifier := NewNotifier[string](1)
|
||||
|
||||
err := notifier.SendAndCreate("1", "a")
|
||||
require.NoError(err)
|
||||
|
||||
err = notifier.SendAndCreate("1", "b")
|
||||
|
||||
assert.Error(err)
|
||||
}
|
74
backend/router.go
Normal file
74
backend/router.go
Normal file
@ -0,0 +1,74 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/auth"
|
||||
"screenmark/screenmark/images"
|
||||
"screenmark/screenmark/limits"
|
||||
"screenmark/screenmark/models"
|
||||
"screenmark/screenmark/stacks"
|
||||
|
||||
ourmiddleware "screenmark/screenmark/middleware"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
)
|
||||
|
||||
type TestAiClient struct {
|
||||
ImageInfo client.ImageMessageContent
|
||||
}
|
||||
|
||||
func (client TestAiClient) GetImageInfo(imageName string, imageData []byte) (client.ImageMessageContent, error) {
|
||||
return client.ImageInfo, nil
|
||||
}
|
||||
|
||||
func setupRouter(db *sql.DB) chi.Router {
|
||||
imageModel := models.NewImageModel(db)
|
||||
stackModel := models.NewListModel(db)
|
||||
|
||||
limitsManager := limits.CreateLimitsManager(db)
|
||||
|
||||
stackHandler := stacks.CreateStackHandler(db, limitsManager)
|
||||
authHandler := auth.CreateAuthHandler(db)
|
||||
imageHandler := images.CreateImageHandler(db, limitsManager)
|
||||
|
||||
notifier := NewNotifier[Notification](10)
|
||||
|
||||
// Only start event listeners if not in test environment
|
||||
if os.Getenv("GO_TEST_ENVIRONMENT") != "true" {
|
||||
|
||||
// TODO: should extract these into a notification manager
|
||||
// And actually make them the same code.
|
||||
// The events are basically the same.
|
||||
|
||||
go ListenNewImageEvents(db)
|
||||
go ListenProcessingImageStatus(db, imageModel, ¬ifier)
|
||||
go ListenNewStackEvents(db)
|
||||
go ListenProcessingStackStatus(db, stackModel, ¬ifier)
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(ourmiddleware.CorsMiddleware)
|
||||
|
||||
r.Route("/stacks", stackHandler.CreateRoutes)
|
||||
r.Route("/auth", authHandler.CreateRoutes)
|
||||
r.Route("/images", imageHandler.CreateRoutes)
|
||||
|
||||
r.Route("/notifications", func(r chi.Router) {
|
||||
r.Use(ourmiddleware.GetUserIdFromUrl)
|
||||
|
||||
r.Get("/", CreateEventsHandler(¬ifier))
|
||||
})
|
||||
|
||||
logWriter := DatabaseWriter{
|
||||
dbPool: db,
|
||||
}
|
||||
|
||||
r.Route("/logs", createLogHandler(&logWriter))
|
||||
|
||||
return r
|
||||
}
|
@ -4,7 +4,7 @@ CREATE SCHEMA haystack;
|
||||
|
||||
/* -----| Enums |----- */
|
||||
|
||||
CREATE TYPE haystack.progress AS ENUM('not-started','in-progress');
|
||||
CREATE TYPE haystack.progress AS ENUM('not-started','in-progress', 'complete');
|
||||
|
||||
/* -----| Schema tables |----- */
|
||||
|
||||
@ -16,6 +16,7 @@ CREATE TABLE haystack.users (
|
||||
CREATE TABLE haystack.image (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
image_name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
image BYTEA NOT NULL
|
||||
);
|
||||
|
||||
@ -29,127 +30,74 @@ CREATE TABLE haystack.user_images_to_process (
|
||||
CREATE TABLE haystack.user_images (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
image_id uuid NOT NULL UNIQUE REFERENCES haystack.image (id),
|
||||
user_id uuid NOT NULL REFERENCES haystack.users (id)
|
||||
user_id uuid NOT NULL REFERENCES haystack.users (id),
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.user_tags (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tag VARCHAR(32) UNIQUE NOT NULL,
|
||||
user_id uuid NOT NULL REFERENCES haystack.users (id)
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.image_tags (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tag_id UUID NOT NULL REFERENCES haystack.user_tags (id),
|
||||
image_id UUID NOT NULL REFERENCES haystack.image (id)
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.image_text (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
image_text TEXT NOT NULL,
|
||||
image_id UUID NOT NULL REFERENCES haystack.image (id)
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.image_links (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
link TEXT NOT NULL,
|
||||
image_id UUID NOT NULL REFERENCES haystack.image (id)
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.locations (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL,
|
||||
address TEXT,
|
||||
description TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.image_locations (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
location_id UUID NOT NULL REFERENCES haystack.locations (id),
|
||||
image_id UUID NOT NULL REFERENCES haystack.image (id)
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.user_locations (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
location_id UUID NOT NULL REFERENCES haystack.locations (id),
|
||||
user_id UUID NOT NULL REFERENCES haystack.users (id)
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.contacts (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- It seems name and description are frequent. We could use table inheritance.
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
phone_number TEXT,
|
||||
email TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.user_contacts (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES haystack.users (id),
|
||||
contact_id UUID NOT NULL REFERENCES haystack.contacts (id)
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.image_contacts (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
CREATE TABLE haystack.logs (
|
||||
log TEXT NOT NULL,
|
||||
image_id UUID NOT NULL REFERENCES haystack.image (id),
|
||||
contact_id UUID NOT NULL REFERENCES haystack.contacts (id)
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.events (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- It seems name and description are frequent. We could use table inheritance.
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
start_date_time TIMESTAMP,
|
||||
end_date_time TIMESTAMP,
|
||||
|
||||
location_id UUID REFERENCES haystack.locations (id),
|
||||
organizer_id UUID REFERENCES haystack.contacts (id)
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.image_events (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_id UUID NOT NULL REFERENCES haystack.events (id),
|
||||
image_id UUID NOT NULL REFERENCES haystack.image (id)
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.user_events (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_id UUID NOT NULL REFERENCES haystack.events (id),
|
||||
user_id UUID NOT NULL REFERENCES haystack.users (id)
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.notes (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
-- It seems name and description are frequent. We could use table inheritance.
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
content TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.image_notes (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
image_id UUID NOT NULL REFERENCES haystack.image (id),
|
||||
note_id UUID NOT NULL REFERENCES haystack.notes (id)
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.user_notes (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
CREATE TABLE haystack.lists (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES haystack.users (id),
|
||||
note_id UUID NOT NULL REFERENCES haystack.notes (id)
|
||||
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.processing_lists (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES haystack.users (id),
|
||||
|
||||
title TEXT NOT NULL,
|
||||
fields TEXT NOT NULL,
|
||||
|
||||
status haystack.progress NOT NULL DEFAULT 'not-started',
|
||||
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.image_lists (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
image_id UUID NOT NULL REFERENCES haystack.image (id),
|
||||
list_id UUID NOT NULL REFERENCES haystack.lists (id)
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.schemas (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
list_id UUID NOT NULL REFERENCES haystack.lists (id)
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.schema_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
item TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
|
||||
schema_id UUID NOT NULL REFERENCES haystack.schemas (id)
|
||||
);
|
||||
|
||||
CREATE TABLE haystack.image_schema_items (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
value TEXT,
|
||||
|
||||
schema_item_id UUID NOT NULL REFERENCES haystack.schema_items (id),
|
||||
image_id UUID NOT NULL REFERENCES haystack.image (id)
|
||||
);
|
||||
|
||||
/* -----| Indexes |----- */
|
||||
|
||||
CREATE INDEX user_tags_index ON haystack.user_tags(tag);
|
||||
|
||||
/* -----| Stored Procedures |----- */
|
||||
|
||||
CREATE OR REPLACE FUNCTION notify_new_image()
|
||||
@ -163,7 +111,25 @@ $$ LANGUAGE plpgsql;
|
||||
CREATE OR REPLACE FUNCTION notify_new_processing_image_status()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify('new_processing_image_status', NEW.id::text || NEW.status::text);
|
||||
IF NEW.status <> 'not-started' THEN
|
||||
PERFORM pg_notify('new_processing_image_status', NEW.id::text || NEW.status::text);
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION notify_new_stacks()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify('new_stack', NEW.id::text);
|
||||
RETURN NEW;
|
||||
END
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION notify_new_processing_stack_status()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify('new_processing_stack_status', NEW.id::text || NEW.status::text);
|
||||
RETURN NEW;
|
||||
END
|
||||
$$ LANGUAGE plpgsql;
|
||||
@ -181,102 +147,13 @@ ON haystack.user_images_to_process
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE notify_new_processing_image_status();
|
||||
|
||||
/* -----| Test Data |----- */
|
||||
CREATE OR REPLACE TRIGGER on_new_image AFTER INSERT
|
||||
ON haystack.processing_lists
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE notify_new_stacks();
|
||||
|
||||
-- Insert a user
|
||||
INSERT INTO haystack.users (id, email) VALUES ('1db09f34-b155-4bf2-b606-dda25365fc89', 'me@email.com');
|
||||
|
||||
-- Insert images
|
||||
INSERT INTO haystack.image (id, image_name, image) VALUES
|
||||
('3bd3fa04-e4b4-4ffb-b282-d573a092eb71', 'Sample Image 1', 'sample_image_1_bytes'),
|
||||
('f4560a78-d5d3-433e-8d90-b75c66e25423', 'Sample Image 2', 'sample_image_2_bytes');
|
||||
|
||||
-- Insert user images to process
|
||||
INSERT INTO haystack.user_images_to_process (id, image_id, user_id) VALUES
|
||||
('abe3679c-e787-4670-b5da-570453938f18', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71', '1db09f34-b155-4bf2-b606-dda25365fc89'),
|
||||
('8f3727e8-03fa-49bf-b0fe-ba8762df0902', 'f4560a78-d5d3-433e-8d90-b75c66e25423', '1db09f34-b155-4bf2-b606-dda25365fc89');
|
||||
|
||||
-- Insert user images
|
||||
INSERT INTO haystack.user_images (id, image_id, user_id) VALUES
|
||||
('28ade3a5-30c0-4f0a-93ff-5d062ba5c253', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71', '1db09f34-b155-4bf2-b606-dda25365fc89'),
|
||||
('c9425f01-a496-4c0a-919e-54b58c8ba600', 'f4560a78-d5d3-433e-8d90-b75c66e25423', '1db09f34-b155-4bf2-b606-dda25365fc89');
|
||||
|
||||
-- Insert user tags
|
||||
INSERT INTO haystack.user_tags (id, tag, user_id) VALUES
|
||||
('118c9491-a1ea-4930-88ee-33edfbc61cd3', 'vacation', '1db09f34-b155-4bf2-b606-dda25365fc89'),
|
||||
('c3e8c00a-4af6-45c6-acc3-53aa7ce2024a', 'family', '1db09f34-b155-4bf2-b606-dda25365fc89');
|
||||
|
||||
-- Insert image tags
|
||||
INSERT INTO haystack.image_tags (id, tag_id, image_id) VALUES
|
||||
('38ec5481-7b09-4e50-98b8-a85bbd5f6c6e', '118c9491-a1ea-4930-88ee-33edfbc61cd3', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71'),
|
||||
('9d64f58e-1d61-4c97-ae8b-a38bc3519fe1', 'c3e8c00a-4af6-45c6-acc3-53aa7ce2024a', 'f4560a78-d5d3-433e-8d90-b75c66e25423');
|
||||
|
||||
-- Insert image text
|
||||
INSERT INTO haystack.image_text (id, image_text, image_id) VALUES
|
||||
('fdd7a9f4-2a9a-494e-89d2-a63df8e45d62', 'Sample text for image 1', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71'),
|
||||
('95516f15-575c-485b-92ab-22eb18a306c1', 'Sample text for image 2', 'f4560a78-d5d3-433e-8d90-b75c66e25423');
|
||||
|
||||
-- Insert image links
|
||||
INSERT INTO haystack.image_links (id, link, image_id) VALUES
|
||||
('bbcc284f-c1f6-47ac-8d54-65b7729f03be', 'http://example.com/image1', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71'),
|
||||
('7391b2d1-6141-4195-8a4c-9c8ba4491b5a', 'http://example.com/image2', 'f4560a78-d5d3-433e-8d90-b75c66e25423');
|
||||
|
||||
-- Insert locations
|
||||
INSERT INTO haystack.locations (id, name, address, description) VALUES
|
||||
('5ac6f116-c21a-408b-9d2b-e8227a9a8503', 'Sample Location 1', '123 Sample St', 'A sample location'),
|
||||
('cd4b1815-5019-406d-9f1d-e9e5ac34c5f1', 'Sample Location 2', '456 Sample Ave', 'Another sample location');
|
||||
|
||||
-- Insert image locations
|
||||
INSERT INTO haystack.image_locations (id, location_id, image_id) VALUES
|
||||
('0e0c5cc2-b5b3-4b26-9d9c-2517b9358eb3', '5ac6f116-c21a-408b-9d2b-e8227a9a8503', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71'),
|
||||
('98facc74-cfc0-41cd-87e1-5e3822ae3407', 'cd4b1815-5019-406d-9f1d-e9e5ac34c5f1', 'f4560a78-d5d3-433e-8d90-b75c66e25423');
|
||||
|
||||
-- Insert user locations
|
||||
INSERT INTO haystack.user_locations (id, location_id, user_id) VALUES
|
||||
('1427ca1c-293f-4fab-b813-2acf145715f5', '5ac6f116-c21a-408b-9d2b-e8227a9a8503', '1db09f34-b155-4bf2-b606-dda25365fc89'),
|
||||
('343f9321-f63d-4248-aaab-3a1264d9cb5e', 'cd4b1815-5019-406d-9f1d-e9e5ac34c5f1', '1db09f34-b155-4bf2-b606-dda25365fc89');
|
||||
|
||||
-- Insert contacts
|
||||
INSERT INTO haystack.contacts (id, name, description, phone_number, email) VALUES
|
||||
('943be2ab-4db4-4e4e-bd1c-b78ad96df0d1', 'Contact 1', 'Sample contact description', '123-456-7890', 'contact1@example.com'),
|
||||
('09e2bf18-09b7-4553-971e-45136bd5b12f', 'Contact 2', 'Another sample contact description', '098-765-4321', 'contact2@example.com');
|
||||
|
||||
-- Insert user contacts
|
||||
INSERT INTO haystack.user_contacts (id, user_id, contact_id) VALUES
|
||||
('d74125e4-cbe4-4b83-8432-e0a3206af91c', '1db09f34-b155-4bf2-b606-dda25365fc89', '943be2ab-4db4-4e4e-bd1c-b78ad96df0d1'),
|
||||
('46e8cbd4-46a6-4499-9575-d3aad003fd1c', '1db09f34-b155-4bf2-b606-dda25365fc89', '09e2bf18-09b7-4553-971e-45136bd5b12f');
|
||||
|
||||
-- Insert image contacts
|
||||
INSERT INTO haystack.image_contacts (id, image_id, contact_id) VALUES
|
||||
('db075381-e89b-4582-800e-07561f9139e8', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71', '943be2ab-4db4-4e4e-bd1c-b78ad96df0d1'),
|
||||
('7384970d-3d3c-4e29-b158-edf200c53169', 'f4560a78-d5d3-433e-8d90-b75c66e25423', '09e2bf18-09b7-4553-971e-45136bd5b12f');
|
||||
|
||||
-- Insert events
|
||||
INSERT INTO haystack.events (id, name, description, start_date_time, end_date_time, location_id, organizer_id) VALUES
|
||||
('24a9dcbc-f8dc-4fca-835b-7ea57850d0b7', 'Sample Event 1', 'A sample event description', '2023-01-01 10:00:00', '2023-01-01 12:00:00', '5ac6f116-c21a-408b-9d2b-e8227a9a8503', '943be2ab-4db4-4e4e-bd1c-b78ad96df0d1'),
|
||||
('9cb6b0ae-3b02-4343-9858-5a07dd248562', 'Sample Event 2', 'Another sample event description', '2023-02-01 14:00:00', '2023-02-01 16:00:00', 'cd4b1815-5019-406d-9f1d-e9e5ac34c5f1', '09e2bf18-09b7-4553-971e-45136bd5b12f');
|
||||
|
||||
-- Insert image events
|
||||
INSERT INTO haystack.image_events (id, event_id, image_id) VALUES
|
||||
('5268a005-b3eb-4a30-8823-c8e9666507bb', '24a9dcbc-f8dc-4fca-835b-7ea57850d0b7', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71'),
|
||||
('9d6d4d26-c2a2-427f-92ed-34dc8c2d3e5f', '9cb6b0ae-3b02-4343-9858-5a07dd248562', 'f4560a78-d5d3-433e-8d90-b75c66e25423');
|
||||
|
||||
-- Insert user events
|
||||
INSERT INTO haystack.user_events (id, event_id, user_id) VALUES
|
||||
('16d815e4-6387-4fe9-b31d-5baff0567345', '24a9dcbc-f8dc-4fca-835b-7ea57850d0b7', '1db09f34-b155-4bf2-b606-dda25365fc89'),
|
||||
('43078366-d265-4ff9-9210-e11680bd6bcd', '9cb6b0ae-3b02-4343-9858-5a07dd248562', '1db09f34-b155-4bf2-b606-dda25365fc89');
|
||||
|
||||
-- Insert notes
|
||||
INSERT INTO haystack.notes (id, name, description, content) VALUES
|
||||
('6524f6b9-c659-409e-b2a0-abd3c3f5b5bb', 'Sample Note 1', 'A sample note description', 'This is the content of the sample note 1'),
|
||||
('a274b9b3-024f-457d-b4a0-d4535c2cca54', 'Sample Note 2', 'Another sample note description', 'This is the content of the sample note 2');
|
||||
|
||||
-- Insert image notes
|
||||
INSERT INTO haystack.image_notes (id, image_id, note_id) VALUES
|
||||
('6062fceb-7b3f-41fb-8509-489218968204', '3bd3fa04-e4b4-4ffb-b282-d573a092eb71', '6524f6b9-c659-409e-b2a0-abd3c3f5b5bb'),
|
||||
('956dd3f6-4513-4cbc-9a5e-03dbec769402', 'f4560a78-d5d3-433e-8d90-b75c66e25423', 'a274b9b3-024f-457d-b4a0-d4535c2cca54');
|
||||
|
||||
-- Insert user notes
|
||||
INSERT INTO haystack.user_notes (id, user_id, note_id) VALUES
|
||||
('e3fa7a74-acbf-4aa9-930b-f10bd8a6ced5', '1db09f34-b155-4bf2-b606-dda25365fc89', '6524f6b9-c659-409e-b2a0-abd3c3f5b5bb'),
|
||||
('ebaef76b-3b78-491c-93f7-19510080284d', '1db09f34-b155-4bf2-b606-dda25365fc89', 'a274b9b3-024f-457d-b4a0-d4535c2cca54');
|
||||
CREATE OR REPLACE TRIGGER on_update_stack_progress
|
||||
AFTER UPDATE OF status
|
||||
ON haystack.processing_lists
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE notify_new_processing_stack_status();
|
||||
|
164
backend/stacks/handler.go
Normal file
164
backend/stacks/handler.go
Normal file
@ -0,0 +1,164 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/limits"
|
||||
"screenmark/screenmark/middleware"
|
||||
"screenmark/screenmark/models"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type StackHandler struct {
|
||||
logger *log.Logger
|
||||
stackModel models.ListModel
|
||||
limitsManager limits.LimitsManagerMethods
|
||||
}
|
||||
|
||||
func (h *StackHandler) getAllStacks(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
userID, err := middleware.GetUserID(ctx, h.logger, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
lists, err := h.stackModel.List(ctx, userID)
|
||||
if err != nil {
|
||||
h.logger.Warn("could not get stacks", "err", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
middleware.WriteJsonOrError(h.logger, lists, w)
|
||||
}
|
||||
|
||||
func (h *StackHandler) getStackItems(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
_, err := middleware.GetUserID(ctx, h.logger, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
listID, err := middleware.GetPathParamID(h.logger, "listID", w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: must check for permission here.
|
||||
|
||||
lists, err := h.stackModel.ListItems(ctx, listID)
|
||||
if err != nil {
|
||||
h.logger.Warn("could not get list items", "err", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
middleware.WriteJsonOrError(h.logger, lists, w)
|
||||
}
|
||||
|
||||
type EditStack struct {
|
||||
Hello string `json:"hello"`
|
||||
}
|
||||
|
||||
func (h *StackHandler) editStack(req EditStack, w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
func (h *StackHandler) deleteStack(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
userID, err := middleware.GetUserID(ctx, h.logger, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
listID, err := middleware.GetPathParamID(h.logger, "listID", w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = h.stackModel.Delete(ctx, listID, userID)
|
||||
if err != nil {
|
||||
h.logger.Warn("could not delete stack", "err", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
type CreateStackBody struct {
|
||||
Title string `json:"title"`
|
||||
|
||||
// We want a regular string because AI will take care of creating these for us.
|
||||
Fields string `json:"fields"`
|
||||
}
|
||||
|
||||
func (h *StackHandler) createStack(body CreateStackBody, w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
userID, err := middleware.GetUserID(ctx, h.logger, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Convert fields string to basic schema items
|
||||
// For now, create a simple schema item for each field
|
||||
var schemaItems []SchemaItems
|
||||
if body.Fields != "" {
|
||||
fields := strings.Split(body.Fields, ",")
|
||||
for i, field := range fields {
|
||||
field = strings.TrimSpace(field)
|
||||
if field != "" {
|
||||
schemaItems = append(schemaItems, SchemaItems{
|
||||
Item: field,
|
||||
Value: "",
|
||||
Description: fmt.Sprintf("Field %d: %s", i+1, field),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use empty description for now since the API doesn't provide one
|
||||
_, err = h.stackModel.Save(ctx, userID, body.Title, "", schemaItems)
|
||||
if err != nil {
|
||||
h.logger.Warn("could not save stack", "err", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (h *StackHandler) CreateRoutes(r chi.Router) {
|
||||
h.logger.Info("Mounting stack router")
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.ProtectedRoute)
|
||||
r.Use(middleware.SetJson)
|
||||
|
||||
r.Get("/", h.getAllStacks)
|
||||
r.Get("/{listID}", h.getStackItems)
|
||||
|
||||
r.Post("/", middleware.WithLimit(h.logger, h.limitsManager.HasReachedStackLimit, middleware.WithValidatedPost(h.createStack)))
|
||||
r.Patch("/{listID}", middleware.WithValidatedPost(h.editStack))
|
||||
r.Delete("/{listID}", h.deleteStack)
|
||||
})
|
||||
}
|
||||
|
||||
func CreateStackHandler(db *sql.DB, limitsManager limits.LimitsManagerMethods) StackHandler {
|
||||
stackModel := models.NewListModel(db)
|
||||
logger := log.New(os.Stdout).WithPrefix("Stacks")
|
||||
|
||||
return StackHandler{
|
||||
logger,
|
||||
stackModel,
|
||||
limitsManager,
|
||||
}
|
||||
}
|
4
backup.bash
Normal file
4
backup.bash
Normal file
@ -0,0 +1,4 @@
|
||||
name=haystack-db-dump-$(date "+%Y-%m-%d").sql
|
||||
pg_dump haystack > $name
|
||||
rsync -avH $name zh3586@zh3586.rsync.net:Backups/Haystack/
|
||||
rm $name
|
BIN
frontend/.DS_Store
vendored
Normal file
BIN
frontend/.DS_Store
vendored
Normal file
Binary file not shown.
2
frontend/.gitignore
vendored
2
frontend/.gitignore
vendored
@ -3,3 +3,5 @@ db
|
||||
screenmark
|
||||
node_modules
|
||||
dist
|
||||
tsconfig.node.tsbuildinfo
|
||||
tsconfig.tsbuildinfo
|
||||
|
3
frontend/.idea/.gitignore
generated
vendored
Normal file
3
frontend/.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user