483 Commits

Author SHA1 Message Date
106d3b1fa1 fix? 2025-10-05 21:25:44 +01:00
b9f6b77286 another fix 2025-10-05 21:13:53 +01:00
3c8fd843e6 debug: info 2025-10-05 21:07:24 +01:00
e61af3007f fix! 2025-10-05 20:55:26 +01:00
3594baceb5 lets pretend it is working 2025-10-05 20:47:28 +01:00
d534779fad fix: swift stuff 2025-10-05 20:35:49 +01:00
a776c88cab fix: more swift stuff 2025-10-05 20:34:23 +01:00
72de7c7648 fix: swift 2025-10-05 20:31:59 +01:00
a8b150857c fix 2025-10-05 19:56:48 +01:00
dd4f508346 fix 2025-10-05 19:05:17 +01:00
f21ee57632 fix: search 2025-10-05 16:27:38 +01:00
0e42c9002b fix: minor bugs 2025-10-05 16:25:00 +01:00
9e60a41f0a fix: processing images indicator 2025-10-05 15:59:14 +01:00
eaff553dc9 fix: image stacks on image page 2025-10-05 15:55:50 +01:00
6880811236 fix: delete schema column button 2025-10-05 15:54:46 +01:00
38bda46dcf hack: to get response format to work properly
I think this might be a bug with requesty
2025-10-05 15:35:09 +01:00
bd86ad499b fix: setting entities to done 2025-10-05 15:20:35 +01:00
838ab37fc1 refactor: list -> stack 2025-10-05 15:12:37 +01:00
9948d2521b feat: creating list processor 2025-10-05 15:08:25 +01:00
64abf79f9c feat: stack model processor 2025-10-05 14:54:24 +01:00
0d41a65435 feat: deleting column from frontend 2025-10-05 14:53:04 +01:00
ecd1529130 feat: implementing delete schema column function 2025-10-05 13:59:54 +01:00
015a7cb5cd fix: saving image schema items 2025-10-05 13:44:50 +01:00
980b42aa44 fix: notification system 2025-10-05 12:10:06 +01:00
649cfe0b02 fix: frontend with new backend schema 2025-10-05 10:44:57 +01:00
1fb9616aa7 WIP: image processing is back and working 2025-09-21 22:07:56 +01:00
013447fa90 wip: processing images 2025-09-21 21:48:22 +01:00
221afb599b BIG MASSIVE REFACTOR OMG
Ripped out literally everything to simplify the backend as much as
possible.

Some of the code was so horrifically complicated it's insaneeee
2025-09-21 21:31:44 +01:00
f8619d3ef7 fix: stuff 2025-09-21 16:48:17 +01:00
f6393c9a59 suspending 2025-09-21 15:54:24 +01:00
561064a194 using resources to always have a valid access token 2025-09-21 15:51:33 +01:00
3015d7bac2 fully working refresh tokens. No more expiring :) 2025-09-21 15:43:14 +01:00
a3345afbfa AI: checking the actual expire 2025-09-21 14:51:02 +01:00
f078ac7d0b AI: refactoring JWT tokens to jwt token manager 2025-09-21 14:42:06 +01:00
e28d9e5d16 using access token in header! 2025-09-15 21:50:15 +01:00
29c56bee1c protecting backend images 2025-09-14 19:09:50 +01:00
3ebc0810e7 Revert "frontend method to refresh image"
This reverts commit ce2cd977ac18c498e0601b3a5f631587f7cd5272.
2025-09-14 18:04:00 +01:00
0c595f76a3 disabling my mistakes 2025-09-14 18:03:52 +01:00
176d2b0bd4 reprocessing images now works 2025-09-14 18:02:40 +01:00
115d08a245 going back to how it was 2025-09-14 17:45:30 +01:00
b4b600bd7c Revert "refactor: creating image process to handle processing of images"
This reverts commit 8b6b9453a8f86d705ec2319c51f429faf687cb52.
2025-09-14 17:44:33 +01:00
ce2cd977ac frontend method to refresh image 2025-09-14 17:42:16 +01:00
8b6b9453a8 refactor: creating image process to handle processing of images
Decoupling this from the DB, it's a good step.

Not yet perfect however.
2025-09-14 17:42:16 +01:00
2dd9f33303 feat: allowing users to delete images from lists 2025-08-30 21:38:01 +01:00
94ee8bdb7e fix: cascade deleting of image properties 2025-08-30 21:03:15 +01:00
5d1c758451 feat: adding button to delete image 2025-08-30 20:46:05 +01:00
00359e2e8d feat: handling image delete 2025-08-30 20:34:57 +01:00
95330c163b fix: only firing update status when image is not 'not-started' 2025-08-30 20:32:05 +01:00
84a0996be9 feat: checking users limits when AI scans their images 2025-08-30 20:32:05 +01:00
48579267b5 feat: showing limits for list creation 2025-08-30 11:17:24 +01:00
8b54d502f2 feat: showing limits error on frontend 2025-08-30 11:15:17 +01:00
e45688d57e fix: allowing nullable user lists 2025-08-30 10:53:10 +01:00
f7c9c97f0a fix: allowing images without lists to survive 2025-08-30 10:49:28 +01:00
76924a0332 chore: using requesty policy instead of direct models 2025-08-30 10:46:37 +01:00
d97593d487 feat: adding limits 2025-08-30 10:45:53 +01:00
de96f12b55 chore: removing comment 2025-08-30 10:10:46 +01:00
70161da3ed opencode: delete method for stacks 2025-08-25 15:30:34 +01:00
3a182fc49b working notifications across backend and frontend 2025-08-25 15:22:24 +01:00
ec7bd469f9 sending notifications about new stacks 2025-08-25 15:13:29 +01:00
6523b10699 showing a full height image in image page 2025-08-25 14:34:55 +01:00
61d2b81e8c better handling of empty lists 2025-08-25 14:31:11 +01:00
fe0968716d am i finished? 2025-08-25 14:23:57 +01:00
769f3981cd feat: adding integration tests 2025-08-25 13:42:30 +01:00
a78f766122 more refactoring into seperate handlers 2025-08-25 13:16:40 +01:00
10cea769bf creating stacks using a user request 2025-08-20 21:38:55 +01:00
f5e65524aa improving by extracting common userID method 2025-08-19 21:51:08 +01:00
390a216260 implementing get list items 2025-08-19 21:44:11 +01:00
3e57d10360 a good start 2025-08-19 21:27:37 +01:00
28a4b37dde feat: showing the lists which an image is a part of 2025-07-29 15:54:51 +01:00
4de4431390 feat: getting list that an image belongs in 2025-07-29 15:48:41 +01:00
5ff7788a7b feat: style changes to image page 2025-07-29 15:41:05 +01:00
13170a33e8 fix: adding wait group so we can concurrently call these 2025-07-29 15:40:18 +01:00
5024933852 feat: making the description markdown 2025-07-29 15:40:11 +01:00
706d562e3e feat: the lesser evil 2025-07-29 15:36:43 +01:00
fda09ae07a chores
fix
2025-07-29 15:31:09 +01:00
5de5e0b56e fix: image sizing 2025-07-29 15:18:46 +01:00
a0bf27dd16 Haystack V2: Removing entities completely 2025-07-29 14:52:33 +01:00
3d05ff708e feat: giving agent enough information to add to list instead of creating one 2025-07-29 12:14:45 +01:00
ee109f05a0 feat: showing table on frontend for pages 2025-07-29 12:01:35 +01:00
f4d8c9f083 feat: getting schema information from images 2025-07-29 11:44:08 +01:00
a1af3feb1d chore: removing deadcode 2025-07-29 11:37:53 +01:00
8597584cf0 feat: initial draft of generating a schema from one image
fix: error formatting
2025-07-29 11:37:23 +01:00
88d033314e feat: initial attempt to create a schema 2025-07-29 09:47:59 +01:00
9cae780431 fix: column generation 2025-07-29 09:47:47 +01:00
fa71f68de5 wip: search feature for image descriptions
The search only works for entities currently, but we need to make it
useful for images too.

This also goes back to entity vs image question. I don't think people
find the entities super useful actually? But I know they could be.

I need to find a way for them to properly co-exist
2025-07-24 14:31:08 +01:00
0058cdce40 feat: showing description on image page in frontend 2025-07-24 14:23:20 +01:00
37f966e508 feat: finishing description agent infrastructure on backend 2025-07-24 14:12:41 +01:00
59bf884f5d refactor: changing notes to be a simple image description
Notes would generate too often and not be very useful. This is much
better.
2025-07-24 13:59:24 +01:00
2ac996db73 feat: displaying generated lists on the frontend 2025-07-22 20:02:16 +01:00
e19e6562bb feat: getting new lists onto the frontend 2025-07-22 19:49:36 +01:00
a283bc1bcd feat: new AI generated lists
I think this could be how we generate other lists

Problems:
- Knowing it's a location is good because you can do nice stuff on the
frontend.
- Same for contacts & events.

So a good alternative, is to still use this type, but perhaps change the
database such that all lists live within the new tables (lists,
image_lists). But have special tags.

This would also make it easier on the AI I think.
2025-07-22 19:40:56 +01:00
1b816e512a fix: using tagged switch 2025-07-22 18:52:26 +01:00
4d0dcccf94 fix: linting 2025-07-21 17:01:15 +01:00
bc4e4ab36c fix: dynamic height
idk why but 100% height makes the scrolling weird
2025-07-21 17:00:45 +01:00
a663f27fb2 fix: adding some background to all images page 2025-07-21 16:59:06 +01:00
744b300d00 fix: tyding up weird html 2025-07-21 16:57:53 +01:00
818a163235 fix: removing left-over line from testing 2025-07-21 16:56:52 +01:00
2b1eb2b948 fix: styling of entity and images page 2025-07-21 16:55:51 +01:00
018f0e96d4 feat: virtualized all images page 2025-07-21 16:52:57 +01:00
d5594c6e32 feat: sorting images by date 2025-07-21 16:12:33 +01:00
7d9845737e wip: all images page with virtualization 2025-07-21 15:36:24 +01:00
68010503ab feat: search feature working well! 2025-07-21 15:05:28 +01:00
251e2bc553 wip: search page 2025-07-21 14:38:02 +01:00
5d0fa51e01 fix: entity page looking bussin
whoops
2025-07-21 14:09:30 +01:00
ad4967a97d fix: using image component on image page 2025-07-21 14:04:22 +01:00
eb0914c9ca fix: showing settings page 2025-07-18 16:26:55 +01:00
5c8e0094f4 fix: image gallery height 2025-07-18 16:13:45 +01:00
4b85cae22c fix: recent pictures styles 2025-07-18 16:07:34 +01:00
75b2cc53a4 fix: gallery 2025-07-18 16:02:26 +01:00
bb3ae507ea fix: UI overflowing issues 2025-07-18 16:00:43 +01:00
0d5e6146f2 fix: styling of processing images 2025-07-18 15:54:46 +01:00
ec18cb0ee0 fix: re-adding send image listener
wip

