Compare commits
291 Commits
feat/split
...
3a182fc49b
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 {
|
var Progress = &struct {
|
||||||
NotStarted postgres.StringExpression
|
NotStarted postgres.StringExpression
|
||||||
InProgress postgres.StringExpression
|
InProgress postgres.StringExpression
|
||||||
|
Complete postgres.StringExpression
|
||||||
}{
|
}{
|
||||||
NotStarted: postgres.NewEnumValue("not-started"),
|
NotStarted: postgres.NewEnumValue("not-started"),
|
||||||
InProgress: postgres.NewEnumValue("in-progress"),
|
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 {
|
type Image struct {
|
||||||
ID uuid.UUID `sql:"primary_key"`
|
ID uuid.UUID `sql:"primary_key"`
|
||||||
ImageName string
|
ImageName string
|
||||||
Image []byte
|
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"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ImageNotes struct {
|
type ImageLists struct {
|
||||||
ID uuid.UUID `sql:"primary_key"`
|
ID uuid.UUID `sql:"primary_key"`
|
||||||
ImageID uuid.UUID
|
ImageID uuid.UUID
|
||||||
NoteID uuid.UUID
|
ListID uuid.UUID
|
||||||
}
|
}
|
||||||
@ -11,8 +11,9 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ImageLocations struct {
|
type ImageSchemaItems struct {
|
||||||
ID uuid.UUID `sql:"primary_key"`
|
ID uuid.UUID `sql:"primary_key"`
|
||||||
LocationID uuid.UUID
|
Value *string
|
||||||
ImageID uuid.UUID
|
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 (
|
import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Contacts struct {
|
type Lists struct {
|
||||||
ID uuid.UUID `sql:"primary_key"`
|
ID uuid.UUID `sql:"primary_key"`
|
||||||
|
UserID uuid.UUID
|
||||||
Name string
|
Name string
|
||||||
Description *string
|
Description string
|
||||||
PhoneNumber *string
|
CreatedAt *time.Time
|
||||||
Email *string
|
|
||||||
}
|
}
|
||||||
@ -9,10 +9,11 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ImageText struct {
|
type Logs struct {
|
||||||
ID uuid.UUID `sql:"primary_key"`
|
Log string
|
||||||
ImageText string
|
|
||||||
ImageID uuid.UUID
|
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 (
|
import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserContacts struct {
|
type ProcessingLists struct {
|
||||||
ID uuid.UUID `sql:"primary_key"`
|
ID uuid.UUID `sql:"primary_key"`
|
||||||
UserID uuid.UUID
|
UserID uuid.UUID
|
||||||
ContactID uuid.UUID
|
Title string
|
||||||
|
Fields string
|
||||||
|
Status Progress
|
||||||
|
CreatedAt *time.Time
|
||||||
}
|
}
|
||||||
@ -14,11 +14,13 @@ type Progress string
|
|||||||
const (
|
const (
|
||||||
Progress_NotStarted Progress = "not-started"
|
Progress_NotStarted Progress = "not-started"
|
||||||
Progress_InProgress Progress = "in-progress"
|
Progress_InProgress Progress = "in-progress"
|
||||||
|
Progress_Complete Progress = "complete"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ProgressAllValues = []Progress{
|
var ProgressAllValues = []Progress{
|
||||||
Progress_NotStarted,
|
Progress_NotStarted,
|
||||||
Progress_InProgress,
|
Progress_InProgress,
|
||||||
|
Progress_Complete,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Progress) Scan(value interface{}) error {
|
func (e *Progress) Scan(value interface{}) error {
|
||||||
@ -37,6 +39,8 @@ func (e *Progress) Scan(value interface{}) error {
|
|||||||
*e = Progress_NotStarted
|
*e = Progress_NotStarted
|
||||||
case "in-progress":
|
case "in-progress":
|
||||||
*e = Progress_InProgress
|
*e = Progress_InProgress
|
||||||
|
case "complete":
|
||||||
|
*e = Progress_Complete
|
||||||
default:
|
default:
|
||||||
return errors.New("jet: Invalid scan value '" + enumValue + "' for Progress enum")
|
return errors.New("jet: Invalid scan value '" + enumValue + "' for Progress enum")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,9 +11,10 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Locations struct {
|
type SchemaItems struct {
|
||||||
ID uuid.UUID `sql:"primary_key"`
|
ID uuid.UUID `sql:"primary_key"`
|
||||||
Name string
|
Item string
|
||||||
Address *string
|
Value string
|
||||||
Description *string
|
Description string
|
||||||
|
SchemaID uuid.UUID
|
||||||
}
|
}
|
||||||
@ -11,8 +11,7 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserTags struct {
|
type Schemas struct {
|
||||||
ID uuid.UUID `sql:"primary_key"`
|
ID uuid.UUID `sql:"primary_key"`
|
||||||
Tag string
|
ListID 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 UserEvents struct {
|
|
||||||
ID uuid.UUID `sql:"primary_key"`
|
|
||||||
EventID uuid.UUID
|
|
||||||
UserID uuid.UUID
|
|
||||||
}
|
|
||||||
@ -9,10 +9,12 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserImages struct {
|
type UserImages struct {
|
||||||
ID uuid.UUID `sql:"primary_key"`
|
ID uuid.UUID `sql:"primary_key"`
|
||||||
ImageID uuid.UUID
|
ImageID uuid.UUID
|
||||||
UserID 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
|
postgres.Table
|
||||||
|
|
||||||
// Columns
|
// Columns
|
||||||
ID postgres.ColumnString
|
ID postgres.ColumnString
|
||||||
ImageName postgres.ColumnString
|
ImageName postgres.ColumnString
|
||||||
Image postgres.ColumnString
|
Description postgres.ColumnString
|
||||||
|
Image postgres.ColumnBytea
|
||||||
|
|
||||||
AllColumns postgres.ColumnList
|
AllColumns postgres.ColumnList
|
||||||
MutableColumns postgres.ColumnList
|
MutableColumns postgres.ColumnList
|
||||||
|
DefaultColumns postgres.ColumnList
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageTable struct {
|
type ImageTable struct {
|
||||||
@ -60,22 +62,26 @@ func newImageTable(schemaName, tableName, alias string) *ImageTable {
|
|||||||
|
|
||||||
func newImageTableImpl(schemaName, tableName, alias string) imageTable {
|
func newImageTableImpl(schemaName, tableName, alias string) imageTable {
|
||||||
var (
|
var (
|
||||||
IDColumn = postgres.StringColumn("id")
|
IDColumn = postgres.StringColumn("id")
|
||||||
ImageNameColumn = postgres.StringColumn("image_name")
|
ImageNameColumn = postgres.StringColumn("image_name")
|
||||||
ImageColumn = postgres.StringColumn("image")
|
DescriptionColumn = postgres.StringColumn("description")
|
||||||
allColumns = postgres.ColumnList{IDColumn, ImageNameColumn, ImageColumn}
|
ImageColumn = postgres.ByteaColumn("image")
|
||||||
mutableColumns = postgres.ColumnList{ImageNameColumn, ImageColumn}
|
allColumns = postgres.ColumnList{IDColumn, ImageNameColumn, DescriptionColumn, ImageColumn}
|
||||||
|
mutableColumns = postgres.ColumnList{ImageNameColumn, DescriptionColumn, ImageColumn}
|
||||||
|
defaultColumns = postgres.ColumnList{IDColumn}
|
||||||
)
|
)
|
||||||
|
|
||||||
return imageTable{
|
return imageTable{
|
||||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||||
|
|
||||||
//Columns
|
//Columns
|
||||||
ID: IDColumn,
|
ID: IDColumn,
|
||||||
ImageName: ImageNameColumn,
|
ImageName: ImageNameColumn,
|
||||||
Image: ImageColumn,
|
Description: DescriptionColumn,
|
||||||
|
Image: ImageColumn,
|
||||||
|
|
||||||
AllColumns: allColumns,
|
AllColumns: allColumns,
|
||||||
MutableColumns: mutableColumns,
|
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
|
// 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.
|
// this method only once at the beginning of the program.
|
||||||
func UseSchema(schema string) {
|
func UseSchema(schema string) {
|
||||||
Contacts = Contacts.FromSchema(schema)
|
|
||||||
Events = Events.FromSchema(schema)
|
|
||||||
Image = Image.FromSchema(schema)
|
Image = Image.FromSchema(schema)
|
||||||
ImageContacts = ImageContacts.FromSchema(schema)
|
ImageLists = ImageLists.FromSchema(schema)
|
||||||
ImageEvents = ImageEvents.FromSchema(schema)
|
ImageSchemaItems = ImageSchemaItems.FromSchema(schema)
|
||||||
ImageLinks = ImageLinks.FromSchema(schema)
|
Lists = Lists.FromSchema(schema)
|
||||||
ImageLocations = ImageLocations.FromSchema(schema)
|
Logs = Logs.FromSchema(schema)
|
||||||
ImageNotes = ImageNotes.FromSchema(schema)
|
ProcessingLists = ProcessingLists.FromSchema(schema)
|
||||||
ImageTags = ImageTags.FromSchema(schema)
|
SchemaItems = SchemaItems.FromSchema(schema)
|
||||||
ImageText = ImageText.FromSchema(schema)
|
Schemas = Schemas.FromSchema(schema)
|
||||||
Locations = Locations.FromSchema(schema)
|
|
||||||
Notes = Notes.FromSchema(schema)
|
|
||||||
UserContacts = UserContacts.FromSchema(schema)
|
|
||||||
UserEvents = UserEvents.FromSchema(schema)
|
|
||||||
UserImages = UserImages.FromSchema(schema)
|
UserImages = UserImages.FromSchema(schema)
|
||||||
UserImagesToProcess = UserImagesToProcess.FromSchema(schema)
|
UserImagesToProcess = UserImagesToProcess.FromSchema(schema)
|
||||||
UserLocations = UserLocations.FromSchema(schema)
|
|
||||||
UserNotes = UserNotes.FromSchema(schema)
|
|
||||||
UserTags = UserTags.FromSchema(schema)
|
|
||||||
Users = Users.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
|
postgres.Table
|
||||||
|
|
||||||
// Columns
|
// Columns
|
||||||
ID postgres.ColumnString
|
ID postgres.ColumnString
|
||||||
ImageID postgres.ColumnString
|
ImageID postgres.ColumnString
|
||||||
UserID postgres.ColumnString
|
UserID postgres.ColumnString
|
||||||
|
CreatedAt postgres.ColumnTimestampz
|
||||||
|
|
||||||
AllColumns postgres.ColumnList
|
AllColumns postgres.ColumnList
|
||||||
MutableColumns postgres.ColumnList
|
MutableColumns postgres.ColumnList
|
||||||
|
DefaultColumns postgres.ColumnList
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserImagesTable struct {
|
type UserImagesTable struct {
|
||||||
@ -60,22 +62,26 @@ func newUserImagesTable(schemaName, tableName, alias string) *UserImagesTable {
|
|||||||
|
|
||||||
func newUserImagesTableImpl(schemaName, tableName, alias string) userImagesTable {
|
func newUserImagesTableImpl(schemaName, tableName, alias string) userImagesTable {
|
||||||
var (
|
var (
|
||||||
IDColumn = postgres.StringColumn("id")
|
IDColumn = postgres.StringColumn("id")
|
||||||
ImageIDColumn = postgres.StringColumn("image_id")
|
ImageIDColumn = postgres.StringColumn("image_id")
|
||||||
UserIDColumn = postgres.StringColumn("user_id")
|
UserIDColumn = postgres.StringColumn("user_id")
|
||||||
allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, UserIDColumn}
|
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||||
mutableColumns = postgres.ColumnList{ImageIDColumn, UserIDColumn}
|
allColumns = postgres.ColumnList{IDColumn, ImageIDColumn, UserIDColumn, CreatedAtColumn}
|
||||||
|
mutableColumns = postgres.ColumnList{ImageIDColumn, UserIDColumn, CreatedAtColumn}
|
||||||
|
defaultColumns = postgres.ColumnList{IDColumn, CreatedAtColumn}
|
||||||
)
|
)
|
||||||
|
|
||||||
return userImagesTable{
|
return userImagesTable{
|
||||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||||
|
|
||||||
//Columns
|
//Columns
|
||||||
ID: IDColumn,
|
ID: IDColumn,
|
||||||
ImageID: ImageIDColumn,
|
ImageID: ImageIDColumn,
|
||||||
UserID: UserIDColumn,
|
UserID: UserIDColumn,
|
||||||
|
CreatedAt: CreatedAtColumn,
|
||||||
|
|
||||||
AllColumns: allColumns,
|
AllColumns: allColumns,
|
||||||
MutableColumns: mutableColumns,
|
MutableColumns: mutableColumns,
|
||||||
|
DefaultColumns: defaultColumns,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ type userImagesToProcessTable struct {
|
|||||||
|
|
||||||
AllColumns postgres.ColumnList
|
AllColumns postgres.ColumnList
|
||||||
MutableColumns postgres.ColumnList
|
MutableColumns postgres.ColumnList
|
||||||
|
DefaultColumns postgres.ColumnList
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserImagesToProcessTable struct {
|
type UserImagesToProcessTable struct {
|
||||||
@ -67,6 +68,7 @@ func newUserImagesToProcessTableImpl(schemaName, tableName, alias string) userIm
|
|||||||
UserIDColumn = postgres.StringColumn("user_id")
|
UserIDColumn = postgres.StringColumn("user_id")
|
||||||
allColumns = postgres.ColumnList{IDColumn, StatusColumn, ImageIDColumn, UserIDColumn}
|
allColumns = postgres.ColumnList{IDColumn, StatusColumn, ImageIDColumn, UserIDColumn}
|
||||||
mutableColumns = postgres.ColumnList{StatusColumn, ImageIDColumn, UserIDColumn}
|
mutableColumns = postgres.ColumnList{StatusColumn, ImageIDColumn, UserIDColumn}
|
||||||
|
defaultColumns = postgres.ColumnList{IDColumn, StatusColumn}
|
||||||
)
|
)
|
||||||
|
|
||||||
return userImagesToProcessTable{
|
return userImagesToProcessTable{
|
||||||
@ -80,5 +82,6 @@ func newUserImagesToProcessTableImpl(schemaName, tableName, alias string) userIm
|
|||||||
|
|
||||||
AllColumns: allColumns,
|
AllColumns: allColumns,
|
||||||
MutableColumns: mutableColumns,
|
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
|
AllColumns postgres.ColumnList
|
||||||
MutableColumns postgres.ColumnList
|
MutableColumns postgres.ColumnList
|
||||||
|
DefaultColumns postgres.ColumnList
|
||||||
}
|
}
|
||||||
|
|
||||||
type UsersTable struct {
|
type UsersTable struct {
|
||||||
@ -63,6 +64,7 @@ func newUsersTableImpl(schemaName, tableName, alias string) usersTable {
|
|||||||
EmailColumn = postgres.StringColumn("email")
|
EmailColumn = postgres.StringColumn("email")
|
||||||
allColumns = postgres.ColumnList{IDColumn, EmailColumn}
|
allColumns = postgres.ColumnList{IDColumn, EmailColumn}
|
||||||
mutableColumns = postgres.ColumnList{EmailColumn}
|
mutableColumns = postgres.ColumnList{EmailColumn}
|
||||||
|
defaultColumns = postgres.ColumnList{IDColumn}
|
||||||
)
|
)
|
||||||
|
|
||||||
return usersTable{
|
return usersTable{
|
||||||
@ -74,5 +76,6 @@ func newUsersTableImpl(schemaName, tableName, alias string) usersTable {
|
|||||||
|
|
||||||
AllColumns: allColumns,
|
AllColumns: allColumns,
|
||||||
MutableColumns: mutableColumns,
|
MutableColumns: mutableColumns,
|
||||||
|
DefaultColumns: defaultColumns,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,8 +65,8 @@ func (m ChatUserMessage) MarshalJSON() ([]byte, error) {
|
|||||||
})
|
})
|
||||||
case ArrayMessage:
|
case ArrayMessage:
|
||||||
return json.Marshal(&struct {
|
return json.Marshal(&struct {
|
||||||
Role UserRole `json:"role"`
|
Role UserRole `json:"role"`
|
||||||
Content []ImageMessageContent `json:"content"`
|
Content []MessageContentMessage `json:"content"`
|
||||||
}{
|
}{
|
||||||
Role: User,
|
Role: User,
|
||||||
Content: t.Content,
|
Content: t.Content,
|
||||||
@ -121,16 +121,37 @@ func (m SingleMessage) IsSingleMessage() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ArrayMessage struct {
|
type ArrayMessage struct {
|
||||||
Content []ImageMessageContent `json:"content"`
|
Content []MessageContentMessage `json:"content"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m ArrayMessage) IsSingleMessage() bool {
|
func (m ArrayMessage) IsSingleMessage() bool {
|
||||||
return false
|
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 {
|
type ImageMessageContent struct {
|
||||||
ImageType string `json:"type"`
|
ImageType string `json:"type"`
|
||||||
ImageUrl string `json:"image_url"`
|
ImageUrl ImageMessageUrl `json:"image_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageMessageUrl struct {
|
||||||
|
Url string `json:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m ImageMessageContent) IsImageMessage() bool {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageContentUrl struct {
|
type ImageContentUrl struct {
|
||||||
@ -144,6 +165,7 @@ type ImageContentUrl struct {
|
|||||||
type ToolCall struct {
|
type ToolCall struct {
|
||||||
Index int `json:"index"`
|
Index int `json:"index"`
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
|
Type string `json:"type,omitzero"`
|
||||||
Function FunctionCall `json:"function"`
|
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)
|
extension := filepath.Ext(imageName)
|
||||||
if len(extension) == 0 {
|
if len(extension) == 0 {
|
||||||
// TODO: could also validate for image types we support.
|
// 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:]
|
extension = extension[1:]
|
||||||
|
|
||||||
encodedString := base64.StdEncoding.EncodeToString(image)
|
encodedString := base64.StdEncoding.EncodeToString(image)
|
||||||
|
|
||||||
messageContent := ArrayMessage{
|
contentLength := 1
|
||||||
Content: make([]ImageMessageContent, 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",
|
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}
|
arrayMessage := ChatUserMessage{Role: User, MessageContent: messageContent}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@ -14,7 +15,7 @@ import (
|
|||||||
|
|
||||||
type ResponseFormat struct {
|
type ResponseFormat struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
JsonSchema any `json:"json_schema"`
|
JsonSchema any `json:"json_schema,omitzero"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentRequestBody struct {
|
type AgentRequestBody struct {
|
||||||
@ -25,6 +26,8 @@ type AgentRequestBody struct {
|
|||||||
Tools *any `json:"tools,omitempty"`
|
Tools *any `json:"tools,omitempty"`
|
||||||
ToolChoice *string `json:"tool_choice,omitempty"`
|
ToolChoice *string `json:"tool_choice,omitempty"`
|
||||||
|
|
||||||
|
RandomSeed *int `json:"random_seed,omitempty"`
|
||||||
|
|
||||||
EndToolCall string `json:"-"`
|
EndToolCall string `json:"-"`
|
||||||
|
|
||||||
Chat *Chat `json:"messages"`
|
Chat *Chat `json:"messages"`
|
||||||
@ -73,32 +76,46 @@ type AgentClient struct {
|
|||||||
|
|
||||||
Log *log.Logger
|
Log *log.Logger
|
||||||
|
|
||||||
|
Reply string
|
||||||
|
|
||||||
Do func(req *http.Request) (*http.Response, error)
|
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)
|
apiKey := os.Getenv(OPENAI_API_KEY)
|
||||||
|
|
||||||
if len(apiKey) == 0 {
|
if len(apiKey) == 0 {
|
||||||
return AgentClient{}, errors.New(OPENAI_API_KEY + " was not found.")
|
panic("No api key")
|
||||||
}
|
}
|
||||||
|
|
||||||
return AgentClient{
|
return AgentClient{
|
||||||
apiKey: apiKey,
|
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) {
|
Do: func(req *http.Request) (*http.Response, error) {
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
return client.Do(req)
|
return client.Do(req)
|
||||||
},
|
},
|
||||||
|
|
||||||
Log: log,
|
Log: options.Log,
|
||||||
|
|
||||||
ToolHandler: ToolsHandlers{
|
ToolHandler: ToolsHandlers{
|
||||||
handlers: map[string]ToolHandler{},
|
handlers: map[string]ToolHandler{},
|
||||||
},
|
},
|
||||||
}, nil
|
|
||||||
|
Options: options,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client AgentClient) getRequest(body []byte) (*http.Request, error) {
|
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) {
|
func (client AgentClient) Request(req *AgentRequestBody) (AgentResponse, error) {
|
||||||
jsonAiRequest, err := json.Marshal(req)
|
jsonAiRequest, err := json.Marshal(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return AgentResponse{}, err
|
return AgentResponse{}, fmt.Errorf("Could not format JSON", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
httpRequest, err := client.getRequest(jsonAiRequest)
|
httpRequest, err := client.getRequest(jsonAiRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return AgentResponse{}, err
|
return AgentResponse{}, fmt.Errorf("Could not get request", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.Do(httpRequest)
|
resp, err := client.Do(httpRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return AgentResponse{}, err
|
return AgentResponse{}, fmt.Errorf("Could not send request", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := io.ReadAll(resp.Body)
|
response, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return AgentResponse{}, err
|
return AgentResponse{}, fmt.Errorf("Could not read body", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
agentResponse := AgentResponse{}
|
agentResponse := AgentResponse{}
|
||||||
err = json.Unmarshal(response, &agentResponse)
|
err = json.Unmarshal(response, &agentResponse)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return AgentResponse{}, err
|
return AgentResponse{}, fmt.Errorf("Could not unmarshal response, response: %s", string(response), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(agentResponse.Choices) != 1 {
|
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.")
|
return AgentResponse{}, errors.New("Unsupported. We currently only accept 1 choice from AI.")
|
||||||
}
|
}
|
||||||
|
|
||||||
client.Log.SetLevel(log.DebugLevel)
|
|
||||||
|
|
||||||
msg := agentResponse.Choices[0].Message
|
msg := agentResponse.Choices[0].Message
|
||||||
|
req.Chat.AddAiResponse(msg)
|
||||||
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)
|
|
||||||
|
|
||||||
return agentResponse, nil
|
return agentResponse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client AgentClient) ToolLoop(info ToolHandlerInfo, req *AgentRequestBody) error {
|
func (client *AgentClient) ToolLoop(info ToolHandlerInfo, req *AgentRequestBody) error {
|
||||||
for {
|
for {
|
||||||
err := client.Process(info, req)
|
response, err := client.Request(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 != nil {
|
||||||
|
|
||||||
|
if err == FinishedCall {
|
||||||
|
client.Log.Debug("Agent is finished")
|
||||||
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -186,7 +196,7 @@ func (client AgentClient) ToolLoop(info ToolHandlerInfo, req *AgentRequestBody)
|
|||||||
|
|
||||||
var FinishedCall = errors.New("Last tool tool was called")
|
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
|
var err error
|
||||||
|
|
||||||
message, err := req.Chat.GetLatest()
|
message, err := req.Chat.GetLatest()
|
||||||
@ -211,8 +221,11 @@ func (client AgentClient) Process(info ToolHandlerInfo, req *AgentRequestBody) e
|
|||||||
|
|
||||||
toolResponse := client.ToolHandler.Handle(info, toolCall)
|
toolResponse := client.ToolHandler.Handle(info, toolCall)
|
||||||
|
|
||||||
client.Log.SetLevel(log.DebugLevel)
|
if toolCall.Function.Name == "reply" {
|
||||||
client.Log.Debugf("Response: %s", toolResponse.Content)
|
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)
|
req.Chat.AddToolResponse(toolResponse)
|
||||||
}
|
}
|
||||||
@ -220,18 +233,23 @@ func (client AgentClient) Process(info ToolHandlerInfo, req *AgentRequestBody) e
|
|||||||
return err
|
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
|
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{
|
request := AgentRequestBody{
|
||||||
Tools: &tools,
|
Tools: &tools,
|
||||||
ToolChoice: &toolChoice,
|
ToolChoice: &toolChoice,
|
||||||
Model: "pixtral-12b-2409",
|
Model: "google/gemini-2.5-flash",
|
||||||
|
RandomSeed: &seed,
|
||||||
Temperature: 0.3,
|
Temperature: 0.3,
|
||||||
EndToolCall: endToolCall,
|
EndToolCall: client.Options.EndToolCall,
|
||||||
ResponseFormat: ResponseFormat{
|
ResponseFormat: ResponseFormat{
|
||||||
Type: "text",
|
Type: "text",
|
||||||
},
|
},
|
||||||
@ -240,17 +258,49 @@ func (client AgentClient) RunAgent(systemPrompt string, jsonTools string, endToo
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
request.Chat.AddSystem(systemPrompt)
|
request.Chat.AddSystem(client.Options.SystemPrompt)
|
||||||
request.Chat.AddImage(imageName, imageData)
|
request.Chat.AddImage(imageName, imageData, client.Options.Query)
|
||||||
|
|
||||||
_, err = client.Request(&request)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
toolHandlerInfo := ToolHandlerInfo{
|
toolHandlerInfo := ToolHandlerInfo{
|
||||||
ImageId: imageId,
|
ImageId: imageId,
|
||||||
UserId: userId,
|
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: "google/gemini-2.5-flash",
|
||||||
|
RandomSeed: &seed,
|
||||||
|
Temperature: 0.3,
|
||||||
|
EndToolCall: client.Options.EndToolCall,
|
||||||
|
ResponseFormat: ResponseFormat{
|
||||||
|
Type: "text",
|
||||||
|
},
|
||||||
|
Chat: &Chat{
|
||||||
|
Messages: make([]ChatMessage, 0),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Chat.AddSystem(client.Options.SystemPrompt)
|
||||||
|
request.Chat.AddUser(userReq)
|
||||||
|
|
||||||
|
toolHandlerInfo := ToolHandlerInfo{
|
||||||
|
UserId: userID,
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.ToolLoop(toolHandlerInfo, &request)
|
return client.ToolLoop(toolHandlerInfo, &request)
|
||||||
|
|||||||
@ -8,8 +8,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ToolHandlerInfo struct {
|
type ToolHandlerInfo struct {
|
||||||
UserId uuid.UUID
|
UserId uuid.UUID
|
||||||
ImageId uuid.UUID
|
ImageId uuid.UUID
|
||||||
|
ImageName string
|
||||||
|
|
||||||
|
// Pointer because we don't want to copy this around too much.
|
||||||
|
Image *[]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToolHandler struct {
|
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: "google/gemini-2.5-flash",
|
||||||
|
Temperature: 0.3,
|
||||||
|
ResponseFormat: client.ResponseFormat{
|
||||||
|
Type: "json_object",
|
||||||
|
JsonSchema: listJsonSchema,
|
||||||
|
},
|
||||||
|
Chat: &client.Chat{
|
||||||
|
Messages: make([]client.ChatMessage, 0),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Chat.AddSystem(agent.client.Options.SystemPrompt)
|
||||||
|
request.Chat.AddUser(userReq)
|
||||||
|
|
||||||
|
resp, err := agent.client.Request(&request)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
structuredOutput := resp.Choices[0].Message.Content
|
||||||
|
|
||||||
|
var createListArgs createNewListArguments
|
||||||
|
err = json.Unmarshal([]byte(structuredOutput), &createListArgs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaItems := make([]model.SchemaItems, 0)
|
||||||
|
for _, field := range createListArgs.Fields {
|
||||||
|
schemaItems = append(schemaItems, model.SchemaItems{
|
||||||
|
Item: field.Name,
|
||||||
|
Description: field.Description,
|
||||||
|
|
||||||
|
Value: "string", // keep it simple for now.
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.listModel.Save(ctx, userID, createListArgs.Title, createListArgs.Description, schemaItems)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCreateListAgent(log *log.Logger, listModel models.ListModel) CreateListAgent {
|
||||||
|
client := client.CreateAgentClient(client.CreateAgentClientOptions{
|
||||||
|
SystemPrompt: createListAgentPrompt,
|
||||||
|
Log: log,
|
||||||
|
})
|
||||||
|
|
||||||
|
agent := CreateListAgent{
|
||||||
|
client,
|
||||||
|
listModel,
|
||||||
|
}
|
||||||
|
|
||||||
|
return agent
|
||||||
|
}
|
||||||
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: "google/gemini-2.5-flash-lite-preview-06-17",
|
||||||
|
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
|
|
||||||
}
|
|
||||||
235
backend/agents/list_agent.go
Normal file
235
backend/agents/list_agent.go
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
package agents
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||||
|
"screenmark/screenmark/agents/client"
|
||||||
|
"screenmark/screenmark/models"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const 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) 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
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
savedList, err := listModel.Save(ctx, info.UserId, args.Name, args.Desription, args.Schema)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug(savedList)
|
||||||
|
|
||||||
|
return savedList, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
agentClient.ToolHandler.AddTool("listLists", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
|
||||||
|
return listModel.List(context.Background(), info.UserId)
|
||||||
|
})
|
||||||
|
|
||||||
|
agentClient.ToolHandler.AddTool("addToList", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
|
||||||
|
args := addToListArguments{}
|
||||||
|
err := json.Unmarshal([]byte(_args), &args)
|
||||||
|
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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -18,7 +17,7 @@ type Auth struct {
|
|||||||
mailer Mailer
|
mailer Mailer
|
||||||
}
|
}
|
||||||
|
|
||||||
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz")
|
var letterRunes = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||||
|
|
||||||
func randString(n int) string {
|
func randString(n int) string {
|
||||||
b := make([]rune, n)
|
b := make([]rune, n)
|
||||||
@ -44,7 +43,6 @@ func (a *Auth) CreateCode(email string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Auth) IsCodeValid(email string, code string) bool {
|
func (a *Auth) IsCodeValid(email string, code string) bool {
|
||||||
fmt.Println(a.codes)
|
|
||||||
existingCode, exists := a.codes[email]
|
existingCode, exists := a.codes[email]
|
||||||
if !exists {
|
if !exists {
|
||||||
return false
|
return false
|
||||||
@ -55,7 +53,6 @@ func (a *Auth) IsCodeValid(email string, code string) bool {
|
|||||||
|
|
||||||
func (a *Auth) UseCode(email string, code string) error {
|
func (a *Auth) UseCode(email string, code string) error {
|
||||||
if valid := a.IsCodeValid(email, code); !valid {
|
if valid := a.IsCodeValid(email, code); !valid {
|
||||||
fmt.Println("returning error?")
|
|
||||||
return errors.New("This code is invalid.")
|
return errors.New("This code is invalid.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@ -1,9 +1,9 @@
|
|||||||
package main
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
"github.com/wneessen/go-mail"
|
"github.com/wneessen/go-mail"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -11,7 +11,9 @@ type MailClient struct {
|
|||||||
client *mail.Client
|
client *mail.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
type TestMailClient struct{}
|
type TestMailClient struct {
|
||||||
|
logger *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
type Mailer interface {
|
type Mailer interface {
|
||||||
SendCode(to string, code string) error
|
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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateMailClient() (Mailer, error) {
|
func CreateMailClient(log *log.Logger) (Mailer, error) {
|
||||||
mode := os.Getenv("MODE")
|
mode := os.Getenv("MODE")
|
||||||
if mode == "DEV" {
|
if mode == "DEV" {
|
||||||
return TestMailClient{}, nil
|
return TestMailClient{
|
||||||
|
log,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := mail.NewClient(
|
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,79 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"screenmark/screenmark/agents"
|
"screenmark/screenmark/agents"
|
||||||
|
"screenmark/screenmark/middleware"
|
||||||
"screenmark/screenmark/models"
|
"screenmark/screenmark/models"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/lib/pq"
|
"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) {
|
listener := pq.NewListener(os.Getenv("DB_CONNECTION"), time.Second, time.Second, func(event pq.ListenerEventType, err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -22,89 +83,70 @@ func ListenNewImageEvents(db *sql.DB, eventManager *EventManager) {
|
|||||||
})
|
})
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
|
|
||||||
locationModel := models.NewLocationModel(db)
|
|
||||||
eventModel := models.NewEventModel(db)
|
|
||||||
noteModel := models.NewNoteModel(db)
|
|
||||||
imageModel := models.NewImageModel(db)
|
imageModel := models.NewImageModel(db)
|
||||||
contactModel := models.NewContactModel(db)
|
listModel := models.NewListModel(db)
|
||||||
|
|
||||||
|
databaseEventLog := createLogger("Database Events 🤖", os.Stdout)
|
||||||
|
databaseEventLog.SetLevel(log.DebugLevel)
|
||||||
|
|
||||||
err := listener.Listen("new_image")
|
err := listener.Listen("new_image")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for parameters := range listener.Notify {
|
||||||
select {
|
imageId := uuid.MustParse(parameters.Extra)
|
||||||
case parameters := <-listener.Notify:
|
|
||||||
imageId := uuid.MustParse(parameters.Extra)
|
|
||||||
eventManager.listeners[parameters.Extra] = make(chan string)
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(2)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
noteAgent, err := agents.NewNoteAgent(noteModel)
|
defer wg.Done()
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
contactAgent, err := agents.NewContactAgent(contactModel)
|
descriptionAgent.Describe(createLogger("Description 📓", splitWriter), image.Image.ID, image.Image.ImageName, image.Image.Image)
|
||||||
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)
|
|
||||||
}()
|
}()
|
||||||
}
|
|
||||||
|
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 {
|
func ListenProcessingImageStatus(db *sql.DB, images models.ImageModel, notifier *Notifier[Notification]) {
|
||||||
// Maps processing image UUID to a channel
|
|
||||||
listeners map[string]chan string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewEventManager() EventManager {
|
|
||||||
return EventManager{
|
|
||||||
listeners: make(map[string]chan string),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ListenProcessingImageStatus(db *sql.DB, eventManager *EventManager) {
|
|
||||||
listener := pq.NewListener(os.Getenv("DB_CONNECTION"), time.Second, time.Second, func(event pq.ListenerEventType, err error) {
|
listener := pq.NewListener(os.Getenv("DB_CONNECTION"), time.Second, time.Second, func(event pq.ListenerEventType, err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -112,28 +154,209 @@ func ListenProcessingImageStatus(db *sql.DB, eventManager *EventManager) {
|
|||||||
})
|
})
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
|
|
||||||
|
logger := createLogger("Image Status 📊", os.Stdout)
|
||||||
|
|
||||||
if err := listener.Listen("new_processing_image_status"); err != nil {
|
if err := listener.Listen("new_processing_image_status"); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for data := range listener.Notify {
|
||||||
select {
|
imageStringUuid := data.Extra[0:36]
|
||||||
case data := <-listener.Notify:
|
status := data.Extra[36:]
|
||||||
stringUuid := data.Extra[0:36]
|
|
||||||
status := data.Extra[36:]
|
|
||||||
|
|
||||||
fmt.Printf("UUID: %s\n", stringUuid)
|
imageUuid, err := uuid.Parse(imageStringUuid)
|
||||||
fmt.Printf("Receiving :s\n", data.Extra)
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
imageListener, exists := eventManager.listeners[stringUuid]
|
processingImage, err := images.GetToProcess(context.Background(), imageUuid)
|
||||||
if !exists {
|
if err != nil {
|
||||||
continue
|
logger.Error("GetToProcess failed", "err", err)
|
||||||
}
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
imageListener <- status
|
logger.Info("Update", "id", imageStringUuid, "status", status)
|
||||||
|
|
||||||
close(imageListener)
|
notification := getImageNotification(imageNotification{
|
||||||
delete(eventManager.listeners, stringUuid)
|
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/charmbracelet/x/ansi v0.4.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-chi/chi/v5 v5.2.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/go-logfmt/logfmt v0.6.0 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
@ -20,6 +20,7 @@ require (
|
|||||||
github.com/muesli/termenv v0.16.0 // indirect
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // 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/stretchr/testify v1.10.0 // indirect
|
||||||
github.com/wneessen/go-mail v0.6.2 // indirect
|
github.com/wneessen/go-mail v0.6.2 // indirect
|
||||||
golang.org/x/crypto v0.33.0 // 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/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 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk=
|
||||||
github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
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 h1:z2JfvBAZgsfxlQz6NXBYdZTXc7ep3jhbszTLtETv1JE=
|
||||||
github.com/go-jet/jet/v2 v2.12.0/go.mod h1:ufQVRQeI1mbcO5R8uCEVcVf3Foej9kReBdwDx7YMWUM=
|
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 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
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=
|
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/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 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
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 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
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/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=
|
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/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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
170
backend/images/handler.go
Normal file
170
backend/images/handler.go
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package images
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||||
|
"screenmark/screenmark/middleware"
|
||||||
|
"screenmark/screenmark/models"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ImageHandler struct {
|
||||||
|
logger *log.Logger
|
||||||
|
imageModel models.ImageModel
|
||||||
|
userModel models.UserModel
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImagesReturn struct {
|
||||||
|
UserImages []models.UserImageWithImage `json:"userImages"`
|
||||||
|
ProcessingImages []models.UserProcessingImage `json:"processingImages"`
|
||||||
|
Lists []models.ListsWithImages `json:"lists"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ImageHandler) serveImage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
imageId, err := middleware.GetPathParamID(h.logger, "id", w, r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
image, err := h.imageModel.Get(r.Context(), imageId)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
fmt.Fprintf(w, "Could not get image")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this could be part of the db table
|
||||||
|
extension := filepath.Ext(image.ImageName)
|
||||||
|
if len(extension) == 0 {
|
||||||
|
// Same hack
|
||||||
|
extension = "png"
|
||||||
|
}
|
||||||
|
extension = extension[1:]
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "image/"+extension)
|
||||||
|
w.Write(image.Image)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ImageHandler) listImages(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userId, err := middleware.GetUserID(r.Context(), h.logger, w)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
images, err := h.userModel.GetUserImages(r.Context(), userId)
|
||||||
|
if err != nil {
|
||||||
|
middleware.WriteErrorInternal(h.logger, "could not get user images", w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
processingImages, err := h.imageModel.GetProcessing(r.Context(), userId)
|
||||||
|
if err != nil {
|
||||||
|
middleware.WriteErrorInternal(h.logger, "could not get processing images", w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
listsWithImages, err := h.userModel.ListWithImages(r.Context(), userId)
|
||||||
|
if err != nil {
|
||||||
|
middleware.WriteErrorInternal(h.logger, "could not get lists with images", w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
imagesReturn := ImagesReturn{
|
||||||
|
UserImages: images,
|
||||||
|
ProcessingImages: processingImages,
|
||||||
|
Lists: listsWithImages,
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware.WriteJsonOrError(h.logger, imagesReturn, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ImageHandler) uploadImage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
imageName := chi.URLParam(r, "name")
|
||||||
|
if len(imageName) == 0 {
|
||||||
|
middleware.WriteErrorBadRequest(h.logger, "you need to provide a name in the path", w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := middleware.GetUserID(r.Context(), h.logger, w)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := r.Header.Get("Content-Type")
|
||||||
|
|
||||||
|
image := make([]byte, 0)
|
||||||
|
switch contentType {
|
||||||
|
case "application/base64":
|
||||||
|
decoder := base64.NewDecoder(base64.StdEncoding, r.Body)
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
|
||||||
|
_, err := io.Copy(buf, decoder)
|
||||||
|
if err != nil {
|
||||||
|
middleware.WriteErrorBadRequest(h.logger, "base64 decoding failed", w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
image = buf.Bytes()
|
||||||
|
case "application/oclet-stream", "image/png":
|
||||||
|
bodyData, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
middleware.WriteErrorBadRequest(h.logger, "binary data reading failed", w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO: check headers
|
||||||
|
|
||||||
|
image = bodyData
|
||||||
|
default:
|
||||||
|
middleware.WriteErrorBadRequest(h.logger, "unsupported content type, need octet-stream or base64", w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userImage, err := h.imageModel.Process(r.Context(), userId, model.Image{
|
||||||
|
Image: image,
|
||||||
|
ImageName: imageName,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
middleware.WriteErrorInternal(h.logger, "could not save image to DB", w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware.WriteJsonOrError(h.logger, userImage, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ImageHandler) CreateRoutes(r chi.Router) {
|
||||||
|
h.logger.Info("Mounting image router")
|
||||||
|
|
||||||
|
// Public route for serving images (not protected)
|
||||||
|
r.Get("/{id}", h.serveImage)
|
||||||
|
|
||||||
|
// Protected routes
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Use(middleware.ProtectedRoute)
|
||||||
|
r.Use(middleware.SetJson)
|
||||||
|
|
||||||
|
r.Get("/", h.listImages)
|
||||||
|
r.Post("/{name}", h.uploadImage)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateImageHandler(db *sql.DB) ImageHandler {
|
||||||
|
imageModel := models.NewImageModel(db)
|
||||||
|
userModel := models.NewUserModel(db)
|
||||||
|
logger := log.New(os.Stdout).WithPrefix("Images")
|
||||||
|
|
||||||
|
return ImageHandler{
|
||||||
|
logger: logger,
|
||||||
|
imageModel: imageModel,
|
||||||
|
userModel: userModel,
|
||||||
|
}
|
||||||
|
}
|
||||||
639
backend/integration_test.go
Normal file
639
backend/integration_test.go
Normal file
@ -0,0 +1,639 @@
|
|||||||
|
// 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("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)
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
400
backend/main.go
400
backend/main.go
@ -1,23 +1,20 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"database/sql"
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"os"
|
||||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
|
||||||
"screenmark/screenmark/agents/client"
|
"screenmark/screenmark/agents/client"
|
||||||
|
"screenmark/screenmark/auth"
|
||||||
|
"screenmark/screenmark/images"
|
||||||
"screenmark/screenmark/models"
|
"screenmark/screenmark/models"
|
||||||
"time"
|
"screenmark/screenmark/stacks"
|
||||||
|
|
||||||
|
ourmiddleware "screenmark/screenmark/middleware"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -29,6 +26,53 @@ func (client TestAiClient) GetImageInfo(imageName string, imageData []byte) (cli
|
|||||||
return client.ImageInfo, nil
|
return client.ImageInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupRouter(db *sql.DB) chi.Router {
|
||||||
|
imageModel := models.NewImageModel(db)
|
||||||
|
stackModel := models.NewListModel(db)
|
||||||
|
|
||||||
|
stackHandler := stacks.CreateStackHandler(db)
|
||||||
|
authHandler := auth.CreateAuthHandler(db)
|
||||||
|
imageHandler := images.CreateImageHandler(db)
|
||||||
|
|
||||||
|
notifier := NewNotifier[Notification](10)
|
||||||
|
|
||||||
|
// Only start event listeners if not in test environment
|
||||||
|
if os.Getenv("GO_TEST_ENVIRONMENT") != "true" {
|
||||||
|
|
||||||
|
// TODO: should extract these into a notification manager
|
||||||
|
// And actually make them the same code.
|
||||||
|
// The events are basically the same.
|
||||||
|
|
||||||
|
go ListenNewImageEvents(db)
|
||||||
|
go ListenProcessingImageStatus(db, imageModel, ¬ifier)
|
||||||
|
go ListenNewStackEvents(db)
|
||||||
|
go ListenProcessingStackStatus(db, stackModel, ¬ifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
r.Use(middleware.Logger)
|
||||||
|
r.Use(ourmiddleware.CorsMiddleware)
|
||||||
|
|
||||||
|
r.Route("/stacks", stackHandler.CreateRoutes)
|
||||||
|
r.Route("/auth", authHandler.CreateRoutes)
|
||||||
|
r.Route("/images", imageHandler.CreateRoutes)
|
||||||
|
|
||||||
|
r.Route("/notifications", func(r chi.Router) {
|
||||||
|
r.Use(ourmiddleware.GetUserIdFromUrl)
|
||||||
|
|
||||||
|
r.Get("/", CreateEventsHandler(¬ifier))
|
||||||
|
})
|
||||||
|
|
||||||
|
logWriter := DatabaseWriter{
|
||||||
|
dbPool: db,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Route("/logs", createLogHandler(&logWriter))
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := godotenv.Load()
|
err := godotenv.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -40,332 +84,20 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
imageModel := models.NewImageModel(db)
|
router := setupRouter(db)
|
||||||
userModel := models.NewUserModel(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 {
|
if err != nil {
|
||||||
panic(err)
|
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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
116
backend/middleware/middleware.go
Normal file
116
backend/middleware/middleware.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CorsMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||||
|
w.Header().Add("Access-Control-Allow-Headers", "*")
|
||||||
|
|
||||||
|
// Access-Control-Allow-Methods is often needed for preflight OPTIONS requests
|
||||||
|
w.Header().Add("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||||||
|
|
||||||
|
// The client makes an OPTIONS preflight request before a complex request.
|
||||||
|
// We must handle this and respond with the appropriate headers.
|
||||||
|
if r.Method == "OPTIONS" {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const USER_ID = "UserID"
|
||||||
|
|
||||||
|
func GetUserID(ctx context.Context, logger *log.Logger, w http.ResponseWriter) (uuid.UUID, error) {
|
||||||
|
userId := ctx.Value(USER_ID)
|
||||||
|
|
||||||
|
if userId == nil {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
logger.Warn("UserID not present in request")
|
||||||
|
return uuid.Nil, errors.New("context does not contain a user id")
|
||||||
|
}
|
||||||
|
|
||||||
|
userIdUuid, ok := userId.(uuid.UUID)
|
||||||
|
if !ok {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
logger.Warn("UserID not of correct type")
|
||||||
|
return uuid.Nil, fmt.Errorf("context user id is not of type uuid, got: %t", userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return userIdUuid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProtectedRoute(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token := r.Header.Get("Authorization")
|
||||||
|
if len(token) < len("Bearer ") {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := GetUserIdFromAccess(token[len("Bearer "):])
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contextWithUserId := context.WithValue(r.Context(), USER_ID, userId)
|
||||||
|
|
||||||
|
newR := r.WithContext(contextWithUserId)
|
||||||
|
next.ServeHTTP(w, newR)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserIdFromUrl(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
token := r.URL.Query().Get("token")
|
||||||
|
|
||||||
|
if len(token) == 0 {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := GetUserIdFromAccess(token)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contextWithUserId := context.WithValue(r.Context(), USER_ID, userId)
|
||||||
|
|
||||||
|
newR := r.WithContext(contextWithUserId)
|
||||||
|
next.ServeHTTP(w, newR)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPathParamID(logger *log.Logger, param string, w http.ResponseWriter, r *http.Request) (uuid.UUID, error) {
|
||||||
|
pathParam := r.PathValue(param)
|
||||||
|
if len(pathParam) == 0 {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
|
||||||
|
err := fmt.Errorf("%s was not present", param)
|
||||||
|
logger.Warn(err)
|
||||||
|
return uuid.Nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
uuidParam, err := uuid.Parse(pathParam)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
|
||||||
|
err := fmt.Errorf("could not parse param: %w", err)
|
||||||
|
logger.Warn(err)
|
||||||
|
return uuid.Nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return uuidParam, nil
|
||||||
|
}
|
||||||
48
backend/middleware/util.go
Normal file
48
backend/middleware/util.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WriteJsonOrError[K any](logger *log.Logger, object K, w http.ResponseWriter) {
|
||||||
|
jsonObject, err := json.Marshal(object)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("could not marshal json object", "err", err)
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write(jsonObject)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorObject struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeError(logger *log.Logger, error string, w http.ResponseWriter, code int) {
|
||||||
|
e := ErrorObject{
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonObject, err := json.Marshal(e)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("could not marshal json object", "err", err)
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write(jsonObject)
|
||||||
|
w.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteErrorBadRequest(logger *log.Logger, error string, w http.ResponseWriter) {
|
||||||
|
writeError(logger, error, w, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteErrorInternal(logger *log.Logger, error string, w http.ResponseWriter) {
|
||||||
|
writeError(logger, error, w, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
@ -1,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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"screenmark/screenmark/.gen/haystack/haystack/enum"
|
||||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||||
|
|
||||||
@ -29,21 +29,27 @@ type ProcessingImageData struct {
|
|||||||
Image model.Image
|
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) {
|
func (m ImageModel) Process(ctx context.Context, userId uuid.UUID, image model.Image) (model.UserImagesToProcess, error) {
|
||||||
tx, err := m.dbPool.BeginTx(ctx, nil)
|
tx, err := m.dbPool.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.UserImagesToProcess{}, err
|
return model.UserImagesToProcess{}, fmt.Errorf("Failed to begin transaction", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
insertImageStmt := Image.
|
insertImageStmt := Image.
|
||||||
INSERT(Image.ImageName, Image.Image).
|
INSERT(Image.ImageName, Image.Image, Image.Description).
|
||||||
VALUES(image.ImageName, image.Image).
|
VALUES(image.ImageName, image.Image, image.Description).
|
||||||
RETURNING(Image.ID)
|
RETURNING(Image.ID)
|
||||||
|
|
||||||
insertedImage := model.Image{}
|
insertedImage := model.Image{}
|
||||||
err = insertImageStmt.QueryContext(ctx, tx, &insertedImage)
|
err = insertImageStmt.QueryContext(ctx, tx, &insertedImage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.UserImagesToProcess{}, err
|
return model.UserImagesToProcess{}, fmt.Errorf("Could not insert/query new image. SQL %s.", insertImageStmt.DebugSql(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
stmt := UserImagesToProcess.
|
stmt := UserImagesToProcess.
|
||||||
@ -54,7 +60,7 @@ func (m ImageModel) Process(ctx context.Context, userId uuid.UUID, image model.I
|
|||||||
userImage := model.UserImagesToProcess{}
|
userImage := model.UserImagesToProcess{}
|
||||||
err = stmt.QueryContext(ctx, tx, &userImage)
|
err = stmt.QueryContext(ctx, tx, &userImage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.UserImagesToProcess{}, err
|
return model.UserImagesToProcess{}, fmt.Errorf("Could not insert user_image", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
@ -62,16 +68,20 @@ func (m ImageModel) Process(ctx context.Context, userId uuid.UUID, image model.I
|
|||||||
return userImage, err
|
return userImage, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m ImageModel) GetToProcess(ctx context.Context, imageId uuid.UUID) (model.UserImagesToProcess, error) {
|
func (m ImageModel) GetToProcess(ctx context.Context, imageId uuid.UUID) (UserProcessingImage, error) {
|
||||||
getToProcessStmt := UserImagesToProcess.
|
getToProcessStmt := SELECT(UserImagesToProcess.AllColumns, Image.ID, Image.ImageName).
|
||||||
SELECT(UserImagesToProcess.AllColumns).
|
FROM(
|
||||||
|
UserImagesToProcess.INNER_JOIN(
|
||||||
|
Image, Image.ID.EQ(UserImagesToProcess.ImageID),
|
||||||
|
),
|
||||||
|
).
|
||||||
WHERE(UserImagesToProcess.ID.EQ(UUID(imageId)))
|
WHERE(UserImagesToProcess.ID.EQ(UUID(imageId)))
|
||||||
|
|
||||||
images := []model.UserImagesToProcess{}
|
images := []UserProcessingImage{}
|
||||||
err := getToProcessStmt.QueryContext(ctx, m.dbPool, &images)
|
err := getToProcessStmt.QueryContext(ctx, m.dbPool, &images)
|
||||||
|
|
||||||
if len(images) != 1 {
|
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
|
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)
|
err := stmt.QueryContext(ctx, m.dbPool, &images)
|
||||||
|
|
||||||
if len(images) != 1 {
|
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
|
return images[0], err
|
||||||
@ -117,15 +127,35 @@ func (m ImageModel) FinishProcessing(ctx context.Context, imageId uuid.UUID) (mo
|
|||||||
return model.UserImages{}, err
|
return model.UserImages{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
removeProcessingStmt := UserImagesToProcess.
|
// Hacky. Update the status before removing so we can get our regular triggers
|
||||||
DELETE().
|
// to work.
|
||||||
|
|
||||||
|
updateStatusStmt := UserImagesToProcess.
|
||||||
|
UPDATE(UserImagesToProcess.Status).
|
||||||
|
SET(model.Progress_Complete).
|
||||||
WHERE(UserImagesToProcess.ID.EQ(UUID(imageToProcess.ID)))
|
WHERE(UserImagesToProcess.ID.EQ(UUID(imageToProcess.ID)))
|
||||||
|
|
||||||
_, err = removeProcessingStmt.ExecContext(ctx, tx)
|
_, err = updateStatusStmt.ExecContext(ctx, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.UserImages{}, err
|
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()
|
err = tx.Commit()
|
||||||
return userImage, err
|
return userImage, err
|
||||||
}
|
}
|
||||||
@ -141,21 +171,41 @@ func (m ImageModel) StartProcessing(ctx context.Context, processingImageId uuid.
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m ImageModel) Get(ctx context.Context, imageId uuid.UUID) (ImageData, error) {
|
func (m ImageModel) Get(ctx context.Context, imageId uuid.UUID) (model.Image, error) {
|
||||||
getImageStmt := SELECT(UserImages.AllColumns, Image.AllColumns).
|
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(
|
FROM(
|
||||||
UserImages.INNER_JOIN(Image, Image.ID.EQ(UserImages.ImageID)),
|
UserImagesToProcess.INNER_JOIN(
|
||||||
).
|
Image, Image.ID.EQ(UserImagesToProcess.ImageID),
|
||||||
WHERE(UserImages.ID.EQ(UUID(imageId)))
|
),
|
||||||
|
).WHERE(
|
||||||
|
UserImagesToProcess.UserID.EQ(UUID(userId)).
|
||||||
|
AND(UserImagesToProcess.Status.NOT_EQ(enum.Progress.Complete)),
|
||||||
|
)
|
||||||
|
|
||||||
images := []ImageData{}
|
images := []UserProcessingImage{}
|
||||||
err := getImageStmt.QueryContext(ctx, m.dbPool, &images)
|
err := getProcessingStmt.QueryContext(ctx, m.dbPool, &images)
|
||||||
|
|
||||||
if len(images) != 1 {
|
return images, err
|
||||||
return ImageData{}, errors.New(fmt.Sprintf("Expected 1, got %d\n", len(images)))
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
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}
|
|
||||||
}
|
|
||||||
252
backend/models/lists.go
Normal file
252
backend/models/lists.go
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"screenmark/screenmark/.gen/haystack/haystack/model"
|
"screenmark/screenmark/.gen/haystack/haystack/model"
|
||||||
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
. "screenmark/screenmark/.gen/haystack/haystack/table"
|
||||||
|
|
||||||
. "github.com/go-jet/jet/v2/postgres"
|
. "github.com/go-jet/jet/v2/postgres"
|
||||||
|
"github.com/go-jet/jet/v2/qrm"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -21,89 +19,6 @@ type ImageWithProperties struct {
|
|||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
|
|
||||||
Image model.Image
|
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) {
|
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
|
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)).
|
||||||
|
INNER_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 {
|
func NewUserModel(db *sql.DB) UserModel {
|
||||||
return UserModel{dbPool: db}
|
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)
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@ CREATE SCHEMA haystack;
|
|||||||
|
|
||||||
/* -----| Enums |----- */
|
/* -----| Enums |----- */
|
||||||
|
|
||||||
CREATE TYPE haystack.progress AS ENUM('not-started','in-progress');
|
CREATE TYPE haystack.progress AS ENUM('not-started','in-progress', 'complete');
|
||||||
|
|
||||||
/* -----| Schema tables |----- */
|
/* -----| Schema tables |----- */
|
||||||
|
|
||||||
@ -16,6 +16,7 @@ CREATE TABLE haystack.users (
|
|||||||
CREATE TABLE haystack.image (
|
CREATE TABLE haystack.image (
|
||||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
image_name TEXT NOT NULL,
|
image_name TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
image BYTEA NOT NULL
|
image BYTEA NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -29,127 +30,74 @@ CREATE TABLE haystack.user_images_to_process (
|
|||||||
CREATE TABLE haystack.user_images (
|
CREATE TABLE haystack.user_images (
|
||||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
image_id uuid NOT NULL UNIQUE REFERENCES haystack.image (id),
|
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 (
|
CREATE TABLE haystack.logs (
|
||||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
log TEXT NOT NULL,
|
||||||
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(),
|
|
||||||
image_id UUID NOT NULL REFERENCES haystack.image (id),
|
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 (
|
CREATE TABLE haystack.lists (
|
||||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
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(),
|
|
||||||
user_id UUID NOT NULL REFERENCES haystack.users (id),
|
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 |----- */
|
/* -----| Indexes |----- */
|
||||||
|
|
||||||
CREATE INDEX user_tags_index ON haystack.user_tags(tag);
|
|
||||||
|
|
||||||
/* -----| Stored Procedures |----- */
|
/* -----| Stored Procedures |----- */
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION notify_new_image()
|
CREATE OR REPLACE FUNCTION notify_new_image()
|
||||||
@ -168,6 +116,22 @@ PERFORM pg_notify('new_processing_image_status', NEW.id::text || NEW.status::tex
|
|||||||
END
|
END
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION notify_new_stacks()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM pg_notify('new_stack', NEW.id::text);
|
||||||
|
RETURN NEW;
|
||||||
|
END
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION notify_new_processing_stack_status()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM pg_notify('new_processing_stack_status', NEW.id::text || NEW.status::text);
|
||||||
|
RETURN NEW;
|
||||||
|
END
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
/* -----| Triggers |----- */
|
/* -----| Triggers |----- */
|
||||||
|
|
||||||
CREATE OR REPLACE TRIGGER on_new_image AFTER INSERT
|
CREATE OR REPLACE TRIGGER on_new_image AFTER INSERT
|
||||||
@ -181,102 +145,15 @@ ON haystack.user_images_to_process
|
|||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
EXECUTE PROCEDURE notify_new_processing_image_status();
|
EXECUTE PROCEDURE notify_new_processing_image_status();
|
||||||
|
|
||||||
|
CREATE OR REPLACE TRIGGER on_new_image AFTER INSERT
|
||||||
|
ON haystack.processing_lists
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE PROCEDURE notify_new_stacks();
|
||||||
|
|
||||||
|
CREATE OR REPLACE TRIGGER on_update_stack_progress
|
||||||
|
AFTER UPDATE OF status
|
||||||
|
ON haystack.processing_lists
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE PROCEDURE notify_new_processing_stack_status();
|
||||||
|
|
||||||
/* -----| Test Data |----- */
|
/* -----| Test Data |----- */
|
||||||
|
|
||||||
-- 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');
|
|
||||||
|
|||||||
116
backend/stacks/handler.go
Normal file
116
backend/stacks/handler.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package stacks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"screenmark/screenmark/middleware"
|
||||||
|
"screenmark/screenmark/models"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/log"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StackHandler struct {
|
||||||
|
logger *log.Logger
|
||||||
|
stackModel models.ListModel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *StackHandler) getAllStacks(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
userID, err := middleware.GetUserID(ctx, h.logger, w)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lists, err := h.stackModel.List(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Warn("could not get stacks", "err", err)
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware.WriteJsonOrError(h.logger, lists, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *StackHandler) getStackItems(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
_, err := middleware.GetUserID(ctx, h.logger, w)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
listID, err := middleware.GetPathParamID(h.logger, "listID", w, r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: must check for permission here.
|
||||||
|
|
||||||
|
lists, err := h.stackModel.ListItems(ctx, listID)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Warn("could not get list items", "err", err)
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware.WriteJsonOrError(h.logger, lists, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
type EditStack struct {
|
||||||
|
Hello string `json:"hello"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *StackHandler) editStack(req EditStack, w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.stackModel.SaveProcessing(ctx, userID, body.Title, body.Fields)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Warn("could not save processing", "err", err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *StackHandler) CreateRoutes(r chi.Router) {
|
||||||
|
h.logger.Info("Mounting stack router")
|
||||||
|
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Use(middleware.ProtectedRoute)
|
||||||
|
r.Use(middleware.SetJson)
|
||||||
|
|
||||||
|
r.Get("/", h.getAllStacks)
|
||||||
|
r.Get("/{listID}", h.getStackItems)
|
||||||
|
|
||||||
|
r.Post("/", middleware.WithValidatedPost(h.createStack))
|
||||||
|
r.Patch("/{listID}", middleware.WithValidatedPost(h.editStack))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateStackHandler(db *sql.DB) StackHandler {
|
||||||
|
stackModel := models.NewListModel(db)
|
||||||
|
logger := log.New(os.Stdout).WithPrefix("Stacks")
|
||||||
|
|
||||||
|
return StackHandler{
|
||||||
|
logger,
|
||||||
|
stackModel,
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
screenmark
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
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
|
||||||
607
frontend/.idea/caches/deviceStreaming.xml
generated
Normal file
607
frontend/.idea/caches/deviceStreaming.xml
generated
Normal file
@ -0,0 +1,607 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DeviceStreaming">
|
||||||
|
<option name="deviceSelectionList">
|
||||||
|
<list>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="27" />
|
||||||
|
<option name="brand" value="DOCOMO" />
|
||||||
|
<option name="codename" value="F01L" />
|
||||||
|
<option name="id" value="F01L" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="FUJITSU" />
|
||||||
|
<option name="name" value="F-01L" />
|
||||||
|
<option name="screenDensity" value="360" />
|
||||||
|
<option name="screenX" value="720" />
|
||||||
|
<option name="screenY" value="1280" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="OnePlus" />
|
||||||
|
<option name="codename" value="OP5552L1" />
|
||||||
|
<option name="id" value="OP5552L1" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="OnePlus" />
|
||||||
|
<option name="name" value="CPH2415" />
|
||||||
|
<option name="screenDensity" value="480" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2412" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="OPPO" />
|
||||||
|
<option name="codename" value="OP573DL1" />
|
||||||
|
<option name="id" value="OP573DL1" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="OPPO" />
|
||||||
|
<option name="name" value="CPH2557" />
|
||||||
|
<option name="screenDensity" value="480" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="28" />
|
||||||
|
<option name="brand" value="DOCOMO" />
|
||||||
|
<option name="codename" value="SH-01L" />
|
||||||
|
<option name="id" value="SH-01L" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="SHARP" />
|
||||||
|
<option name="name" value="AQUOS sense2 SH-01L" />
|
||||||
|
<option name="screenDensity" value="480" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2160" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="Lenovo" />
|
||||||
|
<option name="codename" value="TB370FU" />
|
||||||
|
<option name="formFactor" value="Tablet" />
|
||||||
|
<option name="id" value="TB370FU" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Lenovo" />
|
||||||
|
<option name="name" value="Tab P12" />
|
||||||
|
<option name="screenDensity" value="340" />
|
||||||
|
<option name="screenX" value="1840" />
|
||||||
|
<option name="screenY" value="2944" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="a15" />
|
||||||
|
<option name="id" value="a15" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="A15" />
|
||||||
|
<option name="screenDensity" value="450" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2340" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="a35x" />
|
||||||
|
<option name="id" value="a35x" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="A35" />
|
||||||
|
<option name="screenDensity" value="450" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2340" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="31" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="a51" />
|
||||||
|
<option name="id" value="a51" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy A51" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="akita" />
|
||||||
|
<option name="id" value="akita" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 8a" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="motorola" />
|
||||||
|
<option name="codename" value="arcfox" />
|
||||||
|
<option name="id" value="arcfox" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Motorola" />
|
||||||
|
<option name="name" value="razr plus 2024" />
|
||||||
|
<option name="screenDensity" value="360" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="1272" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="33" />
|
||||||
|
<option name="brand" value="motorola" />
|
||||||
|
<option name="codename" value="austin" />
|
||||||
|
<option name="id" value="austin" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Motorola" />
|
||||||
|
<option name="name" value="moto g 5G (2022)" />
|
||||||
|
<option name="screenDensity" value="280" />
|
||||||
|
<option name="screenX" value="720" />
|
||||||
|
<option name="screenY" value="1600" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="33" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="b0q" />
|
||||||
|
<option name="id" value="b0q" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy S22 Ultra" />
|
||||||
|
<option name="screenDensity" value="600" />
|
||||||
|
<option name="screenX" value="1440" />
|
||||||
|
<option name="screenY" value="3088" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="32" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="bluejay" />
|
||||||
|
<option name="id" value="bluejay" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 6a" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="caiman" />
|
||||||
|
<option name="id" value="caiman" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 9 Pro" />
|
||||||
|
<option name="screenDensity" value="360" />
|
||||||
|
<option name="screenX" value="960" />
|
||||||
|
<option name="screenY" value="2142" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="comet" />
|
||||||
|
<option name="default" value="true" />
|
||||||
|
<option name="id" value="comet" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 9 Pro Fold" />
|
||||||
|
<option name="screenDensity" value="390" />
|
||||||
|
<option name="screenX" value="2076" />
|
||||||
|
<option name="screenY" value="2152" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="29" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="crownqlteue" />
|
||||||
|
<option name="id" value="crownqlteue" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy Note9" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="2220" />
|
||||||
|
<option name="screenY" value="1080" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="dm2q" />
|
||||||
|
<option name="id" value="dm2q" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="S23 Plus" />
|
||||||
|
<option name="screenDensity" value="450" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2340" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="dm3q" />
|
||||||
|
<option name="id" value="dm3q" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy S23 Ultra" />
|
||||||
|
<option name="screenDensity" value="600" />
|
||||||
|
<option name="screenX" value="1440" />
|
||||||
|
<option name="screenY" value="3088" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="e1q" />
|
||||||
|
<option name="default" value="true" />
|
||||||
|
<option name="id" value="e1q" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy S24" />
|
||||||
|
<option name="screenDensity" value="480" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2340" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="e3q" />
|
||||||
|
<option name="id" value="e3q" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy S24 Ultra" />
|
||||||
|
<option name="screenDensity" value="450" />
|
||||||
|
<option name="screenX" value="1440" />
|
||||||
|
<option name="screenY" value="3120" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="33" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="eos" />
|
||||||
|
<option name="id" value="eos" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Eos" />
|
||||||
|
<option name="screenDensity" value="320" />
|
||||||
|
<option name="screenX" value="384" />
|
||||||
|
<option name="screenY" value="384" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="33" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="felix" />
|
||||||
|
<option name="id" value="felix" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel Fold" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="2208" />
|
||||||
|
<option name="screenY" value="1840" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="felix" />
|
||||||
|
<option name="id" value="felix" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel Fold" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="2208" />
|
||||||
|
<option name="screenY" value="1840" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="33" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="felix_camera" />
|
||||||
|
<option name="id" value="felix_camera" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel Fold (Camera-enabled)" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="2208" />
|
||||||
|
<option name="screenY" value="1840" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="motorola" />
|
||||||
|
<option name="codename" value="fogona" />
|
||||||
|
<option name="id" value="fogona" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Motorola" />
|
||||||
|
<option name="name" value="moto g play - 2024" />
|
||||||
|
<option name="screenDensity" value="280" />
|
||||||
|
<option name="screenX" value="720" />
|
||||||
|
<option name="screenY" value="1600" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="g0q" />
|
||||||
|
<option name="id" value="g0q" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="SM-S906U1" />
|
||||||
|
<option name="screenDensity" value="450" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2340" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="gta9pwifi" />
|
||||||
|
<option name="id" value="gta9pwifi" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="SM-X210" />
|
||||||
|
<option name="screenDensity" value="240" />
|
||||||
|
<option name="screenX" value="1200" />
|
||||||
|
<option name="screenY" value="1920" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="gts7xllite" />
|
||||||
|
<option name="id" value="gts7xllite" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="SM-T738U" />
|
||||||
|
<option name="screenDensity" value="340" />
|
||||||
|
<option name="screenX" value="1600" />
|
||||||
|
<option name="screenY" value="2560" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="33" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="gts8uwifi" />
|
||||||
|
<option name="formFactor" value="Tablet" />
|
||||||
|
<option name="id" value="gts8uwifi" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy Tab S8 Ultra" />
|
||||||
|
<option name="screenDensity" value="320" />
|
||||||
|
<option name="screenX" value="1848" />
|
||||||
|
<option name="screenY" value="2960" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="gts8wifi" />
|
||||||
|
<option name="formFactor" value="Tablet" />
|
||||||
|
<option name="id" value="gts8wifi" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy Tab S8" />
|
||||||
|
<option name="screenDensity" value="274" />
|
||||||
|
<option name="screenX" value="1600" />
|
||||||
|
<option name="screenY" value="2560" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="gts9fe" />
|
||||||
|
<option name="id" value="gts9fe" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy Tab S9 FE 5G" />
|
||||||
|
<option name="screenDensity" value="280" />
|
||||||
|
<option name="screenX" value="1440" />
|
||||||
|
<option name="screenY" value="2304" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="husky" />
|
||||||
|
<option name="id" value="husky" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 8 Pro" />
|
||||||
|
<option name="screenDensity" value="390" />
|
||||||
|
<option name="screenX" value="1008" />
|
||||||
|
<option name="screenY" value="2244" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="30" />
|
||||||
|
<option name="brand" value="motorola" />
|
||||||
|
<option name="codename" value="java" />
|
||||||
|
<option name="id" value="java" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Motorola" />
|
||||||
|
<option name="name" value="G20" />
|
||||||
|
<option name="screenDensity" value="280" />
|
||||||
|
<option name="screenX" value="720" />
|
||||||
|
<option name="screenY" value="1600" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="komodo" />
|
||||||
|
<option name="id" value="komodo" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 9 Pro XL" />
|
||||||
|
<option name="screenDensity" value="360" />
|
||||||
|
<option name="screenX" value="1008" />
|
||||||
|
<option name="screenY" value="2244" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="33" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="lynx" />
|
||||||
|
<option name="id" value="lynx" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 7a" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="33" />
|
||||||
|
<option name="brand" value="motorola" />
|
||||||
|
<option name="codename" value="maui" />
|
||||||
|
<option name="id" value="maui" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Motorola" />
|
||||||
|
<option name="name" value="moto g play - 2023" />
|
||||||
|
<option name="screenDensity" value="280" />
|
||||||
|
<option name="screenX" value="720" />
|
||||||
|
<option name="screenY" value="1600" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="o1q" />
|
||||||
|
<option name="id" value="o1q" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy S21" />
|
||||||
|
<option name="screenDensity" value="421" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="31" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="oriole" />
|
||||||
|
<option name="id" value="oriole" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 6" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="33" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="panther" />
|
||||||
|
<option name="id" value="panther" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 7" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="q5q" />
|
||||||
|
<option name="id" value="q5q" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy Z Fold5" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1812" />
|
||||||
|
<option name="screenY" value="2176" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="q6q" />
|
||||||
|
<option name="id" value="q6q" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy Z Fold6" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1856" />
|
||||||
|
<option name="screenY" value="2160" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="30" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="r11" />
|
||||||
|
<option name="formFactor" value="Wear OS" />
|
||||||
|
<option name="id" value="r11" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel Watch" />
|
||||||
|
<option name="screenDensity" value="320" />
|
||||||
|
<option name="screenX" value="384" />
|
||||||
|
<option name="screenY" value="384" />
|
||||||
|
<option name="type" value="WEAR_OS" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="r11q" />
|
||||||
|
<option name="id" value="r11q" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="SM-S711U" />
|
||||||
|
<option name="screenDensity" value="450" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2340" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="30" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="redfin" />
|
||||||
|
<option name="id" value="redfin" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 5" />
|
||||||
|
<option name="screenDensity" value="440" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2340" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="shiba" />
|
||||||
|
<option name="id" value="shiba" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 8" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="samsung" />
|
||||||
|
<option name="codename" value="t2q" />
|
||||||
|
<option name="id" value="t2q" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Samsung" />
|
||||||
|
<option name="name" value="Galaxy S21 Plus" />
|
||||||
|
<option name="screenDensity" value="394" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2400" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="33" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="tangorpro" />
|
||||||
|
<option name="formFactor" value="Tablet" />
|
||||||
|
<option name="id" value="tangorpro" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel Tablet" />
|
||||||
|
<option name="screenDensity" value="320" />
|
||||||
|
<option name="screenX" value="1600" />
|
||||||
|
<option name="screenY" value="2560" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="34" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="tokay" />
|
||||||
|
<option name="default" value="true" />
|
||||||
|
<option name="id" value="tokay" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 9" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2424" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="35" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="tokay" />
|
||||||
|
<option name="default" value="true" />
|
||||||
|
<option name="id" value="tokay" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="Google" />
|
||||||
|
<option name="name" value="Pixel 9" />
|
||||||
|
<option name="screenDensity" value="420" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2424" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
9
frontend/.idea/frontend.iml
generated
Normal file
9
frontend/.idea/frontend.iml
generated
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
frontend/.idea/misc.xml
generated
Normal file
6
frontend/.idea/misc.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user