fix: re-adding send image listener
2025-07-18 15:51:14 +01:00
510cb3012b refactor: image processing notifications 2025-07-18 15:43:06 +01:00
6e2c6acd9d refactor: moving dock and topbar together with app wrapper 2025-07-18 15:36:20 +01:00
5130691ab9 refactor: moving to components 2025-07-18 15:28:34 +01:00
1a9731c4bb fix: gap in front page 2025-07-18 15:25:04 +01:00
300a4925df feat: frontend page images 2025-07-18 15:24:44 +01:00
4870a8b1b1 chore: ignoring buildinfo 2025-07-18 15:18:51 +01:00
275ae4598f chore 2025-07-18 15:16:47 +01:00
eeb6d2bb3b stuff 2025-07-18 15:14:35 +01:00
a89c6dc658 refactor: shifting lots of stuff around 2025-07-18 15:14:30 +01:00
c6ad67345e chore: using ts aliases 2025-07-18 14:59:24 +01:00
459c8e1c4e chore: more cleaning 2025-07-18 14:50:31 +01:00
ec4e8b7e2a chore: removing dead code and cleaning up UI 2025-07-18 14:44:59 +01:00
5a1f3bb75b feat: recent displayed on front page 2025-07-18 14:26:23 +01:00
d4b14605c1 feat: images showing up in entity page 2025-07-18 13:51:52 +01:00
00e530df4b feat: navigation for image and entities instead of a modal 2025-07-18 13:48:06 +01:00
16b43ec561 chore: removing dead code 2025-07-18 13:47:54 +01:00
6d235eea36 fix: width 2025-07-18 12:09:12 +01:00
27ad03b1c1 feat: dock 2025-07-18 12:02:46 +01:00
1a845c7846 feat: gallery page working for each individual entity 2025-07-18 11:53:28 +01:00
3c3a25bcfc feat: gallery for each category 2025-07-18 11:46:12 +01:00
e508f03abb fix: categories styling 2025-07-18 11:27:26 +01:00
e74975a52a fix: only show when images are processing 2025-07-18 11:17:33 +01:00
a94c7255c6 wip: different UI
fix
2025-07-02 16:26:58 +01:00
a65ef5f548 fix: styling 2025-07-02 14:28:14 +01:00
be302b77d4 fix: title 2025-07-02 14:26:33 +01:00
a4a8c191b6 feat: styling 2025-07-02 14:24:17 +01:00
6482a76a51 feat: processing images and notification centre 2025-07-02 14:12:19 +01:00
c62378c20a feat: updating notifications system based on incoming request
This is making the frontend data logic a little complex
2025-07-02 13:28:28 +01:00
5cf0b66688 feat: frontend validation
fix
2025-07-02 12:09:01 +01:00
357927e2a0 fix: icon on linux 2025-07-02 12:04:23 +01:00
c632487d7e feat: sending processing images on app load 2025-07-02 12:04:16 +01:00
cb4a03015a feat: backup script 2025-05-19 21:08:22 +01:00
7f8b345e77 ios stuff 2025-05-12 21:27:21 +01:00
df9c42136e feat: submodule 2025-05-12 20:57:17 +01:00
019c4c6b0c chore: removing unnecessary comment 2025-05-12 20:51:21 +01:00
ad2a70aaf3 feat: multiple listeners on channels
Multiple device notifications supported.

Actually beautiful
2025-05-12 20:51:06 +01:00
076e230a01 feat: making SSE work on mobiles 2025-05-12 20:11:58 +01:00
636bd9df0e wipwipwip 2025-05-12 19:54:09 +01:00
3eab20049e fix: back button 2025-05-11 16:19:18 +01:00
64879ac9d6 wip 2025-05-11 16:16:18 +01:00
e674043daa wip 2025-05-11 16:15:15 +01:00
71049a7f26 vibe coding screenshot sharing 2025-05-11 15:18:21 +01:00
e8a51ecc52 fix: the reason this wasn't working
This was also silent failing, so should probably have a look at it
2025-05-11 14:30:31 +01:00
a6a6026a11 Merge branch 'ios/testflight-account' of ssh://192.168.1.111:2222/JohnCosta27/Haystack into ios/testflight-account 2025-05-10 22:19:28 +01:00
a9749f062e feat: setting up a different share extension
idk what happened to the old one
2025-05-10 22:18:49 +01:00
5a2b990c0c fix: adding extension hack
fix
2025-05-10 22:05:24 +01:00
b97eae10a3 hack: allowing demo@email.com to login straight away for test flight submittion 2025-05-10 21:03:32 +01:00
cc07ef983f feat: correctly re-requesting 2025-05-10 17:42:23 +01:00
b4a0383be7 chore: no more processing image stuff 2025-05-10 17:40:16 +01:00
a4b94fc6c2 feat: frontend receiving processing images and showing them as such 2025-05-10 17:39:09 +01:00
71dfe5647e feat: adding processing image component 2025-05-10 16:57:02 +01:00
90b863b6cf feat: frontend notification manager 2025-05-10 16:43:30 +01:00
64439d9041 feat: using struct to send notification and improved image model 2025-05-10 16:03:31 +01:00
2f3d643278 feat: not deleting images to process 2025-05-10 15:12:25 +01:00
8e6424aa63 feat: sending notifications 2025-05-10 15:04:11 +01:00
c69ca7da5c chore: adding log to check error 2025-05-10 14:30:26 +01:00
7b0c84e88e feat: adding allowed keys to only send when we have a userId
This is to prevent users that aren't connected to the socket (somehow),
to not fill up memory with buffered messages we'll never need.
2025-05-10 14:29:48 +01:00
ff7960e2dd feat: notifier module with buffered channels 2025-05-10 14:21:18 +01:00
d08fd452f9 chore: git ignore 2025-05-10 13:35:11 +01:00
0d64e90bbf fix: android intents 2025-05-08 18:05:38 +01:00
a7119dfda4 fix 2025-05-08 18:05:15 +01:00
c0f6af7a05 hack: setting prod url to build macos app 2025-05-08 18:04:56 +01:00
ac92f80dc6 feat: IOS stuff 2025-05-08 10:18:33 +01:00
52cb50b168 hack: using prod url 2025-05-08 09:52:46 +01:00
eaa029cce1 fix 2025-05-08 09:52:04 +01:00
9a7654ae2d fix: logs 2025-05-08 09:44:44 +01:00
a9ab92b7b3 key 2025-05-07 16:26:40 +01:00
378900d1b1 feat: better UI when clicking on cards 2025-05-07 13:15:49 +01:00
4fa8bfb7bd fix: UI for images 2025-05-07 10:36:36 +01:00
ac4fd30b0a feat: entity dialog extract out 2025-05-07 10:31:16 +01:00
7d1498c3eb fix: login UI 2025-05-07 10:21:17 +01:00
ce32291437 fix: allowing date created to be null 2025-05-07 10:21:13 +01:00
33b8d51f89 fix: adding full height to cards 2025-05-07 10:21:07 +01:00
a2ba328097 feat: adding a back button 2025-05-05 17:18:28 +01:00
23d91890f5 wip: UI to show image and associated properties
wip

fix
2025-05-05 16:24:36 +01:00
07b83aa728 feat: allowing user to get a list of their images
feat: UI to show and organise user images
2025-05-05 16:24:15 +01:00
9c325c7799 feat: adding timestamps to all important tables 2025-05-05 15:38:05 +01:00
7970e8670c refactor(App.tsx): re-enable event listener setup for Android platform 2025-05-04 14:06:35 +02:00
2deba39907 chore: update .DS_Store binary file 2025-05-04 14:06:23 +02:00
0a766e1ebb feat: add bun.lockb and refactor App.tsx for improved event handling 2025-05-04 13:57:43 +02:00
6119938e52 mini just is better :( 2025-05-04 10:19:39 +01:00
a8d12b5d53 fix: sending multiple images at the same time 2025-05-04 10:14:20 +01:00
ce8d546447 fix: not sleeping 2025-05-04 10:07:31 +01:00
b57a703812 feat: improving search 2025-05-04 09:56:41 +01:00
6952aa16da feat: refresh button 2025-05-03 19:03:24 +01:00
63e3081a69 feat(tray-menu): for screenshot 2025-05-03 18:56:01 +01:00
b046a928b0 feat: using gpt-4.1-mini
feat: createExistingContact

feat: using nano instead of mini so I don't run out of money instantly
2025-05-03 18:25:35 +01:00
9860dd2dc5 feat(agents): providing a seed so it has more predictable results 2025-05-03 17:31:55 +01:00
94920c01fb fix: unique android app name 2025-05-03 15:11:12 +01:00
bb280f52fe feat: android version correctly working alongside iOS version 2025-05-03 13:16:28 +01:00
92e346578a chore: removing submodule of forked version of sharetarget 2025-05-03 13:00:22 +01:00
e9617f86ec feat: only saving token on iOS 2025-05-03 12:46:40 +01:00
875d1d778c chore: cleaning up code to allow all platforms to co-exist 2025-05-03 12:43:47 +01:00
372a891f97 feat: iOS sharing is now working 2025-05-03 11:21:58 +01:00
9ea466610b fix 2025-05-03 11:14:57 +01:00
9fb926db03 fixc 2025-05-03 11:03:46 +01:00
50b8645897 fix: build 2025-05-03 11:02:00 +01:00
4541b366e5 fix 2025-05-03 11:01:10 +01:00
4ed42678f1 fix: naming 2025-05-03 10:59:06 +01:00
a93fd7500a fix: permissions 2025-05-03 10:54:29 +01:00
b50ca077e0 fix 2025-05-03 10:51:21 +01:00
12cd338967 debugging 2025-05-03 10:51:15 +01:00
2a838c81f2 feat: extension code to send image to servers 2025-05-03 10:40:36 +01:00
cd39559834 changes 2025-05-03 10:40:08 +01:00
4c5f3d92e6 fix: app grup id 2025-05-03 10:39:35 +01:00
54bb75956c feat: app group + share extension 2025-05-03 10:26:04 +01:00
0a2d27c150 wip 2025-05-01 18:45:59 +01:00
a05a625516 wip 2025-05-01 18:43:41 +01:00
7e9b33f625 wip(ios): correct command and permissions 2025-05-01 18:40:30 +01:00
9f3a2a473a FIXUP: removing target 2025-05-01 18:23:06 +01:00
61e9258538 feat: saveToken plugin for app groups on iOS
This will allow the share extension to access the Bearer token to send
images to the backend.
2025-05-01 18:20:26 +01:00
c8d9ae7aff wip: using local package for development 2025-05-01 16:14:20 +01:00
2eda77827a wip: showing the file name 2025-05-01 15:50:03 +01:00
afd2e03234 wip: forking sharetarget plugin to add iOS support 2025-05-01 15:30:00 +01:00
3a3acc4a1c fix(ios): fully working mobile versions
fix

fix

fix

testing

testing

fix
2025-04-29 17:27:34 +01:00
d102ab3f6e feat: managing dependencies and building on different platforms 2025-04-29 16:48:04 +01:00
9b006836c6 fix: holy motherfucking shit that took so long 2025-04-29 15:31:32 +01:00
385a0cd186 feat(ios): adding iOS build files 2025-04-27 17:06:54 +01:00
365ef387dd feat(os-info): only showing screenshot button on linux 2025-04-27 15:01:37 +01:00
4922df6682 fix(sse): some bugs 2025-04-27 14:56:06 +01:00
a9ecd5818a fix: re-requesting image when all agents are done
This means the agents are no longer parallel. Which will eventually be a
bottleneck, but I need to spend a bit more time on allowing for it to be
parallel, but now is not the time.

TODO: add a ticket
2025-04-27 14:45:23 +01:00
84d66a1c3b shameful comment 2025-04-26 20:36:10 +01:00
d34805030f feat: frontend responding to backend SSE and refetching images 2025-04-26 20:32:01 +01:00
78a28dee8d fix: working http client 2025-04-26 17:17:19 +01:00
7f7a2975af fix
wip

fix
2025-04-26 17:08:33 +01:00
151142fa9b feat: linux screenshots 2025-04-26 16:12:48 +01:00
fa187b3a79 fix: rebasing errors 2025-04-26 11:51:22 +01:00
b27e191e5c feat(processing-image): displaying initial status from response 2025-04-26 11:47:51 +01:00
e2a4b85d15 WIP: not closing channels quite yet 2025-04-26 11:47:33 +01:00
f1500837e0 feat: request to get a singular image 2025-04-26 11:47:33 +01:00
e6c027aca7 fix: returning correct array of entity deps 2025-04-26 11:47:33 +01:00
495cd742b0 feat(frontend): validators 2025-04-26 11:47:33 +01:00
8cdb4367c7 feat: returning better values to frontend 2025-04-26 11:47:33 +01:00
1388383909 feat(sql): model function to allow user to request image properties based on events 2025-04-26 11:47:33 +01:00
3cd60d4dfc feat(sse): adding info logs 2025-04-26 11:47:33 +01:00
526044d1e3 fix(sse): sending correctly formatted events & exiting function 2025-04-26 11:47:33 +01:00
90ea845521 feat: bringing back shortcuts 2025-04-26 11:47:33 +01:00
dcd3bbb4fb chore(platforms): seperating permissions and inits on different platforms 2025-04-26 11:47:30 +01:00
7aef91c5e0 feat(images): share target working and receiving images! 2025-04-26 11:47:20 +01:00
9245187056 feat: working android dev environment 2025-04-26 11:47:05 +01:00
e84655a181 wip: working release sdk 2025-04-26 11:46:35 +01:00
9a25d2e839 wip: sorting out various versioning problems 2025-04-26 11:46:27 +01:00
f02b22f2fa BIGWIP(android): trying to make an android release
fucking stupid shit why is it so hard
2025-04-26 11:46:09 +01:00
6e9dc81e2b fix: rebasing errors 2025-04-26 11:45:19 +01:00
08b4175b73 fix: mobile entry point cfg_attr + java sdk versions 2025-04-26 11:44:02 +01:00
fa5d38d796 feat(share-target): seperate component to handle sharing 2025-04-26 11:44:02 +01:00
fdb607caea feat: bringing back shortcuts 2025-04-26 11:44:02 +01:00
169b95c450 chore(platforms): seperating permissions and inits on different platforms 2025-04-26 11:43:57 +01:00
191ed3db40 fix: backend err 2025-04-26 11:43:37 +01:00
88bb2fafe2 feat: sending image to the backend 2025-04-26 11:43:37 +01:00
a859abfc17 feat(images): share target working and receiving images! 2025-04-26 11:43:37 +01:00
8cad29a661 feat: working android dev environment 2025-04-26 11:43:10 +01:00
a5d74a97a6 wip: working release sdk 2025-04-26 11:43:00 +01:00
cf71d26f14 wip: sorting out various versioning problems 2025-04-26 11:43:00 +01:00
7e31af27f1 BIGWIP(android): trying to make an android release
fucking stupid shit why is it so hard
2025-04-26 11:43:00 +01:00
78fe25497b refactor(image-viewer): update ImageViewer component structure and remove commented code 2025-04-22 22:13:40 +02:00
dc83bdb3fb feat(screenshot): implement area screenshot functionality and integrate with shortcut management 2025-04-22 22:04:26 +02:00
f6f31540af feat(shortcuts): add screenshot shortcut management and registration 2025-04-22 21:36:37 +02:00
2eb346bb6a feat(shortcuts): improve global shortcut management and documentation 2025-04-22 21:00:48 +02:00
2b022c31cb feat(image-viewer): enhance window focus and visibility handling in ImageViewer component 2025-04-22 20:56:08 +02:00
c3f4403145 fix(linux-tauri): window building on linux 2025-04-19 15:42:14 +01:00
1d07fa271d wip(logs): displaying image
WIP because we need to bypass authorization here
2025-04-19 14:06:05 +01:00
839a1af51b feat(logs): route to view the logs for each image 2025-04-19 14:03:18 +01:00
0324216753 chore(code): cleaning 2025-04-19 12:16:48 +01:00
335d4403f1 feat(logging): split logging to stdout & database to allow us to view it on webbrowser 2025-04-19 12:14:04 +01:00
89ba950c5b fix: enabling note agent 2025-04-19 10:30:49 +01:00
2b8e0695c6 feat(events): a better prompt with good integration with location agent 2025-04-19 10:30:29 +01:00
d448a41a9f feat(contacts): not creating duplicates 2025-04-19 10:07:51 +01:00
a69d4e4d55 feat(location): prompt tweak + going back to faster model 2025-04-18 15:36:51 +01:00
6edc1e2915 feat(location): correctly updating an image if it contains a duplicate locatino 2025-04-18 15:32:07 +01:00
57f1e70c98 feat(agents): improving rationality by adding tool to allow the models to think through choices.
This works pretty nicely actually. I'm starting to understand how to
demistify the system prompt and have the tools the agent needs to do a
good job.
2025-04-18 15:06:20 +01:00
1b1f957e01 wip 2025-04-18 14:21:23 +01:00
49969b0608 feat(location-agent): using createLocation instead of updateLocation to simplify 2025-04-18 13:26:42 +01:00
9b95ffb59e feat(contact-agent): using createContact with an ID field to provide updates 2025-04-17 18:57:13 +01:00
c9560f6881 feat(event-agent): update events function 2025-04-17 18:19:54 +01:00
c5535a5b3b feat(location-agent): seperating the tool to allow for replying
This means it makes less mistakes and doesnt get as confused.
2025-04-17 18:09:00 +01:00
5ab0d13b21 fix(location-events): adding location id to the database from agent call 2025-04-17 15:32:50 +01:00
15289e4965 feat(prompts): adding better prompts & restoring tool_stop
Mistral's models seem to do something really strange if you allow for
`tool_choice` to be anything but `any`. They start putting the tool call
inside the `content` instead of an actual tool call. This means that I
need this `stop` mechanism using a tool call instead because I cannot
trust the model to do it by itself.

I quite like this model though, it's cheap, it's fast and it's open
source. And all the answers are pretty good!
2025-04-17 15:24:21 +01:00
181da1f09d feat(orchestrator): removing the end tool call
fix
2025-04-17 13:00:39 +01:00
90b90a8185 chore: removing unnecessary logging 2025-04-17 13:00:24 +01:00
fb30eb4ad6 wip(orchestrator): improving orchestrator system prompt and tool description 2025-04-17 12:52:54 +01:00
5454a1cfaf feat(event-location): communicating using tool calls correctly 2025-04-17 11:15:02 +01:00
3716d22eca fix(logger): nil pointer error + log debug level clean 2025-04-17 11:07:37 +01:00
6d2f0c6108 refactor(agents): not returning an error on factory method 2025-04-17 11:02:11 +01:00
61c158d5b6 refactor(agents): encapsulating prompt and calls inside factory method 2025-04-17 10:58:19 +01:00
82331c0833 fix: using correct eventAgent instead of orchestrator bug + better logging 2025-04-17 10:48:30 +01:00
e42aa75639 refactor(agents): no need to wrap them in another struct 2025-04-17 10:36:11 +01:00
fa486153b4 feat: event agent calling location agent about location ID
This is pretty nice. We can now have agents spawn other agents and
actually get super cool functionality from it.

The pattern might be a little fragile.
2025-04-16 14:43:07 +01:00
aacecfffac wip(agents): allowing event agent to call location agent 2025-04-15 16:44:00 +01:00
e89a342751 feat: Adding text message to describe an action3 2025-04-15 16:43:27 +01:00
e16b6f4529 fix 2025-04-14 20:08:07 +01:00
6ddae3426d rollback: not using link functions as they are very problematic 2025-04-14 10:59:08 +01:00
67468bddb6 fix(network): restore conditional base URL for development environment
- Reintroduced conditional logic for the base URL to switch between local and production endpoints based on the environment.
2025-04-14 11:41:29 +02:00
10bc0a04a2 Merge branch 'main' of https://git.johncosta.tech/JohnCosta27/Haystack 2025-04-14 11:41:11 +02:00
8a57236f04 ffix 2025-04-14 10:40:02 +01:00
b138661991 prompt 2025-04-14 10:38:25 +01:00
6db9bb2ab3 Merge branch 'main' of https://git.johncosta.tech/JohnCosta27/Haystack 2025-04-14 11:37:34 +02:00
6ae2458186 refactor(network): simplify base URL and clean up validators
- Updated the base URL to a fixed production endpoint, removing the conditional logic for development.
- Commented out Location and Organizer validations in the eventValidator for future consideration.
- Added a console log in getUserImages to assist with backend response tracking.
2025-04-14 11:37:29 +02:00
51d36bf15b more prompt 2025-04-14 10:36:21 +01:00
ecc2da5f86 fix more prompt 2025-04-14 10:33:56 +01:00
d7ab3f56dc stupid 2025-04-14 10:30:46 +01:00
55aa1e67ba horrible 2025-04-14 10:30:21 +01:00
1f83b721a6 fix: prompts 2025-04-14 10:28:31 +01:00
0596ea2b1e debug 2025-04-14 10:22:54 +01:00
3c1f6ba40f fix(network): update base URL for development and production environments
- Changed the base URL to use localhost for development and the production URL for other environments.
- Added Location validation back into the eventValidator and removed commented-out code for clarity.
- Cleaned up debugging logs in getUserImages function.
2025-04-14 10:56:32 +02:00
0eff145f02 push 2025-04-14 09:55:50 +01:00
1fa1db7d1b Merge branch 'main' of https://git.johncosta.tech/JohnCosta27/Haystack 2025-04-14 10:54:24 +02:00
a1369719d7 feat(search): improve Search component with conditional rendering and debugging logs
- Added conditional rendering for the "No results found" message using the Show component.
- Introduced debugging logs in getUserImages and Search component to track data flow.
- Cleaned up the data mapping process in getUserImages for better readability.
2025-04-14 10:54:18 +02:00
40ddf737c8 pushhh 2025-04-14 09:54:09 +01:00
ad14254ecb debnug 2025-04-14 09:50:03 +01:00
e8d996cec5 debug 2025-04-14 09:44:29 +01:00
0ed6b4c123 debug 2025-04-14 09:44:19 +01:00
0bc556f47c Merge branch 'main' of https://git.johncosta.tech/JohnCosta27/Haystack 2025-04-14 10:36:58 +02:00
5a530b2e39 feat(login): implement logout functionality and redirect after login
- Added a logout function in the Settings component to clear user session data and redirect to the login page.
- Updated the Login component to redirect to the home page upon successful login.
- Adjusted styling in the Search component for better spacing in the "No results found" message.
2025-04-14 10:36:55 +02:00
868c8e6409 fix 2025-04-14 09:31:27 +01:00
30143019d6 feat: making all codes upper case + fetching fixes 2025-04-14 09:28:08 +01:00
cd5dd347d3 fix 2025-04-14 09:15:48 +01:00
ab09378fcd chore: removing SQL debug 2025-04-14 09:12:16 +01:00
18f85a8929 feat(search): enhance Search component with shortcuts and item modal
- Added functionality to fetch and display global shortcuts in the Search component.
- Introduced ItemModal for displaying detailed information about selected items.
- Updated SearchCard components to improve layout and information presentation.
- Enhanced user experience with better styling and accessibility features.
2025-04-14 10:03:37 +02:00
55614b34c7 feat(image-viewer): integrate ImageViewer component and update FolderPicker layout
- Added ImageViewer component to the App for displaying processed images.
- Updated FolderPicker layout for improved user guidance and aesthetics.
- Refactored ShortcutItem and Shortcuts components for better structure and clarity.
- Introduced ItemModal component for future use.
2025-04-14 09:25:53 +02:00
664918f431 Merge branch 'main' of https://git.johncosta.tech/JohnCosta27/Haystack 2025-04-14 08:55:42 +02:00
048fc38032 refactor(settings): reorganize FolderPicker component and update layout
- Moved FolderPicker to a new folder structure for better organization.
- Updated the Settings page layout to enhance visual hierarchy by increasing the title size.
- Removed the old FolderPicker component file after restructuring.
2025-04-14 08:55:36 +02:00
2f26b5dfd9 feat(app): restructure routing and implement Search component
- Refactored the App component to streamline routing using the Router and Route components.
- Introduced a new Search component to handle search functionality, including input handling and result display.
- Removed inline search logic from the App component for better separation of concerns.
- Updated index.tsx to render the App component directly, simplifying the routing structure.
2025-04-14 08:47:57 +02:00
4f6c198307 feat: registering users if their email is not known 2025-04-13 22:29:25 +01:00
c99d6e4e6b feat(app): refactor App component and add Settings page
- Refactored the App component to utilize a new SearchCard component for rendering search results.
- Introduced a Settings page with FolderPicker and Shortcuts components for user configuration.
- Removed the ImagePage component as it was no longer needed.
- Updated routing to include the new Settings page and adjusted imports accordingly.
- Added a settings button to the main interface for easy access to the new settings functionality.
2025-04-13 22:48:26 +02:00
b97cf63484 feat(search): add autofocus to search input and emit focus event on shortcut
- Added autofocus attribute to the search input field for improved user experience.
- Updated global shortcut handling to emit a "focus-search" event when the shortcut is triggered, enhancing the application's responsiveness to user actions.
- Updated dependencies in Cargo.toml to specific beta versions for better compatibility.
2025-04-13 21:57:36 +02:00
7af536bd9c feat(capabilities): add localhost URL to default permissions
- Updated the default capabilities configuration to allow access to http://localhost:3040 in addition to the existing https://haystack.johncosta.tech URL.
2025-04-13 21:28:14 +02:00
5406e79fc8 chore: update dependencies and add new packages
- Updated several dependencies in Cargo.lock to their latest versions, including `anyhow`, `ashpd`, `bitflags`, `chrono`, and `http`.
- Added new dependencies such as `tauri-plugin-http`, `cookie_store`, and `h2`.
- Removed outdated dependencies related to `wayland` and updated the `windows` related packages for better compatibility.
2025-04-13 21:24:20 +02:00
0e88f77474 Merge branch 'main' of https://git.johncosta.tech/JohnCosta27/Haystack 2025-04-13 21:21:43 +02:00
878a47ffd1 refactor: using tauri http client 2025-04-13 19:34:02 +01:00
eba4268718 fix 2025-04-13 19:18:07 +01:00
5ae6a3403f chore: removing old agent that was messy and too coupled
chore
2025-04-13 16:30:20 +01:00
3156cea904 feat(event): seperate event agent 2025-04-13 16:30:20 +01:00
d432d16752 feat(location): agent to create locations 2025-04-13 16:30:20 +01:00
98328be39d fix(email) 2025-04-13 16:28:40 +01:00
4d903f40bf feat: add global shortcut functionality and update dependencies
- Introduced global shortcut management in the Tauri application, allowing users to set, change, and unregister shortcuts.
- Added new dependencies for global shortcut functionality in Cargo.toml and updated package.json.
- Enhanced the default capabilities to include global shortcut permissions.
- Refactored the main application logic to integrate the new shortcut features.
2025-04-13 16:40:04 +02:00
24bed2aafb Merge branch 'main' of https://git.johncosta.tech/JohnCosta27/Haystack 2025-04-13 16:20:32 +02:00
349dcc2275 feat: update app description and enhance folder watching functionality
- Updated the app description in package.json and Cargo.toml to "Screenshots that organize themselves".
- Refactored the Tauri backend to introduce a new command for handling folder selection and watching for PNG file changes.
- Added utility functions for processing PNG files and managing the watcher state.
- Improved the frontend by integrating an ImageViewer component and setting up event listeners for search input focus.
2025-04-13 16:19:51 +02:00
47c871523d feat(sse): very rough events. Not used in the client yet
feat(sse): very rough events. Not used in the client yet
2025-04-13 14:27:59 +01:00
dcfed6a746 Revert "FIXUP wip: notifications on starting progress"
This reverts commit 91b9e5402e9f153348f1326ee269533e1e47f777.
2025-04-12 15:57:36 +01:00
91b9e5402e FIXUP wip: notifications on starting progress 2025-04-12 15:55:58 +01:00
cf7d5e0305 chore: removing unused files 2025-04-12 14:44:16 +01:00
9bb07c1b9b fix: tests 2025-04-12 14:43:01 +01:00
959b741fcb refactor(agent): main agent loop extracted away
Still not super sure how to represent these agents in code.
It doesn't make the most amount of sense to keep them in structs. A
curried function is more like it, with system prompt and tooling.

Maybe that's what I'll end up doing.
2025-04-12 14:39:16 +01:00
91cc54aaec fix(event) 2025-04-12 14:15:07 +01:00
d786ab15c9 fix(orchestrator): better describing the note taking agent 2025-04-12 07:53:43 +01:00
47e65e1609 fix(notes): improving note taking capabilities 2025-04-12 07:48:42 +01:00
91dd2f54ef fix(log): removing access token logging 2025-04-12 07:46:07 +01:00
42771ea958 feat(contact-agent): linking to existing instead of creating new ones 2025-04-12 07:29:29 +01:00
77a0901352 fix: removing extra log line 2025-04-12 07:22:35 +01:00
a43efa014f feat(log): pretty logging agent responses and tool calls 2025-04-12 07:16:30 +01:00
4990cf9c43 feat(contact-agent): working contact agent
Built this in under 20 minutes. Getting some really good agents
2025-04-11 21:12:06 +01:00
9660c99a14 feat: contacts working 2025-04-11 20:31:51 +01:00
f89de6db50 feat(authorization): e2e working authorization 2025-04-11 19:58:25 +01:00
6290c4b843 feat: checking user authorization on image retrieval 2025-04-11 19:41:36 +01:00
fba1618888 wip(token): verifying user when getting the image 2025-04-11 19:35:49 +01:00
5fee1f9ccc fix: not using cookies anymore
I think Tauri doesn't like it very much
2025-04-11 13:27:19 +01:00
3960203d26 feat(cookies): using HTTP setCookie instead of manually doing it 2025-04-11 11:34:32 +01:00
2302ba5eeb feat: minimal UI for login 2025-04-11 11:08:33 +01:00
c9a6c83649 feat(jwt): validating token 2025-04-10 15:49:53 +01:00
3294c1854c feat(jwt): adding access and refresh token generation 2025-04-10 15:35:35 +01:00
29a5adb40a feat(email): endpoint for sending auth code 2025-04-09 18:24:26 +01:00
51dc8daf35 wip(email-auth): auth and email modules 2025-04-09 18:13:49 +01:00
a22c56fd2c refactor: moving image listener to own function 2025-04-09 17:20:27 +01:00
11c5c8921b feat(orchestrator): async processing and ending the loop3 2025-04-09 15:23:51 +01:00
1a503c8320 fix(tool-calls): ToolLoop 2025-04-09 15:15:31 +01:00
f169fd2ba2 fix(tools): testing and processing
fix
2025-04-09 13:56:30 +01:00
d36dec8d60 fix(types): agent processing stuff 2025-04-09 12:12:09 +01:00
e065492dd4 feat(chat): more simplified chat messages and tool handling 2025-04-09 12:04:44 +01:00
26c6edb6ba fixup(chat): better way to organize agent messages and tool calls 2025-04-06 20:24:40 +01:00
5c5df168ad fix(tools): dont error if AI invested a tool 2025-04-05 15:04:09 +01:00
e101070851 refactor(tools): removing pointer map
This is not needed
2025-04-05 14:59:50 +01:00
5278727c51 feat(tools): return error to agent if any happened 2025-04-05 14:58:38 +01:00
9a354c38a5 test(tools): more robust multiple tool call handling 2025-04-05 14:52:31 +01:00
cd8375ce0f test(tools): starting test suite for tools 2025-04-05 14:35:54 +01:00
6549643340 feat(orchestrator): calling needed agents when it needs to 2025-04-05 11:01:43 +01:00
33fb206e2f fix(tool): raw text not scaling so well ey? 2025-04-04 22:50:19 +01:00
49f1990341 refactor(agents): working e2e now
I guess some repeated code doesnt hurt anyone, if it keeps things
simpler. Trying to be fancy with the interfaces didn't work so well.
2025-04-04 22:40:45 +01:00
40392e6da3 refactor(tool-calls): to be handled more generally 2025-04-04 22:17:58 +01:00
d3bc840555 refactor(ai-client): moving tool handling and client into seperate folders 2025-04-04 22:03:46 +01:00
ede5f16dc1 wip(orchestrator): basic scaffolding for the agent 2025-04-04 20:40:31 +01:00
75132503c0 feat: sample data 2025-04-02 17:32:52 +00:00
393eaea2f4 feat(notes): allowing frontend to save 2025-04-01 20:54:15 +00:00
a385ef21cf feat(notes): saving the notes for any images for easy text searching 2025-04-01 20:45:43 +00:00
a37818fc49 feat(events): search through organizer 2025-04-01 19:59:17 +00:00
0d3f86532e feat: adding rawData search 2025-04-01 19:48:06 +00:00
55e50d31ca fix(validators): allowing location in event fields 2025-04-01 19:33:31 +00:00
1b2a99a3c8 fix: adding location to general query 2025-04-01 19:32:55 +00:00
ae62d2bea5 Merge branch 'integrating-frontend' 2025-04-01 19:30:51 +00:00
0126125837 fix 2025-03-31 20:17:14 +00:00
c5278554cc feat(contacts): events can now have organizers 2025-03-31 18:40:36 +00:00
bb5f2bc2fe feat(schema): basic contact tables 2025-03-31 18:10:04 +00:00
b7ed4e2169 chore(imports): organisation 2025-03-31 18:03:02 +00:00
c609b45d99 feat(cards): adjusting for backend data types 2025-03-31 17:49:17 +00:00
c817654f3e feat(events): adding start and end times 2025-03-31 17:32:55 +00:00
3f53317c06 feat(schema): removing coordinates and adding start times to events
.
2025-03-31 16:44:42 +00:00
254edf3421 fix(backend): SQL statements without returns 2025-03-26 16:51:46 +00:00
0814e19a68 refactor(validators): frontend to new schema 2025-03-26 16:51:35 +00:00
382a1f53bd feat: attaching both to image 2025-03-26 16:22:23 +00:00
f90876f499 feat: creating events and attaching locations 2025-03-26 16:16:48 +00:00
caf168c7a1 feat(frontend): add new search card components and update styling
- Introduced new search card components for Contact, Event, Location, Note, Receipt, and Website.
- Updated the App component to utilize these new components for displaying search results.
- Changed the default font from Manrope to Switzer and updated related styles.
- Added a new dependency `solid-motionone` to package.json.
- Improved search functionality with a new sample data structure and enhanced search logic.
2025-03-23 21:56:09 +01:00
4c85f1de79 refactor(frontend): clean up App component and improve search functionality 2025-03-23 19:10:18 +01:00
410df01b4d feat(tool-calling) Big refactor on how tool calling is handled
these commits are too big
2025-03-22 20:46:26 +00:00
13e5ed9f9e feat(locations): allowing AI to attach it to the image 2025-03-22 17:47:02 +00:00
dfb4b34de3 feat(location): working e2e with tool calling 2025-03-22 12:22:31 +00:00
7b6c7090f8 feat(tool-calls): listLocation tool call handling 2025-03-22 11:14:00 +00:00
87869543f7 feat: using tools for event loocation agent 2025-03-22 10:12:51 +00:00
1cd4698969 refactor(naming): using Agent instead of openai 2025-03-21 17:07:00 +00:00
4ea817e81f fix: docker image 2025-03-21 14:49:51 +00:00
3541a4755c wip(frontend): adding more information 2025-03-21 14:36:03 +00:00
ea5802b61b fix: app to re-include images 2025-03-21 14:23:38 +00:00
cf703f3eee fix: re-adding location to event
Figured it out
2025-03-21 14:08:06 +00:00
84881c5c2d fix: simplifying query
I do have some learning to do about go-jet specifically.

I can't include the Location field on events, because the QRM will add
any location to the events
2025-03-21 14:01:21 +00:00
992a8ea282 feat: frontend validation 2025-03-21 13:44:42 +00:00
f7382b0d2b feat: returning events and locations from end point 2025-03-20 18:34:11 +00:00
47dd025ae3 feat: working e2e solution 2025-03-20 17:59:00 +00:00
f114ca06d8 merging 2025-03-19 09:46:52 +00:00
20213ff17b feat: adding events and locations to json schema 2025-03-19 09:46:42 +00:00
9932568986 feat(events): also working 2025-03-18 18:37:12 +00:00
3a0f93e406 feat(locations): now working 2025-03-18 18:18:01 +00:00
b09063f74a refactor(text,tags,links): to foreign key to image instead of user_image 2025-03-18 17:48:38 +00:00
b3b37d252d wip: add sample data and types 2025-03-17 22:20:26 +01:00
3c71fddbd2 fix: linter and format issues
format
2025-03-17 21:16:40 +01:00
a3e1db3d77 Merge branch 'main' of https://git.johncosta.tech/JohnCosta27/Haystack 2025-03-17 21:01:15 +01:00
5a766b8371 wip 2025-03-17 21:00:52 +01:00
fd804ae515 fix: frontend parsers 2025-03-16 18:41:26 +00:00
4b120982d0 fix: returning whole tag object 2025-03-16 18:29:15 +00:00
7582e4d8d9 fix: returns correct image ID 2025-03-16 18:20:29 +00:00
8acf25a2a7 refactor(models): using more organised structure 2025-03-16 18:13:30 +00:00
e505a1617e Merge branch 'main' of https://github.com/dimuuu/haystack-app 2025-03-13 17:25:16 +01:00
028e45bb7a wip 2025-03-13 17:24:53 +01:00
536a49fe1c feat(tags): correctly inserting new tags and adding them to images 2025-03-11 22:47:28 +00:00
40e854fb87 feat(tags): creating and getting user tags 2025-03-11 21:23:41 +00:00
5df6c67ee5 feat: new schema to support user tags better 2025-03-11 20:29:56 +00:00
05263d1089 fix: actually searching properly 2025-03-08 15:50:21 +00:00
1bc1b79042 feat: better result display 2025-03-08 15:42:16 +00:00
863716c096 feat: super basic image search 2025-03-08 15:37:10 +00:00
53ebbb6e8d feat: sending images and receiving them is now working 2025-03-08 13:13:05 +00:00
bf07c18fd7 feat: sending base64 image to backend
This is silly, but binary is apparently hard to do????
2025-03-08 12:30:16 +00:00
d212584486 wip: dialog to choose folder to watch 2025-03-08 11:58:25 +00:00
1424ec22f4 fix: using json response header 2025-03-07 14:14:40 +00:00
e595783d89 wip: Using mistral instead of OpenAi 2025-03-07 13:42:50 +00:00
2df18869e5 chore: running format 2025-02-26 21:27:43 +00:00
ee69d9c2fe feat: network file with validators for backend requests 2025-02-26 21:27:37 +00:00
7e7f3ff732 feat: making prompt be more generic with tags 2025-02-26 20:53:12 +00:00
3fe48464e4 feat: using different prompt 2025-02-26 20:49:55 +00:00
d1d6ee6762 fix: some spam from get images request 2025-02-26 20:48:52 +00:00
ad61b8e1fa fix: getting user images 2025-02-26 20:09:19 +00:00
d8095b0c67 refactor: tables for image and processing_image
This allows a single table to be used to process images, meaning if
anything happens to the system we can always return to polling the
database and process these images individually.

Because of this we also want an `image` table to contain the actual
binary data for the image, so we aren't selecting and writing it each
time, as it is potentially a bottleneck.
2025-02-26 20:01:56 +00:00
410270e217 refactor: using chi router + bug fixes 2025-02-26 18:04:30 +00:00
5bec6c9590 fix: actually saving to the correct db table 2025-02-26 15:54:27 +00:00
971f705288 fix: actually returning all the user images
fix
2025-02-24 21:19:59 +00:00
13ebd80ce9 chore: documentation 2025-02-24 21:04:25 +00:00
b99432c202 feat: saving AI information to database 2025-02-24 21:00:05 +00:00
ee0587a16b feat: method for getting images 2025-02-24 20:05:56 +00:00
64f6bde6a9 feat: methods to get image 2025-02-24 20:02:58 +00:00
f49589907a feat: instructions for docker compose 2025-02-24 20:02:54 +00:00
2115da85b5 feat: working docker image and compose file 2025-02-24 19:44:19 +00:00
43092fa4f5 feat: using parsing on response 2025-02-24 19:05:11 +00:00
46e4043994 chore: updating gitignore
fix
2025-02-24 19:01:18 +00:00
24ef31e00f feat: parsing response from open ai
bruh
2025-02-24 19:01:18 +00:00
993fbb30eb build very basic ui 2025-02-23 22:16:41 +01:00
8cc7e4002f some updates 2025-02-23 20:11:58 +01:00
1fc1079484 messing around with ui 2025-02-23 20:02:06 +01:00
df16298b1f added a bunch of frontend things 2025-02-23 19:30:11 +01:00
f4690b52a9 Merge branch 'main' of https://github.com/dimuuu/haystack-app 2025-02-23 14:59:04 +01:00
81590fe622 some window ui tweaks 2025-02-23 14:59:01 +01:00
97b1619b01 refactor: moving all files to backend 2025-02-22 23:30:59 +00:00
c0ce4892cd Merge branch 'main' of github.com:dimuuu/haystack-app 2025-02-22 23:29:49 +00:00
90431f824a add biome 2025-02-22 16:41:52 +01:00
fe60149769 v0 2025-02-22 16:30:41 +01:00
248 changed files with 11076 additions and 7507 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View 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 users specs exactly. If a specs folder exists, check it before coding.
Begin with a detailed pseudo-code plan and confirm it with the user before writing actual code.
Write correct, complete, idiomatic, secure, performant, and bug-free code.
Prioritize readability unless performance is explicitly required.
Fully implement all requested features—no TODOs, stubs, or placeholders.
Use TypeScript's type system thoroughly for clarity and safety.
Style with TailwindCSS using utility-first principles.
Use Kobalte components effectively, building with Solids reactive model in mind.
Offload performance-heavy logic to Rust and ensure smooth integration with Tauri.
Guarantee tight coordination between SolidJS, Tauri, and Rust for a polished desktop UX.
When needed, provide bash scripts to generate config files or folder structures.
Be concise—cut the fluff.
If there's no solid answer, say so. If you're unsure, don't guess—own it.

3
.gitmodules vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 ImageText struct {
ID uuid.UUID `sql:"primary_key"`
ImageText string
ImageID uuid.UUID
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 UserContacts struct {
ID uuid.UUID `sql:"primary_key"`
UserID uuid.UUID
ContactID uuid.UUID
}

View File

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

View File

@ -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 UserImages struct {
ID uuid.UUID `sql:"primary_key"`
ImageID uuid.UUID
UserID uuid.UUID
}

View File

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

View File

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

View File

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

View File

@ -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 UserTags struct {
ID uuid.UUID `sql:"primary_key"`
Tag string
UserID uuid.UUID
}

View File

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

View File

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

View File

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

View File

@ -17,9 +17,13 @@ type imageTable struct {
postgres.Table
// Columns
ID postgres.ColumnString
ImageName postgres.ColumnString
Image postgres.ColumnString
ID postgres.ColumnString
UserID postgres.ColumnString
ImageName postgres.ColumnString
Description postgres.ColumnString
Status postgres.ColumnString
Image postgres.ColumnString
CreatedAt postgres.ColumnTimestampz
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
@ -60,20 +64,28 @@ func newImageTable(schemaName, tableName, alias string) *ImageTable {
func newImageTableImpl(schemaName, tableName, alias string) imageTable {
var (
IDColumn = postgres.StringColumn("id")
ImageNameColumn = postgres.StringColumn("image_name")
ImageColumn = postgres.StringColumn("image")
allColumns = postgres.ColumnList{IDColumn, ImageNameColumn, ImageColumn}
mutableColumns = postgres.ColumnList{ImageNameColumn, ImageColumn}
IDColumn = postgres.StringColumn("id")
UserIDColumn = postgres.StringColumn("user_id")
ImageNameColumn = postgres.StringColumn("image_name")
DescriptionColumn = postgres.StringColumn("description")
StatusColumn = postgres.StringColumn("status")
ImageColumn = postgres.StringColumn("image")
CreatedAtColumn = postgres.TimestampzColumn("created_at")
allColumns = postgres.ColumnList{IDColumn, UserIDColumn, ImageNameColumn, DescriptionColumn, StatusColumn, ImageColumn, CreatedAtColumn}
mutableColumns = postgres.ColumnList{UserIDColumn, ImageNameColumn, DescriptionColumn, StatusColumn, ImageColumn, CreatedAtColumn}
)
return imageTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
ID: IDColumn,
ImageName: ImageNameColumn,
Image: ImageColumn,
ID: IDColumn,
UserID: UserIDColumn,
ImageName: ImageNameColumn,
Description: DescriptionColumn,
Status: StatusColumn,
Image: ImageColumn,
CreatedAt: CreatedAtColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,78 +0,0 @@
//
// Code generated by go-jet DO NOT EDIT.
//
// WARNING: Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated
//
package table
import (
"github.com/go-jet/jet/v2/postgres"
)
var Logs = newLogsTable("haystack", "logs", "")
type logsTable struct {
postgres.Table
// Columns
Log postgres.ColumnString
ImageID postgres.ColumnString
AllColumns postgres.ColumnList
MutableColumns 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")
allColumns = postgres.ColumnList{LogColumn, ImageIDColumn}
mutableColumns = postgres.ColumnList{LogColumn, ImageIDColumn}
)
return logsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
Log: LogColumn,
ImageID: ImageIDColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,177 +0,0 @@
package agents
import (
"context"
"encoding/json"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/models"
"github.com/charmbracelet/log"
"github.com/google/uuid"
)
const contactPrompt = `
**Role:** AI Contact Processor from Images.
**Goal:** Extract contacts from an image, check against existing list using listContacts, add *only* new contacts using createContact, and call stopAgent when finished. Avoid duplicates.
**Input:** Image potentially containing contact info (Name, Phone, Email, Address).
**Workflow:**
1. **Scan Image:** Extract all contact details. If none, call stopAgent.
2. **Think:** Using the think tool, you must layout your thoughts about the contacts on the image. If they are duplicates or not, and what your next action should be,
3. **Check Duplicates:** If contacts found, *first* call listContacts. Compare extracted info to list. If all found contacts already exist, call stopAgent.
4. **Add New:** If you detect a new contact on the image, call createContact to create a new contact.
5. **Finish:** Call stopAgent once all new contacts are created OR if steps 1 or 2 determined no action/creation was needed.
**Tools:**
* listContacts: Check existing contacts (Use first if contacts found).
* createContact: Add a NEW contact (Name required).
* stopAgent: Signal task completion.
`
const contactTools = `
[
{
"type": "function",
"function": {
"name": "think",
"description": "Use this tool to think through the image, evaluating the contact and whether or not it exists in the users listContacts.",
"parameters": {
"type": "object",
"properties": {
"thought": {
"type": "string",
"description": "A singular thought about the image"
}
},
"required": ["thought"]
}
}
},
{
"type": "function",
"function": {
"name": "listContacts",
"description": "Retrieves the complete list of the user's currently saved contacts (e.g., names, phone numbers, emails if available in the stored data). This tool is essential and **must** be called *before* attempting to create a new contact if potential contact info is found in the image, to check if the person already exists and prevent duplicate entries.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "createContact",
"description": "Saves a new contact to the user's contact list. Only use this function **after** confirming the contact does not already exist by checking the output of listContacts. Provide all available extracted information for the new contact. Process one new contact per call.",
"parameters": {
"type": "object",
"properties": {
"contactId": {
"type": "string",
"description": "The UUID of the contact. You should only provide this IF you believe the contact already exists, from listContacts."
},
"name": {
"type": "string",
"description": "The full name of the person being added as a contact. This field is mandatory."
},
"phoneNumber": {
"type": "string",
"description": "The contact's primary phone number, including area or country code if available. Provide this if extracted from the image."
},
"address": {
"type": "string",
"description": "The complete physical mailing address of the contact (e.g., street number, street name, city, state/province, postal code, country). Provide this if extracted from the image."
},
"email": {
"type": "string",
"description": "The contact's primary email address. Provide this if extracted from the image."
}
},
"required": ["name"]
}
}
},
{
"type": "function",
"function": {
"name": "stopAgent",
"description": "Use this tool to signal that the contact processing for the current image is complete. Call this *only* when: 1) No contact info was found initially, OR 2) All found contacts were confirmed to already exist after calling listContacts, OR 3) All necessary createContact calls for new individuals have been completed.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
}
]
`
type listContactsArguments struct{}
type createContactsArguments struct {
Name string `json:"name"`
ContactID *string `json:"contactId"`
PhoneNumber *string `json:"phoneNumber"`
Address *string `json:"address"`
Email *string `json:"email"`
}
func NewContactAgent(log *log.Logger, contactModel models.ContactModel) client.AgentClient {
agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: contactPrompt,
JsonTools: contactTools,
Log: log,
EndToolCall: "stopAgent",
})
agentClient.ToolHandler.AddTool("think", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return "Thought", nil
})
agentClient.ToolHandler.AddTool("listContacts", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return contactModel.List(context.Background(), info.UserId)
})
agentClient.ToolHandler.AddTool("createContact", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
args := createContactsArguments{}
err := json.Unmarshal([]byte(_args), &args)
if err != nil {
return model.Contacts{}, err
}
ctx := context.Background()
contactId := uuid.Nil
if args.ContactID != nil {
contactUuid, err := uuid.Parse(*args.ContactID)
if err != nil {
return model.Contacts{}, err
}
contactId = contactUuid
}
contact, err := contactModel.Save(ctx, info.UserId, model.Contacts{
ID: contactId,
Name: args.Name,
PhoneNumber: args.PhoneNumber,
Email: args.Email,
})
if err != nil {
return model.Contacts{}, err
}
_, err = contactModel.SaveToImage(ctx, info.ImageId, contact.ID)
if err != nil {
return model.Contacts{}, err
}
return contact, nil
})
return agentClient
}

View File

@ -0,0 +1,157 @@
package agents
import (
"context"
"encoding/json"
"fmt"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/models"
"strings"
"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.
You must respond in json format, do not add backticks to the json. ONLY valid json.
`
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
stackModel models.StackModel
}
func (agent *CreateListAgent) CreateList(log *log.Logger, userID uuid.UUID, stackID uuid.UUID, title string, userReq string) error {
request := client.AgentRequestBody{
Model: "policy/images",
Temperature: 0.3,
ResponseFormat: client.ResponseFormat{
Type: "json_schema",
JsonSchema: listJsonSchema,
},
Chat: &client.Chat{
Messages: make([]client.ChatMessage, 0),
},
}
request.Chat.AddSystem(agent.client.Options.SystemPrompt)
req := fmt.Sprintf("List title: %s | Users list description: %s", title, userReq)
request.Chat.AddUser(req)
resp, err := agent.client.Request(&request)
if err != nil {
return fmt.Errorf("request: %w", err)
}
ctx := context.Background()
content := resp.Choices[0].Message.Content
if strings.HasPrefix(content, "```json") {
content = content[len("```json") : len(content)-3]
}
log.Info("", "res", content)
var createListArgs createNewListArguments
err = json.Unmarshal([]byte(content), &createListArgs)
if err != nil {
return err
}
schemaItems := make([]model.SchemaItems, 0)
for _, field := range createListArgs.Fields {
schemaItems = append(schemaItems, model.SchemaItems{
StackID: stackID,
Item: field.Name,
Description: field.Description,
Value: "string", // keep it simple for now.
})
}
err = agent.stackModel.SaveItems(ctx, schemaItems)
if err != nil {
return fmt.Errorf("creating list agent, saving items: %w", err)
}
return nil
}
func NewCreateListAgent(log *log.Logger, listModel models.StackModel) CreateListAgent {
client := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: createListAgentPrompt,
Log: log,
})
agent := CreateListAgent{
client,
listModel,
}
return agent
}

View File

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

View File

@ -1,250 +0,0 @@
package agents
import (
"context"
"encoding/json"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/models"
"time"
"github.com/charmbracelet/log"
"github.com/google/uuid"
)
const eventPrompt = `
**You are an AI processing events from images using internal thought.**
**Task:** Extract event details (Name, Date/Time, Location). Use think before deciding actions. Check duplicates with listEvents. Handle new events via getEventLocationId (if location exists) and createEvent. Use finish if no event or duplicate found.
1. **Analyze Image & Think:** Extract details. Use think to confirm if a valid event exists. If not -> stopAgent.
2. **Event Confirmed?** -> *Must* call listEvents, to check for existing events and prevent duplicates.
3. **Detect Duplicates** -> If the input contains an event that already exists from listEvents, then you should call stopAgent.
4. **New Events**
* If you think the input contains a location, then you can use getEventLocationId to retrieve the ID of the location. Only use this IF the input contains a location.
* Call createEvent.
5. **Multiple Events:** Process sequentially using this logic.
**Tools:**
* think: Internal reasoning/planning step.
* listEvents: Check for duplicates (mandatory first step for found events).
* getEventLocationId: Get ID for location text.
* createEvent: Add new event (Name req.). Terminal action for new events.
* stopAgent: Signal completion (no event/duplicate found). Terminal action.
`
const eventTools = `
[
{
"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": "listEvents",
"description": "Retrieves the list of the user's currently scheduled events. Essential for checking if an event identified in the image already exists to prevent duplicates. Must be called before potentially creating an event.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "createEvent",
"description": "Creates a new event in the user's calendar or list. Use only after listEvents confirms the event is new. Provide all extracted details.",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name or title of the event. This field is mandatory."
},
"startDateTime": {
"type": "string",
"description": "The event's start date and time in ISO 8601 format (e.g., '2025-04-18T10:00:00Z'). Include if available."
},
"endDateTime": {
"type": "string",
"description": "The event's end date and time in ISO 8601 format. Optional, include if available and different from startDateTime."
},
"locationId": {
"type": "string",
"description": "The unique identifier (UUID or similar) for the event's location. Use this if available, do not invent it."
}
},
"required": ["name"]
}
}
},
{
"type": "function",
"function": {
"name": "updateEvent",
"description": "Updates an existing event record identified by its eventId. Use this tool when listEvents indicates a match for the event details found in the current input.",
"parameters": {
"type": "object",
"properties": {
"eventId": {
"type": "string",
"description": "The UUID of the existing event"
}
},
"required": ["eventId"]
}
}
},
{
"type": "function",
"function": {
"name": "getEventLocationId",
"description": "Retrieves a unique identifier for a location description associated with an event. Use this before createEvent if a new event specifies a location.",
"parameters": {
"type": "object",
"properties": {
"locationDescription": {
"type": "string",
"description": "The text describing the location extracted from the image (e.g., 'Conference Room B', '123 Main St, Anytown', 'Zoom Link details')."
}
},
"required": ["locationDescription"]
}
}
},
{
"type": "function",
"function": {
"name": "stopAgent",
"description": "Call this tool only when event processing for the current image is fully complete. This occurs if: 1) No event info was found, OR 2) The found event already exists, OR 3) A new event has been successfully created.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
}
]`
type listEventArguments struct{}
type createEventArguments struct {
Name string `json:"name"`
StartDateTime *string `json:"startDateTime"`
EndDateTime *string `json:"endDateTime"`
OrganizerName *string `json:"organizerName"`
LocationID *string `json:"locationId"`
}
type updateEventArguments struct {
EventID string `json:"eventId"`
}
func NewEventAgent(log *log.Logger, eventsModel models.EventModel, locationModel models.LocationModel) client.AgentClient {
agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: eventPrompt,
JsonTools: eventTools,
Log: log,
EndToolCall: "stopAgent",
})
locationAgent := NewLocationAgentWithComm(log.WithPrefix("Events 📅 > Locations 📍"), locationModel)
locationQuery := "Can you get me the ID of the location present in this image?"
locationAgent.Options.Query = &locationQuery
agentClient.ToolHandler.AddTool("think", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return "Thought", nil
})
agentClient.ToolHandler.AddTool("listEvents", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return eventsModel.List(context.Background(), info.UserId)
})
agentClient.ToolHandler.AddTool("createEvent", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
args := createEventArguments{}
err := json.Unmarshal([]byte(_args), &args)
if err != nil {
return model.Events{}, err
}
ctx := context.Background()
layout := "2006-01-02T15:04:05Z"
// TODO: check for nil pointers.
startTime, err := time.Parse(layout, *args.StartDateTime)
if err != nil {
return model.Events{}, err
}
endTime, err := time.Parse(layout, *args.EndDateTime)
if err != nil {
return model.Events{}, err
}
locationId, err := uuid.Parse(*args.LocationID)
if err != nil {
return model.Events{}, err
}
events, err := eventsModel.Save(ctx, info.UserId, model.Events{
Name: args.Name,
StartDateTime: &startTime,
EndDateTime: &endTime,
LocationID: &locationId,
})
if err != nil {
return model.Events{}, err
}
_, err = eventsModel.SaveToImage(ctx, info.ImageId, events.ID)
if err != nil {
return model.Events{}, err
}
return events, nil
})
agentClient.ToolHandler.AddTool("updateEvent", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
args := updateEventArguments{}
err := json.Unmarshal([]byte(_args), &args)
if err != nil {
return "", err
}
ctx := context.Background()
contactUuid, err := uuid.Parse(args.EventID)
if err != nil {
return "", err
}
eventsModel.SaveToImage(ctx, info.ImageId, contactUuid)
return "Saved", nil
})
agentClient.ToolHandler.AddTool("getEventLocationId", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
// TODO: reenable this when I'm creating the agent locally instead of getting it from above.
locationAgent.RunAgent(info.UserId, info.ImageId, info.ImageName, *info.Image)
log.Debugf("Reply from location %s\n", locationAgent.Reply)
return locationAgent.Reply, nil
})
return agentClient
}

View File

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

View File

@ -1,248 +0,0 @@
package agents
import (
"context"
"encoding/json"
"fmt"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/models"
"github.com/charmbracelet/log"
"github.com/google/uuid"
)
const locationPrompt = `
Role: Location AI Assistant
Objective: Identify locations from images/text, manage a saved list, and answer user queries about saved locations using the provided tools.
The user does not want to have duplicate entries on their saved location list. So you should only create a new location if listLocation doesnt return
what would be a duplicate.
Core Logic:
**Extract Location Details:** Attempt to extract location details (like InputName, InputAddress) from the user's input.
* If no details can be extracted, inform the user and use stopAgent.
**Check for Existing Location:** If details *were* extracted:
* Use listLocations with the extracted InputName and/or InputAddress to search for potentially matching locations already saved in the list.
Action loop:
**Thinking**
* Use the think tool to analytise the image.
* You should think about whether listLocations already contains this location, or if it is a new location.
* You should always call this after listLocations.
* You must think about whether or not listLocations already has this location.
**Decide Action based on Search Results:**
* If no existing location looks like the location on the input. You should use createLocation.
* Do not use this tool if this location already exists.
* If the input contains a location that already exists, you should use createExistingLocation.
* If there is a similar location in listLocation, you should use this tool. It doesnt have to be an exact match.
* Lastly, if the user asked a specific question about a location. You must do all the actions but also always use the reply tool to answer the user.
* This is the only way you can communicate with the user if they asked a query.
You should repeat the action loop until all locations on the image are done.
Once you are done, use stopAgent.
`
const replyTool = `
{
"type": "function",
"function": {
"name": "reply",
"description": "Signals intent to provide information about a specific known location in response to a user's query. Use only if the user asked a question and the location's ID was found via listLocations.",
"parameters": {
"type": "object",
"properties": {
"locationId": {
"type": "string",
"description": "The unique identifier of the saved location that the user is asking about."
}
},
"required": ["locationId"]
}
}
},`
const locationTools = `
[
{
"type": "function",
"function": {
"name": "think",
"description": "Use this tool to think through the image, evaluating the location and whether or not it exists in the users listLocations. You should also ask yourself if the user has asked a query, and if you've used the correct tool to reply to them.",
"parameters": {
"type": "object",
"properties": {
"thought": {
"type": "string",
"description": "A singular thought about the image"
}
},
"required": ["thought"]
}
}
},
{
"type": "function",
"function": {
"name": "listLocations",
"description": "Retrieves the list of the user's currently saved locations (names, addresses, IDs). Use this first to check if a location from an image already exists, or to find the ID of a location the user is asking about.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "createLocation",
"description": "Creates a new location with as much information as you can extract. Be precise. You should only add the parameters you can actually see on the image.",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The primary name of the location"
},
"address": {
"type": "string",
"description": "The address of the location"
}
},
"required": ["name"]
}
}
},
{
"type": "function",
"function": {
"name": "createExistingLocation",
"description": "Called when a location already exists in the users list, from listLocations. Only call this to indicate this image contains a duplicate. And only after using the doesLocationExist tol",
"parameters": {
"type": "object",
"properties": {
"locationId": {
"type": "string",
"description": "The UUID of the location, from listLocations"
}
},
"required": ["locationId"]
}
}
},
%s
{
"type": "function",
"function": {
"name": "stopAgent",
"description": "Use this tool to signal that the contact processing for the current image is complete.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
}
]`
func getLocationAgentTools(allowReply bool) string {
if allowReply {
return fmt.Sprintf(locationTools, replyTool)
} else {
return fmt.Sprintf(locationTools, "")
}
}
type listLocationArguments struct{}
type createLocationArguments struct {
Name string `json:"name"`
Address *string `json:"address"`
}
type createExistingLocationArguments struct {
LocationID string `json:"locationId"`
}
func NewLocationAgentWithComm(log *log.Logger, locationModel models.LocationModel) client.AgentClient {
client := NewLocationAgent(log, locationModel)
client.Options.JsonTools = getLocationAgentTools(true)
return client
}
func NewLocationAgent(log *log.Logger, locationModel models.LocationModel) client.AgentClient {
agentClient := client.CreateAgentClient(client.CreateAgentClientOptions{
SystemPrompt: locationPrompt,
JsonTools: getLocationAgentTools(false),
Log: log,
EndToolCall: "stopAgent",
})
agentClient.ToolHandler.AddTool("listLocations", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return locationModel.List(context.Background(), info.UserId)
})
agentClient.ToolHandler.AddTool("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()
// TODO: this tool could be simplier, as the model could have a SaveToImage joined with the save.
location, err := locationModel.Save(ctx, info.UserId, model.Locations{
Name: args.Name,
Address: args.Address,
})
if err != nil {
return model.Locations{}, err
}
_, err = locationModel.SaveToImage(ctx, info.ImageId, location.ID)
if err != nil {
return model.Locations{}, err
}
return location, nil
})
agentClient.ToolHandler.AddTool("createExistingLocation", func(info client.ToolHandlerInfo, _args string, call client.ToolCall) (any, error) {
args := createExistingLocationArguments{}
err := json.Unmarshal([]byte(_args), &args)
if err != nil {
return "", err
}
ctx := context.Background()
locationId, err := uuid.Parse(args.LocationID)
if err != nil {
return "", err
}
_, err = locationModel.SaveToImage(ctx, info.ImageId, locationId)
if err != nil {
return "", err
}
return "", nil
})
agentClient.ToolHandler.AddTool("reply", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return "ok", nil
})
agentClient.ToolHandler.AddTool("think", func(info client.ToolHandlerInfo, args string, call client.ToolCall) (any, error) {
return "ok", nil
})
return agentClient
}

View File

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

View File

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

View File

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

View File

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

View File

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

135
backend/auth/handler.go Normal file
View File

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

View File

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

View File

@ -9,7 +9,7 @@ require (
github.com/charmbracelet/x/ansi v0.4.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-chi/chi/v5 v5.2.1 // indirect
github.com/go-jet/jet/v2 v2.12.0 // indirect
github.com/go-jet/jet/v2 v2.13.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect

View File

@ -13,6 +13,8 @@ github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-jet/jet/v2 v2.12.0 h1:z2JfvBAZgsfxlQz6NXBYdZTXc7ep3jhbszTLtETv1JE=
github.com/go-jet/jet/v2 v2.12.0/go.mod h1:ufQVRQeI1mbcO5R8uCEVcVf3Foej9kReBdwDx7YMWUM=
github.com/go-jet/jet/v2 v2.13.0 h1:DcD2IJRGos+4X40IQRV6S6q9onoOfZY/GPdvU6ImZcQ=
github.com/go-jet/jet/v2 v2.13.0/go.mod h1:YhT75U1FoYAxFOObbQliHmXVYQeffkBKWT7ZilZ3zPc=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=

232
backend/images/handler.go Normal file
View File

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

803
backend/integration_test.go Normal file
View File

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

View File

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

61
backend/limits/limits.go Normal file
View File

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

View File

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

View File

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

View File

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

View 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)
}
}

View 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)
})
}

136
backend/middleware/jwt.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -1,117 +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) Get(ctx context.Context, contactId uuid.UUID) (model.Contacts, error) {
getContactStmt := Contacts.
SELECT(Contacts.AllColumns).
WHERE(Contacts.ID.EQ(UUID(contactId)))
contact := model.Contacts{}
err := getContactStmt.QueryContext(ctx, m.dbPool, &contact)
return contact, err
}
func (m ContactModel) Update(ctx context.Context, contact model.Contacts) (model.Contacts, error) {
existingContact, err := m.Get(ctx, contact.ID)
if err != nil {
return model.Contacts{}, err
}
existingContact.Name = contact.Name
if contact.Description != nil {
existingContact.Description = contact.Description
}
if contact.PhoneNumber != nil {
existingContact.PhoneNumber = contact.PhoneNumber
}
if contact.Email != nil {
existingContact.Email = contact.Email
}
updateContactStmt := Contacts.
UPDATE(Contacts.MutableColumns).
MODEL(existingContact).
WHERE(Contacts.ID.EQ(UUID(contact.ID))).
RETURNING(Contacts.AllColumns)
updatedContact := model.Contacts{}
err = updateContactStmt.QueryContext(ctx, m.dbPool, &updatedContact)
return updatedContact, err
}
func (m ContactModel) Save(ctx context.Context, userId uuid.UUID, contact model.Contacts) (model.Contacts, error) {
// TODO: make this a transaction
if contact.ID != uuid.Nil {
return m.Update(ctx, contact)
}
insertContactStmt := Contacts.
INSERT(Contacts.Name, Contacts.Description, Contacts.PhoneNumber, Contacts.Email).
VALUES(contact.Name, contact.Description, contact.PhoneNumber, contact.Email).
RETURNING(Contacts.AllColumns)
insertedContact := model.Contacts{}
err := insertContactStmt.QueryContext(ctx, m.dbPool, &insertedContact)
if err != nil {
return insertedContact, err
}
insertUserContactStmt := UserContacts.
INSERT(UserContacts.UserID, UserContacts.ContactID).
VALUES(userId, insertedContact.ID)
_, err = insertUserContactStmt.ExecContext(ctx, m.dbPool)
return insertedContact, err
}
func (m ContactModel) SaveToImage(ctx context.Context, imageId uuid.UUID, contactId uuid.UUID) (model.ImageContacts, error) {
insertImageContactStmt := ImageContacts.
INSERT(ImageContacts.ImageID, ImageContacts.ContactID).
VALUES(imageId, contactId).
RETURNING(ImageContacts.AllColumns)
imageContact := model.ImageContacts{}
err := insertImageContactStmt.QueryContext(ctx, m.dbPool, &imageContact)
return imageContact, err
}
func NewContactModel(db *sql.DB) ContactModel {
return ContactModel{dbPool: db}
}

View File

@ -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.MutableColumns).
MODEL(event).
RETURNING(Events.AllColumns)
insertedEvent := model.Events{}
err := insertEventStmt.QueryContext(ctx, m.dbPool, &insertedEvent)
if err != nil {
return insertedEvent, err
}
insertUserEventStmt := UserEvents.
INSERT(UserEvents.UserID, UserEvents.EventID).
VALUES(userId, insertedEvent.ID)
_, err = insertUserEventStmt.ExecContext(ctx, m.dbPool)
return insertedEvent, err
}
func (m EventModel) SaveToImage(ctx context.Context, imageId uuid.UUID, eventId uuid.UUID) (model.ImageEvents, error) {
insertImageEventStmt := ImageEvents.
INSERT(ImageEvents.ImageID, ImageEvents.EventID).
VALUES(imageId, eventId).
RETURNING(ImageEvents.AllColumns)
imageEvent := model.ImageEvents{}
err := insertImageEventStmt.QueryContext(ctx, m.dbPool, &imageEvent)
return imageEvent, err
}
func (m EventModel) UpdateLocation(ctx context.Context, eventId uuid.UUID, locationId uuid.UUID) (model.Events, error) {
updateEventLocationStmt := Events.
UPDATE(Events.LocationID).
SET(locationId).
WHERE(Events.ID.EQ(UUID(eventId))).
RETURNING(Events.AllColumns)
updatedEvent := model.Events{}
err := updateEventLocationStmt.QueryContext(ctx, m.dbPool, &updatedEvent)
return updatedEvent, err
}
func (m EventModel) UpdateOrganizer(ctx context.Context, eventId uuid.UUID, organizerId uuid.UUID) (model.Events, error) {
updateEventContactStmt := Events.
UPDATE(Events.OrganizerID).
SET(organizerId).
WHERE(Events.ID.EQ(UUID(eventId))).
RETURNING(Events.AllColumns)
updatedEvent := model.Events{}
err := updateEventContactStmt.QueryContext(ctx, m.dbPool, &updatedEvent)
return updatedEvent, err
}
func NewEventModel(db *sql.DB) EventModel {
return EventModel{dbPool: db}
}

View File

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

View File

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

View File

@ -1,130 +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) Get(ctx context.Context, locationId uuid.UUID) (model.Locations, error) {
getLocationStmt := Locations.
SELECT(Locations.AllColumns).
WHERE(Locations.ID.EQ(UUID(locationId)))
location := model.Locations{}
err := getLocationStmt.QueryContext(ctx, m.dbPool, &location)
return location, err
}
func (m LocationModel) Update(ctx context.Context, location model.Locations) (model.Locations, error) {
existingLocation, err := m.Get(ctx, location.ID)
if err != nil {
return model.Locations{}, err
}
existingLocation.Name = location.Name
if location.Description != nil {
existingLocation.Description = location.Description
}
if location.Address != nil {
existingLocation.Address = location.Address
}
updateLocationStmt := Locations.
UPDATE(Locations.MutableColumns).
MODEL(existingLocation).
WHERE(Locations.ID.EQ(UUID(location.ID))).
RETURNING(Locations.AllColumns)
updatedLocation := model.Locations{}
err = updateLocationStmt.QueryContext(ctx, m.dbPool, &updatedLocation)
return updatedLocation, err
}
func (m LocationModel) Save(ctx context.Context, userId uuid.UUID, location model.Locations) (model.Locations, error) {
if location.ID != uuid.Nil {
return m.Update(ctx, location)
}
insertLocationStmt := Locations.
INSERT(Locations.Name, Locations.Address, Locations.Description).
VALUES(location.Name, location.Address, location.Description).
RETURNING(Locations.AllColumns)
insertedLocation := model.Locations{}
err := insertLocationStmt.QueryContext(ctx, m.dbPool, &insertedLocation)
if err != nil {
return model.Locations{}, err
}
insertUserLocationStmt := UserLocations.
INSERT(UserLocations.UserID, UserLocations.LocationID).
VALUES(userId, insertedLocation.ID)
_, err = insertUserLocationStmt.ExecContext(ctx, m.dbPool)
return insertedLocation, err
}
func (m LocationModel) SaveToImage(ctx context.Context, imageId uuid.UUID, locationId uuid.UUID) (model.ImageLocations, error) {
imageLocation := model.ImageLocations{}
checkExistingStmt := ImageLocations.
SELECT(ImageLocations.AllColumns).
WHERE(
ImageLocations.ImageID.EQ(UUID(imageId)).
AND(ImageLocations.LocationID.EQ(UUID(locationId))),
)
err := checkExistingStmt.QueryContext(ctx, m.dbPool, &imageLocation)
if err != nil && err != qrm.ErrNoRows {
// A real error
return model.ImageLocations{}, err
}
if err == nil {
// Already exists.
return imageLocation, nil
}
insertImageLocationStmt := ImageLocations.
INSERT(ImageLocations.ImageID, ImageLocations.LocationID).
VALUES(imageId, locationId).
RETURNING(ImageLocations.AllColumns)
err = insertImageLocationStmt.QueryContext(ctx, m.dbPool, &imageLocation)
return imageLocation, err
}
func NewLocationModel(db *sql.DB) LocationModel {
return LocationModel{dbPool: db}
}

View File

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

200
backend/models/stacks.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,38 @@
package notifications
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),
}
}

View File

@ -0,0 +1,64 @@
package notifications
import (
"encoding/json"
"fmt"
"github.com/google/uuid"
)
const (
IMAGE_TYPE = "image"
STACK_TYPE = "stack"
)
type ImageNotification struct {
Type string
ImageID uuid.UUID
ImageName string
Status string
}
type StackNotification struct {
Type string
StackID uuid.UUID
Name string
Status string
}
type Notification struct {
image *ImageNotification
stack *StackNotification
}
func GetImageNotification(image ImageNotification) Notification {
return Notification{
image: &image,
}
}
func GetStackNotification(list StackNotification) Notification {
return Notification{
stack: &list,
}
}
func (n Notification) MarshalJSON() ([]byte, error) {
if n.image != nil {
return json.Marshal(n.image)
}
if n.stack != nil {
return json.Marshal(n.stack)
}
return nil, fmt.Errorf("no image or list present")
}
func (n *Notification) UnmarshalJSON(data []byte) error {
return fmt.Errorf("unimplemented")
}

View File

@ -0,0 +1,58 @@
package notifications
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),
}
}

View File

@ -0,0 +1,48 @@
package notifications
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)
}

157
backend/processor/image.go Normal file
View File

@ -0,0 +1,157 @@
package processor
import (
"context"
"fmt"
"screenmark/screenmark/.gen/haystack/haystack/model"
"screenmark/screenmark/agents"
"screenmark/screenmark/agents/client"
"screenmark/screenmark/limits"
"screenmark/screenmark/models"
"screenmark/screenmark/notifications"
"sync"
"github.com/charmbracelet/log"
)
const IMAGE_PROCESS_AT_A_TIME = 10
type ImageProcessor struct {
imageModel models.ImageModel
logger *log.Logger
descriptionAgent agents.DescriptionAgent
stackAgent client.AgentClient
Processor *Processor[model.Image]
notifier *notifications.Notifier[notifications.Notification]
}
func (p *ImageProcessor) setImageToProcess(ctx context.Context, image model.Image) {
err := p.imageModel.UpdateProcess(ctx, image.ID, model.Progress_InProgress)
if err != nil {
// TODO: what can we actually do here for the errors?
// We can't stop the work for the others
p.logger.Error("failed to update image", "err", err)
// TODO: we can use context here to actually pass some information through
return
}
}
func (p *ImageProcessor) setImageToDone(ctx context.Context, image model.Image) {
err := p.imageModel.UpdateProcess(ctx, image.ID, model.Progress_Complete)
if err != nil {
// TODO: what can we actually do here for the errors?
// We can't stop the work for the others
p.logger.Error("failed to update image", "err", err)
// TODO: we can use context here to actually pass some information through
return
}
}
func (p *ImageProcessor) describe(ctx context.Context, image model.Image) {
descriptionSubLogger := p.logger.With("describe image", image.ID)
err := p.descriptionAgent.Describe(descriptionSubLogger, image.ID, image.ImageName, image.Image)
if err != nil {
// Again, wtf do we do?
// Although i think the agent actually returns an error when it's finished
p.logger.Error("failed to describe image", "err", err)
return
}
}
func (p *ImageProcessor) extractInfo(ctx context.Context, image model.Image) {
err := p.stackAgent.RunAgent(image.UserID, image.ID, image.ImageName, image.Image)
if err != nil {
// Again, wtf do we do?
// Although i think the agent actually returns an error when it's finished
p.logger.Error("failed to process image", "err", err)
return
}
}
func (p *ImageProcessor) processImage(image model.Image) {
p.logger.Info("Processing image", "ID", image.ID)
ctx := context.Background()
p.setImageToProcess(ctx, image)
var wg sync.WaitGroup
wg.Add(2)
imageNotification := notifications.GetImageNotification(notifications.ImageNotification{
Type: notifications.IMAGE_TYPE,
ImageID: image.ID,
ImageName: image.ImageName,
Status: string(model.Progress_InProgress),
})
err := p.notifier.SendAndCreate(image.UserID.String(), imageNotification)
if err != nil {
p.logger.Error("sending in progress notification", "err", err)
return
}
go func() {
p.describe(ctx, image)
wg.Done()
}()
go func() {
p.extractInfo(ctx, image)
wg.Done()
}()
wg.Wait()
p.setImageToDone(ctx, image)
// TODO: there is some repeated code here. The ergonomicts of the notifications,
// isn't the best.
imageNotification = notifications.GetImageNotification(notifications.ImageNotification{
Type: notifications.IMAGE_TYPE,
ImageID: image.ID,
ImageName: image.ImageName,
Status: string(model.Progress_Complete),
})
err = p.notifier.SendAndCreate(image.UserID.String(), imageNotification)
if err != nil {
p.logger.Error("sending done notification", "err", err)
return
}
}
func NewImageProcessor(
logger *log.Logger,
imageModel models.ImageModel,
listModel models.StackModel,
limitsManager limits.LimitsManagerMethods,
notifier *notifications.Notifier[notifications.Notification],
) (ImageProcessor, error) {
if notifier == nil {
return ImageProcessor{}, fmt.Errorf("notifier is nil")
}
descriptionAgent := agents.NewDescriptionAgent(logger, imageModel)
stackAgent := agents.NewStackAgent(logger, listModel, limitsManager)
imageProcessor := ImageProcessor{
imageModel: imageModel,
logger: logger,
descriptionAgent: descriptionAgent,
stackAgent: stackAgent,
notifier: notifier,
}
imageProcessor.Processor = NewProcessor(int(IMAGE_PROCESS_AT_A_TIME), imageProcessor.processImage)
return imageProcessor, nil
}

View File

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

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