49 Commits

Author SHA1 Message Date
32f85fd0be bump: v0.0.10 -> v0.0.11 2025-04-10 19:16:35 -04:00
f9772bce47 feat: migrations 2025-04-10 19:15:21 -04:00
1667b78a0a build(nix): updated nix hashes 2025-04-10 19:14:39 -04:00
b741e5f1a2 build(server): updated go dependencies 2025-04-10 19:13:34 -04:00
439adecf0e build(client): updated npm dependencies 2025-04-10 19:13:31 -04:00
e9c44cbc94 feat: bob 2025-04-10 00:59:28 -04:00
dfd6789aa9 build(nix): updated nix hashes 2025-04-10 00:13:56 -04:00
8d36962bef build(server): updated go dependencies 2025-04-10 00:08:42 -04:00
35a2f0a918 build(client): updated npm dependencies 2025-04-10 00:08:36 -04:00
dd0995b241 WIP: stuff 2025-04-05 14:27:36 -04:00
93bc18022a WIP: passkey auth 2025-03-23 14:33:25 -04:00
f05e745d05 feat: shallow routing 2025-03-19 03:25:58 -04:00
8b494430a5 fix: daterangepicker key 2025-03-19 02:20:39 -04:00
d2238bdf9b fix: daterangepicker key 2025-03-19 02:12:27 -04:00
dc3106b4a4 bump: v0.0.9 -> v0.0.10 2025-03-19 00:46:21 -04:00
bc0e8c55e3 revert: use npm ci 2025-03-19 00:43:10 -04:00
124ddc20df fix: use nix packages for linting 2025-03-19 00:35:56 -04:00
218da3aef8 build(nix): updated nix hashes 2025-03-19 00:35:35 -04:00
e18457bacd build(client): updated npm dependencies 2025-03-19 00:34:26 -04:00
afb85cec88 build(nix): updated nix hashes 2025-03-19 00:33:28 -04:00
4e96ee38ca build(client): updated npm dependencies 2025-03-19 00:32:16 -04:00
89129f3495 bump: v0.0.8 -> v0.0.9 2025-03-18 23:27:02 -04:00
f8fb729c03 fix: use cachix for release workflow as well 2025-03-18 23:22:47 -04:00
b906e614ba fix: use cachix because github cache sucks 2025-03-18 23:18:30 -04:00
645faf398e fix: rely entirely on nix packages 2025-03-18 22:53:05 -04:00
add5afb17c fix: use legaly peer deps? 2025-03-18 22:38:33 -04:00
ab4ccc3d46 build(nix): updated nix hashes 2025-03-18 22:35:51 -04:00
ed7489f53d build(client): updated npm dependencies 2025-03-18 22:34:36 -04:00
94366907d8 fix: install npm packages before linting 2025-03-18 22:32:02 -04:00
ef11ac81a3 feat: lint workflow 2025-03-18 22:26:01 -04:00
73e7c563e0 bump: v0.0.7 -> v0.0.8 2025-03-18 22:14:11 -04:00
cd3da0aa8f feat: go caching 2025-03-18 22:12:56 -04:00
6706330252 bump: v0.0.6 -> v0.0.7 2025-03-18 22:07:05 -04:00
2b03164307 feat: workflow caching 2025-03-18 22:05:36 -04:00
0ffc1cd7f9 bump: v0.0.5 -> v0.0.6 2025-03-18 21:53:16 -04:00
c418792653 fix: select all build files 2025-03-18 21:52:00 -04:00
8956317197 bump: v0.0.4 -> v0.0.5 2025-03-18 21:42:25 -04:00
2361602b62 fix: use different release action 2025-03-18 21:40:43 -04:00
eba0067e1e bump: 0.0.3 -> 0.0.4 2025-03-18 21:25:42 -04:00
8d78f79fe2 fix: use nix develop in build action 2025-03-18 21:24:19 -04:00
5328e5b4a1 bump: 0.0.2 -> 0.0.3 2025-03-18 21:23:13 -04:00
81855d079f feat: release workflow 2025-03-18 21:21:16 -04:00
e857a14fd7 fix: add git dep 2025-03-18 19:25:18 -04:00
5e5a2cbaaa refactor: seperate default from trevstack in nix packages 2025-03-18 19:13:15 -04:00
267d293927 feat: linting 2025-03-18 19:02:50 -04:00
d8de02f789 refactor: move handlers into versioned dirs 2025-03-18 18:25:37 -04:00
be0981f7b7 feat: auth redirect 2025-03-18 17:46:54 -04:00
86b74e0ebc fix: conditional client build 2025-03-18 17:41:29 -04:00
21cd91156c fix: select dropdown transition 2025-03-18 17:05:21 -04:00
87 changed files with 8123 additions and 1922 deletions

View File

@ -1,13 +1,15 @@
.direnv
.env .env
build
result /docker-compose.*
/.direnv/
/build/
/result/
# Client # Client
/client/node_modules /client/node_modules/
/client/.svelte-kit /client/.svelte-kit/
# Server # Server
/server/client /server/client/
/server/tmp /server/tmp/
/server/internal/handlers/client/client

28
.github/workflows/lint.yaml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Lint Workflow
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@v31
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Install NPM Packages
working-directory: ./client
run: npm ci --legacy-peer-deps
- name: Lint
run: nix develop --command ts-lint

30
.github/workflows/release.yaml vendored Normal file
View File

@ -0,0 +1,30 @@
name: Release Workflow
on:
push:
tags:
- '*'
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@v31
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Build
run: nix develop --command ts-build
- name: Create Release
uses: softprops/action-gh-release@v2
with:
files: |-
build/**

14
.gitignore vendored
View File

@ -1,13 +1,7 @@
.direnv
.env .env
build
result
# Client /docker-compose.*
/client/node_modules
/client/.svelte-kit
# Server /.direnv/
/server/client /build/
/server/tmp /result/
/server/internal/handlers/client/client

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"go.lintTool": "revive",
"go.lintFlags": [
"--config=server/revive.toml"
],
}

2
client/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/node_modules/
/.svelte-kit/

View File

@ -2,3 +2,14 @@
package-lock.json package-lock.json
pnpm-lock.yaml pnpm-lock.yaml
yarn.lock yarn.lock
# Build output
build
.svelte-kit
node_modules
# Static
static
# Generated
src/lib/services

View File

@ -3,10 +3,7 @@
"singleQuote": true, "singleQuote": true,
"trailingComma": "none", "trailingComma": "none",
"printWidth": 100, "printWidth": 100,
"plugins": [ "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"prettier-plugin-svelte",
"prettier-plugin-tailwindcss"
],
"overrides": [ "overrides": [
{ {
"files": "*.svelte", "files": "*.svelte",

View File

@ -6,7 +6,7 @@ import globals from 'globals';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint'; import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js'; import svelteConfig from './svelte.config.js';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); const gitignorePath = fileURLToPath(new URL('./.prettierignore', import.meta.url));
export default ts.config( export default ts.config(
includeIgnoreFile(gitignorePath), includeIgnoreFile(gitignorePath),

1148
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{ {
"name": "trevstack", "name": "trevstack",
"private": true, "private": true,
"version": "0.0.2", "version": "0.0.11",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
@ -16,31 +16,33 @@
"devDependencies": { "devDependencies": {
"@connectrpc/connect": "^2.0.2", "@connectrpc/connect": "^2.0.2",
"@connectrpc/connect-web": "^2.0.2", "@connectrpc/connect-web": "^2.0.2",
"@eslint/compat": "^1.2.7", "@eslint/compat": "^1.2.8",
"@eslint/js": "^9.18.0", "@eslint/js": "^9.18.0",
"@lucide/svelte": "^0.479.0", "@lucide/svelte": "^0.479.0",
"@scalar/api-reference": "^1.28.5", "@scalar/api-reference": "^1.28.17",
"@simplewebauthn/browser": "^13.1.0",
"@sveltejs/adapter-static": "^3.0.8", "@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/kit": "^2.20.1", "@sveltejs/kit": "^2.20.5",
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/vite": "^4.0.14", "@tailwindcss/vite": "^4.1.3",
"bits-ui": "^1.3.13", "bits-ui": "^1.3.19",
"cbor2": "^1.12.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"eslint": "^9.22.0", "eslint": "^9.24.0",
"eslint-config-prettier": "^10.1.1", "eslint-config-prettier": "^10.1.2",
"eslint-plugin-svelte": "^3.3.2", "eslint-plugin-svelte": "^3.5.1",
"globals": "^16.0.0", "globals": "^16.0.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3", "prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.11",
"svelte": "^5.23.2", "svelte": "^5.25.12",
"svelte-check": "^4.1.5", "svelte-check": "^4.1.5",
"svelte-sonner": "^0.3.28", "svelte-sonner": "^0.3.28",
"tailwind-merge": "^3.0.2", "tailwind-merge": "^3.2.0",
"tailwindcss": "^4.0.13", "tailwindcss": "^4.0.13",
"tw-animate-css": "^1.2.3", "tw-animate-css": "^1.2.5",
"typescript": "^5.8.2", "typescript": "^5.8.3",
"typescript-eslint": "^8.26.1", "typescript-eslint": "^8.29.1",
"vite": "^6.2.2" "vite": "^6.2.6"
} }
} }

View File

@ -1,5 +1,5 @@
@import "tailwindcss"; @import 'tailwindcss';
@import "tw-animate-css"; @import 'tw-animate-css';
@theme { @theme {
--color-crust: #11111b; --color-crust: #11111b;

View File

@ -8,7 +8,7 @@
<title>TrevStack</title> <title>TrevStack</title>
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="tap" class="min-h-screen bg-base text-text"> <body data-sveltekit-preload-data="tap" class="bg-base text-text min-h-screen">
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>
</body> </body>
</html> </html>

View File

@ -1 +0,0 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@ -12,16 +12,16 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file item/v1/item.proto. * Describes the file item/v1/item.proto.
*/ */
export const file_item_v1_item: GenFile = /*@__PURE__*/ export const file_item_v1_item: GenFile = /*@__PURE__*/
fileDesc("ChJpdGVtL3YxL2l0ZW0ucHJvdG8SB2l0ZW0udjEinAEKBEl0ZW0SDwoCaWQYASABKA1IAIgBARIMCgRuYW1lGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEg0KBXByaWNlGAQgASgCEhAKCHF1YW50aXR5GAUgASgNEi4KBWFkZGVkGAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgBiAEBQgUKA19pZEIICgZfYWRkZWQiHAoOR2V0SXRlbVJlcXVlc3QSCgoCaWQYASABKA0iLgoPR2V0SXRlbVJlc3BvbnNlEhsKBGl0ZW0YASABKAsyDS5pdGVtLnYxLkl0ZW0i3wEKD0dldEl0ZW1zUmVxdWVzdBIuCgVzdGFydBgBIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIAIgBARIsCgNlbmQYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSAGIAQESEwoGZmlsdGVyGAMgASgJSAKIAQESEgoFbGltaXQYBCABKA1IA4gBARITCgZvZmZzZXQYBSABKA1IBIgBAUIICgZfc3RhcnRCBgoEX2VuZEIJCgdfZmlsdGVyQggKBl9saW1pdEIJCgdfb2Zmc2V0Ij8KEEdldEl0ZW1zUmVzcG9uc2USHAoFaXRlbXMYASADKAsyDS5pdGVtLnYxLkl0ZW0SDQoFY291bnQYAiABKAQiMAoRQ3JlYXRlSXRlbVJlcXVlc3QSGwoEaXRlbRgBIAEoCzINLml0ZW0udjEuSXRlbSIxChJDcmVhdGVJdGVtUmVzcG9uc2USGwoEaXRlbRgBIAEoCzINLml0ZW0udjEuSXRlbSIwChFVcGRhdGVJdGVtUmVxdWVzdBIbCgRpdGVtGAEgASgLMg0uaXRlbS52MS5JdGVtIjEKElVwZGF0ZUl0ZW1SZXNwb25zZRIbCgRpdGVtGAEgASgLMg0uaXRlbS52MS5JdGVtIh8KEURlbGV0ZUl0ZW1SZXF1ZXN0EgoKAmlkGAEgASgNIhQKEkRlbGV0ZUl0ZW1SZXNwb25zZTLrAgoLSXRlbVNlcnZpY2USPgoHR2V0SXRlbRIXLml0ZW0udjEuR2V0SXRlbVJlcXVlc3QaGC5pdGVtLnYxLkdldEl0ZW1SZXNwb25zZSIAEkEKCEdldEl0ZW1zEhguaXRlbS52MS5HZXRJdGVtc1JlcXVlc3QaGS5pdGVtLnYxLkdldEl0ZW1zUmVzcG9uc2UiABJHCgpDcmVhdGVJdGVtEhouaXRlbS52MS5DcmVhdGVJdGVtUmVxdWVzdBobLml0ZW0udjEuQ3JlYXRlSXRlbVJlc3BvbnNlIgASRwoKVXBkYXRlSXRlbRIaLml0ZW0udjEuVXBkYXRlSXRlbVJlcXVlc3QaGy5pdGVtLnYxLlVwZGF0ZUl0ZW1SZXNwb25zZSIAEkcKCkRlbGV0ZUl0ZW0SGi5pdGVtLnYxLkRlbGV0ZUl0ZW1SZXF1ZXN0GhsuaXRlbS52MS5EZWxldGVJdGVtUmVzcG9uc2UiAEKdAQoLY29tLml0ZW0udjFCCUl0ZW1Qcm90b1ABWkZnaXRodWIuY29tL3Nwb3RkZW1vNC90cmV2c3RhY2svc2VydmVyL2ludGVybmFsL3NlcnZpY2VzL2l0ZW0vdjE7aXRlbXYxogIDSVhYqgIHSXRlbS5WMcoCB0l0ZW1cVjHiAhNJdGVtXFYxXEdQQk1ldGFkYXRh6gIISXRlbTo6VjFiBnByb3RvMw", [file_google_protobuf_timestamp]); fileDesc("ChJpdGVtL3YxL2l0ZW0ucHJvdG8SB2l0ZW0udjEinAEKBEl0ZW0SDwoCaWQYASABKANIAIgBARIMCgRuYW1lGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEg0KBXByaWNlGAQgASgCEhAKCHF1YW50aXR5GAUgASgFEi4KBWFkZGVkGAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgBiAEBQgUKA19pZEIICgZfYWRkZWQiHAoOR2V0SXRlbVJlcXVlc3QSCgoCaWQYASABKAMiLgoPR2V0SXRlbVJlc3BvbnNlEhsKBGl0ZW0YASABKAsyDS5pdGVtLnYxLkl0ZW0i3wEKD0dldEl0ZW1zUmVxdWVzdBIuCgVzdGFydBgBIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIAIgBARIsCgNlbmQYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSAGIAQESEwoGZmlsdGVyGAMgASgJSAKIAQESEgoFbGltaXQYBCABKAVIA4gBARITCgZvZmZzZXQYBSABKAVIBIgBAUIICgZfc3RhcnRCBgoEX2VuZEIJCgdfZmlsdGVyQggKBl9saW1pdEIJCgdfb2Zmc2V0Ij8KEEdldEl0ZW1zUmVzcG9uc2USHAoFaXRlbXMYASADKAsyDS5pdGVtLnYxLkl0ZW0SDQoFY291bnQYAiABKAMiMAoRQ3JlYXRlSXRlbVJlcXVlc3QSGwoEaXRlbRgBIAEoCzINLml0ZW0udjEuSXRlbSIxChJDcmVhdGVJdGVtUmVzcG9uc2USGwoEaXRlbRgBIAEoCzINLml0ZW0udjEuSXRlbSIwChFVcGRhdGVJdGVtUmVxdWVzdBIbCgRpdGVtGAEgASgLMg0uaXRlbS52MS5JdGVtIjEKElVwZGF0ZUl0ZW1SZXNwb25zZRIbCgRpdGVtGAEgASgLMg0uaXRlbS52MS5JdGVtIh8KEURlbGV0ZUl0ZW1SZXF1ZXN0EgoKAmlkGAEgASgDIhQKEkRlbGV0ZUl0ZW1SZXNwb25zZTLrAgoLSXRlbVNlcnZpY2USPgoHR2V0SXRlbRIXLml0ZW0udjEuR2V0SXRlbVJlcXVlc3QaGC5pdGVtLnYxLkdldEl0ZW1SZXNwb25zZSIAEkEKCEdldEl0ZW1zEhguaXRlbS52MS5HZXRJdGVtc1JlcXVlc3QaGS5pdGVtLnYxLkdldEl0ZW1zUmVzcG9uc2UiABJHCgpDcmVhdGVJdGVtEhouaXRlbS52MS5DcmVhdGVJdGVtUmVxdWVzdBobLml0ZW0udjEuQ3JlYXRlSXRlbVJlc3BvbnNlIgASRwoKVXBkYXRlSXRlbRIaLml0ZW0udjEuVXBkYXRlSXRlbVJlcXVlc3QaGy5pdGVtLnYxLlVwZGF0ZUl0ZW1SZXNwb25zZSIAEkcKCkRlbGV0ZUl0ZW0SGi5pdGVtLnYxLkRlbGV0ZUl0ZW1SZXF1ZXN0GhsuaXRlbS52MS5EZWxldGVJdGVtUmVzcG9uc2UiAEKdAQoLY29tLml0ZW0udjFCCUl0ZW1Qcm90b1ABWkZnaXRodWIuY29tL3Nwb3RkZW1vNC90cmV2c3RhY2svc2VydmVyL2ludGVybmFsL3NlcnZpY2VzL2l0ZW0vdjE7aXRlbXYxogIDSVhYqgIHSXRlbS5WMcoCB0l0ZW1cVjHiAhNJdGVtXFYxXEdQQk1ldGFkYXRh6gIISXRlbTo6VjFiBnByb3RvMw", [file_google_protobuf_timestamp]);
/** /**
* @generated from message item.v1.Item * @generated from message item.v1.Item
*/ */
export type Item = Message<"item.v1.Item"> & { export type Item = Message<"item.v1.Item"> & {
/** /**
* @generated from field: optional uint32 id = 1; * @generated from field: optional int64 id = 1;
*/ */
id?: number; id?: bigint;
/** /**
* @generated from field: string name = 2; * @generated from field: string name = 2;
@ -39,7 +39,7 @@ export type Item = Message<"item.v1.Item"> & {
price: number; price: number;
/** /**
* @generated from field: uint32 quantity = 5; * @generated from field: int32 quantity = 5;
*/ */
quantity: number; quantity: number;
@ -61,9 +61,9 @@ export const ItemSchema: GenMessage<Item> = /*@__PURE__*/
*/ */
export type GetItemRequest = Message<"item.v1.GetItemRequest"> & { export type GetItemRequest = Message<"item.v1.GetItemRequest"> & {
/** /**
* @generated from field: uint32 id = 1; * @generated from field: int64 id = 1;
*/ */
id: number; id: bigint;
}; };
/** /**
@ -110,12 +110,12 @@ export type GetItemsRequest = Message<"item.v1.GetItemsRequest"> & {
filter?: string; filter?: string;
/** /**
* @generated from field: optional uint32 limit = 4; * @generated from field: optional int32 limit = 4;
*/ */
limit?: number; limit?: number;
/** /**
* @generated from field: optional uint32 offset = 5; * @generated from field: optional int32 offset = 5;
*/ */
offset?: number; offset?: number;
}; };
@ -137,7 +137,7 @@ export type GetItemsResponse = Message<"item.v1.GetItemsResponse"> & {
items: Item[]; items: Item[];
/** /**
* @generated from field: uint64 count = 2; * @generated from field: int64 count = 2;
*/ */
count: bigint; count: bigint;
}; };
@ -222,9 +222,9 @@ export const UpdateItemResponseSchema: GenMessage<UpdateItemResponse> = /*@__PUR
*/ */
export type DeleteItemRequest = Message<"item.v1.DeleteItemRequest"> & { export type DeleteItemRequest = Message<"item.v1.DeleteItemRequest"> & {
/** /**
* @generated from field: uint32 id = 1; * @generated from field: int64 id = 1;
*/ */
id: number; id: bigint;
}; };
/** /**

View File

@ -10,16 +10,16 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file user/v1/user.proto. * Describes the file user/v1/user.proto.
*/ */
export const file_user_v1_user: GenFile = /*@__PURE__*/ export const file_user_v1_user: GenFile = /*@__PURE__*/
fileDesc("ChJ1c2VyL3YxL3VzZXIucHJvdG8SB3VzZXIudjEiVgoEVXNlchIKCgJpZBgBIAEoDRIQCgh1c2VybmFtZRgCIAEoCRIcCg9wcm9maWxlX3BpY3R1cmUYAyABKAlIAIgBAUISChBfcHJvZmlsZV9waWN0dXJlIhAKDkdldFVzZXJSZXF1ZXN0Ii4KD0dldFVzZXJSZXNwb25zZRIbCgR1c2VyGAEgASgLMg0udXNlci52MS5Vc2VyIl0KFVVwZGF0ZVBhc3N3b3JkUmVxdWVzdBIUCgxvbGRfcGFzc3dvcmQYASABKAkSFAoMbmV3X3Bhc3N3b3JkGAIgASgJEhgKEGNvbmZpcm1fcGFzc3dvcmQYAyABKAkiNQoWVXBkYXRlUGFzc3dvcmRSZXNwb25zZRIbCgR1c2VyGAEgASgLMg0udXNlci52MS5Vc2VyIj4KEEdldEFQSUtleVJlcXVlc3QSEAoIcGFzc3dvcmQYASABKAkSGAoQY29uZmlybV9wYXNzd29yZBgCIAEoCSIgChFHZXRBUElLZXlSZXNwb25zZRILCgNrZXkYASABKAkiPgobVXBkYXRlUHJvZmlsZVBpY3R1cmVSZXF1ZXN0EhEKCWZpbGVfbmFtZRgBIAEoCRIMCgRkYXRhGAIgASgMIjsKHFVwZGF0ZVByb2ZpbGVQaWN0dXJlUmVzcG9uc2USGwoEdXNlchgBIAEoCzINLnVzZXIudjEuVXNlcjLPAgoLVXNlclNlcnZpY2USPgoHR2V0VXNlchIXLnVzZXIudjEuR2V0VXNlclJlcXVlc3QaGC51c2VyLnYxLkdldFVzZXJSZXNwb25zZSIAElMKDlVwZGF0ZVBhc3N3b3JkEh4udXNlci52MS5VcGRhdGVQYXNzd29yZFJlcXVlc3QaHy51c2VyLnYxLlVwZGF0ZVBhc3N3b3JkUmVzcG9uc2UiABJECglHZXRBUElLZXkSGS51c2VyLnYxLkdldEFQSUtleVJlcXVlc3QaGi51c2VyLnYxLkdldEFQSUtleVJlc3BvbnNlIgASZQoUVXBkYXRlUHJvZmlsZVBpY3R1cmUSJC51c2VyLnYxLlVwZGF0ZVByb2ZpbGVQaWN0dXJlUmVxdWVzdBolLnVzZXIudjEuVXBkYXRlUHJvZmlsZVBpY3R1cmVSZXNwb25zZSIAQp0BCgtjb20udXNlci52MUIJVXNlclByb3RvUAFaRmdpdGh1Yi5jb20vc3BvdGRlbW80L3RyZXZzdGFjay9zZXJ2ZXIvaW50ZXJuYWwvc2VydmljZXMvdXNlci92MTt1c2VydjGiAgNVWFiqAgdVc2VyLlYxygIHVXNlclxWMeICE1VzZXJcVjFcR1BCTWV0YWRhdGHqAghVc2VyOjpWMWIGcHJvdG8z"); fileDesc("ChJ1c2VyL3YxL3VzZXIucHJvdG8SB3VzZXIudjEiXAoEVXNlchIKCgJpZBgBIAEoAxIQCgh1c2VybmFtZRgCIAEoCRIfChJwcm9maWxlX3BpY3R1cmVfaWQYAyABKANIAIgBAUIVChNfcHJvZmlsZV9waWN0dXJlX2lkIhAKDkdldFVzZXJSZXF1ZXN0Ii4KD0dldFVzZXJSZXNwb25zZRIbCgR1c2VyGAEgASgLMg0udXNlci52MS5Vc2VyIl0KFVVwZGF0ZVBhc3N3b3JkUmVxdWVzdBIUCgxvbGRfcGFzc3dvcmQYASABKAkSFAoMbmV3X3Bhc3N3b3JkGAIgASgJEhgKEGNvbmZpcm1fcGFzc3dvcmQYAyABKAkiNQoWVXBkYXRlUGFzc3dvcmRSZXNwb25zZRIbCgR1c2VyGAEgASgLMg0udXNlci52MS5Vc2VyIj4KEEdldEFQSUtleVJlcXVlc3QSEAoIcGFzc3dvcmQYASABKAkSGAoQY29uZmlybV9wYXNzd29yZBgCIAEoCSIgChFHZXRBUElLZXlSZXNwb25zZRILCgNrZXkYASABKAkiPgobVXBkYXRlUHJvZmlsZVBpY3R1cmVSZXF1ZXN0EhEKCWZpbGVfbmFtZRgBIAEoCRIMCgRkYXRhGAIgASgMIjsKHFVwZGF0ZVByb2ZpbGVQaWN0dXJlUmVzcG9uc2USGwoEdXNlchgBIAEoCzINLnVzZXIudjEuVXNlcjLPAgoLVXNlclNlcnZpY2USPgoHR2V0VXNlchIXLnVzZXIudjEuR2V0VXNlclJlcXVlc3QaGC51c2VyLnYxLkdldFVzZXJSZXNwb25zZSIAElMKDlVwZGF0ZVBhc3N3b3JkEh4udXNlci52MS5VcGRhdGVQYXNzd29yZFJlcXVlc3QaHy51c2VyLnYxLlVwZGF0ZVBhc3N3b3JkUmVzcG9uc2UiABJECglHZXRBUElLZXkSGS51c2VyLnYxLkdldEFQSUtleVJlcXVlc3QaGi51c2VyLnYxLkdldEFQSUtleVJlc3BvbnNlIgASZQoUVXBkYXRlUHJvZmlsZVBpY3R1cmUSJC51c2VyLnYxLlVwZGF0ZVByb2ZpbGVQaWN0dXJlUmVxdWVzdBolLnVzZXIudjEuVXBkYXRlUHJvZmlsZVBpY3R1cmVSZXNwb25zZSIAQp0BCgtjb20udXNlci52MUIJVXNlclByb3RvUAFaRmdpdGh1Yi5jb20vc3BvdGRlbW80L3RyZXZzdGFjay9zZXJ2ZXIvaW50ZXJuYWwvc2VydmljZXMvdXNlci92MTt1c2VydjGiAgNVWFiqAgdVc2VyLlYxygIHVXNlclxWMeICE1VzZXJcVjFcR1BCTWV0YWRhdGHqAghVc2VyOjpWMWIGcHJvdG8z");
/** /**
* @generated from message user.v1.User * @generated from message user.v1.User
*/ */
export type User = Message<"user.v1.User"> & { export type User = Message<"user.v1.User"> & {
/** /**
* @generated from field: uint32 id = 1; * @generated from field: int64 id = 1;
*/ */
id: number; id: bigint;
/** /**
* @generated from field: string username = 2; * @generated from field: string username = 2;
@ -27,9 +27,9 @@ export type User = Message<"user.v1.User"> & {
username: string; username: string;
/** /**
* @generated from field: optional string profile_picture = 3; * @generated from field: optional int64 profile_picture_id = 3;
*/ */
profilePicture?: string; profilePictureId?: bigint;
}; };
/** /**

View File

@ -1,5 +1,5 @@
import type { User } from "./services/user/v1/user_pb" import type { User } from './services/user/v1/user_pb';
export let userState: { user: User | undefined } = $state({ export const userState: { user: User | undefined } = $state({
user: undefined user: undefined
}); });

View File

@ -1,9 +1,9 @@
import { createConnectTransport } from "@connectrpc/connect-web" import { createConnectTransport } from '@connectrpc/connect-web';
import { Code, ConnectError, createClient, type Interceptor } from "@connectrpc/connect" import { Code, ConnectError, createClient, type Interceptor } from '@connectrpc/connect';
import { AuthService } from "$lib/services/user/v1/auth_pb"; import { AuthService } from '$lib/services/user/v1/auth_pb';
import { UserService } from "$lib/services/user/v1/user_pb"; import { UserService } from '$lib/services/user/v1/user_pb';
import { ItemService } from "$lib/services/item/v1/item_pb"; import { ItemService } from '$lib/services/item/v1/item_pb';
import { goto } from "$app/navigation"; import { goto } from '$app/navigation';
const redirector: Interceptor = (next) => async (req) => { const redirector: Interceptor = (next) => async (req) => {
try { try {
@ -19,7 +19,7 @@ const redirector: Interceptor = (next) => async (req) => {
const transport = createConnectTransport({ const transport = createConnectTransport({
baseUrl: `${window.location.origin}/grpc`, baseUrl: `${window.location.origin}/grpc`,
interceptors: [redirector], interceptors: [redirector]
}); });
export const AuthClient = createClient(AuthService, transport); export const AuthClient = createClient(AuthService, transport);

View File

@ -5,7 +5,7 @@
<Avatar.Root class="flex h-full w-full items-center justify-center"> <Avatar.Root class="flex h-full w-full items-center justify-center">
<Avatar.Image <Avatar.Image
src={userState.user?.profilePicture} src={userState.user?.profilePictureId ? '/file/' + userState.user.profilePictureId : null}
alt={`${userState.user?.username}'s avatar`} alt={`${userState.user?.username}'s avatar`}
class="rounded-full" class="rounded-full"
/> />

View File

@ -1,9 +1,10 @@
<script lang="ts"> <script lang="ts">
import { Button } from 'bits-ui'; import { Button } from 'bits-ui';
import { cn } from '$lib/utils'; import { cn } from '$lib/utils';
import type { MouseEventHandler } from 'svelte/elements';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
type me = MouseEvent & { currentTarget: EventTarget & HTMLButtonElement };
let { let {
className, className,
type, type,
@ -12,7 +13,7 @@
}: { }: {
className?: string; className?: string;
type?: 'submit' | 'reset' | 'button' | null; type?: 'submit' | 'reset' | 'button' | null;
onclick?: () => MouseEventHandler<HTMLButtonElement> | null | undefined; onclick?: (e: me) => void;
children?: Snippet<[]>; children?: Snippet<[]>;
} = $props(); } = $props();
</script> </script>
@ -20,10 +21,12 @@
<Button.Root <Button.Root
{type} {type}
class={cn( class={cn(
'bg-sky text-crust flex justify-center items-center hover:brightness-120 focus:outline-sky w-fit cursor-pointer rounded p-2 px-4 text-sm font-medium transition-all focus:outline-2 focus:outline-offset-1', 'bg-sky text-crust focus:outline-sky flex w-fit cursor-pointer items-center justify-center rounded p-2 px-4 text-sm font-medium transition-all hover:brightness-120 focus:outline-2 focus:outline-offset-1',
className className
)} )}
{onclick} onclick={(e: me) => {
onclick?.(e);
}}
> >
{@render children?.()} {@render children?.()}
</Button.Root> </Button.Root>

View File

@ -42,22 +42,22 @@
<div <div
class="bg-mantle border-surface-0 hover:border-surface-2 flex items-center rounded border pl-2 text-sm drop-shadow-md transition-all" class="bg-mantle border-surface-0 hover:border-surface-2 flex items-center rounded border pl-2 text-sm drop-shadow-md transition-all"
> >
<div class="grow flex items-center justify-center"> <div class="flex grow items-center justify-center">
{#each ['start', 'end'] as const as type} {#each ['start', 'end'] as const as type (type)}
<DateRangePicker.Input {type}> <DateRangePicker.Input {type}>
{#snippet children({ segments })} {#snippet children({ segments })}
{#each segments as { part, value }} {#each segments as seg (seg)}
<div class="inline-block select-none"> <div class="inline-block select-none">
{#if part === 'literal'} {#if seg.part === 'literal'}
<DateRangePicker.Segment {part} class="text-overlay-0 p-1"> <DateRangePicker.Segment part={seg.part} class="text-overlay-0 p-1">
{value} {seg.value}
</DateRangePicker.Segment> </DateRangePicker.Segment>
{:else} {:else}
<DateRangePicker.Segment <DateRangePicker.Segment
{part} part={seg.part}
class="aria-[valuetext=Empty]:text-overlay-0 hover:bg-surface-0 focus:bg-surface-0 focus:outline-sky rounded p-0.5 transition-all focus:outline focus:outline-offset-1" class="aria-[valuetext=Empty]:text-overlay-0 hover:bg-surface-0 focus:bg-surface-0 focus:outline-sky rounded p-0.5 transition-all focus:outline focus:outline-offset-1"
> >
{value} {seg.value}
</DateRangePicker.Segment> </DateRangePicker.Segment>
{/if} {/if}
</div> </div>
@ -72,7 +72,7 @@
{/each} {/each}
</div> </div>
<DateRangePicker.Trigger <DateRangePicker.Trigger
class="text-overlay-2 hover:bg-surface-0 grow flex justify-center items-center focus:outline-sky ml-1 cursor-pointer p-2 transition-all focus:outline focus:outline-offset-1" class="text-overlay-2 hover:bg-surface-0 focus:outline-sky ml-1 flex grow cursor-pointer items-center justify-center p-2 transition-all focus:outline focus:outline-offset-1"
> >
<Calendar size="20" /> <Calendar size="20" />
</DateRangePicker.Trigger> </DateRangePicker.Trigger>
@ -114,21 +114,21 @@
> >
<ArrowLeft /> <ArrowLeft />
</DateRangePicker.PrevButton> </DateRangePicker.PrevButton>
<DateRangePicker.Heading class="select-none font-medium" /> <DateRangePicker.Heading class="font-medium select-none" />
<DateRangePicker.NextButton <DateRangePicker.NextButton
class="hover:bg-surface-0 inline-flex size-10 cursor-pointer items-center justify-center rounded transition-all active:scale-[0.98]" class="hover:bg-surface-0 inline-flex size-10 cursor-pointer items-center justify-center rounded transition-all active:scale-[0.98]"
> >
<ArrowRight /> <ArrowRight />
</DateRangePicker.NextButton> </DateRangePicker.NextButton>
</DateRangePicker.Header> </DateRangePicker.Header>
<div class="flex flex-col space-y-4 pt-4 sm:flex-row sm:space-x-4 sm:space-y-0"> <div class="flex flex-col space-y-4 pt-4 sm:flex-row sm:space-y-0 sm:space-x-4">
{#each months as month} {#each months as month, i (i)}
<DateRangePicker.Grid class="w-full border-collapse select-none space-y-1"> <DateRangePicker.Grid class="w-full border-collapse space-y-1 select-none">
<DateRangePicker.GridHead> <DateRangePicker.GridHead>
<DateRangePicker.GridRow class="mb-1 flex w-full justify-between"> <DateRangePicker.GridRow class="mb-1 flex w-full justify-between">
{#each weekdays as day} {#each weekdays as day, i (i)}
<DateRangePicker.HeadCell <DateRangePicker.HeadCell
class="text-overlay-0 font-normal! w-10 rounded text-xs" class="text-overlay-0 w-10 rounded text-xs font-normal!"
> >
{day.slice(0, 2)} {day.slice(0, 2)}
</DateRangePicker.HeadCell> </DateRangePicker.HeadCell>
@ -136,19 +136,19 @@
</DateRangePicker.GridRow> </DateRangePicker.GridRow>
</DateRangePicker.GridHead> </DateRangePicker.GridHead>
<DateRangePicker.GridBody> <DateRangePicker.GridBody>
{#each month.weeks as weekDates} {#each month.weeks as weekDates, i (i)}
<DateRangePicker.GridRow class="flex w-full"> <DateRangePicker.GridRow class="flex w-full">
{#each weekDates as date} {#each weekDates as date, i (i)}
<DateRangePicker.Cell <DateRangePicker.Cell
{date} {date}
month={month.value} month={month.value}
class="p-0! relative m-0 size-10 overflow-visible text-center text-sm focus-within:relative focus-within:z-20" class="relative m-0 size-10 overflow-visible p-0! text-center text-sm focus-within:relative focus-within:z-20"
> >
<DateRangePicker.Day <DateRangePicker.Day
class={'hover:border-sky focus-visible:ring-foreground! data-selected:rounded-none data-selection-end:rounded-r data-selection-start:rounded-l data-highlighted:bg-surface-0 data-selected:bg-surface-1 data-selection-end:bg-surface-2 data-selection-start:bg-surface-2 data-disabled:text-text/30 data-unavailable:text-overlay-0 data-disabled:pointer-events-none data-outside-month:pointer-events-none data-highlighted:rounded-none data-unavailable:line-through group relative inline-flex size-10 items-center justify-center overflow-visible whitespace-nowrap rounded border border-transparent bg-transparent p-0 text-sm font-normal transition-all'} class="hover:border-sky focus-visible:ring-foreground! data-highlighted:bg-surface-0 data-selected:bg-surface-1 data-selection-end:bg-surface-2 data-selection-start:bg-surface-2 data-disabled:text-text/30 data-unavailable:text-overlay-0 group relative inline-flex size-10 items-center justify-center overflow-visible rounded border border-transparent bg-transparent p-0 text-sm font-normal whitespace-nowrap transition-all data-disabled:pointer-events-none data-highlighted:rounded-none data-outside-month:pointer-events-none data-selected:rounded-none data-selection-end:rounded-r data-selection-start:rounded-l data-unavailable:line-through"
> >
<div <div
class="bg-sky group-data-selected:bg-background group-data-today:block absolute top-[5px] hidden size-1 rounded-full transition-all" class="bg-sky group-data-selected:bg-background absolute top-[5px] hidden size-1 rounded-full transition-all group-data-today:block"
></div> ></div>
{date.day} {date.day}
</DateRangePicker.Day> </DateRangePicker.Day>

View File

@ -2,7 +2,8 @@
import { X } from '@lucide/svelte'; import { X } from '@lucide/svelte';
import { Dialog } from 'bits-ui'; import { Dialog } from 'bits-ui';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { Snippet } from 'svelte'; import { type Snippet } from 'svelte';
import { pushState } from '$app/navigation';
let { let {
trigger, trigger,
@ -17,7 +18,24 @@
} = $props(); } = $props();
</script> </script>
<Dialog.Root bind:open> <svelte:window
onpopstate={() => {
if (open) {
open = false;
}
}}
/>
<Dialog.Root
bind:open
onOpenChange={(e) => {
if (e) {
pushState('', '#modal');
} else {
history.back();
}
}}
>
<Dialog.Trigger> <Dialog.Trigger>
{#snippet child({ props })} {#snippet child({ props })}
{@render trigger(props)} {@render trigger(props)}
@ -48,7 +66,7 @@
}} }}
> >
<div <div
class="bg-mantle border-surface-0 fixed inset-0 left-[50%] top-[50%] z-50 size-fit w-96 -translate-x-1/2 -translate-y-1/2 transform overflow-y-auto rounded-xl border pb-1 drop-shadow-md" class="bg-mantle border-surface-0 fixed inset-0 top-[50%] left-[50%] z-50 size-fit w-96 -translate-x-1/2 -translate-y-1/2 transform overflow-y-auto rounded-xl border pb-1 drop-shadow-md"
> >
<div class="border-surface-0 flex justify-between border-b p-2"> <div class="border-surface-0 flex justify-between border-b p-2">
<h1 class="grow truncate p-1 text-center text-xl font-bold"> <h1 class="grow truncate p-1 text-center text-xl font-bold">

View File

@ -2,6 +2,8 @@
import { cn } from '$lib/utils'; import { cn } from '$lib/utils';
import { ChevronLeft, ChevronRight } from '@lucide/svelte'; import { ChevronLeft, ChevronRight } from '@lucide/svelte';
import { Pagination } from 'bits-ui'; import { Pagination } from 'bits-ui';
import { pushState, replaceState } from '$app/navigation';
import { onMount, tick } from 'svelte';
let { let {
count = $bindable(), count = $bindable(),
@ -16,15 +18,36 @@
className?: string; className?: string;
onchange?: (e: number) => void; onchange?: (e: number) => void;
} = $props(); } = $props();
let page: number = $state(1);
onMount(async () => {
await tick();
replaceState('', `${page}`);
});
</script> </script>
<svelte:window
onpopstate={(e) => {
const lastPage: number = Number(e.state['sveltekit:states']);
if (!isNaN(lastPage)) {
page = lastPage;
offset = (lastPage - 1) * limit;
window.scrollTo(0, 0);
onchange?.(lastPage);
}
}}
/>
{#key count && limit} {#key count && limit}
<Pagination.Root <Pagination.Root
{count} {count}
bind:page
perPage={limit} perPage={limit}
onPageChange={(e) => { onPageChange={(e) => {
offset = (e - 1) * limit; offset = (e - 1) * limit;
window.scrollTo(0, 0); window.scrollTo(0, 0);
pushState('', `${e}`);
onchange?.(e); onchange?.(e);
}} }}
> >
@ -38,11 +61,11 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
{#each pages as page (page.key)} {#each pages as page (page.key)}
{#if page.type === 'ellipsis'} {#if page.type === 'ellipsis'}
<div class="select-none font-medium">...</div> <div class="font-medium select-none">...</div>
{:else} {:else}
<Pagination.Page <Pagination.Page
{page} {page}
class="hover:bg-surface-0 data-selected:bg-surface-0 data-selected:text-background inline-flex size-10 cursor-pointer select-none items-center justify-center rounded bg-transparent font-medium transition-all disabled:cursor-not-allowed disabled:opacity-50 hover:disabled:bg-transparent" class="hover:bg-surface-0 data-selected:bg-surface-0 data-selected:text-background inline-flex size-10 cursor-pointer items-center justify-center rounded bg-transparent font-medium transition-all select-none disabled:cursor-not-allowed disabled:opacity-50 hover:disabled:bg-transparent"
> >
{page.value} {page.value}
</Pagination.Page> </Pagination.Page>

View File

@ -2,6 +2,7 @@
import { cn } from '$lib/utils'; import { cn } from '$lib/utils';
import { Check, ChevronsDown, ChevronsUp, ChevronsUpDown, X } from '@lucide/svelte'; import { Check, ChevronsDown, ChevronsUp, ChevronsUpDown, X } from '@lucide/svelte';
import { Select } from 'bits-ui'; import { Select } from 'bits-ui';
import { fade } from 'svelte/transition';
let { let {
value = $bindable('10'), value = $bindable('10'),
@ -30,16 +31,23 @@
> >
<Select.Root type="single" {items} bind:value onValueChange={onchange}> <Select.Root type="single" {items} bind:value onValueChange={onchange}>
<Select.Trigger <Select.Trigger
class="focus:outline-sky data-placeholder:text-overlay-0 gap-2 inline-flex grow cursor-pointer select-none items-center justify-between rounded-l py-2 pl-2 text-sm transition-colors focus:outline focus:outline-offset-1" class="focus:outline-sky data-placeholder:text-overlay-0 inline-flex grow cursor-pointer items-center justify-between gap-2 rounded-l py-2 pl-2 text-sm transition-colors select-none focus:outline focus:outline-offset-1"
aria-label={placeholder} aria-label={placeholder}
> >
{selectedLabel} {selectedLabel}
<ChevronsUpDown class="text-overlay-0" size="20" /> <ChevronsUpDown class="text-overlay-0" size="20" />
</Select.Trigger> </Select.Trigger>
<Select.Portal> <Select.Portal>
<Select.Content <Select.Content forceMount>
class="focus-override border-surface-0 bg-mantle shadow-popover data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 outline-hidden z-50 select-none rounded border p-1" {#snippet child({ wrapperProps, props, open })}
sideOffset={10} {#if open}
<div {...wrapperProps}>
<div
{...props}
class="border-surface-0 bg-mantle shadow-popover z-50 mt-1 rounded border p-1 outline-hidden select-none"
transition:fade={{
duration: 100
}}
> >
<Select.ScrollUpButton class="flex w-full items-center justify-center"> <Select.ScrollUpButton class="flex w-full items-center justify-center">
<ChevronsUp size="20" /> <ChevronsUp size="20" />
@ -47,7 +55,7 @@
<Select.Viewport class="p-1"> <Select.Viewport class="p-1">
{#each items as item, i (i + item.value)} {#each items as item, i (i + item.value)}
<Select.Item <Select.Item
class="data-disabled:cursor-not-allowed data-highlighted:bg-surface-0 outline-hidden data-disabled:opacity-50 flex h-10 w-full cursor-pointer select-none items-center gap-4 rounded px-5 py-3 text-sm capitalize" class="data-highlighted:bg-surface-0 flex h-10 w-full cursor-pointer items-center gap-4 rounded px-5 py-3 text-sm capitalize outline-hidden select-none data-disabled:cursor-not-allowed data-disabled:opacity-50"
value={item.value} value={item.value}
label={item.label} label={item.label}
disabled={item.disabled} disabled={item.disabled}
@ -66,6 +74,10 @@
<Select.ScrollDownButton class="flex w-full items-center justify-center"> <Select.ScrollDownButton class="flex w-full items-center justify-center">
<ChevronsDown size="20" /> <ChevronsDown size="20" />
</Select.ScrollDownButton> </Select.ScrollDownButton>
</div>
</div>
{/if}
{/snippet}
</Select.Content> </Select.Content>
</Select.Portal> </Select.Portal>
</Select.Root> </Select.Root>

View File

@ -1,5 +1,5 @@
import { type ClassValue, clsx } from "clsx"; import { type ClassValue, clsx } from 'clsx';
import { twMerge } from "tailwind-merge"; import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));

119
client/src/lib/webauthn.ts Normal file
View File

@ -0,0 +1,119 @@
import { decode } from 'cbor2';
import { page } from '$app/state';
interface CreateCredential extends Credential {
response: AuthenticatorAttestationResponse;
}
interface AttestationObject {
authData: Uint8Array;
fmt: string;
}
interface DecodedPublicKeyObject {
[key: number]: number | Uint8Array;
}
export async function createPasskey(username: string, userid: number, challenge: string) {
const challengeBuffer = Uint8Array.from(challenge, (c) => c.charCodeAt(0));
const idBuffer = Uint8Array.from(userid.toString(), (c) => c.charCodeAt(0));
const credential = (await navigator.credentials.create({
publicKey: {
challenge: challengeBuffer,
rp: { id: page.url.hostname, name: 'TrevStack' },
user: {
id: idBuffer,
name: username,
displayName: username
},
pubKeyCredParams: [
{
type: 'public-key',
alg: -7
},
{
type: 'public-key',
alg: -257
}
],
timeout: 60000,
attestation: 'none'
}
})) as CreateCredential | null;
if (!credential) {
throw new Error('Could not create passkey');
}
console.log(credential.id);
//console.log(credential.type);
const utf8Decoder = new TextDecoder('utf-8');
const decodedClientData = utf8Decoder.decode(credential.response.clientDataJSON);
const clientDataObj = JSON.parse(decodedClientData);
console.log(clientDataObj);
const attestationObject = new Uint8Array(credential.response.attestationObject);
const decodedAttestationObject = decode(attestationObject) as AttestationObject;
const { authData } = decodedAttestationObject;
// get the length of the credential ID
const dataView = new DataView(new ArrayBuffer(2));
const idLenBytes = authData.slice(53, 55);
idLenBytes.forEach((value, index) => dataView.setUint8(index, value));
const credentialIdLength = dataView.getUint16(0);
// get the credential ID
// const credentialId = authData.slice(55, 55 + credentialIdLength);
// get the public key object
const publicKeyBytes = authData.slice(55 + credentialIdLength);
console.log(publicKeyBytes);
// the publicKeyBytes are encoded again as CBOR
const publicKeyObject = new Uint8Array(publicKeyBytes.buffer);
const decodedPublicKeyObject = decode(publicKeyObject) as DecodedPublicKeyObject;
console.log(decodedPublicKeyObject);
return {
id: credential.id,
publicKey: publicKeyBytes,
algorithm: decodedPublicKeyObject[3]
};
}
interface GetCredential extends Credential {
response: AuthenticatorAssertionResponse;
}
export async function getPasskey(passkeyids: string[], challenge: string) {
const challengeBuffer = Uint8Array.from(challenge, (c) => c.charCodeAt(0));
const credential = (await navigator.credentials.get({
publicKey: {
challenge: challengeBuffer,
allowCredentials: passkeyids.map((passkeyid) => {
return {
id: Uint8Array.from(passkeyid, (c) => c.charCodeAt(0)),
type: 'public-key'
};
}),
timeout: 60000
}
})) as GetCredential | null;
if (!credential) {
throw new Error('Could not get passkey');
}
const signature = credential.response.signature;
return {
signature
};
}

View File

@ -98,14 +98,14 @@
> >
<NavigationMenu.Root orientation="vertical"> <NavigationMenu.Root orientation="vertical">
<NavigationMenu.List <NavigationMenu.List
class="flex w-full flex-col gap-2 overflow-y-auto overflow-x-hidden p-2" class="flex w-full flex-col gap-2 overflow-x-hidden overflow-y-auto p-2"
> >
{#each menuItems as item} {#each menuItems as item (item.name)}
{@const Icon = item.icon} {@const Icon = item.icon}
<NavigationMenu.Item> <NavigationMenu.Item>
<NavigationMenu.Link <NavigationMenu.Link
class={cn( class={cn(
'hover:bg-surface-0 flex select-none gap-2 whitespace-nowrap rounded-lg p-2 transition-all', 'hover:bg-surface-0 flex gap-2 rounded-lg p-2 whitespace-nowrap transition-all select-none',
page.url.pathname === item.href && 'bg-surface-0' page.url.pathname === item.href && 'bg-surface-0'
)} )}
href={item.href} href={item.href}
@ -126,7 +126,7 @@
<div class="border-surface-0 flex flex-col gap-2 border-t p-2"> <div class="border-surface-0 flex flex-col gap-2 border-t p-2">
<a <a
href="/settings" href="/settings"
class="hover:bg-surface-0 flex select-none items-center gap-2 rounded-lg p-2 transition-all" class="hover:bg-surface-0 flex items-center gap-2 rounded-lg p-2 transition-all select-none"
onclick={() => { onclick={() => {
if (sidebarOpen) { if (sidebarOpen) {
sidebarOpen = false; sidebarOpen = false;
@ -138,7 +138,7 @@
</a> </a>
<button <button
class="hover:bg-surface-0 flex w-full cursor-pointer items-center gap-2 whitespace-nowrap rounded-lg p-2 transition-all" class="hover:bg-surface-0 flex w-full cursor-pointer items-center gap-2 rounded-lg p-2 whitespace-nowrap transition-all"
onclick={logout} onclick={logout}
> >
<LogOut size="20" /> <LogOut size="20" />
@ -152,7 +152,7 @@
</Dialog.Portal> </Dialog.Portal>
</Dialog.Root> </Dialog.Root>
<a href="/" class="flex select-none items-center gap-2 text-2xl font-bold tracking-wider"> <a href="/" class="flex items-center gap-2 text-2xl font-bold tracking-wider select-none">
TrevStack TrevStack
<LayoutGrid /> <LayoutGrid />
</a> </a>
@ -160,11 +160,11 @@
<NavigationMenu.Root class="hidden md:block"> <NavigationMenu.Root class="hidden md:block">
<NavigationMenu.List class="flex gap-2"> <NavigationMenu.List class="flex gap-2">
{#each menuItems as item} {#each menuItems as item (item.name)}
<NavigationMenu.Item> <NavigationMenu.Item>
<NavigationMenu.Link <NavigationMenu.Link
class={cn( class={cn(
'hover:bg-surface-0 flex select-none gap-2 rounded-lg p-1 px-2 transition-all', 'hover:bg-surface-0 flex gap-2 rounded-lg p-1 px-2 transition-all select-none',
page.url.pathname === item.href && 'bg-surface-0' page.url.pathname === item.href && 'bg-surface-0'
)} )}
href={item.href} href={item.href}
@ -179,7 +179,7 @@
<Popover.Root bind:open={popupOpen}> <Popover.Root bind:open={popupOpen}>
<Popover.Trigger <Popover.Trigger
class="outline-surface-2 hover:brightness-120 bg-text text-crust h-9 w-9 cursor-pointer rounded-full text-sm outline outline-offset-2 transition-all" class="outline-surface-2 bg-text text-crust h-9 w-9 cursor-pointer rounded-full text-sm outline outline-offset-2 transition-all hover:brightness-120"
> >
<Avatar /> <Avatar />
</Popover.Trigger> </Popover.Trigger>

View File

@ -26,8 +26,8 @@
// Open // Open
let addedOpen = $state(false); let addedOpen = $state(false);
let deletesOpen: SvelteMap<number, boolean> = new SvelteMap(); let deletesOpen: SvelteMap<bigint, boolean> = new SvelteMap();
let editsOpen: SvelteMap<number, boolean> = new SvelteMap(); let editsOpen: SvelteMap<bigint, boolean> = new SvelteMap();
async function getItems() { async function getItems() {
return await ItemClient.getItems({ return await ItemClient.getItems({
@ -50,12 +50,7 @@
</script> </script>
<div class="mx-4 my-2 flex flex-wrap items-center justify-center gap-2"> <div class="mx-4 my-2 flex flex-wrap items-center justify-center gap-2">
<Input <Input bind:value={filter} className="bg-mantle" placeholder="Filter" onchange={updateItems} />
bind:value={filter}
className="bg-mantle"
placeholder="Filter"
onchange={updateItems}
/>
<Select <Select
items={[ items={[
{ {
@ -238,7 +233,7 @@
<td class="w-8"></td> <td class="w-8"></td>
</tr> </tr>
{:then items} {:then items}
{#each items as item} {#each items as item (item.id)}
<tr class="border-surface-0 border-b"> <tr class="border-surface-0 border-b">
<td class="px-6 py-3"> <td class="px-6 py-3">
{item.added ? timestampDate(item.added).toLocaleString() : ''} {item.added ? timestampDate(item.added).toLocaleString() : ''}
@ -287,7 +282,7 @@
</div> </div>
</div> </div>
{:then items} {:then items}
{#each items as item} {#each items as item (item.id)}
<div <div
class="border-surface-0 bg-mantle flex w-full flex-wrap gap-6 rounded border p-5 drop-shadow-md" class="border-surface-0 bg-mantle flex w-full flex-wrap gap-6 rounded border p-5 drop-shadow-md"
> >
@ -313,7 +308,7 @@
<span class="text-subtext-0 text-sm">Quantity</span> <span class="text-subtext-0 text-sm">Quantity</span>
<span class="truncate">{item.quantity}</span> <span class="truncate">{item.quantity}</span>
</div> </div>
<div class="flex justify-end ml-auto gap-2"> <div class="ml-auto flex justify-end gap-2">
{@render editModal(item)} {@render editModal(item)}
{@render deleteModal(item)} {@render deleteModal(item)}
</div> </div>
@ -322,7 +317,7 @@
{/await} {/await}
</div> </div>
<div class="mx-4 mb-4 mt-2 flex justify-end sm:mt-1"> <div class="mx-4 mt-2 mb-4 flex justify-end sm:mt-1">
<Modal bind:open={addedOpen}> <Modal bind:open={addedOpen}>
{#snippet trigger(props)} {#snippet trigger(props)}
<Button {...props} className="bg-sky"> <Button {...props} className="bg-sky">

View File

@ -17,7 +17,7 @@
<div class="m-auto flex w-96 flex-col gap-4 p-4"> <div class="m-auto flex w-96 flex-col gap-4 p-4">
<div class="flex items-center justify-center gap-4"> <div class="flex items-center justify-center gap-4">
<div <div
class="outline-surface-2 bg-text text-crust h-9 w-9 select-none rounded-full text-sm outline outline-offset-2" class="outline-surface-2 bg-text text-crust h-9 w-9 rounded-full text-sm outline outline-offset-2 select-none"
> >
<Avatar /> <Avatar />
</div> </div>
@ -26,7 +26,7 @@
<Separator.Root class="bg-surface-0 h-px" /> <Separator.Root class="bg-surface-0 h-px" />
<div class="flex justify-around gap-2"> <div class="flex flex-wrap justify-around gap-2">
<Modal> <Modal>
{#snippet trigger(props)} {#snippet trigger(props)}
<Button {...props} className="bg-text">Generate API Key</Button> <Button {...props} className="bg-text">Generate API Key</Button>
@ -133,6 +133,15 @@
</form> </form>
{/snippet} {/snippet}
</Modal> </Modal>
<Button
className="bg-text"
onclick={async () => {
if (userState.user) {
//await createPasskey(userState.user.username, userState.user.id, "what");
}
}}>Register Device</Button
>
</div> </div>
<form <form

View File

@ -5,10 +5,12 @@
let { children } = $props(); let { children } = $props();
</script> </script>
<Toaster toastOptions={{ <Toaster
toastOptions={{
classes: { classes: {
toast: '!bg-mantle !text-text !border-surface-0', toast: '!bg-mantle !text-text !border-surface-0'
} }
}} /> }}
/>
{@render children()} {@render children()}

View File

@ -7,6 +7,7 @@
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import Button from '$lib/ui/Button.svelte'; import Button from '$lib/ui/Button.svelte';
import Input from '$lib/ui/Input.svelte'; import Input from '$lib/ui/Input.svelte';
import { page } from '$app/state';
let tab = $state('login'); let tab = $state('login');
</script> </script>
@ -41,6 +42,7 @@
const formData = new FormData(e.target as HTMLFormElement); const formData = new FormData(e.target as HTMLFormElement);
const username = formData.get('login-username')?.toString(); const username = formData.get('login-username')?.toString();
const password = formData.get('login-password')?.toString(); const password = formData.get('login-password')?.toString();
const redir = page.url.searchParams.get('redir') || '/';
try { try {
const response = await AuthClient.login({ const response = await AuthClient.login({
@ -49,7 +51,7 @@
}); });
if (response.token && username) { if (response.token && username) {
goto('/'); goto(redir);
} }
} catch (err) { } catch (err) {
const error = ConnectError.from(err); const error = ConnectError.from(err);

View File

@ -5,7 +5,7 @@
"display": "standalone", "display": "standalone",
"start_url": "/", "start_url": "/",
"background_color": "#181825", "background_color": "#181825",
"theme_color": "#89dceb", "theme_color": "#11111b",
"icons": [ "icons": [
{ {
"src": "icon.png", "src": "icon.png",

View File

@ -128,8 +128,11 @@ components:
type: object type: object
properties: properties:
id: id:
type: integer type:
- integer
- string
title: id title: id
format: int64
title: DeleteItemRequest title: DeleteItemRequest
additionalProperties: false additionalProperties: false
item.v1.DeleteItemResponse: item.v1.DeleteItemResponse:
@ -140,8 +143,11 @@ components:
type: object type: object
properties: properties:
id: id:
type: integer type:
- integer
- string
title: id title: id
format: int64
title: GetItemRequest title: GetItemRequest
additionalProperties: false additionalProperties: false
item.v1.GetItemResponse: item.v1.GetItemResponse:
@ -170,10 +176,12 @@ components:
limit: limit:
type: integer type: integer
title: limit title: limit
format: int32
nullable: true nullable: true
offset: offset:
type: integer type: integer
title: offset title: offset
format: int32
nullable: true nullable: true
title: GetItemsRequest title: GetItemsRequest
additionalProperties: false additionalProperties: false
@ -197,8 +205,11 @@ components:
type: object type: object
properties: properties:
id: id:
type: integer type:
- integer
- string
title: id title: id
format: int64
nullable: true nullable: true
name: name:
type: string type: string
@ -213,6 +224,7 @@ components:
quantity: quantity:
type: integer type: integer
title: quantity title: quantity
format: int32
added: added:
title: added title: added
nullable: true nullable: true
@ -414,14 +426,20 @@ components:
type: object type: object
properties: properties:
id: id:
type: integer type:
- integer
- string
title: id title: id
format: int64
username: username:
type: string type: string
title: username title: username
profilePicture: profilePictureId:
type: string type:
title: profile_picture - integer
- string
title: profile_picture_id
format: int64
nullable: true nullable: true
title: User title: User
additionalProperties: false additionalProperties: false

View File

@ -1,23 +1,20 @@
import { sveltekit } from '@sveltejs/kit/vite'; import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite' import tailwindcss from '@tailwindcss/vite';
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [tailwindcss(), sveltekit()],
tailwindcss(),
sveltekit()
],
server: { server: {
proxy: { proxy: {
'/grpc': { '/grpc': {
target: 'http://localhost:8080', target: 'http://localhost:8080',
changeOrigin: true, changeOrigin: true
}, },
'/file': { '/file': {
target: 'http://localhost:8080', target: 'http://localhost:8080',
changeOrigin: true, changeOrigin: true
}
}, },
}, host: '0.0.0.0'
host: '0.0.0.0',
} }
}); });

99
flake.lock generated
View File

@ -18,6 +18,24 @@
"type": "github" "type": "github"
} }
}, },
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gitignore": { "gitignore": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@ -38,13 +56,50 @@
"type": "github" "type": "github"
} }
}, },
"gitignore_2": {
"inputs": {
"nixpkgs": [
"treli",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1741513245, "lastModified": 1744098102,
"narHash": "sha256-7rTAMNTY1xoBwz0h7ZMtEcd8LELk9R5TzBPoHuhNSCk=", "narHash": "sha256-tzCdyIJj9AjysC3OuKA+tMD/kDEDAF9mICPDU7ix0JA=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "e3e32b642a31e6714ec1b712de8c91a3352ce7e1", "rev": "c8cd81426f45942bb2906d5ed2fe21d2f19d95b7",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1743583204,
"narHash": "sha256-F7n4+KOIfWrwoQjXrL2wD9RhFYLs2/GGe/MQY1sSdlE=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "2c8d3f48d33929642c1c12cd243df4cc7d2ce434",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -58,7 +113,8 @@
"inputs": { "inputs": {
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"gitignore": "gitignore", "gitignore": "gitignore",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs",
"treli": "treli"
} }
}, },
"systems": { "systems": {
@ -75,6 +131,41 @@
"repo": "default", "repo": "default",
"type": "github" "type": "github"
} }
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treli": {
"inputs": {
"flake-utils": "flake-utils_2",
"gitignore": "gitignore_2",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1744262571,
"narHash": "sha256-zYYx5DCQuyGsEKGStakQW1eSXPofRA3LeufIEVhE/4Q=",
"owner": "spotdemo4",
"repo": "treli",
"rev": "00b55f3cdc82e61a6c4f46c6cb745c71203ccde3",
"type": "github"
},
"original": {
"owner": "spotdemo4",
"repo": "treli",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

174
flake.nix
View File

@ -1,21 +1,22 @@
{ {
description = "A trevstack development environment"; description = "A template for trevstack";
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
treli.url = "github:spotdemo4/treli";
gitignore = { gitignore = {
url = "github:hercules-ci/gitignore.nix"; url = "github:hercules-ci/gitignore.nix";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
}; };
outputs = { self, nixpkgs, flake-utils, gitignore }: outputs = { self, nixpkgs, flake-utils, gitignore, treli }:
flake-utils.lib.eachDefaultSystem (system: flake-utils.lib.eachDefaultSystem (system:
let let
pname = "trevstack"; pname = "trevstack";
version = "0.0.2"; version = "0.0.11";
pkgs = import nixpkgs { pkgs = import nixpkgs {
inherit system; inherit system;
@ -34,11 +35,25 @@
nativeCheckInputs = with pkgs; [ less ]; nativeCheckInputs = with pkgs; [ less ];
}; };
bobgen = pkgs.buildGoModule {
name = "bobgen";
src = pkgs.fetchFromGitHub {
owner = "stephenafamo";
repo = "bob";
rev = "v0.31.0";
sha256 = "sha256-APAckQ+EDAu459NTPXUISLIrcAcX3aQ5B/jrMUEW0EY=";
};
vendorHash = "sha256-3blGiSxlKpWH8k0acAXXks8nCdnoWmXLmzPStJmmGcM=";
subPackages = [
"gen/bobgen-sql"
];
};
client = pkgs.buildNpmPackage { client = pkgs.buildNpmPackage {
pname = "${pname}-client"; pname = "${pname}-client";
inherit version; inherit version;
src = gitignore.lib.gitignoreSource ./client; src = gitignore.lib.gitignoreSource ./client;
npmDepsHash = "sha256-53n7y10JA4unu1lsMZyxN4rQnV9CtKGySRdEsBCy75U="; npmDepsHash = "sha256-Mu04whysDA1U5wvECJJ+KopGfSzTPR/OhWz9cjTRIfU=";
nodejs = pkgs.nodejs_22; nodejs = pkgs.nodejs_22;
npmFlags = [ "--legacy-peer-deps" ]; npmFlags = [ "--legacy-peer-deps" ];
@ -48,15 +63,33 @@
''; '';
}; };
server = pkgs.buildGoModule {
inherit client pname version;
src = gitignore.lib.gitignoreSource ./server;
vendorHash = "sha256-YmMKl9X1kVz6dk/JOSi2jghCUKObUKdm2O+JpO9PDCA=";
env.CGO_ENABLED = 0;
preBuild = ''
cp -r ${client} client
'';
};
in in
{ {
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
packages = with pkgs; [ packages = with pkgs; [
git
nix-update
treli.packages."${system}".default
sqlite
# Go backend # Go backend
go go
gotools gotools
gopls gopls
air revive
bobgen
dbmate
# Protobuf middleware # Protobuf middleware
buf buf
@ -64,54 +97,11 @@
protoc-gen-connect-go protoc-gen-connect-go
protoc-gen-es protoc-gen-es
protoc-gen-connect-openapi protoc-gen-connect-openapi
inotify-tools
# Svelte frontend # Svelte frontend
nodejs_22 nodejs_22
# Nix # Update
nix-update
# Helper scripts
(writeShellApplication {
name = "ts-run";
text = ''
git_root=$(git rev-parse --show-toplevel)
(cd "''${git_root}/server" && air) &
P1=$!
(cd "''${git_root}/client" && npm run dev) &
P2=$!
(cd "''${git_root}" && ts-pbwatch) &
P3=$!
trap 'kill $P1 $P2 $P3' SIGINT SIGTERM
wait $P1
wait $P2
wait $P3
kill $P1 $P2 $P3
'';
})
(writeShellApplication {
name = "ts-pbwatch";
text = ''
inotifywait -mre close_write,moved_to,create proto | while read -r _ _ basename;
do
echo "file changed: $basename"
if buf lint ; then
buf generate
fi
echo "regenerated proto services"
done
'';
})
(writeShellApplication { (writeShellApplication {
name = "ts-update"; name = "ts-update";
@ -144,44 +134,92 @@
''; '';
}) })
# Bump version
(writeShellApplication { (writeShellApplication {
name = "ts-release"; name = "ts-bump";
text = '' text = ''
git_root=$(git rev-parse --show-toplevel) git_root=$(git rev-parse --show-toplevel)
next_version=$(echo "${version}" | awk -F. -v OFS=. '{$NF += 1 ; print}')
version=$(git describe --abbrev=0)
version_no_v="''${version:1}"
next_version=$(echo "''${version}" | awk -F. -v OFS=. '{$NF += 1 ; print}')
next_version_no_v=$(echo "''${version_no_v}" | awk -F. -v OFS=. '{$NF += 1 ; print}')
cd "''${git_root}/client" cd "''${git_root}/client"
npm version "''${next_version_no_v}" npm version "''${next_version}"
git add package-lock.json git add package-lock.json
git add package.json git add package.json
cd "''${git_root}" cd "''${git_root}"
nix-update --flake --version "''${next_version_no_v}" --subpackage client default nix-update --flake --version "''${next_version}" --subpackage client default
git add flake.nix git add flake.nix
git commit -m "release: ''${version} -> ''${next_version}" git commit -m "bump: v${version} -> v''${next_version}"
git push origin main git push origin main
git tag -a "''${next_version}" -m "release: ''${version} -> ''${next_version}" git tag -a "v''${next_version}" -m "bump: v${version} -> v''${next_version}"
git push origin "''${next_version}" git push origin "v''${next_version}"
'';
})
# Lint
(writeShellApplication {
name = "ts-lint";
text = ''
git_root=$(git rev-parse --show-toplevel)
if [ -n "''${1:-}" ]; then
cd "''${git_root}/client"
npm run format
fi
cd "''${git_root}"
echo "Linting protobuf"
buf lint
cd "''${git_root}/client"
echo "Linting client"
npm run check
npm run lint
cd "''${git_root}/server"
echo "Linting server"
revive -config revive.toml -set_exit_status ./...
'';
})
# Build
(writeShellApplication {
name = "ts-build";
text = ''
git_root=$(git rev-parse --show-toplevel)
cd "''${git_root}"
echo "Building client"
nix build .#trevstack-client
cp -a result/. server/client
chmod -R u+w server/client
cd "''${git_root}/server"
echo "Building ${pname}-windows-amd64-${version}.exe"
GOOS=windows GOARCH=amd64 go build -o "../build/${pname}-windows-amd64-${version}.exe" .
echo "Building ${pname}-linux-amd64-${version}"
GOOS=linux GOARCH=amd64 go build -o "../build/${pname}-linux-amd64-${version}" .
echo "Building ${pname}-linux-amd64-${version}"
GOOS=linux GOARCH=arm64 go build -o "../build/${pname}-linux-arm64-${version}" .
echo "Building ${pname}-linux-arm-${version}"
GOOS=linux GOARCH=arm go build -o "../build/${pname}-linux-arm-${version}" .
''; '';
}) })
]; ];
}; };
packages.default = pkgs.buildGoModule { packages = rec {
inherit client pname version; default = trevstack;
src = gitignore.lib.gitignoreSource ./server;
vendorHash = "sha256-sANPwYLGwMcWyMR7Veho81aAMfIQpVzZS5Q9eveR8o8=";
preBuild = '' trevstack = server;
cp -r ${client} internal/handlers/client/client trevstack-client = client;
'';
}; };
} }
); );

View File

@ -5,11 +5,11 @@ package item.v1;
import "google/protobuf/timestamp.proto"; import "google/protobuf/timestamp.proto";
message Item { message Item {
optional uint32 id = 1; optional int64 id = 1;
string name = 2; string name = 2;
string description = 3; string description = 3;
float price = 4; float price = 4;
uint32 quantity = 5; int32 quantity = 5;
optional google.protobuf.Timestamp added = 6; optional google.protobuf.Timestamp added = 6;
} }
@ -22,7 +22,7 @@ service ItemService {
} }
message GetItemRequest { message GetItemRequest {
uint32 id = 1; int64 id = 1;
} }
message GetItemResponse { message GetItemResponse {
Item item = 1; Item item = 1;
@ -32,12 +32,12 @@ message GetItemsRequest {
optional google.protobuf.Timestamp start = 1; optional google.protobuf.Timestamp start = 1;
optional google.protobuf.Timestamp end = 2; optional google.protobuf.Timestamp end = 2;
optional string filter = 3; optional string filter = 3;
optional uint32 limit = 4; optional int32 limit = 4;
optional uint32 offset = 5; optional int32 offset = 5;
} }
message GetItemsResponse { message GetItemsResponse {
repeated Item items = 1; repeated Item items = 1;
uint64 count = 2; int64 count = 2;
} }
message CreateItemRequest { message CreateItemRequest {
@ -55,6 +55,6 @@ message UpdateItemResponse {
} }
message DeleteItemRequest { message DeleteItemRequest {
uint32 id = 1; int64 id = 1;
} }
message DeleteItemResponse {} message DeleteItemResponse {}

View File

@ -6,6 +6,9 @@ service AuthService {
rpc Login (LoginRequest) returns (LoginResponse) {} rpc Login (LoginRequest) returns (LoginResponse) {}
rpc SignUp (SignUpRequest) returns (SignUpResponse) {} rpc SignUp (SignUpRequest) returns (SignUpResponse) {}
rpc Logout (LogoutRequest) returns (LogoutResponse) {} rpc Logout (LogoutRequest) returns (LogoutResponse) {}
// rpc GetPasskeyIDs (GetPasskeyIDsRequest) returns (GetPasskeyIDsResponse) {}
// rpc BeginPasskeyLogin (BeginPasskeyLoginRequest) returns (BeginPasskeyLoginResponse) {}
// rpc FinishPasskeyLogin (FinishPasskeyLoginRequest) returns (FinishPasskeyLoginResponse) {}
} }
message LoginRequest { message LoginRequest {
@ -25,3 +28,16 @@ message SignUpResponse {}
message LogoutRequest {} message LogoutRequest {}
message LogoutResponse {} message LogoutResponse {}
// message GetPasskeyIDsRequest {
// string username = 1;
// }
// message GetPasskeyIDsResponse {
// repeated string passkey_ids = 1;
// }
// message BeginPasskeyLoginRequest {}
// message BeginPasskeyLoginResponse {}
// message FinishPasskeyLoginRequest {}
// message FinishPasskeyLoginResponse {}

View File

@ -3,9 +3,9 @@ syntax = "proto3";
package user.v1; package user.v1;
message User { message User {
uint32 id = 1; int64 id = 1;
string username = 2; string username = 2;
optional string profile_picture = 3; optional int64 profile_picture_id = 3;
} }
service UserService { service UserService {
@ -13,6 +13,8 @@ service UserService {
rpc UpdatePassword (UpdatePasswordRequest) returns (UpdatePasswordResponse) {} rpc UpdatePassword (UpdatePasswordRequest) returns (UpdatePasswordResponse) {}
rpc GetAPIKey (GetAPIKeyRequest) returns (GetAPIKeyResponse) {} rpc GetAPIKey (GetAPIKeyRequest) returns (GetAPIKeyResponse) {}
rpc UpdateProfilePicture (UpdateProfilePictureRequest) returns (UpdateProfilePictureResponse) {} rpc UpdateProfilePicture (UpdateProfilePictureRequest) returns (UpdateProfilePictureResponse) {}
// rpc BeginPasskeyRegistration (BeginPasskeyRegistrationRequest) returns (BeginPasskeyRegistrationResponse) {}
// rpc FinishPasskeyRegistration (FinishPasskeyRegistrationRequest) returns (FinishPasskeyRegistrationResponse) {}
} }
message GetUserRequest {} message GetUserRequest {}
@ -44,3 +46,9 @@ message UpdateProfilePictureRequest {
message UpdateProfilePictureResponse { message UpdateProfilePictureResponse {
User user = 1; User user = 1;
} }
// message BeginPasskeyRegistrationRequest {}
// message BeginPasskeyRegistrationResponse {}
// message FinishPasskeyRegistrationRequest {}
// message FinishPasskeyRegistrationResponse {}

View File

@ -1,52 +0,0 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
silent = false
time = false
[misc]
clean_on_exit = false
[proxy]
app_port = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = false
keep_scroll = true

2
server/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/client/
/tmp/

16
server/bobgen.yaml Normal file
View File

@ -0,0 +1,16 @@
wipe: true
replacements:
- match:
db_type: "INTEGER"
replace: "int64"
- match:
db_type: "INTEGER"
nullable: true
replace: "int64"
sql:
dialect: sqlite
dir: db
output: internal/models

View File

@ -0,0 +1,35 @@
-- migrate:up
CREATE TABLE user(
id INTEGER PRIMARY KEY NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL,
profile_picture_id INTEGER,
FOREIGN KEY(profile_picture_id) REFERENCES file(id)
);
CREATE TABLE file(
id INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
data BLOB NOT NULL,
user_id INTEGER NOT NULL,
FOREIGN KEY(user_id) REFERENCES user(id)
);
CREATE TABLE item(
id INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
added DATETIME NOT NULL,
description TEXT NOT NULL,
price REAL NOT NULL,
quantity INTEGER NOT NULL,
user_id INTEGER NOT NULL,
FOREIGN KEY(user_id) REFERENCES user(id)
);
-- migrate:down
drop table user;
drop table file;
drop table item;

31
server/db/schema.sql Normal file
View File

@ -0,0 +1,31 @@
CREATE TABLE IF NOT EXISTS "schema_migrations" (version varchar(128) primary key);
CREATE TABLE user(
id INTEGER PRIMARY KEY NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL,
profile_picture_id INTEGER,
FOREIGN KEY(profile_picture_id) REFERENCES file(id)
);
CREATE TABLE file(
id INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
data BLOB NOT NULL,
user_id INTEGER NOT NULL,
FOREIGN KEY(user_id) REFERENCES user(id)
);
CREATE TABLE item(
id INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
added DATETIME NOT NULL,
description TEXT NOT NULL,
price REAL NOT NULL,
quantity INTEGER NOT NULL,
user_id INTEGER NOT NULL,
FOREIGN KEY(user_id) REFERENCES user(id)
);
-- Dbmate schema migrations
INSERT INTO "schema_migrations" (version) VALUES
('20250410195416');

View File

@ -1,41 +1,39 @@
module github.com/spotdemo4/trevstack/server module github.com/spotdemo4/trevstack/server
go 1.23.6 go 1.24.1
require ( require (
connectrpc.com/connect v1.18.1 connectrpc.com/connect v1.18.1
connectrpc.com/cors v0.1.0 connectrpc.com/cors v0.1.0
github.com/glebarez/sqlite v1.11.0 github.com/aarondl/opt v0.0.0-20240623220848-083f18ab9536
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/amacneil/dbmate/v2 v2.26.0
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/jaswdr/faker/v2 v2.3.3
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.10.9
github.com/rs/cors v1.11.1 github.com/rs/cors v1.11.1
golang.org/x/crypto v0.36.0 github.com/spotdemo4/dbmate-sqlite-modernc v0.0.2
golang.org/x/net v0.37.0 github.com/stephenafamo/bob v0.31.0
golang.org/x/crypto v0.37.0
golang.org/x/net v0.39.0
golang.org/x/time v0.11.0 golang.org/x/time v0.11.0
google.golang.org/protobuf v1.36.5 google.golang.org/protobuf v1.36.6
gorm.io/driver/postgres v1.5.11 modernc.org/sqlite v1.37.0
gorm.io/gorm v1.25.12
) )
require ( require (
github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.2 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect github.com/stephenafamo/scan v0.6.2 // indirect
golang.org/x/sync v0.12.0 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/sys v0.31.0 // indirect golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.23.0 // indirect golang.org/x/text v0.24.0 // indirect
modernc.org/libc v1.61.13 // indirect modernc.org/libc v1.62.1 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.9.0 // indirect modernc.org/memory v1.9.1 // indirect
modernc.org/sqlite v1.36.1 // indirect
) )

View File

@ -2,101 +2,112 @@ connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw=
connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
connectrpc.com/cors v0.1.0 h1:f3gTXJyDZPrDIZCQ567jxfD9PAIpopHiRDnJRt3QuOQ= connectrpc.com/cors v0.1.0 h1:f3gTXJyDZPrDIZCQ567jxfD9PAIpopHiRDnJRt3QuOQ=
connectrpc.com/cors v0.1.0/go.mod h1:v8SJZCPfHtGH1zsm+Ttajpozd4cYIUryl4dFB6QEpfg= connectrpc.com/cors v0.1.0/go.mod h1:v8SJZCPfHtGH1zsm+Ttajpozd4cYIUryl4dFB6QEpfg=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf h1:+edM69bH/X6JpYPmJYBRLanAMe1V5yRXYU3hHUovGcE=
github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf/go.mod h1:FZqLhJSj2tg0ZN48GB1zvj00+ZYcHPqgsC7yzcgCq6k=
github.com/aarondl/opt v0.0.0-20240623220848-083f18ab9536 h1:vhpjulzH5Tr4S3uJ3Y/9pNL481kPq5ERj13ceAW0/uE=
github.com/aarondl/opt v0.0.0-20240623220848-083f18ab9536/go.mod h1:l4/5NZtYd/SIohsFhaJQQe+sPOTG22furpZ5FvcYOzk=
github.com/amacneil/dbmate/v2 v2.26.0 h1:74ykEWh0V41BU3wesgmTowMn/2x9rUfIxIG+Q+vuUm0=
github.com/amacneil/dbmate/v2 v2.26.0/go.mod h1:cnjZKm5x/gKMLPfExXbEDUUQi1Sv5UqY3AIgbnxql84=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jaswdr/faker/v2 v2.3.3 h1:0mA+B5YGjqgpOPdDY/72d6pDv7Z/5t6F1XzIfkUfgC4=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jaswdr/faker/v2 v2.3.3/go.mod h1:ROK8xwQV0hYOLDUtxCQgHGcl10jbVzIvqHxcIDdwY2Q=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 h1:wSmWgpuccqS2IOfmYrbRiUgv+g37W5suLLLxwwniTSc=
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494/go.mod h1:yipyliwI08eQ6XwDm1fEwKPdF/xdbkiHtrU+1Hg+vc4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/spotdemo4/dbmate-sqlite-modernc v0.0.2 h1:537TA0HvjoWJxPzAhP+t2Gt82NpxGhCdmDsJpta7sdc=
github.com/spotdemo4/dbmate-sqlite-modernc v0.0.2/go.mod h1:JwuvDDgb1VLGyHgSsQN3I+dx6QkEGk86o6Yc8vxzG/4=
github.com/stephenafamo/bob v0.31.0 h1:Nx80wK0N+gTZEdRCkmAzoYTJ3L3DPIF7Zt+Br3wwRJA=
github.com/stephenafamo/bob v0.31.0/go.mod h1:CU2OifyJmHdGvNPEGhZE2p92KirNfFiQm9g2hr9f/cY=
github.com/stephenafamo/fakedb v0.0.0-20221230081958-0b86f816ed97 h1:XItoZNmhOih06TC02jK7l3wlpZ0XT/sPQYutDcGOQjg=
github.com/stephenafamo/fakedb v0.0.0-20221230081958-0b86f816ed97/go.mod h1:bM3Vmw1IakoaXocHmMIGgJFYob0vuK+CFWiJHQvz0jQ=
github.com/stephenafamo/scan v0.6.2 h1:mEjx1P1MuimqALCXfZEV8+KAiVcByrgngqKatgHag9I=
github.com/stephenafamo/scan v0.6.2/go.mod h1:FhIUJ8pLNyex36xGFiazDJJ5Xry0UkAi+RkWRrEcRMg=
github.com/stephenafamo/sqlparser v0.0.0-20241111104950-b04fa8a26c9c h1:JFga++XBnZG2xlnvQyHJkeBWZ9G9mGdtgvLeSRbp/BA=
github.com/stephenafamo/sqlparser v0.0.0-20241111104950-b04fa8a26c9c/go.mod h1:4iveRk8mkzQZxDuK/W0MGLrGmu/igyDYWNDD4a6v0r0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04 h1:qXafrlZL1WsJW5OokjraLLRURHiw0OzKHD/RNdspp4w=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04/go.mod h1:FiwNQxz6hGoNFBC4nIx+CxZhI3nne5RmIOlT/MXcSD4=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic=
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw=
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8= modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s=
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E= modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.9.0 h1:smV8d5mrOAvj5QIYbc2XLSRWvAIyPI+kQHqxZaxEqCM= modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g=
modernc.org/memory v1.9.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.36.1 h1:bDa8BJUH4lg6EGkLbahKe/8QqoF8p9gArSc6fTqYhyQ= modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
modernc.org/sqlite v1.36.1/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU= modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

@ -1,22 +0,0 @@
package database
import (
"log"
"os"
"time"
"gorm.io/gorm/logger"
)
func NewLogger() logger.Interface {
return logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Silent, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
ParameterizedQueries: true, // Don't include params in the SQL log
Colorful: true, // Disable color
},
)
}

View File

@ -1,12 +1,34 @@
package database package database
import ( import (
"github.com/spotdemo4/trevstack/server/internal/models" "embed"
"gorm.io/gorm" "log"
"net/url"
"github.com/amacneil/dbmate/v2/pkg/dbmate"
_ "github.com/spotdemo4/dbmate-sqlite-modernc/pkg/driver/sqlite" // Modernc sqlite
) )
func Migrate(db *gorm.DB) error { func Migrate(url *url.URL, dbFS *embed.FS) error {
err := db.AutoMigrate(&models.User{}, &models.Item{}, &models.File{}) if dbFS == nil {
return nil
}
db := dbmate.New(url)
db.Driver()
db.FS = dbFS
log.Println("Migrations:")
migrations, err := db.FindMigrations()
if err != nil {
return err
}
for _, m := range migrations {
log.Println(m.Version, m.FilePath)
}
log.Println("\nApplying...")
err = db.CreateAndMigrate()
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,18 +1,74 @@
package database package database
import ( import (
"gorm.io/driver/postgres" "database/sql"
"gorm.io/gorm" "fmt"
"net/url"
"runtime"
_ "github.com/lib/pq" // Postgres
"github.com/stephenafamo/bob"
) )
func NewPostgresConnection(user, pass, host, port, name string) (*gorm.DB, error) { func NewPostgresConnection(url *url.URL) (*bob.DB, error) {
dsn := "host=" + host + " user=" + user + " password=" + pass + " dbname=" + name + " port=" + port + " sslmode=disable TimeZone=UTC" db, err := sql.Open("postgres", postgresConnectionString(url))
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: NewLogger(),
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
return db, nil bobdb := bob.NewDB(db)
return &bobdb, nil
}
func postgresConnectionString(u *url.URL) string {
hostname := u.Hostname()
port := u.Port()
query := u.Query()
// support socket parameter for consistency with mysql
if query.Get("socket") != "" {
query.Set("host", query.Get("socket"))
query.Del("socket")
}
// default hostname
if hostname == "" && query.Get("host") == "" {
switch runtime.GOOS {
case "linux":
query.Set("host", "/var/run/postgresql")
case "darwin", "freebsd", "dragonfly", "openbsd", "netbsd":
query.Set("host", "/tmp")
default:
hostname = "localhost"
}
}
// host param overrides url hostname
if query.Get("host") != "" {
hostname = ""
}
// always specify a port
if query.Get("port") != "" {
port = query.Get("port")
query.Del("port")
}
if port == "" {
switch u.Scheme {
case "redshift":
port = "5439"
default:
port = "5432"
}
}
// generate output URL
out, _ := url.Parse(u.String())
// force scheme back to postgres if there was another postgres-compatible scheme
out.Scheme = "postgres"
out.Host = fmt.Sprintf("%s:%s", hostname, port)
out.RawQuery = query.Encode()
return out.String()
} }

View File

@ -1,35 +1,66 @@
package database package database
import ( import (
"os" "database/sql"
"path/filepath" "net/url"
"regexp"
"github.com/glebarez/sqlite" "github.com/stephenafamo/bob"
"gorm.io/gorm" _ "modernc.org/sqlite" // Sqlite
) )
func NewSQLiteConnection(name string) (*gorm.DB, error) { func NewSQLiteConnection(url *url.URL) (*bob.DB, error) {
// Find config diretory db, err := sql.Open("sqlite", sqliteConnectionString(url))
configDir, err := os.UserConfigDir()
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Create database directory if not exists // Create new bob db
settingsPath := filepath.Join(configDir, "trevstack") bobdb := bob.NewDB(db)
err = os.MkdirAll(settingsPath, 0766)
if err != nil {
return nil, err
}
// Open database return &bobdb, nil
dbPath := filepath.Join(settingsPath, name) }
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
Logger: NewLogger(), // ConnectionString converts a URL into a valid connection string
}) func sqliteConnectionString(u *url.URL) string {
if err != nil { // duplicate URL and remove scheme
return nil, err newURL := *u
} newURL.Scheme = ""
return db, nil if newURL.Opaque == "" && newURL.Path != "" {
// When the DSN is in the form "scheme:/absolute/path" or
// "scheme://absolute/path" or "scheme:///absolute/path", url.Parse
// will consider the file path as :
// - "absolute" as the hostname
// - "path" (and the rest until "?") as the URL path.
// Instead, when the DSN is in the form "scheme:", the (relative) file
// path is stored in the "Opaque" field.
// See: https://pkg.go.dev/net/url#URL
//
// While Opaque is not escaped, the URL Path is. So, if .Path contains
// the file path, we need to un-escape it, and rebuild the full path.
newURL.Opaque = "//" + newURL.Host + mustUnescapePath(newURL.Path)
newURL.Path = ""
}
// trim duplicate leading slashes
str := regexp.MustCompile("^//+").ReplaceAllString(newURL.String(), "/")
return str
}
// MustUnescapePath unescapes a URL path, and panics if it fails.
// It is used during in cases where we are parsing a generated path.
func mustUnescapePath(s string) string {
if s == "" {
panic("missing path")
}
path, err := url.PathUnescape(s)
if err != nil {
panic(err)
}
return path
} }

View File

@ -1,131 +0,0 @@
package handlers
import (
"context"
"errors"
"net/http"
"strconv"
"time"
"connectrpc.com/connect"
"github.com/golang-jwt/jwt/v5"
"github.com/spotdemo4/trevstack/server/internal/interceptors"
"github.com/spotdemo4/trevstack/server/internal/models"
userv1 "github.com/spotdemo4/trevstack/server/internal/services/user/v1"
"github.com/spotdemo4/trevstack/server/internal/services/user/v1/userv1connect"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type AuthHandler struct {
db *gorm.DB
key []byte
}
func (h *AuthHandler) Login(ctx context.Context, req *connect.Request[userv1.LoginRequest]) (*connect.Response[userv1.LoginResponse], error) {
// Validate
user := models.User{}
if err := h.db.First(&user, "username = ?", req.Msg.Username).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("invalid username or password"))
}
return nil, connect.NewError(connect.CodeInternal, err)
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Msg.Password)); err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("invalid username or password"))
}
// Generate JWT
t := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
Issuer: "trevstack",
Subject: strconv.FormatUint(uint64(user.ID), 10),
IssuedAt: &jwt.NumericDate{
Time: time.Now(),
},
ExpiresAt: &jwt.NumericDate{
Time: time.Now().Add(time.Hour * 24),
},
})
ss, err := t.SignedString(h.key)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
// Create cookie
cookie := http.Cookie{
Name: "token",
Value: ss,
Path: "/",
MaxAge: 86400,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
}
res := connect.NewResponse(&userv1.LoginResponse{
Token: ss,
})
res.Header().Set("Set-Cookie", cookie.String())
return res, nil
}
func (h *AuthHandler) SignUp(ctx context.Context, req *connect.Request[userv1.SignUpRequest]) (*connect.Response[userv1.SignUpResponse], error) {
// Validate
if err := h.db.First(&models.User{}, "username = ?", req.Msg.Username).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, connect.NewError(connect.CodeInternal, err)
}
} else {
return nil, connect.NewError(connect.CodeAlreadyExists, errors.New("username already exists"))
}
if req.Msg.Password != req.Msg.ConfirmPassword {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("passwords do not match"))
}
// Hash password
hash, err := bcrypt.GenerateFromPassword([]byte(req.Msg.Password), bcrypt.DefaultCost)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
// Create user
user := models.User{
Username: req.Msg.Username,
Password: string(hash),
}
if err := h.db.Create(&user).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
res := connect.NewResponse(&userv1.SignUpResponse{})
return res, nil
}
func (h *AuthHandler) Logout(ctx context.Context, req *connect.Request[userv1.LogoutRequest]) (*connect.Response[userv1.LogoutResponse], error) {
// Clear cookie
cookie := http.Cookie{
Name: "token",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
}
res := connect.NewResponse(&userv1.LogoutResponse{})
res.Header().Set("Set-Cookie", cookie.String())
return res, nil
}
func NewAuthHandler(db *gorm.DB, key string) (string, http.Handler) {
interceptors := connect.WithInterceptors(interceptors.NewRateLimitInterceptor(key))
return userv1connect.NewAuthServiceHandler(
&AuthHandler{
db: db,
key: []byte(key),
},
interceptors,
)
}

View File

@ -1,17 +1,23 @@
package client package client
import ( import (
"embed"
"io/fs"
"net/http" "net/http"
"github.com/spotdemo4/trevstack/server/internal/interceptors" "github.com/spotdemo4/trevstack/server/internal/interceptors"
) )
var embedfs *http.FileSystem func NewClientHandler(key string, clientFS *embed.FS) http.Handler {
if clientFS == nil {
func NewClientHandler(key string) http.Handler { return http.NotFoundHandler()
if embedfs != nil {
return interceptors.WithAuthRedirect(http.FileServer(*embedfs), key)
} }
client, err := fs.Sub(clientFS, "client")
if err != nil {
return http.NotFoundHandler() return http.NotFoundHandler()
}
fs := http.FS(client)
return interceptors.WithAuthRedirect(http.FileServer(fs), key)
} }

View File

@ -1,16 +0,0 @@
// go:build !dev
package client
import (
"embed"
"net/http"
)
//go:embed client
var eclient embed.FS
func init() {
fs := http.FS(eclient)
embedfs = &fs
}

View File

@ -1,18 +1,21 @@
package handlers package file
import ( import (
"context"
"database/sql"
"errors" "errors"
"log"
"net/http" "net/http"
"strconv"
"strings" "strings"
"github.com/spotdemo4/trevstack/server/internal/interceptors" "github.com/spotdemo4/trevstack/server/internal/interceptors"
"github.com/spotdemo4/trevstack/server/internal/models" "github.com/spotdemo4/trevstack/server/internal/models"
"gorm.io/gorm" "github.com/stephenafamo/bob"
"github.com/stephenafamo/bob/dialect/sqlite"
) )
type FileHandler struct { type FileHandler struct {
db *gorm.DB db *bob.DB
key []byte key []byte
} }
@ -23,39 +26,45 @@ func (h *FileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
// Make sure this is a GET request
if r.Method != http.MethodGet {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
// Get the file id from the path // Get the file id from the path
pathItems := strings.Split(r.URL.Path, "/") pathItems := strings.Split(r.URL.Path, "/")
if len(pathItems) < 3 { if len(pathItems) < 3 {
http.Redirect(w, r, "/auth", http.StatusFound) http.Redirect(w, r, "/auth", http.StatusFound)
return return
} }
id := pathItems[2] id, err := strconv.Atoi(pathItems[2])
if err != nil {
// Get the file from the database
file := models.File{}
if err := h.db.First(&file, "id = ? AND user_id = ?", id, userid).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
http.Error(w, "File not found", http.StatusNotFound)
return
}
log.Println(err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return return
} }
// Serve the file // Get the file from the database
if r.Method == http.MethodGet { file, err := models.Files.Query(
w.Header().Set("Content-Type", http.DetectContentType(file.Data)) sqlite.WhereAnd(
w.Write(file.Data) models.SelectWhere.Files.ID.EQ(int64(id)),
return models.SelectWhere.Files.UserID.EQ(userid),
} else { ),
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) ).One(context.Background(), h.db)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
http.Error(w, "Not Found", http.StatusNotFound)
}
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return return
} }
w.Header().Set("Content-Type", http.DetectContentType(file.Data))
w.Write(file.Data)
} }
func NewFileHandler(db *gorm.DB, key string) http.Handler { func NewFileHandler(db *bob.DB, key string) http.Handler {
return interceptors.WithAuthRedirect( return interceptors.WithAuthRedirect(
&FileHandler{ &FileHandler{
db: db, db: db,

View File

@ -1,171 +0,0 @@
package handlers
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"connectrpc.com/connect"
"github.com/spotdemo4/trevstack/server/internal/interceptors"
"github.com/spotdemo4/trevstack/server/internal/models"
itemv1 "github.com/spotdemo4/trevstack/server/internal/services/item/v1"
"github.com/spotdemo4/trevstack/server/internal/services/item/v1/itemv1connect"
"gorm.io/gorm"
)
type ItemHandler struct {
db *gorm.DB
key []byte
}
func (h *ItemHandler) GetItem(ctx context.Context, req *connect.Request[itemv1.GetItemRequest]) (*connect.Response[itemv1.GetItemResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
// Get item
item := models.Item{}
if err := h.db.First(&item, "id = ? AND user_id = ?", req.Msg.Id, userid).Error; err != nil {
return nil, connect.NewError(connect.CodeNotFound, err)
}
res := connect.NewResponse(&itemv1.GetItemResponse{
Item: item.ToConnectV1(),
})
return res, nil
}
func (h *ItemHandler) GetItems(ctx context.Context, req *connect.Request[itemv1.GetItemsRequest]) (*connect.Response[itemv1.GetItemsResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
// Filters
sql := h.db.Where("user_id = ?", userid)
if req.Msg.Start != nil {
sql = sql.Where("added >= ?", req.Msg.Start.AsTime())
}
if req.Msg.End != nil {
sql = sql.Where("added <= ?", req.Msg.End.AsTime())
}
if req.Msg.Filter != nil {
sql = sql.Where("name LIKE ?", fmt.Sprintf("%%%s%%", *req.Msg.Filter))
}
// Uncounted filters
sqlu := sql.Session(&gorm.Session{})
if req.Msg.Limit != nil {
sqlu = sqlu.Limit(int(*req.Msg.Limit))
}
if req.Msg.Offset != nil {
sqlu = sqlu.Offset(int(*req.Msg.Offset))
}
// Get items & count
items := []models.Item{}
var count int64
if err := sqlu.Order("added desc").Find(&items).Error; err != nil {
return nil, connect.NewError(connect.CodeNotFound, err)
}
if err := sql.Model(&items).Count(&count).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
// Convert to connect v1 items
resItems := []*itemv1.Item{}
for _, item := range items {
resItems = append(resItems, item.ToConnectV1())
}
res := connect.NewResponse(&itemv1.GetItemsResponse{
Items: resItems,
Count: uint64(count),
})
return res, nil
}
func (h *ItemHandler) CreateItem(ctx context.Context, req *connect.Request[itemv1.CreateItemRequest]) (*connect.Response[itemv1.CreateItemResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
// Create item
item := models.Item{
Name: req.Msg.Item.Name,
Description: req.Msg.Item.Description,
Price: req.Msg.Item.Price,
Quantity: int(req.Msg.Item.Quantity),
Added: time.Now(),
UserID: uint(userid),
}
if err := h.db.Create(&item).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
res := connect.NewResponse(&itemv1.CreateItemResponse{
Item: item.ToConnectV1(),
})
return res, nil
}
func (h *ItemHandler) UpdateItem(ctx context.Context, req *connect.Request[itemv1.UpdateItemRequest]) (*connect.Response[itemv1.UpdateItemResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
// Validate
if req.Msg.Item.Id == nil {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("id is required"))
}
// Update item
item := models.Item{
ID: *req.Msg.Item.Id,
Name: req.Msg.Item.Name,
Description: req.Msg.Item.Description,
Price: req.Msg.Item.Price,
Quantity: int(req.Msg.Item.Quantity),
UserID: uint(userid),
}
if err := h.db.Where("id = ? AND user_id = ?", req.Msg.Item.Id, userid).Updates(&item).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
res := connect.NewResponse(&itemv1.UpdateItemResponse{
Item: item.ToConnectV1(),
})
return res, nil
}
func (h *ItemHandler) DeleteItem(ctx context.Context, req *connect.Request[itemv1.DeleteItemRequest]) (*connect.Response[itemv1.DeleteItemResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
// Delete item
if err := h.db.Delete(&models.Item{}, "id = ? AND user_id = ?", req.Msg.Id, userid).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
res := connect.NewResponse(&itemv1.DeleteItemResponse{})
return res, nil
}
func NewItemHandler(db *gorm.DB, key string) (string, http.Handler) {
interceptors := connect.WithInterceptors(interceptors.NewAuthInterceptor(key))
return itemv1connect.NewItemServiceHandler(
&ItemHandler{
db: db,
key: []byte(key),
},
interceptors,
)
}

View File

@ -0,0 +1,219 @@
package item
import (
"context"
"database/sql"
"errors"
"net/http"
"time"
"connectrpc.com/connect"
"github.com/aarondl/opt/omit"
"github.com/spotdemo4/trevstack/server/internal/interceptors"
"github.com/spotdemo4/trevstack/server/internal/models"
itemv1 "github.com/spotdemo4/trevstack/server/internal/services/item/v1"
"github.com/spotdemo4/trevstack/server/internal/services/item/v1/itemv1connect"
"github.com/stephenafamo/bob"
"github.com/stephenafamo/bob/dialect/sqlite"
"github.com/stephenafamo/bob/dialect/sqlite/sm"
"google.golang.org/protobuf/types/known/timestamppb"
)
func itemToConnect(item *models.Item) *itemv1.Item {
timestamp := timestamppb.New(item.Added)
return &itemv1.Item{
Id: &item.ID,
Name: item.Name,
Description: item.Description,
Price: item.Price,
Quantity: int32(item.Quantity),
Added: timestamp,
}
}
type Handler struct {
db *bob.DB
key []byte
}
func (h *Handler) GetItem(ctx context.Context, req *connect.Request[itemv1.GetItemRequest]) (*connect.Response[itemv1.GetItemResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
// Get item
item, err := models.Items.Query(
sqlite.WhereAnd(
models.SelectWhere.Items.ID.EQ(req.Msg.Id),
models.SelectWhere.Items.UserID.EQ(userid),
),
).One(ctx, h.db)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, connect.NewError(connect.CodeNotFound, err)
}
return nil, connect.NewError(connect.CodeInternal, err)
}
res := connect.NewResponse(&itemv1.GetItemResponse{
Item: itemToConnect(item),
})
return res, nil
}
func (h *Handler) GetItems(ctx context.Context, req *connect.Request[itemv1.GetItemsRequest]) (*connect.Response[itemv1.GetItemsResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
// Filters
query := models.Items.Query(models.SelectWhere.Items.UserID.EQ(userid))
countQuery := models.Items.Query(models.SelectWhere.Items.UserID.EQ(userid))
// Counted filters
if req.Msg.Start != nil {
query.Apply(models.SelectWhere.Items.Added.GTE(req.Msg.Start.AsTime()))
countQuery.Apply(models.SelectWhere.Items.Added.GTE(req.Msg.Start.AsTime()))
}
if req.Msg.End != nil {
query.Apply(models.SelectWhere.Items.Added.LTE(req.Msg.End.AsTime()))
countQuery.Apply(models.SelectWhere.Items.Added.LTE(req.Msg.End.AsTime()))
}
if req.Msg.Filter != nil && *req.Msg.Filter != "" {
query.Apply(models.SelectWhere.Items.Name.Like("%" + *req.Msg.Filter + "%"))
countQuery.Apply(models.SelectWhere.Items.Name.Like(*req.Msg.Filter))
}
// Uncounted filters
if req.Msg.Limit != nil {
query.Apply(sm.Limit(*req.Msg.Limit))
}
if req.Msg.Offset != nil {
query.Apply(sm.Offset(*req.Msg.Offset))
}
// Get items & count
items, err := query.All(ctx, h.db)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, connect.NewError(connect.CodeNotFound, err)
}
return nil, connect.NewError(connect.CodeInternal, err)
}
count, err := query.Count(ctx, h.db)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
// Convert to connect v1 items
resItems := []*itemv1.Item{}
for _, item := range items {
resItems = append(resItems, itemToConnect(item))
}
res := connect.NewResponse(&itemv1.GetItemsResponse{
Items: resItems,
Count: count,
})
return res, nil
}
func (h *Handler) CreateItem(ctx context.Context, req *connect.Request[itemv1.CreateItemRequest]) (*connect.Response[itemv1.CreateItemResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
item, err := models.Items.Insert(&models.ItemSetter{
Name: omit.From(req.Msg.Item.Name),
Description: omit.From(req.Msg.Item.Description),
Price: omit.From(req.Msg.Item.Price),
Quantity: omit.From(int64(req.Msg.Item.Quantity)),
Added: omit.From(time.Now()),
UserID: omit.From(userid),
}).One(ctx, h.db)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
res := connect.NewResponse(&itemv1.CreateItemResponse{
Item: itemToConnect(item),
})
return res, nil
}
func (h *Handler) UpdateItem(ctx context.Context, req *connect.Request[itemv1.UpdateItemRequest]) (*connect.Response[itemv1.UpdateItemResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
// Validate
if req.Msg.Item.Id == nil {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("id is required"))
}
// Update item
item, err := models.Items.Update(
// Set col
models.ItemSetter{
Name: omit.From(req.Msg.Item.Name),
Description: omit.From(req.Msg.Item.Description),
Price: omit.From(req.Msg.Item.Price),
Quantity: omit.From(int64(req.Msg.Item.Quantity)),
}.UpdateMod(),
// Where
sqlite.WhereAnd(
models.UpdateWhere.Items.ID.EQ(*req.Msg.Item.Id),
models.UpdateWhere.Items.UserID.EQ(userid),
),
).One(ctx, h.db)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
res := connect.NewResponse(&itemv1.UpdateItemResponse{
Item: itemToConnect(item),
})
return res, nil
}
func (h *Handler) DeleteItem(ctx context.Context, req *connect.Request[itemv1.DeleteItemRequest]) (*connect.Response[itemv1.DeleteItemResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
// Delete item
_, err := models.Items.Delete(
sqlite.WhereAnd(
models.DeleteWhere.Items.ID.EQ(req.Msg.Id),
models.DeleteWhere.Items.UserID.EQ(userid),
),
).Exec(ctx, h.db)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
res := connect.NewResponse(&itemv1.DeleteItemResponse{})
return res, nil
}
func NewHandler(db *bob.DB, key string) (string, http.Handler) {
interceptors := connect.WithInterceptors(interceptors.NewAuthInterceptor(key))
return itemv1connect.NewItemServiceHandler(
&Handler{
db: db,
key: []byte(key),
},
interceptors,
)
}

View File

@ -1,182 +0,0 @@
package handlers
import (
"context"
"errors"
"net/http"
"strconv"
"time"
"connectrpc.com/connect"
"github.com/golang-jwt/jwt/v5"
"github.com/spotdemo4/trevstack/server/internal/interceptors"
"github.com/spotdemo4/trevstack/server/internal/models"
userv1 "github.com/spotdemo4/trevstack/server/internal/services/user/v1"
"github.com/spotdemo4/trevstack/server/internal/services/user/v1/userv1connect"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type UserHandler struct {
db *gorm.DB
key []byte
}
func (h *UserHandler) GetUser(ctx context.Context, req *connect.Request[userv1.GetUserRequest]) (*connect.Response[userv1.GetUserResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
// Get user
user := models.User{}
if err := h.db.Preload("ProfilePicture").First(&user, "id = ?", userid).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
res := connect.NewResponse(&userv1.GetUserResponse{
User: user.ToConnectV1(),
})
return res, nil
}
func (h *UserHandler) UpdatePassword(ctx context.Context, req *connect.Request[userv1.UpdatePasswordRequest]) (*connect.Response[userv1.UpdatePasswordResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
// Get user
user := models.User{}
if err := h.db.First(&user, "id = ?", userid).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
// Validate
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Msg.OldPassword)); err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("invalid password"))
}
if req.Msg.NewPassword != req.Msg.ConfirmPassword {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("passwords do not match"))
}
// Hash password
hash, err := bcrypt.GenerateFromPassword([]byte(req.Msg.NewPassword), bcrypt.DefaultCost)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
// Update password
if err := h.db.Model(&user).Update("password", string(hash)).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
res := connect.NewResponse(&userv1.UpdatePasswordResponse{})
return res, nil
}
func (h *UserHandler) GetAPIKey(ctx context.Context, req *connect.Request[userv1.GetAPIKeyRequest]) (*connect.Response[userv1.GetAPIKeyResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
// Get user
user := models.User{}
if err := h.db.First(&user, "id = ?", userid).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
// Validate
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Msg.Password)); err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("invalid username or password"))
}
if req.Msg.Password != req.Msg.ConfirmPassword {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("passwords do not match"))
}
// Generate JWT
t := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
Issuer: "trevstack",
Subject: strconv.FormatUint(uint64(user.ID), 10),
IssuedAt: &jwt.NumericDate{
Time: time.Now(),
},
})
ss, err := t.SignedString(h.key)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
res := connect.NewResponse(&userv1.GetAPIKeyResponse{
Key: ss,
})
return res, nil
}
func (h *UserHandler) UpdateProfilePicture(ctx context.Context, req *connect.Request[userv1.UpdateProfilePictureRequest]) (*connect.Response[userv1.UpdateProfilePictureResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
// Validate file
fileType := http.DetectContentType(req.Msg.Data)
if fileType != "image/jpeg" && fileType != "image/png" {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("invalid file type"))
}
// Save bytes into file
file := models.File{
Name: req.Msg.FileName,
Data: req.Msg.Data,
UserID: uint(userid),
}
if err := h.db.Create(&file).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
// Get user info
user := models.User{}
if err := h.db.First(&user, "id = ?", userid).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
// Get old profile picture ID
var ppid *uint32
if user.ProfilePicture != nil {
ppid = &user.ProfilePicture.ID
}
// Update user profile picture
fid := uint(file.ID)
user.ProfilePictureID = &fid
user.ProfilePicture = &file
if err := h.db.Save(&user).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
// Delete old profile picture if exists
if ppid != nil {
if err := h.db.Delete(models.File{}, "id = ?", *ppid).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
}
res := connect.NewResponse(&userv1.UpdateProfilePictureResponse{
User: user.ToConnectV1(),
})
return res, nil
}
func NewUserHandler(db *gorm.DB, key string) (string, http.Handler) {
interceptors := connect.WithInterceptors(interceptors.NewAuthInterceptor(key))
return userv1connect.NewUserServiceHandler(
&UserHandler{
db: db,
key: []byte(key),
},
interceptors,
)
}

View File

@ -0,0 +1,234 @@
package user
import (
"context"
"database/sql"
"errors"
"net/http"
"strconv"
"time"
_ "crypto/sha256" // Crypto
"connectrpc.com/connect"
"github.com/aarondl/opt/omit"
"github.com/golang-jwt/jwt/v5"
"github.com/spotdemo4/trevstack/server/internal/interceptors"
"github.com/spotdemo4/trevstack/server/internal/models"
userv1 "github.com/spotdemo4/trevstack/server/internal/services/user/v1"
"github.com/spotdemo4/trevstack/server/internal/services/user/v1/userv1connect"
"github.com/stephenafamo/bob"
"golang.org/x/crypto/bcrypt"
)
type AuthHandler struct {
db *bob.DB
key []byte
}
func (h *AuthHandler) Login(ctx context.Context, req *connect.Request[userv1.LoginRequest]) (*connect.Response[userv1.LoginResponse], error) {
// Get user
user, err := models.Users.Query(
models.SelectWhere.Users.Username.EQ(req.Msg.Username),
).One(ctx, h.db)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, connect.NewError(connect.CodePermissionDenied, err)
}
return nil, connect.NewError(connect.CodeInternal, err)
}
// Check password
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Msg.Password)); err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("invalid username or password"))
}
// Generate JWT
t := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
Issuer: "trevstack",
Subject: strconv.FormatUint(uint64(user.ID), 10),
IssuedAt: &jwt.NumericDate{
Time: time.Now(),
},
ExpiresAt: &jwt.NumericDate{
Time: time.Now().Add(time.Hour * 24),
},
})
ss, err := t.SignedString(h.key)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
// Create cookie
cookie := http.Cookie{
Name: "token",
Value: ss,
Path: "/",
MaxAge: 86400,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
}
res := connect.NewResponse(&userv1.LoginResponse{
Token: ss,
})
res.Header().Set("Set-Cookie", cookie.String())
return res, nil
}
func (h *AuthHandler) SignUp(ctx context.Context, req *connect.Request[userv1.SignUpRequest]) (*connect.Response[userv1.SignUpResponse], error) {
// Get user
user, err := models.Users.Query(
models.SelectWhere.Users.Username.EQ(req.Msg.Username),
).One(ctx, h.db)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
return nil, connect.NewError(connect.CodeInternal, err)
}
}
if user != nil {
return nil, connect.NewError(connect.CodeAlreadyExists, errors.New("username already exists"))
}
// Check if confirmation passwords match
if req.Msg.Password != req.Msg.ConfirmPassword {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("passwords do not match"))
}
// Hash password
hash, err := bcrypt.GenerateFromPassword([]byte(req.Msg.Password), bcrypt.DefaultCost)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
// Create user
_, err = models.Users.Insert(&models.UserSetter{
Username: omit.From(req.Msg.Username),
Password: omit.From(string(hash)),
}).One(ctx, h.db)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
res := connect.NewResponse(&userv1.SignUpResponse{})
return res, nil
}
func (h *AuthHandler) Logout(_ context.Context, _ *connect.Request[userv1.LogoutRequest]) (*connect.Response[userv1.LogoutResponse], error) {
// Clear cookie
cookie := http.Cookie{
Name: "token",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
}
res := connect.NewResponse(&userv1.LogoutResponse{})
res.Header().Set("Set-Cookie", cookie.String())
return res, nil
}
// func (h *AuthHandler) GetPasskeyIDs(_ context.Context, req *connect.Request[userv1.GetPasskeyIDsRequest]) (*connect.Response[userv1.GetPasskeyIDsResponse], error) {
// // Get user
// user := models.User{}
// if err := h.db.Preload("Passkeys").First(&user, "username = ?", req.Msg.Username).Error; err != nil {
// return nil, connect.NewError(connect.CodeNotFound, err)
// }
// // Get IDs
// ids := []string{}
// for _, passkey := range user.Passkeys {
// ids = append(ids, passkey.ID)
// }
// return connect.NewResponse(&userv1.GetPasskeyIDsResponse{
// PasskeyIds: ids,
// }), nil
// }
// func (h *AuthHandler) PasskeyLogin(_ context.Context, req *connect.Request[userv1.PasskeyLoginRequest]) (*connect.Response[userv1.PasskeyLoginResponse], error) {
// // Get passkey
// passkey := models.Passkey{}
// if err := h.db.First(&passkey, "id = ?", req.Msg.Id).Error; err != nil {
// return nil, connect.NewError(connect.CodeNotFound, err)
// }
// // create a verifier from a trusted private key
// var verifier cose.Verifier
// var err error
// switch req.Msg.Algorithm {
// case -7:
// verifier, err = cose.NewVerifier(cose.AlgorithmES256, passkey.PublicKey)
// case -257:
// verifier, err = cose.NewVerifier(cose.AlgorithmRS256, passkey.PublicKey)
// default:
// return nil, connect.NewError(connect.CodeInternal, errors.New("decode algorithm not implemented"))
// }
// if err != nil {
// return nil, connect.NewError(connect.CodeInternal, err)
// }
// // create a sign message from a raw signature payload
// var msg cose.Sign1Message
// if err = msg.UnmarshalCBOR(req.Msg.Signature); err != nil {
// return nil, connect.NewError(connect.CodeInternal, err)
// }
// // Validate passkey
// err = msg.Verify(nil, verifier)
// if err != nil {
// return nil, connect.NewError(connect.CodeUnauthenticated, err)
// }
// // Generate JWT
// t := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
// Issuer: "trevstack",
// Subject: strconv.FormatUint(uint64(passkey.UserID), 10),
// IssuedAt: &jwt.NumericDate{
// Time: time.Now(),
// },
// ExpiresAt: &jwt.NumericDate{
// Time: time.Now().Add(time.Hour * 24),
// },
// })
// ss, err := t.SignedString(h.key)
// if err != nil {
// return nil, connect.NewError(connect.CodeInternal, err)
// }
// // Create cookie
// cookie := http.Cookie{
// Name: "token",
// Value: ss,
// Path: "/",
// MaxAge: 86400,
// HttpOnly: true,
// Secure: true,
// SameSite: http.SameSiteStrictMode,
// }
// res := connect.NewResponse(&userv1.PasskeyLoginResponse{
// Token: ss,
// })
// res.Header().Set("Set-Cookie", cookie.String())
// return res, nil
// }
func NewAuthHandler(db *bob.DB, key string) (string, http.Handler) {
interceptors := connect.WithInterceptors(interceptors.NewRateLimitInterceptor(key))
return userv1connect.NewAuthServiceHandler(
&AuthHandler{
db: db,
key: []byte(key),
},
interceptors,
)
}

View File

@ -0,0 +1,285 @@
package user
import (
"context"
"database/sql"
"errors"
"net/http"
"strconv"
"time"
"connectrpc.com/connect"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/golang-jwt/jwt/v5"
"github.com/spotdemo4/trevstack/server/internal/interceptors"
"github.com/spotdemo4/trevstack/server/internal/models"
userv1 "github.com/spotdemo4/trevstack/server/internal/services/user/v1"
"github.com/spotdemo4/trevstack/server/internal/services/user/v1/userv1connect"
"github.com/stephenafamo/bob"
"golang.org/x/crypto/bcrypt"
)
func userToConnect(item *models.User) *userv1.User {
return &userv1.User{
Id: item.ID,
Username: item.Username,
ProfilePictureId: item.ProfilePictureID.Ptr(),
}
}
type Handler struct {
db *bob.DB
key []byte
}
func (h *Handler) GetUser(ctx context.Context, _ *connect.Request[userv1.GetUserRequest]) (*connect.Response[userv1.GetUserResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
// Get user
user, err := models.Users.Query(
models.SelectWhere.Users.ID.EQ(userid),
).One(ctx, h.db)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, connect.NewError(connect.CodeNotFound, err)
}
return nil, connect.NewError(connect.CodeInternal, err)
}
res := connect.NewResponse(&userv1.GetUserResponse{
User: userToConnect(user),
})
return res, nil
}
func (h *Handler) UpdatePassword(ctx context.Context, req *connect.Request[userv1.UpdatePasswordRequest]) (*connect.Response[userv1.UpdatePasswordResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
// Get user
user, err := models.Users.Query(
models.SelectWhere.Users.ID.EQ(userid),
).One(ctx, h.db)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, connect.NewError(connect.CodeNotFound, err)
}
return nil, connect.NewError(connect.CodeInternal, err)
}
// Validate
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Msg.OldPassword)); err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("invalid password"))
}
if req.Msg.NewPassword != req.Msg.ConfirmPassword {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("passwords do not match"))
}
// Hash password
hash, err := bcrypt.GenerateFromPassword([]byte(req.Msg.NewPassword), bcrypt.DefaultCost)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
// Update password
err = user.Update(ctx, h.db, &models.UserSetter{
Password: omit.From(string(hash)),
})
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
res := connect.NewResponse(&userv1.UpdatePasswordResponse{})
return res, nil
}
func (h *Handler) GetAPIKey(ctx context.Context, req *connect.Request[userv1.GetAPIKeyRequest]) (*connect.Response[userv1.GetAPIKeyResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
// Get user
user, err := models.Users.Query(
models.SelectWhere.Users.ID.EQ(userid),
).One(ctx, h.db)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, connect.NewError(connect.CodeNotFound, err)
}
return nil, connect.NewError(connect.CodeInternal, err)
}
// Validate
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Msg.Password)); err != nil {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("invalid username or password"))
}
if req.Msg.Password != req.Msg.ConfirmPassword {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("passwords do not match"))
}
// Generate JWT
t := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
Issuer: "trevstack",
Subject: strconv.FormatInt(user.ID, 10),
IssuedAt: &jwt.NumericDate{
Time: time.Now(),
},
})
ss, err := t.SignedString(h.key)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
res := connect.NewResponse(&userv1.GetAPIKeyResponse{
Key: ss,
})
return res, nil
}
func (h *Handler) UpdateProfilePicture(ctx context.Context, req *connect.Request[userv1.UpdateProfilePictureRequest]) (*connect.Response[userv1.UpdateProfilePictureResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
// Validate file
fileType := http.DetectContentType(req.Msg.Data)
if fileType != "image/jpeg" && fileType != "image/png" {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("invalid file type"))
}
// Save bytes into file
file, err := models.Files.Insert(&models.FileSetter{
Name: omit.From(req.Msg.FileName),
Data: omit.From(req.Msg.Data),
UserID: omit.From(userid),
}).One(ctx, h.db)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
// Get user
user, err := models.Users.Query(
models.SelectWhere.Users.ID.EQ(userid),
).One(ctx, h.db)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, connect.NewError(connect.CodeNotFound, err)
}
return nil, connect.NewError(connect.CodeInternal, err)
}
// Get old profile picture ID
var ppid *int64
if user.ProfilePictureID.Ptr() != nil {
ppid = user.ProfilePictureID.Ptr()
}
// Update user profile picture
err = user.Update(ctx, h.db, &models.UserSetter{
ProfilePictureID: omitnull.From(file.ID),
})
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
// Delete old profile picture if exists
if ppid != nil {
_, err = models.Files.Delete(
models.DeleteWhere.Files.ID.EQ(*ppid),
).Exec(ctx, h.db)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
}
res := connect.NewResponse(&userv1.UpdateProfilePictureResponse{
User: userToConnect(user),
})
return res, nil
}
// func (h *Handler) BeginPasskeyRegistration(ctx context.Context, req *connect.Request[userv1.BeginPasskeyRegistrationRequest]) (*connect.Response[userv1.BeginPasskeyRegistrationResponse], error) {
// // Get user ID from context
// userid, ok := interceptors.GetUserContext(ctx)
// if !ok {
// return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("user not authenticated"))
// }
// // Get user
// user := models.User{}
// if err := h.db.First(&user, "id = ?", userid).Error; err != nil {
// return nil, connect.NewError(connect.CodeInternal, err)
// }
// return connect.NewResponse(&userv1.BeginPasskeyRegistrationResponse{}), nil
// }
// func (h *Handler) FinishPasskeyRegistration(ctx context.Context, req *connect.Request[userv1.FinishPasskeyRegistrationRequest]) (*connect.Response[userv1.FinishPasskeyRegistrationResponse], error) {
// // Get user ID from context
// userid, ok := interceptors.GetUserContext(ctx)
// if !ok {
// return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("user not authenticated"))
// }
// // Get user
// user := models.User{}
// if err := h.db.First(&user, "id = ?", userid).Error; err != nil {
// return nil, connect.NewError(connect.CodeInternal, err)
// }
// return connect.NewResponse(&userv1.FinishPasskeyRegistrationResponse{}), nil
// }
// func BeginRegistration(ctx context.Context) error {
// userid, ok := interceptors.GetUserContext(ctx)
// if !ok {
// return nil
// }
// wconfig := &webauthn.Config{
// RPDisplayName: "Go Webauthn", // Display Name for your site
// RPID: "go-webauthn.local", // Generally the FQDN for your site
// RPOrigins: []string{"https://login.go-webauthn.local"}, // The origin URLs allowed for WebAuthn requests
// }
// webAuthn, err := webauthn.New(wconfig)
// if err != nil {
// return nil
// }
// var user webauthn.User
// user.WebAuthnCredentials()
// var cred webauthn.Credential
// cred.Verify()
// var test metadata.Provider
// test.
// options, session, err := webAuthn.BeginRegistration(user)
// return nil
// }
func NewHandler(db *bob.DB, key string) (string, http.Handler) {
interceptors := connect.WithInterceptors(interceptors.NewAuthInterceptor(key))
return userv1connect.NewUserServiceHandler(
&Handler{
db: db,
key: []byte(key),
},
interceptors,
)
}

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"strings" "strings"
@ -49,10 +50,7 @@ func WithAuthRedirect(next http.Handler, key string) http.Handler {
} }
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
case "_app": case "_app", "favicon.png", "icon.png":
next.ServeHTTP(w, r)
case "favicon.png":
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
default: default:
@ -62,22 +60,23 @@ func WithAuthRedirect(next http.Handler, key string) http.Handler {
} }
// Redirect if not authenticated // Redirect if not authenticated
http.Redirect(w, r, "/auth", http.StatusFound) pathRedir := url.QueryEscape(r.URL.Path)
http.Redirect(w, r, fmt.Sprintf("/auth?redir=%s", pathRedir), http.StatusFound)
} }
}) })
} }
type authInterceptor struct { type AuthInterceptor struct {
key string key string
} }
func NewAuthInterceptor(key string) *authInterceptor { func NewAuthInterceptor(key string) *AuthInterceptor {
return &authInterceptor{ return &AuthInterceptor{
key: key, key: key,
} }
} }
func (i *authInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { func (i *AuthInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc {
// Same as previous UnaryInterceptorFunc. // Same as previous UnaryInterceptorFunc.
return connect.UnaryFunc(func( return connect.UnaryFunc(func(
ctx context.Context, ctx context.Context,
@ -121,7 +120,7 @@ func (i *authInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc {
}) })
} }
func (*authInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { func (*AuthInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc {
return connect.StreamingClientFunc(func( return connect.StreamingClientFunc(func(
ctx context.Context, ctx context.Context,
spec connect.Spec, spec connect.Spec,
@ -130,7 +129,7 @@ func (*authInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) co
}) })
} }
func (i *authInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { func (i *AuthInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc {
return connect.StreamingHandlerFunc(func( return connect.StreamingHandlerFunc(func(
ctx context.Context, ctx context.Context,
conn connect.StreamingHandlerConn, conn connect.StreamingHandlerConn,
@ -218,7 +217,7 @@ func validateToken(tokenString string, key string) (subject string, err error) {
// key is an unexported type for keys defined in this package. // key is an unexported type for keys defined in this package.
// This prevents collisions with keys defined in other packages. // This prevents collisions with keys defined in other packages.
type key int type key int64
// userKey is the key for user.User values in Contexts. It is // userKey is the key for user.User values in Contexts. It is
// unexported; clients use user.NewContext and user.FromContext // unexported; clients use user.NewContext and user.FromContext
@ -232,11 +231,11 @@ func newUserContext(ctx context.Context, subject string) (context.Context, error
return nil, err return nil, err
} }
return context.WithValue(ctx, userKey, id), nil return context.WithValue(ctx, userKey, int64(id)), nil
} }
// getUserContext returns the User value stored in ctx, if any. // getUserContext returns the User value stored in ctx, if any.
func GetUserContext(ctx context.Context) (int, bool) { func GetUserContext(ctx context.Context) (int64, bool) {
u, ok := ctx.Value(userKey).(int) u, ok := ctx.Value(userKey).(int64)
return u, ok return u, ok
} }

View File

@ -0,0 +1,19 @@
package interceptors
import (
"net/http"
connectcors "connectrpc.com/cors"
"github.com/rs/cors"
)
// WithCORS adds CORS support to a Connect HTTP handler.
func WithCORS(pattern string, h http.Handler) (string, http.Handler) {
middleware := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: connectcors.AllowedMethods(),
AllowedHeaders: connectcors.AllowedHeaders(),
ExposedHeaders: connectcors.ExposedHeaders(),
})
return pattern, middleware.Handler(h)
}

View File

@ -15,14 +15,14 @@ type visitor struct {
lastSeen time.Time lastSeen time.Time
} }
type ratelimitInterceptor struct { type RatelimitInterceptor struct {
key string key string
visitors map[string]*visitor visitors map[string]*visitor
mu sync.Mutex mu sync.Mutex
} }
func NewRateLimitInterceptor(key string) *ratelimitInterceptor { func NewRateLimitInterceptor(key string) *RatelimitInterceptor {
rl := &ratelimitInterceptor{ rl := &RatelimitInterceptor{
key: key, key: key,
visitors: make(map[string]*visitor), visitors: make(map[string]*visitor),
mu: sync.Mutex{}, mu: sync.Mutex{},
@ -33,7 +33,7 @@ func NewRateLimitInterceptor(key string) *ratelimitInterceptor {
return rl return rl
} }
func (i *ratelimitInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { func (i *RatelimitInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc {
// Same as previous UnaryInterceptorFunc. // Same as previous UnaryInterceptorFunc.
return connect.UnaryFunc(func( return connect.UnaryFunc(func(
ctx context.Context, ctx context.Context,
@ -46,7 +46,7 @@ func (i *ratelimitInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFu
// Get user agent // Get user agent
limiter := i.getVisitor(req.Header().Get("User-Agent")) limiter := i.getVisitor(req.Header().Get("User-Agent"))
if limiter.Allow() == false { if !limiter.Allow() {
return nil, connect.NewError(connect.CodeResourceExhausted, errors.New("rate limit exceeded")) return nil, connect.NewError(connect.CodeResourceExhausted, errors.New("rate limit exceeded"))
} }
@ -54,7 +54,7 @@ func (i *ratelimitInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFu
}) })
} }
func (*ratelimitInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { func (*RatelimitInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc {
return connect.StreamingClientFunc(func( return connect.StreamingClientFunc(func(
ctx context.Context, ctx context.Context,
spec connect.Spec, spec connect.Spec,
@ -63,14 +63,14 @@ func (*ratelimitInterceptor) WrapStreamingClient(next connect.StreamingClientFun
}) })
} }
func (i *ratelimitInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { func (i *RatelimitInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc {
return connect.StreamingHandlerFunc(func( return connect.StreamingHandlerFunc(func(
ctx context.Context, ctx context.Context,
conn connect.StreamingHandlerConn, conn connect.StreamingHandlerConn,
) error { ) error {
// Get user agent // Get user agent
limiter := i.getVisitor(conn.RequestHeader().Get("User-Agent")) limiter := i.getVisitor(conn.RequestHeader().Get("User-Agent"))
if limiter.Allow() == false { if !limiter.Allow() {
return connect.NewError(connect.CodeResourceExhausted, errors.New("rate limit exceeded")) return connect.NewError(connect.CodeResourceExhausted, errors.New("rate limit exceeded"))
} }
@ -78,7 +78,7 @@ func (i *ratelimitInterceptor) WrapStreamingHandler(next connect.StreamingHandle
}) })
} }
func (i *ratelimitInterceptor) getVisitor(userAgent string) *rate.Limiter { func (i *RatelimitInterceptor) getVisitor(userAgent string) *rate.Limiter {
i.mu.Lock() i.mu.Lock()
defer i.mu.Unlock() defer i.mu.Unlock()
@ -97,7 +97,7 @@ func (i *ratelimitInterceptor) getVisitor(userAgent string) *rate.Limiter {
// Every minute check the map for visitors that haven't been seen for // Every minute check the map for visitors that haven't been seen for
// more than 3 minutes and delete the entries. // more than 3 minutes and delete the entries.
func (i *ratelimitInterceptor) cleanupVisitors() { func (i *RatelimitInterceptor) cleanupVisitors() {
for { for {
time.Sleep(time.Minute) time.Sleep(time.Minute)

View File

@ -0,0 +1,170 @@
// Code generated by BobGen sql (devel). DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package models
import (
"hash/maphash"
"strings"
"github.com/stephenafamo/bob"
"github.com/stephenafamo/bob/clause"
"github.com/stephenafamo/bob/dialect/sqlite"
"github.com/stephenafamo/bob/dialect/sqlite/dialect"
sqliteDriver "modernc.org/sqlite"
)
var TableNames = struct {
Files string
Items string
SchemaMigrations string
Users string
}{
Files: "file",
Items: "item",
SchemaMigrations: "schema_migrations",
Users: "user",
}
var ColumnNames = struct {
Files fileColumnNames
Items itemColumnNames
SchemaMigrations schemaMigrationColumnNames
Users userColumnNames
}{
Files: fileColumnNames{
ID: "id",
Name: "name",
Data: "data",
UserID: "user_id",
},
Items: itemColumnNames{
ID: "id",
Name: "name",
Added: "added",
Description: "description",
Price: "price",
Quantity: "quantity",
UserID: "user_id",
},
SchemaMigrations: schemaMigrationColumnNames{
Version: "version",
},
Users: userColumnNames{
ID: "id",
Username: "username",
Password: "password",
ProfilePictureID: "profile_picture_id",
},
}
var (
SelectWhere = Where[*dialect.SelectQuery]()
InsertWhere = Where[*dialect.InsertQuery]()
UpdateWhere = Where[*dialect.UpdateQuery]()
DeleteWhere = Where[*dialect.DeleteQuery]()
)
func Where[Q sqlite.Filterable]() struct {
Files fileWhere[Q]
Items itemWhere[Q]
SchemaMigrations schemaMigrationWhere[Q]
Users userWhere[Q]
} {
return struct {
Files fileWhere[Q]
Items itemWhere[Q]
SchemaMigrations schemaMigrationWhere[Q]
Users userWhere[Q]
}{
Files: buildFileWhere[Q](FileColumns),
Items: buildItemWhere[Q](ItemColumns),
SchemaMigrations: buildSchemaMigrationWhere[Q](SchemaMigrationColumns),
Users: buildUserWhere[Q](UserColumns),
}
}
var (
SelectJoins = getJoins[*dialect.SelectQuery]
UpdateJoins = getJoins[*dialect.UpdateQuery]
)
type joinSet[Q interface{ aliasedAs(string) Q }] struct {
InnerJoin Q
LeftJoin Q
RightJoin Q
}
func (j joinSet[Q]) AliasedAs(alias string) joinSet[Q] {
return joinSet[Q]{
InnerJoin: j.InnerJoin.aliasedAs(alias),
LeftJoin: j.LeftJoin.aliasedAs(alias),
RightJoin: j.RightJoin.aliasedAs(alias),
}
}
type joins[Q dialect.Joinable] struct {
Files joinSet[fileJoins[Q]]
Items joinSet[itemJoins[Q]]
Users joinSet[userJoins[Q]]
}
func buildJoinSet[Q interface{ aliasedAs(string) Q }, C any, F func(C, string) Q](c C, f F) joinSet[Q] {
return joinSet[Q]{
InnerJoin: f(c, clause.InnerJoin),
LeftJoin: f(c, clause.LeftJoin),
RightJoin: f(c, clause.RightJoin),
}
}
func getJoins[Q dialect.Joinable]() joins[Q] {
return joins[Q]{
Files: buildJoinSet[fileJoins[Q]](FileColumns, buildFileJoins),
Items: buildJoinSet[itemJoins[Q]](ItemColumns, buildItemJoins),
Users: buildJoinSet[userJoins[Q]](UserColumns, buildUserJoins),
}
}
type modAs[Q any, C interface{ AliasedAs(string) C }] struct {
c C
f func(C) bob.Mod[Q]
}
func (m modAs[Q, C]) Apply(q Q) {
m.f(m.c).Apply(q)
}
func (m modAs[Q, C]) AliasedAs(alias string) bob.Mod[Q] {
m.c = m.c.AliasedAs(alias)
return m
}
func randInt() int64 {
out := int64(new(maphash.Hash).Sum64())
if out < 0 {
return -out % 10000
}
return out % 10000
}
// ErrUniqueConstraint captures all unique constraint errors by explicitly leaving `s` empty.
var ErrUniqueConstraint = &UniqueConstraintError{s: ""}
type UniqueConstraintError struct {
// s is a string uniquely identifying the constraint in the raw error message returned from the database.
s string
}
func (e *UniqueConstraintError) Error() string {
return e.s
}
func (e *UniqueConstraintError) Is(target error) bool {
err, ok := target.(*sqliteDriver.Error)
if !ok {
return false
}
return err.Code() == 2067 && strings.Contains(err.Error(), e.s)
}

View File

@ -0,0 +1,18 @@
// Code generated by BobGen sql (devel). DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package models
import "github.com/stephenafamo/bob"
// Make sure the type File runs hooks after queries
var _ bob.HookableType = &File{}
// Make sure the type Item runs hooks after queries
var _ bob.HookableType = &Item{}
// Make sure the type SchemaMigration runs hooks after queries
var _ bob.HookableType = &SchemaMigration{}
// Make sure the type User runs hooks after queries
var _ bob.HookableType = &User{}

View File

@ -0,0 +1,37 @@
// Code generated by BobGen sql (devel). DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package factory
import (
"context"
models "github.com/spotdemo4/trevstack/server/internal/models"
)
type contextKey string
var (
fileCtx = newContextual[*models.File]("file")
itemCtx = newContextual[*models.Item]("item")
schemaMigrationCtx = newContextual[*models.SchemaMigration]("schemaMigration")
userCtx = newContextual[*models.User]("user")
)
// Contextual is a convienience wrapper around context.WithValue and context.Value
type contextual[V any] struct {
key contextKey
}
func newContextual[V any](key string) contextual[V] {
return contextual[V]{key: contextKey(key)}
}
func (k contextual[V]) WithValue(ctx context.Context, val V) context.Context {
return context.WithValue(ctx, k.key, val)
}
func (k contextual[V]) Value(ctx context.Context) (V, bool) {
v, ok := ctx.Value(k.key).(V)
return v, ok
}

View File

@ -0,0 +1,95 @@
// Code generated by BobGen sql (devel). DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package factory
type Factory struct {
baseFileMods FileModSlice
baseItemMods ItemModSlice
baseSchemaMigrationMods SchemaMigrationModSlice
baseUserMods UserModSlice
}
func New() *Factory {
return &Factory{}
}
func (f *Factory) NewFile(mods ...FileMod) *FileTemplate {
o := &FileTemplate{f: f}
if f != nil {
f.baseFileMods.Apply(o)
}
FileModSlice(mods).Apply(o)
return o
}
func (f *Factory) NewItem(mods ...ItemMod) *ItemTemplate {
o := &ItemTemplate{f: f}
if f != nil {
f.baseItemMods.Apply(o)
}
ItemModSlice(mods).Apply(o)
return o
}
func (f *Factory) NewSchemaMigration(mods ...SchemaMigrationMod) *SchemaMigrationTemplate {
o := &SchemaMigrationTemplate{f: f}
if f != nil {
f.baseSchemaMigrationMods.Apply(o)
}
SchemaMigrationModSlice(mods).Apply(o)
return o
}
func (f *Factory) NewUser(mods ...UserMod) *UserTemplate {
o := &UserTemplate{f: f}
if f != nil {
f.baseUserMods.Apply(o)
}
UserModSlice(mods).Apply(o)
return o
}
func (f *Factory) ClearBaseFileMods() {
f.baseFileMods = nil
}
func (f *Factory) AddBaseFileMod(mods ...FileMod) {
f.baseFileMods = append(f.baseFileMods, mods...)
}
func (f *Factory) ClearBaseItemMods() {
f.baseItemMods = nil
}
func (f *Factory) AddBaseItemMod(mods ...ItemMod) {
f.baseItemMods = append(f.baseItemMods, mods...)
}
func (f *Factory) ClearBaseSchemaMigrationMods() {
f.baseSchemaMigrationMods = nil
}
func (f *Factory) AddBaseSchemaMigrationMod(mods ...SchemaMigrationMod) {
f.baseSchemaMigrationMods = append(f.baseSchemaMigrationMods, mods...)
}
func (f *Factory) ClearBaseUserMods() {
f.baseUserMods = nil
}
func (f *Factory) AddBaseUserMod(mods ...UserMod) {
f.baseUserMods = append(f.baseUserMods, mods...)
}

View File

@ -0,0 +1,56 @@
// Code generated by BobGen sql (devel). DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package factory
import (
"strings"
"time"
"github.com/jaswdr/faker/v2"
)
var defaultFaker = faker.New()
func random___byte(f *faker.Faker) []byte {
if f == nil {
f = &defaultFaker
}
return []byte(random_string(f))
}
func random_float32(f *faker.Faker) float32 {
if f == nil {
f = &defaultFaker
}
return f.Float32(10, -1_000_000, 1_000_000)
}
func random_int64(f *faker.Faker) int64 {
if f == nil {
f = &defaultFaker
}
return f.Int64()
}
func random_string(f *faker.Faker) string {
if f == nil {
f = &defaultFaker
}
return strings.Join(f.Lorem().Words(f.IntBetween(1, 5)), " ")
}
func random_time_Time(f *faker.Faker) time.Time {
if f == nil {
f = &defaultFaker
}
year := time.Hour * 24 * 365
min := time.Now().Add(-year)
max := time.Now().Add(year)
return f.Time().TimeBetween(min, max)
}

View File

@ -0,0 +1,64 @@
// Code generated by BobGen sql (devel). DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package factory
import (
"bytes"
"testing"
)
func TestRandom_int64(t *testing.T) {
t.Parallel()
val1 := random_int64(nil)
val2 := random_int64(nil)
if val1 == val2 {
t.Fatalf("random_int64() returned the same value twice: %v", val1)
}
}
func TestRandom_string(t *testing.T) {
t.Parallel()
val1 := random_string(nil)
val2 := random_string(nil)
if val1 == val2 {
t.Fatalf("random_string() returned the same value twice: %v", val1)
}
}
func TestRandom___byte(t *testing.T) {
t.Parallel()
val1 := random___byte(nil)
val2 := random___byte(nil)
if bytes.Equal(val1, val2) {
t.Fatalf("random___byte() returned the same value twice: %v", val1)
}
}
func TestRandom_time_Time(t *testing.T) {
t.Parallel()
val1 := random_time_Time(nil)
val2 := random_time_Time(nil)
if val1.Equal(val2) {
t.Fatalf("random_time_Time() returned the same value twice: %v", val1)
}
}
func TestRandom_float32(t *testing.T) {
t.Parallel()
val1 := random_float32(nil)
val2 := random_float32(nil)
if val1 == val2 {
t.Fatalf("random_float32() returned the same value twice: %v", val1)
}
}

View File

@ -0,0 +1,527 @@
// Code generated by BobGen sql (devel). DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package factory
import (
"context"
"testing"
"github.com/aarondl/opt/null"
"github.com/aarondl/opt/omit"
"github.com/jaswdr/faker/v2"
models "github.com/spotdemo4/trevstack/server/internal/models"
"github.com/stephenafamo/bob"
)
type FileMod interface {
Apply(*FileTemplate)
}
type FileModFunc func(*FileTemplate)
func (f FileModFunc) Apply(n *FileTemplate) {
f(n)
}
type FileModSlice []FileMod
func (mods FileModSlice) Apply(n *FileTemplate) {
for _, f := range mods {
f.Apply(n)
}
}
// FileTemplate is an object representing the database table.
// all columns are optional and should be set by mods
type FileTemplate struct {
ID func() int64
Name func() string
Data func() []byte
UserID func() int64
r fileR
f *Factory
}
type fileR struct {
User *fileRUserR
ProfilePictureUsers []*fileRProfilePictureUsersR
}
type fileRUserR struct {
o *UserTemplate
}
type fileRProfilePictureUsersR struct {
number int
o *UserTemplate
}
// Apply mods to the FileTemplate
func (o *FileTemplate) Apply(mods ...FileMod) {
for _, mod := range mods {
mod.Apply(o)
}
}
// toModel returns an *models.File
// this does nothing with the relationship templates
func (o FileTemplate) toModel() *models.File {
m := &models.File{}
if o.ID != nil {
m.ID = o.ID()
}
if o.Name != nil {
m.Name = o.Name()
}
if o.Data != nil {
m.Data = o.Data()
}
if o.UserID != nil {
m.UserID = o.UserID()
}
return m
}
// toModels returns an models.FileSlice
// this does nothing with the relationship templates
func (o FileTemplate) toModels(number int) models.FileSlice {
m := make(models.FileSlice, number)
for i := range m {
m[i] = o.toModel()
}
return m
}
// setModelRels creates and sets the relationships on *models.File
// according to the relationships in the template. Nothing is inserted into the db
func (t FileTemplate) setModelRels(o *models.File) {
if t.r.User != nil {
rel := t.r.User.o.toModel()
rel.R.Files = append(rel.R.Files, o)
o.UserID = rel.ID
o.R.User = rel
}
if t.r.ProfilePictureUsers != nil {
rel := models.UserSlice{}
for _, r := range t.r.ProfilePictureUsers {
related := r.o.toModels(r.number)
for _, rel := range related {
rel.ProfilePictureID = null.From(o.ID)
rel.R.ProfilePictureFile = o
}
rel = append(rel, related...)
}
o.R.ProfilePictureUsers = rel
}
}
// BuildSetter returns an *models.FileSetter
// this does nothing with the relationship templates
func (o FileTemplate) BuildSetter() *models.FileSetter {
m := &models.FileSetter{}
if o.ID != nil {
m.ID = omit.From(o.ID())
}
if o.Name != nil {
m.Name = omit.From(o.Name())
}
if o.Data != nil {
m.Data = omit.From(o.Data())
}
if o.UserID != nil {
m.UserID = omit.From(o.UserID())
}
return m
}
// BuildManySetter returns an []*models.FileSetter
// this does nothing with the relationship templates
func (o FileTemplate) BuildManySetter(number int) []*models.FileSetter {
m := make([]*models.FileSetter, number)
for i := range m {
m[i] = o.BuildSetter()
}
return m
}
// Build returns an *models.File
// Related objects are also created and placed in the .R field
// NOTE: Objects are not inserted into the database. Use FileTemplate.Create
func (o FileTemplate) Build() *models.File {
m := o.toModel()
o.setModelRels(m)
return m
}
// BuildMany returns an models.FileSlice
// Related objects are also created and placed in the .R field
// NOTE: Objects are not inserted into the database. Use FileTemplate.CreateMany
func (o FileTemplate) BuildMany(number int) models.FileSlice {
m := make(models.FileSlice, number)
for i := range m {
m[i] = o.Build()
}
return m
}
func ensureCreatableFile(m *models.FileSetter) {
if m.Name.IsUnset() {
m.Name = omit.From(random_string(nil))
}
if m.Data.IsUnset() {
m.Data = omit.From(random___byte(nil))
}
if m.UserID.IsUnset() {
m.UserID = omit.From(random_int64(nil))
}
}
// insertOptRels creates and inserts any optional the relationships on *models.File
// according to the relationships in the template.
// any required relationship should have already exist on the model
func (o *FileTemplate) insertOptRels(ctx context.Context, exec bob.Executor, m *models.File) (context.Context, error) {
var err error
if o.r.ProfilePictureUsers != nil {
for _, r := range o.r.ProfilePictureUsers {
var rel1 models.UserSlice
ctx, rel1, err = r.o.createMany(ctx, exec, r.number)
if err != nil {
return ctx, err
}
err = m.AttachProfilePictureUsers(ctx, exec, rel1...)
if err != nil {
return ctx, err
}
}
}
return ctx, err
}
// Create builds a file and inserts it into the database
// Relations objects are also inserted and placed in the .R field
func (o *FileTemplate) Create(ctx context.Context, exec bob.Executor) (*models.File, error) {
_, m, err := o.create(ctx, exec)
return m, err
}
// MustCreate builds a file and inserts it into the database
// Relations objects are also inserted and placed in the .R field
// panics if an error occurs
func (o *FileTemplate) MustCreate(ctx context.Context, exec bob.Executor) *models.File {
_, m, err := o.create(ctx, exec)
if err != nil {
panic(err)
}
return m
}
// CreateOrFail builds a file and inserts it into the database
// Relations objects are also inserted and placed in the .R field
// It calls `tb.Fatal(err)` on the test/benchmark if an error occurs
func (o *FileTemplate) CreateOrFail(ctx context.Context, tb testing.TB, exec bob.Executor) *models.File {
tb.Helper()
_, m, err := o.create(ctx, exec)
if err != nil {
tb.Fatal(err)
return nil
}
return m
}
// create builds a file and inserts it into the database
// Relations objects are also inserted and placed in the .R field
// this returns a context that includes the newly inserted model
func (o *FileTemplate) create(ctx context.Context, exec bob.Executor) (context.Context, *models.File, error) {
var err error
opt := o.BuildSetter()
ensureCreatableFile(opt)
var rel0 *models.User
if o.r.User == nil {
var ok bool
rel0, ok = userCtx.Value(ctx)
if !ok {
FileMods.WithNewUser().Apply(o)
}
}
if o.r.User != nil {
ctx, rel0, err = o.r.User.o.create(ctx, exec)
if err != nil {
return ctx, nil, err
}
}
opt.UserID = omit.From(rel0.ID)
m, err := models.Files.Insert(opt).One(ctx, exec)
if err != nil {
return ctx, nil, err
}
ctx = fileCtx.WithValue(ctx, m)
m.R.User = rel0
ctx, err = o.insertOptRels(ctx, exec, m)
return ctx, m, err
}
// CreateMany builds multiple files and inserts them into the database
// Relations objects are also inserted and placed in the .R field
func (o FileTemplate) CreateMany(ctx context.Context, exec bob.Executor, number int) (models.FileSlice, error) {
_, m, err := o.createMany(ctx, exec, number)
return m, err
}
// MustCreateMany builds multiple files and inserts them into the database
// Relations objects are also inserted and placed in the .R field
// panics if an error occurs
func (o FileTemplate) MustCreateMany(ctx context.Context, exec bob.Executor, number int) models.FileSlice {
_, m, err := o.createMany(ctx, exec, number)
if err != nil {
panic(err)
}
return m
}
// CreateManyOrFail builds multiple files and inserts them into the database
// Relations objects are also inserted and placed in the .R field
// It calls `tb.Fatal(err)` on the test/benchmark if an error occurs
func (o FileTemplate) CreateManyOrFail(ctx context.Context, tb testing.TB, exec bob.Executor, number int) models.FileSlice {
tb.Helper()
_, m, err := o.createMany(ctx, exec, number)
if err != nil {
tb.Fatal(err)
return nil
}
return m
}
// createMany builds multiple files and inserts them into the database
// Relations objects are also inserted and placed in the .R field
// this returns a context that includes the newly inserted models
func (o FileTemplate) createMany(ctx context.Context, exec bob.Executor, number int) (context.Context, models.FileSlice, error) {
var err error
m := make(models.FileSlice, number)
for i := range m {
ctx, m[i], err = o.create(ctx, exec)
if err != nil {
return ctx, nil, err
}
}
return ctx, m, nil
}
// File has methods that act as mods for the FileTemplate
var FileMods fileMods
type fileMods struct{}
func (m fileMods) RandomizeAllColumns(f *faker.Faker) FileMod {
return FileModSlice{
FileMods.RandomID(f),
FileMods.RandomName(f),
FileMods.RandomData(f),
FileMods.RandomUserID(f),
}
}
// Set the model columns to this value
func (m fileMods) ID(val int64) FileMod {
return FileModFunc(func(o *FileTemplate) {
o.ID = func() int64 { return val }
})
}
// Set the Column from the function
func (m fileMods) IDFunc(f func() int64) FileMod {
return FileModFunc(func(o *FileTemplate) {
o.ID = f
})
}
// Clear any values for the column
func (m fileMods) UnsetID() FileMod {
return FileModFunc(func(o *FileTemplate) {
o.ID = nil
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
func (m fileMods) RandomID(f *faker.Faker) FileMod {
return FileModFunc(func(o *FileTemplate) {
o.ID = func() int64 {
return random_int64(f)
}
})
}
// Set the model columns to this value
func (m fileMods) Name(val string) FileMod {
return FileModFunc(func(o *FileTemplate) {
o.Name = func() string { return val }
})
}
// Set the Column from the function
func (m fileMods) NameFunc(f func() string) FileMod {
return FileModFunc(func(o *FileTemplate) {
o.Name = f
})
}
// Clear any values for the column
func (m fileMods) UnsetName() FileMod {
return FileModFunc(func(o *FileTemplate) {
o.Name = nil
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
func (m fileMods) RandomName(f *faker.Faker) FileMod {
return FileModFunc(func(o *FileTemplate) {
o.Name = func() string {
return random_string(f)
}
})
}
// Set the model columns to this value
func (m fileMods) Data(val []byte) FileMod {
return FileModFunc(func(o *FileTemplate) {
o.Data = func() []byte { return val }
})
}
// Set the Column from the function
func (m fileMods) DataFunc(f func() []byte) FileMod {
return FileModFunc(func(o *FileTemplate) {
o.Data = f
})
}
// Clear any values for the column
func (m fileMods) UnsetData() FileMod {
return FileModFunc(func(o *FileTemplate) {
o.Data = nil
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
func (m fileMods) RandomData(f *faker.Faker) FileMod {
return FileModFunc(func(o *FileTemplate) {
o.Data = func() []byte {
return random___byte(f)
}
})
}
// Set the model columns to this value
func (m fileMods) UserID(val int64) FileMod {
return FileModFunc(func(o *FileTemplate) {
o.UserID = func() int64 { return val }
})
}
// Set the Column from the function
func (m fileMods) UserIDFunc(f func() int64) FileMod {
return FileModFunc(func(o *FileTemplate) {
o.UserID = f
})
}
// Clear any values for the column
func (m fileMods) UnsetUserID() FileMod {
return FileModFunc(func(o *FileTemplate) {
o.UserID = nil
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
func (m fileMods) RandomUserID(f *faker.Faker) FileMod {
return FileModFunc(func(o *FileTemplate) {
o.UserID = func() int64 {
return random_int64(f)
}
})
}
func (m fileMods) WithUser(rel *UserTemplate) FileMod {
return FileModFunc(func(o *FileTemplate) {
o.r.User = &fileRUserR{
o: rel,
}
})
}
func (m fileMods) WithNewUser(mods ...UserMod) FileMod {
return FileModFunc(func(o *FileTemplate) {
related := o.f.NewUser(mods...)
m.WithUser(related).Apply(o)
})
}
func (m fileMods) WithoutUser() FileMod {
return FileModFunc(func(o *FileTemplate) {
o.r.User = nil
})
}
func (m fileMods) WithProfilePictureUsers(number int, related *UserTemplate) FileMod {
return FileModFunc(func(o *FileTemplate) {
o.r.ProfilePictureUsers = []*fileRProfilePictureUsersR{{
number: number,
o: related,
}}
})
}
func (m fileMods) WithNewProfilePictureUsers(number int, mods ...UserMod) FileMod {
return FileModFunc(func(o *FileTemplate) {
related := o.f.NewUser(mods...)
m.WithProfilePictureUsers(number, related).Apply(o)
})
}
func (m fileMods) AddProfilePictureUsers(number int, related *UserTemplate) FileMod {
return FileModFunc(func(o *FileTemplate) {
o.r.ProfilePictureUsers = append(o.r.ProfilePictureUsers, &fileRProfilePictureUsersR{
number: number,
o: related,
})
})
}
func (m fileMods) AddNewProfilePictureUsers(number int, mods ...UserMod) FileMod {
return FileModFunc(func(o *FileTemplate) {
related := o.f.NewUser(mods...)
m.AddProfilePictureUsers(number, related).Apply(o)
})
}
func (m fileMods) WithoutProfilePictureUsers() FileMod {
return FileModFunc(func(o *FileTemplate) {
o.r.ProfilePictureUsers = nil
})
}

View File

@ -0,0 +1,582 @@
// Code generated by BobGen sql (devel). DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package factory
import (
"context"
"testing"
"time"
"github.com/aarondl/opt/omit"
"github.com/jaswdr/faker/v2"
models "github.com/spotdemo4/trevstack/server/internal/models"
"github.com/stephenafamo/bob"
)
type ItemMod interface {
Apply(*ItemTemplate)
}
type ItemModFunc func(*ItemTemplate)
func (f ItemModFunc) Apply(n *ItemTemplate) {
f(n)
}
type ItemModSlice []ItemMod
func (mods ItemModSlice) Apply(n *ItemTemplate) {
for _, f := range mods {
f.Apply(n)
}
}
// ItemTemplate is an object representing the database table.
// all columns are optional and should be set by mods
type ItemTemplate struct {
ID func() int64
Name func() string
Added func() time.Time
Description func() string
Price func() float32
Quantity func() int64
UserID func() int64
r itemR
f *Factory
}
type itemR struct {
User *itemRUserR
}
type itemRUserR struct {
o *UserTemplate
}
// Apply mods to the ItemTemplate
func (o *ItemTemplate) Apply(mods ...ItemMod) {
for _, mod := range mods {
mod.Apply(o)
}
}
// toModel returns an *models.Item
// this does nothing with the relationship templates
func (o ItemTemplate) toModel() *models.Item {
m := &models.Item{}
if o.ID != nil {
m.ID = o.ID()
}
if o.Name != nil {
m.Name = o.Name()
}
if o.Added != nil {
m.Added = o.Added()
}
if o.Description != nil {
m.Description = o.Description()
}
if o.Price != nil {
m.Price = o.Price()
}
if o.Quantity != nil {
m.Quantity = o.Quantity()
}
if o.UserID != nil {
m.UserID = o.UserID()
}
return m
}
// toModels returns an models.ItemSlice
// this does nothing with the relationship templates
func (o ItemTemplate) toModels(number int) models.ItemSlice {
m := make(models.ItemSlice, number)
for i := range m {
m[i] = o.toModel()
}
return m
}
// setModelRels creates and sets the relationships on *models.Item
// according to the relationships in the template. Nothing is inserted into the db
func (t ItemTemplate) setModelRels(o *models.Item) {
if t.r.User != nil {
rel := t.r.User.o.toModel()
rel.R.Items = append(rel.R.Items, o)
o.UserID = rel.ID
o.R.User = rel
}
}
// BuildSetter returns an *models.ItemSetter
// this does nothing with the relationship templates
func (o ItemTemplate) BuildSetter() *models.ItemSetter {
m := &models.ItemSetter{}
if o.ID != nil {
m.ID = omit.From(o.ID())
}
if o.Name != nil {
m.Name = omit.From(o.Name())
}
if o.Added != nil {
m.Added = omit.From(o.Added())
}
if o.Description != nil {
m.Description = omit.From(o.Description())
}
if o.Price != nil {
m.Price = omit.From(o.Price())
}
if o.Quantity != nil {
m.Quantity = omit.From(o.Quantity())
}
if o.UserID != nil {
m.UserID = omit.From(o.UserID())
}
return m
}
// BuildManySetter returns an []*models.ItemSetter
// this does nothing with the relationship templates
func (o ItemTemplate) BuildManySetter(number int) []*models.ItemSetter {
m := make([]*models.ItemSetter, number)
for i := range m {
m[i] = o.BuildSetter()
}
return m
}
// Build returns an *models.Item
// Related objects are also created and placed in the .R field
// NOTE: Objects are not inserted into the database. Use ItemTemplate.Create
func (o ItemTemplate) Build() *models.Item {
m := o.toModel()
o.setModelRels(m)
return m
}
// BuildMany returns an models.ItemSlice
// Related objects are also created and placed in the .R field
// NOTE: Objects are not inserted into the database. Use ItemTemplate.CreateMany
func (o ItemTemplate) BuildMany(number int) models.ItemSlice {
m := make(models.ItemSlice, number)
for i := range m {
m[i] = o.Build()
}
return m
}
func ensureCreatableItem(m *models.ItemSetter) {
if m.Name.IsUnset() {
m.Name = omit.From(random_string(nil))
}
if m.Added.IsUnset() {
m.Added = omit.From(random_time_Time(nil))
}
if m.Description.IsUnset() {
m.Description = omit.From(random_string(nil))
}
if m.Price.IsUnset() {
m.Price = omit.From(random_float32(nil))
}
if m.Quantity.IsUnset() {
m.Quantity = omit.From(random_int64(nil))
}
if m.UserID.IsUnset() {
m.UserID = omit.From(random_int64(nil))
}
}
// insertOptRels creates and inserts any optional the relationships on *models.Item
// according to the relationships in the template.
// any required relationship should have already exist on the model
func (o *ItemTemplate) insertOptRels(ctx context.Context, exec bob.Executor, m *models.Item) (context.Context, error) {
var err error
return ctx, err
}
// Create builds a item and inserts it into the database
// Relations objects are also inserted and placed in the .R field
func (o *ItemTemplate) Create(ctx context.Context, exec bob.Executor) (*models.Item, error) {
_, m, err := o.create(ctx, exec)
return m, err
}
// MustCreate builds a item and inserts it into the database
// Relations objects are also inserted and placed in the .R field
// panics if an error occurs
func (o *ItemTemplate) MustCreate(ctx context.Context, exec bob.Executor) *models.Item {
_, m, err := o.create(ctx, exec)
if err != nil {
panic(err)
}
return m
}
// CreateOrFail builds a item and inserts it into the database
// Relations objects are also inserted and placed in the .R field
// It calls `tb.Fatal(err)` on the test/benchmark if an error occurs
func (o *ItemTemplate) CreateOrFail(ctx context.Context, tb testing.TB, exec bob.Executor) *models.Item {
tb.Helper()
_, m, err := o.create(ctx, exec)
if err != nil {
tb.Fatal(err)
return nil
}
return m
}
// create builds a item and inserts it into the database
// Relations objects are also inserted and placed in the .R field
// this returns a context that includes the newly inserted model
func (o *ItemTemplate) create(ctx context.Context, exec bob.Executor) (context.Context, *models.Item, error) {
var err error
opt := o.BuildSetter()
ensureCreatableItem(opt)
var rel0 *models.User
if o.r.User == nil {
var ok bool
rel0, ok = userCtx.Value(ctx)
if !ok {
ItemMods.WithNewUser().Apply(o)
}
}
if o.r.User != nil {
ctx, rel0, err = o.r.User.o.create(ctx, exec)
if err != nil {
return ctx, nil, err
}
}
opt.UserID = omit.From(rel0.ID)
m, err := models.Items.Insert(opt).One(ctx, exec)
if err != nil {
return ctx, nil, err
}
ctx = itemCtx.WithValue(ctx, m)
m.R.User = rel0
ctx, err = o.insertOptRels(ctx, exec, m)
return ctx, m, err
}
// CreateMany builds multiple items and inserts them into the database
// Relations objects are also inserted and placed in the .R field
func (o ItemTemplate) CreateMany(ctx context.Context, exec bob.Executor, number int) (models.ItemSlice, error) {
_, m, err := o.createMany(ctx, exec, number)
return m, err
}
// MustCreateMany builds multiple items and inserts them into the database
// Relations objects are also inserted and placed in the .R field
// panics if an error occurs
func (o ItemTemplate) MustCreateMany(ctx context.Context, exec bob.Executor, number int) models.ItemSlice {
_, m, err := o.createMany(ctx, exec, number)
if err != nil {
panic(err)
}
return m
}
// CreateManyOrFail builds multiple items and inserts them into the database
// Relations objects are also inserted and placed in the .R field
// It calls `tb.Fatal(err)` on the test/benchmark if an error occurs
func (o ItemTemplate) CreateManyOrFail(ctx context.Context, tb testing.TB, exec bob.Executor, number int) models.ItemSlice {
tb.Helper()
_, m, err := o.createMany(ctx, exec, number)
if err != nil {
tb.Fatal(err)
return nil
}
return m
}
// createMany builds multiple items and inserts them into the database
// Relations objects are also inserted and placed in the .R field
// this returns a context that includes the newly inserted models
func (o ItemTemplate) createMany(ctx context.Context, exec bob.Executor, number int) (context.Context, models.ItemSlice, error) {
var err error
m := make(models.ItemSlice, number)
for i := range m {
ctx, m[i], err = o.create(ctx, exec)
if err != nil {
return ctx, nil, err
}
}
return ctx, m, nil
}
// Item has methods that act as mods for the ItemTemplate
var ItemMods itemMods
type itemMods struct{}
func (m itemMods) RandomizeAllColumns(f *faker.Faker) ItemMod {
return ItemModSlice{
ItemMods.RandomID(f),
ItemMods.RandomName(f),
ItemMods.RandomAdded(f),
ItemMods.RandomDescription(f),
ItemMods.RandomPrice(f),
ItemMods.RandomQuantity(f),
ItemMods.RandomUserID(f),
}
}
// Set the model columns to this value
func (m itemMods) ID(val int64) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.ID = func() int64 { return val }
})
}
// Set the Column from the function
func (m itemMods) IDFunc(f func() int64) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.ID = f
})
}
// Clear any values for the column
func (m itemMods) UnsetID() ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.ID = nil
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
func (m itemMods) RandomID(f *faker.Faker) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.ID = func() int64 {
return random_int64(f)
}
})
}
// Set the model columns to this value
func (m itemMods) Name(val string) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.Name = func() string { return val }
})
}
// Set the Column from the function
func (m itemMods) NameFunc(f func() string) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.Name = f
})
}
// Clear any values for the column
func (m itemMods) UnsetName() ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.Name = nil
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
func (m itemMods) RandomName(f *faker.Faker) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.Name = func() string {
return random_string(f)
}
})
}
// Set the model columns to this value
func (m itemMods) Added(val time.Time) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.Added = func() time.Time { return val }
})
}
// Set the Column from the function
func (m itemMods) AddedFunc(f func() time.Time) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.Added = f
})
}
// Clear any values for the column
func (m itemMods) UnsetAdded() ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.Added = nil
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
func (m itemMods) RandomAdded(f *faker.Faker) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.Added = func() time.Time {
return random_time_Time(f)
}
})
}
// Set the model columns to this value
func (m itemMods) Description(val string) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.Description = func() string { return val }
})
}
// Set the Column from the function
func (m itemMods) DescriptionFunc(f func() string) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.Description = f
})
}
// Clear any values for the column
func (m itemMods) UnsetDescription() ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.Description = nil
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
func (m itemMods) RandomDescription(f *faker.Faker) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.Description = func() string {
return random_string(f)
}
})
}
// Set the model columns to this value
func (m itemMods) Price(val float32) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.Price = func() float32 { return val }
})
}
// Set the Column from the function
func (m itemMods) PriceFunc(f func() float32) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.Price = f
})
}
// Clear any values for the column
func (m itemMods) UnsetPrice() ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.Price = nil
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
func (m itemMods) RandomPrice(f *faker.Faker) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.Price = func() float32 {
return random_float32(f)
}
})
}
// Set the model columns to this value
func (m itemMods) Quantity(val int64) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.Quantity = func() int64 { return val }
})
}
// Set the Column from the function
func (m itemMods) QuantityFunc(f func() int64) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.Quantity = f
})
}
// Clear any values for the column
func (m itemMods) UnsetQuantity() ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.Quantity = nil
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
func (m itemMods) RandomQuantity(f *faker.Faker) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.Quantity = func() int64 {
return random_int64(f)
}
})
}
// Set the model columns to this value
func (m itemMods) UserID(val int64) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.UserID = func() int64 { return val }
})
}
// Set the Column from the function
func (m itemMods) UserIDFunc(f func() int64) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.UserID = f
})
}
// Clear any values for the column
func (m itemMods) UnsetUserID() ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.UserID = nil
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
func (m itemMods) RandomUserID(f *faker.Faker) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.UserID = func() int64 {
return random_int64(f)
}
})
}
func (m itemMods) WithUser(rel *UserTemplate) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.r.User = &itemRUserR{
o: rel,
}
})
}
func (m itemMods) WithNewUser(mods ...UserMod) ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
related := o.f.NewUser(mods...)
m.WithUser(related).Apply(o)
})
}
func (m itemMods) WithoutUser() ItemMod {
return ItemModFunc(func(o *ItemTemplate) {
o.r.User = nil
})
}

View File

@ -0,0 +1,276 @@
// Code generated by BobGen sql (devel). DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package factory
import (
"context"
"testing"
"github.com/aarondl/opt/omit"
"github.com/jaswdr/faker/v2"
models "github.com/spotdemo4/trevstack/server/internal/models"
"github.com/stephenafamo/bob"
)
type SchemaMigrationMod interface {
Apply(*SchemaMigrationTemplate)
}
type SchemaMigrationModFunc func(*SchemaMigrationTemplate)
func (f SchemaMigrationModFunc) Apply(n *SchemaMigrationTemplate) {
f(n)
}
type SchemaMigrationModSlice []SchemaMigrationMod
func (mods SchemaMigrationModSlice) Apply(n *SchemaMigrationTemplate) {
for _, f := range mods {
f.Apply(n)
}
}
// SchemaMigrationTemplate is an object representing the database table.
// all columns are optional and should be set by mods
type SchemaMigrationTemplate struct {
Version func() string
f *Factory
}
// Apply mods to the SchemaMigrationTemplate
func (o *SchemaMigrationTemplate) Apply(mods ...SchemaMigrationMod) {
for _, mod := range mods {
mod.Apply(o)
}
}
// toModel returns an *models.SchemaMigration
// this does nothing with the relationship templates
func (o SchemaMigrationTemplate) toModel() *models.SchemaMigration {
m := &models.SchemaMigration{}
if o.Version != nil {
m.Version = o.Version()
}
return m
}
// toModels returns an models.SchemaMigrationSlice
// this does nothing with the relationship templates
func (o SchemaMigrationTemplate) toModels(number int) models.SchemaMigrationSlice {
m := make(models.SchemaMigrationSlice, number)
for i := range m {
m[i] = o.toModel()
}
return m
}
// setModelRels creates and sets the relationships on *models.SchemaMigration
// according to the relationships in the template. Nothing is inserted into the db
func (t SchemaMigrationTemplate) setModelRels(o *models.SchemaMigration) {}
// BuildSetter returns an *models.SchemaMigrationSetter
// this does nothing with the relationship templates
func (o SchemaMigrationTemplate) BuildSetter() *models.SchemaMigrationSetter {
m := &models.SchemaMigrationSetter{}
if o.Version != nil {
m.Version = omit.From(o.Version())
}
return m
}
// BuildManySetter returns an []*models.SchemaMigrationSetter
// this does nothing with the relationship templates
func (o SchemaMigrationTemplate) BuildManySetter(number int) []*models.SchemaMigrationSetter {
m := make([]*models.SchemaMigrationSetter, number)
for i := range m {
m[i] = o.BuildSetter()
}
return m
}
// Build returns an *models.SchemaMigration
// Related objects are also created and placed in the .R field
// NOTE: Objects are not inserted into the database. Use SchemaMigrationTemplate.Create
func (o SchemaMigrationTemplate) Build() *models.SchemaMigration {
m := o.toModel()
o.setModelRels(m)
return m
}
// BuildMany returns an models.SchemaMigrationSlice
// Related objects are also created and placed in the .R field
// NOTE: Objects are not inserted into the database. Use SchemaMigrationTemplate.CreateMany
func (o SchemaMigrationTemplate) BuildMany(number int) models.SchemaMigrationSlice {
m := make(models.SchemaMigrationSlice, number)
for i := range m {
m[i] = o.Build()
}
return m
}
func ensureCreatableSchemaMigration(m *models.SchemaMigrationSetter) {
if m.Version.IsUnset() {
m.Version = omit.From(random_string(nil))
}
}
// insertOptRels creates and inserts any optional the relationships on *models.SchemaMigration
// according to the relationships in the template.
// any required relationship should have already exist on the model
func (o *SchemaMigrationTemplate) insertOptRels(ctx context.Context, exec bob.Executor, m *models.SchemaMigration) (context.Context, error) {
var err error
return ctx, err
}
// Create builds a schemaMigration and inserts it into the database
// Relations objects are also inserted and placed in the .R field
func (o *SchemaMigrationTemplate) Create(ctx context.Context, exec bob.Executor) (*models.SchemaMigration, error) {
_, m, err := o.create(ctx, exec)
return m, err
}
// MustCreate builds a schemaMigration and inserts it into the database
// Relations objects are also inserted and placed in the .R field
// panics if an error occurs
func (o *SchemaMigrationTemplate) MustCreate(ctx context.Context, exec bob.Executor) *models.SchemaMigration {
_, m, err := o.create(ctx, exec)
if err != nil {
panic(err)
}
return m
}
// CreateOrFail builds a schemaMigration and inserts it into the database
// Relations objects are also inserted and placed in the .R field
// It calls `tb.Fatal(err)` on the test/benchmark if an error occurs
func (o *SchemaMigrationTemplate) CreateOrFail(ctx context.Context, tb testing.TB, exec bob.Executor) *models.SchemaMigration {
tb.Helper()
_, m, err := o.create(ctx, exec)
if err != nil {
tb.Fatal(err)
return nil
}
return m
}
// create builds a schemaMigration and inserts it into the database
// Relations objects are also inserted and placed in the .R field
// this returns a context that includes the newly inserted model
func (o *SchemaMigrationTemplate) create(ctx context.Context, exec bob.Executor) (context.Context, *models.SchemaMigration, error) {
var err error
opt := o.BuildSetter()
ensureCreatableSchemaMigration(opt)
m, err := models.SchemaMigrations.Insert(opt).One(ctx, exec)
if err != nil {
return ctx, nil, err
}
ctx = schemaMigrationCtx.WithValue(ctx, m)
ctx, err = o.insertOptRels(ctx, exec, m)
return ctx, m, err
}
// CreateMany builds multiple schemaMigrations and inserts them into the database
// Relations objects are also inserted and placed in the .R field
func (o SchemaMigrationTemplate) CreateMany(ctx context.Context, exec bob.Executor, number int) (models.SchemaMigrationSlice, error) {
_, m, err := o.createMany(ctx, exec, number)
return m, err
}
// MustCreateMany builds multiple schemaMigrations and inserts them into the database
// Relations objects are also inserted and placed in the .R field
// panics if an error occurs
func (o SchemaMigrationTemplate) MustCreateMany(ctx context.Context, exec bob.Executor, number int) models.SchemaMigrationSlice {
_, m, err := o.createMany(ctx, exec, number)
if err != nil {
panic(err)
}
return m
}
// CreateManyOrFail builds multiple schemaMigrations and inserts them into the database
// Relations objects are also inserted and placed in the .R field
// It calls `tb.Fatal(err)` on the test/benchmark if an error occurs
func (o SchemaMigrationTemplate) CreateManyOrFail(ctx context.Context, tb testing.TB, exec bob.Executor, number int) models.SchemaMigrationSlice {
tb.Helper()
_, m, err := o.createMany(ctx, exec, number)
if err != nil {
tb.Fatal(err)
return nil
}
return m
}
// createMany builds multiple schemaMigrations and inserts them into the database
// Relations objects are also inserted and placed in the .R field
// this returns a context that includes the newly inserted models
func (o SchemaMigrationTemplate) createMany(ctx context.Context, exec bob.Executor, number int) (context.Context, models.SchemaMigrationSlice, error) {
var err error
m := make(models.SchemaMigrationSlice, number)
for i := range m {
ctx, m[i], err = o.create(ctx, exec)
if err != nil {
return ctx, nil, err
}
}
return ctx, m, nil
}
// SchemaMigration has methods that act as mods for the SchemaMigrationTemplate
var SchemaMigrationMods schemaMigrationMods
type schemaMigrationMods struct{}
func (m schemaMigrationMods) RandomizeAllColumns(f *faker.Faker) SchemaMigrationMod {
return SchemaMigrationModSlice{
SchemaMigrationMods.RandomVersion(f),
}
}
// Set the model columns to this value
func (m schemaMigrationMods) Version(val string) SchemaMigrationMod {
return SchemaMigrationModFunc(func(o *SchemaMigrationTemplate) {
o.Version = func() string { return val }
})
}
// Set the Column from the function
func (m schemaMigrationMods) VersionFunc(f func() string) SchemaMigrationMod {
return SchemaMigrationModFunc(func(o *SchemaMigrationTemplate) {
o.Version = f
})
}
// Clear any values for the column
func (m schemaMigrationMods) UnsetVersion() SchemaMigrationMod {
return SchemaMigrationModFunc(func(o *SchemaMigrationTemplate) {
o.Version = nil
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
func (m schemaMigrationMods) RandomVersion(f *faker.Faker) SchemaMigrationMod {
return SchemaMigrationModFunc(func(o *SchemaMigrationTemplate) {
o.Version = func() string {
return random_string(f)
}
})
}

View File

@ -0,0 +1,598 @@
// Code generated by BobGen sql (devel). DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package factory
import (
"context"
"testing"
"github.com/aarondl/opt/null"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/jaswdr/faker/v2"
models "github.com/spotdemo4/trevstack/server/internal/models"
"github.com/stephenafamo/bob"
)
type UserMod interface {
Apply(*UserTemplate)
}
type UserModFunc func(*UserTemplate)
func (f UserModFunc) Apply(n *UserTemplate) {
f(n)
}
type UserModSlice []UserMod
func (mods UserModSlice) Apply(n *UserTemplate) {
for _, f := range mods {
f.Apply(n)
}
}
// UserTemplate is an object representing the database table.
// all columns are optional and should be set by mods
type UserTemplate struct {
ID func() int64
Username func() string
Password func() string
ProfilePictureID func() null.Val[int64]
r userR
f *Factory
}
type userR struct {
Files []*userRFilesR
Items []*userRItemsR
ProfilePictureFile *userRProfilePictureFileR
}
type userRFilesR struct {
number int
o *FileTemplate
}
type userRItemsR struct {
number int
o *ItemTemplate
}
type userRProfilePictureFileR struct {
o *FileTemplate
}
// Apply mods to the UserTemplate
func (o *UserTemplate) Apply(mods ...UserMod) {
for _, mod := range mods {
mod.Apply(o)
}
}
// toModel returns an *models.User
// this does nothing with the relationship templates
func (o UserTemplate) toModel() *models.User {
m := &models.User{}
if o.ID != nil {
m.ID = o.ID()
}
if o.Username != nil {
m.Username = o.Username()
}
if o.Password != nil {
m.Password = o.Password()
}
if o.ProfilePictureID != nil {
m.ProfilePictureID = o.ProfilePictureID()
}
return m
}
// toModels returns an models.UserSlice
// this does nothing with the relationship templates
func (o UserTemplate) toModels(number int) models.UserSlice {
m := make(models.UserSlice, number)
for i := range m {
m[i] = o.toModel()
}
return m
}
// setModelRels creates and sets the relationships on *models.User
// according to the relationships in the template. Nothing is inserted into the db
func (t UserTemplate) setModelRels(o *models.User) {
if t.r.Files != nil {
rel := models.FileSlice{}
for _, r := range t.r.Files {
related := r.o.toModels(r.number)
for _, rel := range related {
rel.UserID = o.ID
rel.R.User = o
}
rel = append(rel, related...)
}
o.R.Files = rel
}
if t.r.Items != nil {
rel := models.ItemSlice{}
for _, r := range t.r.Items {
related := r.o.toModels(r.number)
for _, rel := range related {
rel.UserID = o.ID
rel.R.User = o
}
rel = append(rel, related...)
}
o.R.Items = rel
}
if t.r.ProfilePictureFile != nil {
rel := t.r.ProfilePictureFile.o.toModel()
rel.R.ProfilePictureUsers = append(rel.R.ProfilePictureUsers, o)
o.ProfilePictureID = null.From(rel.ID)
o.R.ProfilePictureFile = rel
}
}
// BuildSetter returns an *models.UserSetter
// this does nothing with the relationship templates
func (o UserTemplate) BuildSetter() *models.UserSetter {
m := &models.UserSetter{}
if o.ID != nil {
m.ID = omit.From(o.ID())
}
if o.Username != nil {
m.Username = omit.From(o.Username())
}
if o.Password != nil {
m.Password = omit.From(o.Password())
}
if o.ProfilePictureID != nil {
m.ProfilePictureID = omitnull.FromNull(o.ProfilePictureID())
}
return m
}
// BuildManySetter returns an []*models.UserSetter
// this does nothing with the relationship templates
func (o UserTemplate) BuildManySetter(number int) []*models.UserSetter {
m := make([]*models.UserSetter, number)
for i := range m {
m[i] = o.BuildSetter()
}
return m
}
// Build returns an *models.User
// Related objects are also created and placed in the .R field
// NOTE: Objects are not inserted into the database. Use UserTemplate.Create
func (o UserTemplate) Build() *models.User {
m := o.toModel()
o.setModelRels(m)
return m
}
// BuildMany returns an models.UserSlice
// Related objects are also created and placed in the .R field
// NOTE: Objects are not inserted into the database. Use UserTemplate.CreateMany
func (o UserTemplate) BuildMany(number int) models.UserSlice {
m := make(models.UserSlice, number)
for i := range m {
m[i] = o.Build()
}
return m
}
func ensureCreatableUser(m *models.UserSetter) {
if m.Username.IsUnset() {
m.Username = omit.From(random_string(nil))
}
if m.Password.IsUnset() {
m.Password = omit.From(random_string(nil))
}
}
// insertOptRels creates and inserts any optional the relationships on *models.User
// according to the relationships in the template.
// any required relationship should have already exist on the model
func (o *UserTemplate) insertOptRels(ctx context.Context, exec bob.Executor, m *models.User) (context.Context, error) {
var err error
if o.r.Files != nil {
for _, r := range o.r.Files {
var rel0 models.FileSlice
ctx, rel0, err = r.o.createMany(ctx, exec, r.number)
if err != nil {
return ctx, err
}
err = m.AttachFiles(ctx, exec, rel0...)
if err != nil {
return ctx, err
}
}
}
if o.r.Items != nil {
for _, r := range o.r.Items {
var rel1 models.ItemSlice
ctx, rel1, err = r.o.createMany(ctx, exec, r.number)
if err != nil {
return ctx, err
}
err = m.AttachItems(ctx, exec, rel1...)
if err != nil {
return ctx, err
}
}
}
if o.r.ProfilePictureFile != nil {
var rel2 *models.File
ctx, rel2, err = o.r.ProfilePictureFile.o.create(ctx, exec)
if err != nil {
return ctx, err
}
err = m.AttachProfilePictureFile(ctx, exec, rel2)
if err != nil {
return ctx, err
}
}
return ctx, err
}
// Create builds a user and inserts it into the database
// Relations objects are also inserted and placed in the .R field
func (o *UserTemplate) Create(ctx context.Context, exec bob.Executor) (*models.User, error) {
_, m, err := o.create(ctx, exec)
return m, err
}
// MustCreate builds a user and inserts it into the database
// Relations objects are also inserted and placed in the .R field
// panics if an error occurs
func (o *UserTemplate) MustCreate(ctx context.Context, exec bob.Executor) *models.User {
_, m, err := o.create(ctx, exec)
if err != nil {
panic(err)
}
return m
}
// CreateOrFail builds a user and inserts it into the database
// Relations objects are also inserted and placed in the .R field
// It calls `tb.Fatal(err)` on the test/benchmark if an error occurs
func (o *UserTemplate) CreateOrFail(ctx context.Context, tb testing.TB, exec bob.Executor) *models.User {
tb.Helper()
_, m, err := o.create(ctx, exec)
if err != nil {
tb.Fatal(err)
return nil
}
return m
}
// create builds a user and inserts it into the database
// Relations objects are also inserted and placed in the .R field
// this returns a context that includes the newly inserted model
func (o *UserTemplate) create(ctx context.Context, exec bob.Executor) (context.Context, *models.User, error) {
var err error
opt := o.BuildSetter()
ensureCreatableUser(opt)
m, err := models.Users.Insert(opt).One(ctx, exec)
if err != nil {
return ctx, nil, err
}
ctx = userCtx.WithValue(ctx, m)
ctx, err = o.insertOptRels(ctx, exec, m)
return ctx, m, err
}
// CreateMany builds multiple users and inserts them into the database
// Relations objects are also inserted and placed in the .R field
func (o UserTemplate) CreateMany(ctx context.Context, exec bob.Executor, number int) (models.UserSlice, error) {
_, m, err := o.createMany(ctx, exec, number)
return m, err
}
// MustCreateMany builds multiple users and inserts them into the database
// Relations objects are also inserted and placed in the .R field
// panics if an error occurs
func (o UserTemplate) MustCreateMany(ctx context.Context, exec bob.Executor, number int) models.UserSlice {
_, m, err := o.createMany(ctx, exec, number)
if err != nil {
panic(err)
}
return m
}
// CreateManyOrFail builds multiple users and inserts them into the database
// Relations objects are also inserted and placed in the .R field
// It calls `tb.Fatal(err)` on the test/benchmark if an error occurs
func (o UserTemplate) CreateManyOrFail(ctx context.Context, tb testing.TB, exec bob.Executor, number int) models.UserSlice {
tb.Helper()
_, m, err := o.createMany(ctx, exec, number)
if err != nil {
tb.Fatal(err)
return nil
}
return m
}
// createMany builds multiple users and inserts them into the database
// Relations objects are also inserted and placed in the .R field
// this returns a context that includes the newly inserted models
func (o UserTemplate) createMany(ctx context.Context, exec bob.Executor, number int) (context.Context, models.UserSlice, error) {
var err error
m := make(models.UserSlice, number)
for i := range m {
ctx, m[i], err = o.create(ctx, exec)
if err != nil {
return ctx, nil, err
}
}
return ctx, m, nil
}
// User has methods that act as mods for the UserTemplate
var UserMods userMods
type userMods struct{}
func (m userMods) RandomizeAllColumns(f *faker.Faker) UserMod {
return UserModSlice{
UserMods.RandomID(f),
UserMods.RandomUsername(f),
UserMods.RandomPassword(f),
UserMods.RandomProfilePictureID(f),
}
}
// Set the model columns to this value
func (m userMods) ID(val int64) UserMod {
return UserModFunc(func(o *UserTemplate) {
o.ID = func() int64 { return val }
})
}
// Set the Column from the function
func (m userMods) IDFunc(f func() int64) UserMod {
return UserModFunc(func(o *UserTemplate) {
o.ID = f
})
}
// Clear any values for the column
func (m userMods) UnsetID() UserMod {
return UserModFunc(func(o *UserTemplate) {
o.ID = nil
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
func (m userMods) RandomID(f *faker.Faker) UserMod {
return UserModFunc(func(o *UserTemplate) {
o.ID = func() int64 {
return random_int64(f)
}
})
}
// Set the model columns to this value
func (m userMods) Username(val string) UserMod {
return UserModFunc(func(o *UserTemplate) {
o.Username = func() string { return val }
})
}
// Set the Column from the function
func (m userMods) UsernameFunc(f func() string) UserMod {
return UserModFunc(func(o *UserTemplate) {
o.Username = f
})
}
// Clear any values for the column
func (m userMods) UnsetUsername() UserMod {
return UserModFunc(func(o *UserTemplate) {
o.Username = nil
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
func (m userMods) RandomUsername(f *faker.Faker) UserMod {
return UserModFunc(func(o *UserTemplate) {
o.Username = func() string {
return random_string(f)
}
})
}
// Set the model columns to this value
func (m userMods) Password(val string) UserMod {
return UserModFunc(func(o *UserTemplate) {
o.Password = func() string { return val }
})
}
// Set the Column from the function
func (m userMods) PasswordFunc(f func() string) UserMod {
return UserModFunc(func(o *UserTemplate) {
o.Password = f
})
}
// Clear any values for the column
func (m userMods) UnsetPassword() UserMod {
return UserModFunc(func(o *UserTemplate) {
o.Password = nil
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
func (m userMods) RandomPassword(f *faker.Faker) UserMod {
return UserModFunc(func(o *UserTemplate) {
o.Password = func() string {
return random_string(f)
}
})
}
// Set the model columns to this value
func (m userMods) ProfilePictureID(val null.Val[int64]) UserMod {
return UserModFunc(func(o *UserTemplate) {
o.ProfilePictureID = func() null.Val[int64] { return val }
})
}
// Set the Column from the function
func (m userMods) ProfilePictureIDFunc(f func() null.Val[int64]) UserMod {
return UserModFunc(func(o *UserTemplate) {
o.ProfilePictureID = f
})
}
// Clear any values for the column
func (m userMods) UnsetProfilePictureID() UserMod {
return UserModFunc(func(o *UserTemplate) {
o.ProfilePictureID = nil
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
func (m userMods) RandomProfilePictureID(f *faker.Faker) UserMod {
return UserModFunc(func(o *UserTemplate) {
o.ProfilePictureID = func() null.Val[int64] {
if f == nil {
f = &defaultFaker
}
if f.Bool() {
return null.FromPtr[int64](nil)
}
return null.From(random_int64(f))
}
})
}
func (m userMods) WithProfilePictureFile(rel *FileTemplate) UserMod {
return UserModFunc(func(o *UserTemplate) {
o.r.ProfilePictureFile = &userRProfilePictureFileR{
o: rel,
}
})
}
func (m userMods) WithNewProfilePictureFile(mods ...FileMod) UserMod {
return UserModFunc(func(o *UserTemplate) {
related := o.f.NewFile(mods...)
m.WithProfilePictureFile(related).Apply(o)
})
}
func (m userMods) WithoutProfilePictureFile() UserMod {
return UserModFunc(func(o *UserTemplate) {
o.r.ProfilePictureFile = nil
})
}
func (m userMods) WithFiles(number int, related *FileTemplate) UserMod {
return UserModFunc(func(o *UserTemplate) {
o.r.Files = []*userRFilesR{{
number: number,
o: related,
}}
})
}
func (m userMods) WithNewFiles(number int, mods ...FileMod) UserMod {
return UserModFunc(func(o *UserTemplate) {
related := o.f.NewFile(mods...)
m.WithFiles(number, related).Apply(o)
})
}
func (m userMods) AddFiles(number int, related *FileTemplate) UserMod {
return UserModFunc(func(o *UserTemplate) {
o.r.Files = append(o.r.Files, &userRFilesR{
number: number,
o: related,
})
})
}
func (m userMods) AddNewFiles(number int, mods ...FileMod) UserMod {
return UserModFunc(func(o *UserTemplate) {
related := o.f.NewFile(mods...)
m.AddFiles(number, related).Apply(o)
})
}
func (m userMods) WithoutFiles() UserMod {
return UserModFunc(func(o *UserTemplate) {
o.r.Files = nil
})
}
func (m userMods) WithItems(number int, related *ItemTemplate) UserMod {
return UserModFunc(func(o *UserTemplate) {
o.r.Items = []*userRItemsR{{
number: number,
o: related,
}}
})
}
func (m userMods) WithNewItems(number int, mods ...ItemMod) UserMod {
return UserModFunc(func(o *UserTemplate) {
related := o.f.NewItem(mods...)
m.WithItems(number, related).Apply(o)
})
}
func (m userMods) AddItems(number int, related *ItemTemplate) UserMod {
return UserModFunc(func(o *UserTemplate) {
o.r.Items = append(o.r.Items, &userRItemsR{
number: number,
o: related,
})
})
}
func (m userMods) AddNewItems(number int, mods ...ItemMod) UserMod {
return UserModFunc(func(o *UserTemplate) {
related := o.f.NewItem(mods...)
m.AddItems(number, related).Apply(o)
})
}
func (m userMods) WithoutItems() UserMod {
return UserModFunc(func(o *UserTemplate) {
o.r.Items = nil
})
}

View File

@ -0,0 +1,851 @@
// Code generated by BobGen sql (devel). DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package models
import (
"context"
"database/sql"
"errors"
"fmt"
"io"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/stephenafamo/bob"
"github.com/stephenafamo/bob/dialect/sqlite"
"github.com/stephenafamo/bob/dialect/sqlite/dialect"
"github.com/stephenafamo/bob/dialect/sqlite/dm"
"github.com/stephenafamo/bob/dialect/sqlite/sm"
"github.com/stephenafamo/bob/dialect/sqlite/um"
"github.com/stephenafamo/bob/expr"
"github.com/stephenafamo/bob/mods"
"github.com/stephenafamo/bob/orm"
)
// File is an object representing the database table.
type File struct {
ID int64 `db:"id,pk" `
Name string `db:"name" `
Data []byte `db:"data" `
UserID int64 `db:"user_id" `
R fileR `db:"-" `
}
// FileSlice is an alias for a slice of pointers to File.
// This should almost always be used instead of []*File.
type FileSlice []*File
// Files contains methods to work with the file table
var Files = sqlite.NewTablex[*File, FileSlice, *FileSetter]("", "file")
// FilesQuery is a query on the file table
type FilesQuery = *sqlite.ViewQuery[*File, FileSlice]
// fileR is where relationships are stored.
type fileR struct {
User *User // fk_file_0
ProfilePictureUsers UserSlice // fk_user_0
}
type fileColumnNames struct {
ID string
Name string
Data string
UserID string
}
var FileColumns = buildFileColumns("file")
type fileColumns struct {
tableAlias string
ID sqlite.Expression
Name sqlite.Expression
Data sqlite.Expression
UserID sqlite.Expression
}
func (c fileColumns) Alias() string {
return c.tableAlias
}
func (fileColumns) AliasedAs(alias string) fileColumns {
return buildFileColumns(alias)
}
func buildFileColumns(alias string) fileColumns {
return fileColumns{
tableAlias: alias,
ID: sqlite.Quote(alias, "id"),
Name: sqlite.Quote(alias, "name"),
Data: sqlite.Quote(alias, "data"),
UserID: sqlite.Quote(alias, "user_id"),
}
}
type fileWhere[Q sqlite.Filterable] struct {
ID sqlite.WhereMod[Q, int64]
Name sqlite.WhereMod[Q, string]
Data sqlite.WhereMod[Q, []byte]
UserID sqlite.WhereMod[Q, int64]
}
func (fileWhere[Q]) AliasedAs(alias string) fileWhere[Q] {
return buildFileWhere[Q](buildFileColumns(alias))
}
func buildFileWhere[Q sqlite.Filterable](cols fileColumns) fileWhere[Q] {
return fileWhere[Q]{
ID: sqlite.Where[Q, int64](cols.ID),
Name: sqlite.Where[Q, string](cols.Name),
Data: sqlite.Where[Q, []byte](cols.Data),
UserID: sqlite.Where[Q, int64](cols.UserID),
}
}
var FileErrors = &fileErrors{
ErrUniquePkMainFile: &UniqueConstraintError{s: "pk_main_file"},
}
type fileErrors struct {
ErrUniquePkMainFile *UniqueConstraintError
}
// FileSetter is used for insert/upsert/update operations
// All values are optional, and do not have to be set
// Generated columns are not included
type FileSetter struct {
ID omit.Val[int64] `db:"id,pk" `
Name omit.Val[string] `db:"name" `
Data omit.Val[[]byte] `db:"data" `
UserID omit.Val[int64] `db:"user_id" `
}
func (s FileSetter) SetColumns() []string {
vals := make([]string, 0, 4)
if !s.ID.IsUnset() {
vals = append(vals, "id")
}
if !s.Name.IsUnset() {
vals = append(vals, "name")
}
if !s.Data.IsUnset() {
vals = append(vals, "data")
}
if !s.UserID.IsUnset() {
vals = append(vals, "user_id")
}
return vals
}
func (s FileSetter) Overwrite(t *File) {
if !s.ID.IsUnset() {
t.ID, _ = s.ID.Get()
}
if !s.Name.IsUnset() {
t.Name, _ = s.Name.Get()
}
if !s.Data.IsUnset() {
t.Data, _ = s.Data.Get()
}
if !s.UserID.IsUnset() {
t.UserID, _ = s.UserID.Get()
}
}
func (s *FileSetter) Apply(q *dialect.InsertQuery) {
q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) {
return Files.BeforeInsertHooks.RunHooks(ctx, exec, s)
})
if len(q.Table.Columns) == 0 {
q.Table.Columns = s.SetColumns()
}
q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
vals := make([]bob.Expression, 0, 4)
if !s.ID.IsUnset() {
vals = append(vals, sqlite.Arg(s.ID))
}
if !s.Name.IsUnset() {
vals = append(vals, sqlite.Arg(s.Name))
}
if !s.Data.IsUnset() {
vals = append(vals, sqlite.Arg(s.Data))
}
if !s.UserID.IsUnset() {
vals = append(vals, sqlite.Arg(s.UserID))
}
return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "")
}))
}
func (s FileSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] {
return um.Set(s.Expressions()...)
}
func (s FileSetter) Expressions(prefix ...string) []bob.Expression {
exprs := make([]bob.Expression, 0, 4)
if !s.ID.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
sqlite.Quote(append(prefix, "id")...),
sqlite.Arg(s.ID),
}})
}
if !s.Name.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
sqlite.Quote(append(prefix, "name")...),
sqlite.Arg(s.Name),
}})
}
if !s.Data.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
sqlite.Quote(append(prefix, "data")...),
sqlite.Arg(s.Data),
}})
}
if !s.UserID.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
sqlite.Quote(append(prefix, "user_id")...),
sqlite.Arg(s.UserID),
}})
}
return exprs
}
// FindFile retrieves a single record by primary key
// If cols is empty Find will return all columns.
func FindFile(ctx context.Context, exec bob.Executor, IDPK int64, cols ...string) (*File, error) {
if len(cols) == 0 {
return Files.Query(
SelectWhere.Files.ID.EQ(IDPK),
).One(ctx, exec)
}
return Files.Query(
SelectWhere.Files.ID.EQ(IDPK),
sm.Columns(Files.Columns().Only(cols...)),
).One(ctx, exec)
}
// FileExists checks the presence of a single record by primary key
func FileExists(ctx context.Context, exec bob.Executor, IDPK int64) (bool, error) {
return Files.Query(
SelectWhere.Files.ID.EQ(IDPK),
).Exists(ctx, exec)
}
// AfterQueryHook is called after File is retrieved from the database
func (o *File) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error {
var err error
switch queryType {
case bob.QueryTypeSelect:
ctx, err = Files.AfterSelectHooks.RunHooks(ctx, exec, FileSlice{o})
case bob.QueryTypeInsert:
ctx, err = Files.AfterInsertHooks.RunHooks(ctx, exec, FileSlice{o})
case bob.QueryTypeUpdate:
ctx, err = Files.AfterUpdateHooks.RunHooks(ctx, exec, FileSlice{o})
case bob.QueryTypeDelete:
ctx, err = Files.AfterDeleteHooks.RunHooks(ctx, exec, FileSlice{o})
}
return err
}
// PrimaryKeyVals returns the primary key values of the File
func (o *File) PrimaryKeyVals() bob.Expression {
return sqlite.Arg(o.ID)
}
func (o *File) pkEQ() dialect.Expression {
return sqlite.Quote("file", "id").EQ(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
return o.PrimaryKeyVals().WriteSQL(ctx, w, d, start)
}))
}
// Update uses an executor to update the File
func (o *File) Update(ctx context.Context, exec bob.Executor, s *FileSetter) error {
v, err := Files.Update(s.UpdateMod(), um.Where(o.pkEQ())).One(ctx, exec)
if err != nil {
return err
}
o.R = v.R
*o = *v
return nil
}
// Delete deletes a single File record with an executor
func (o *File) Delete(ctx context.Context, exec bob.Executor) error {
_, err := Files.Delete(dm.Where(o.pkEQ())).Exec(ctx, exec)
return err
}
// Reload refreshes the File using the executor
func (o *File) Reload(ctx context.Context, exec bob.Executor) error {
o2, err := Files.Query(
SelectWhere.Files.ID.EQ(o.ID),
).One(ctx, exec)
if err != nil {
return err
}
o2.R = o.R
*o = *o2
return nil
}
// AfterQueryHook is called after FileSlice is retrieved from the database
func (o FileSlice) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error {
var err error
switch queryType {
case bob.QueryTypeSelect:
ctx, err = Files.AfterSelectHooks.RunHooks(ctx, exec, o)
case bob.QueryTypeInsert:
ctx, err = Files.AfterInsertHooks.RunHooks(ctx, exec, o)
case bob.QueryTypeUpdate:
ctx, err = Files.AfterUpdateHooks.RunHooks(ctx, exec, o)
case bob.QueryTypeDelete:
ctx, err = Files.AfterDeleteHooks.RunHooks(ctx, exec, o)
}
return err
}
func (o FileSlice) pkIN() dialect.Expression {
if len(o) == 0 {
return sqlite.Raw("NULL")
}
return sqlite.Quote("file", "id").In(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
pkPairs := make([]bob.Expression, len(o))
for i, row := range o {
pkPairs[i] = row.PrimaryKeyVals()
}
return bob.ExpressSlice(ctx, w, d, start, pkPairs, "", ", ", "")
}))
}
// copyMatchingRows finds models in the given slice that have the same primary key
// then it first copies the existing relationships from the old model to the new model
// and then replaces the old model in the slice with the new model
func (o FileSlice) copyMatchingRows(from ...*File) {
for i, old := range o {
for _, new := range from {
if new.ID != old.ID {
continue
}
new.R = old.R
o[i] = new
break
}
}
}
// UpdateMod modifies an update query with "WHERE primary_key IN (o...)"
func (o FileSlice) UpdateMod() bob.Mod[*dialect.UpdateQuery] {
return bob.ModFunc[*dialect.UpdateQuery](func(q *dialect.UpdateQuery) {
q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) {
return Files.BeforeUpdateHooks.RunHooks(ctx, exec, o)
})
q.AppendLoader(bob.LoaderFunc(func(ctx context.Context, exec bob.Executor, retrieved any) error {
var err error
switch retrieved := retrieved.(type) {
case *File:
o.copyMatchingRows(retrieved)
case []*File:
o.copyMatchingRows(retrieved...)
case FileSlice:
o.copyMatchingRows(retrieved...)
default:
// If the retrieved value is not a File or a slice of File
// then run the AfterUpdateHooks on the slice
_, err = Files.AfterUpdateHooks.RunHooks(ctx, exec, o)
}
return err
}))
q.AppendWhere(o.pkIN())
})
}
// DeleteMod modifies an delete query with "WHERE primary_key IN (o...)"
func (o FileSlice) DeleteMod() bob.Mod[*dialect.DeleteQuery] {
return bob.ModFunc[*dialect.DeleteQuery](func(q *dialect.DeleteQuery) {
q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) {
return Files.BeforeDeleteHooks.RunHooks(ctx, exec, o)
})
q.AppendLoader(bob.LoaderFunc(func(ctx context.Context, exec bob.Executor, retrieved any) error {
var err error
switch retrieved := retrieved.(type) {
case *File:
o.copyMatchingRows(retrieved)
case []*File:
o.copyMatchingRows(retrieved...)
case FileSlice:
o.copyMatchingRows(retrieved...)
default:
// If the retrieved value is not a File or a slice of File
// then run the AfterDeleteHooks on the slice
_, err = Files.AfterDeleteHooks.RunHooks(ctx, exec, o)
}
return err
}))
q.AppendWhere(o.pkIN())
})
}
func (o FileSlice) UpdateAll(ctx context.Context, exec bob.Executor, vals FileSetter) error {
if len(o) == 0 {
return nil
}
_, err := Files.Update(vals.UpdateMod(), o.UpdateMod()).All(ctx, exec)
return err
}
func (o FileSlice) DeleteAll(ctx context.Context, exec bob.Executor) error {
if len(o) == 0 {
return nil
}
_, err := Files.Delete(o.DeleteMod()).Exec(ctx, exec)
return err
}
func (o FileSlice) ReloadAll(ctx context.Context, exec bob.Executor) error {
if len(o) == 0 {
return nil
}
o2, err := Files.Query(sm.Where(o.pkIN())).All(ctx, exec)
if err != nil {
return err
}
o.copyMatchingRows(o2...)
return nil
}
type fileJoins[Q dialect.Joinable] struct {
typ string
User func(context.Context) modAs[Q, userColumns]
ProfilePictureUsers func(context.Context) modAs[Q, userColumns]
}
func (j fileJoins[Q]) aliasedAs(alias string) fileJoins[Q] {
return buildFileJoins[Q](buildFileColumns(alias), j.typ)
}
func buildFileJoins[Q dialect.Joinable](cols fileColumns, typ string) fileJoins[Q] {
return fileJoins[Q]{
typ: typ,
User: filesJoinUser[Q](cols, typ),
ProfilePictureUsers: filesJoinProfilePictureUsers[Q](cols, typ),
}
}
func filesJoinUser[Q dialect.Joinable](from fileColumns, typ string) func(context.Context) modAs[Q, userColumns] {
return func(ctx context.Context) modAs[Q, userColumns] {
return modAs[Q, userColumns]{
c: UserColumns,
f: func(to userColumns) bob.Mod[Q] {
mods := make(mods.QueryMods[Q], 0, 1)
{
mods = append(mods, dialect.Join[Q](typ, Users.Name().As(to.Alias())).On(
to.ID.EQ(from.UserID),
))
}
return mods
},
}
}
}
func filesJoinProfilePictureUsers[Q dialect.Joinable](from fileColumns, typ string) func(context.Context) modAs[Q, userColumns] {
return func(ctx context.Context) modAs[Q, userColumns] {
return modAs[Q, userColumns]{
c: UserColumns,
f: func(to userColumns) bob.Mod[Q] {
mods := make(mods.QueryMods[Q], 0, 1)
{
mods = append(mods, dialect.Join[Q](typ, Users.Name().As(to.Alias())).On(
to.ProfilePictureID.EQ(from.ID),
))
}
return mods
},
}
}
}
// User starts a query for related objects on user
func (o *File) User(mods ...bob.Mod[*dialect.SelectQuery]) UsersQuery {
return Users.Query(append(mods,
sm.Where(UserColumns.ID.EQ(sqlite.Arg(o.UserID))),
)...)
}
func (os FileSlice) User(mods ...bob.Mod[*dialect.SelectQuery]) UsersQuery {
PKArgs := make([]bob.Expression, len(os))
for i, o := range os {
PKArgs[i] = sqlite.ArgGroup(o.UserID)
}
return Users.Query(append(mods,
sm.Where(sqlite.Group(UserColumns.ID).In(PKArgs...)),
)...)
}
// ProfilePictureUsers starts a query for related objects on user
func (o *File) ProfilePictureUsers(mods ...bob.Mod[*dialect.SelectQuery]) UsersQuery {
return Users.Query(append(mods,
sm.Where(UserColumns.ProfilePictureID.EQ(sqlite.Arg(o.ID))),
)...)
}
func (os FileSlice) ProfilePictureUsers(mods ...bob.Mod[*dialect.SelectQuery]) UsersQuery {
PKArgs := make([]bob.Expression, len(os))
for i, o := range os {
PKArgs[i] = sqlite.ArgGroup(o.ID)
}
return Users.Query(append(mods,
sm.Where(sqlite.Group(UserColumns.ProfilePictureID).In(PKArgs...)),
)...)
}
func (o *File) Preload(name string, retrieved any) error {
if o == nil {
return nil
}
switch name {
case "User":
rel, ok := retrieved.(*User)
if !ok {
return fmt.Errorf("file cannot load %T as %q", retrieved, name)
}
o.R.User = rel
if rel != nil {
rel.R.Files = FileSlice{o}
}
return nil
case "ProfilePictureUsers":
rels, ok := retrieved.(UserSlice)
if !ok {
return fmt.Errorf("file cannot load %T as %q", retrieved, name)
}
o.R.ProfilePictureUsers = rels
for _, rel := range rels {
if rel != nil {
rel.R.ProfilePictureFile = o
}
}
return nil
default:
return fmt.Errorf("file has no relationship %q", name)
}
}
func PreloadFileUser(opts ...sqlite.PreloadOption) sqlite.Preloader {
return sqlite.Preload[*User, UserSlice](orm.Relationship{
Name: "User",
Sides: []orm.RelSide{
{
From: TableNames.Files,
To: TableNames.Users,
FromColumns: []string{
ColumnNames.Files.UserID,
},
ToColumns: []string{
ColumnNames.Users.ID,
},
},
},
}, Users.Columns().Names(), opts...)
}
func ThenLoadFileUser(queryMods ...bob.Mod[*dialect.SelectQuery]) sqlite.Loader {
return sqlite.Loader(func(ctx context.Context, exec bob.Executor, retrieved any) error {
loader, isLoader := retrieved.(interface {
LoadFileUser(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error
})
if !isLoader {
return fmt.Errorf("object %T cannot load FileUser", retrieved)
}
err := loader.LoadFileUser(ctx, exec, queryMods...)
// Don't cause an issue due to missing relationships
if errors.Is(err, sql.ErrNoRows) {
return nil
}
return err
})
}
// LoadFileUser loads the file's User into the .R struct
func (o *File) LoadFileUser(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error {
if o == nil {
return nil
}
// Reset the relationship
o.R.User = nil
related, err := o.User(mods...).One(ctx, exec)
if err != nil {
return err
}
related.R.Files = FileSlice{o}
o.R.User = related
return nil
}
// LoadFileUser loads the file's User into the .R struct
func (os FileSlice) LoadFileUser(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error {
if len(os) == 0 {
return nil
}
users, err := os.User(mods...).All(ctx, exec)
if err != nil {
return err
}
for _, o := range os {
for _, rel := range users {
if o.UserID != rel.ID {
continue
}
rel.R.Files = append(rel.R.Files, o)
o.R.User = rel
break
}
}
return nil
}
func ThenLoadFileProfilePictureUsers(queryMods ...bob.Mod[*dialect.SelectQuery]) sqlite.Loader {
return sqlite.Loader(func(ctx context.Context, exec bob.Executor, retrieved any) error {
loader, isLoader := retrieved.(interface {
LoadFileProfilePictureUsers(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error
})
if !isLoader {
return fmt.Errorf("object %T cannot load FileProfilePictureUsers", retrieved)
}
err := loader.LoadFileProfilePictureUsers(ctx, exec, queryMods...)
// Don't cause an issue due to missing relationships
if errors.Is(err, sql.ErrNoRows) {
return nil
}
return err
})
}
// LoadFileProfilePictureUsers loads the file's ProfilePictureUsers into the .R struct
func (o *File) LoadFileProfilePictureUsers(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error {
if o == nil {
return nil
}
// Reset the relationship
o.R.ProfilePictureUsers = nil
related, err := o.ProfilePictureUsers(mods...).All(ctx, exec)
if err != nil {
return err
}
for _, rel := range related {
rel.R.ProfilePictureFile = o
}
o.R.ProfilePictureUsers = related
return nil
}
// LoadFileProfilePictureUsers loads the file's ProfilePictureUsers into the .R struct
func (os FileSlice) LoadFileProfilePictureUsers(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error {
if len(os) == 0 {
return nil
}
users, err := os.ProfilePictureUsers(mods...).All(ctx, exec)
if err != nil {
return err
}
for _, o := range os {
o.R.ProfilePictureUsers = nil
}
for _, o := range os {
for _, rel := range users {
if o.ID != rel.ProfilePictureID.GetOrZero() {
continue
}
rel.R.ProfilePictureFile = o
o.R.ProfilePictureUsers = append(o.R.ProfilePictureUsers, rel)
}
}
return nil
}
func attachFileUser0(ctx context.Context, exec bob.Executor, count int, file0 *File, user1 *User) (*File, error) {
setter := &FileSetter{
UserID: omit.From(user1.ID),
}
err := file0.Update(ctx, exec, setter)
if err != nil {
return nil, fmt.Errorf("attachFileUser0: %w", err)
}
return file0, nil
}
func (file0 *File) InsertUser(ctx context.Context, exec bob.Executor, related *UserSetter) error {
user1, err := Users.Insert(related).One(ctx, exec)
if err != nil {
return fmt.Errorf("inserting related objects: %w", err)
}
_, err = attachFileUser0(ctx, exec, 1, file0, user1)
if err != nil {
return err
}
file0.R.User = user1
user1.R.Files = append(user1.R.Files, file0)
return nil
}
func (file0 *File) AttachUser(ctx context.Context, exec bob.Executor, user1 *User) error {
var err error
_, err = attachFileUser0(ctx, exec, 1, file0, user1)
if err != nil {
return err
}
file0.R.User = user1
user1.R.Files = append(user1.R.Files, file0)
return nil
}
func insertFileProfilePictureUsers0(ctx context.Context, exec bob.Executor, users1 []*UserSetter, file0 *File) (UserSlice, error) {
for i := range users1 {
users1[i].ProfilePictureID = omitnull.From(file0.ID)
}
ret, err := Users.Insert(bob.ToMods(users1...)).All(ctx, exec)
if err != nil {
return ret, fmt.Errorf("insertFileProfilePictureUsers0: %w", err)
}
return ret, nil
}
func attachFileProfilePictureUsers0(ctx context.Context, exec bob.Executor, count int, users1 UserSlice, file0 *File) (UserSlice, error) {
setter := &UserSetter{
ProfilePictureID: omitnull.From(file0.ID),
}
err := users1.UpdateAll(ctx, exec, *setter)
if err != nil {
return nil, fmt.Errorf("attachFileProfilePictureUsers0: %w", err)
}
return users1, nil
}
func (file0 *File) InsertProfilePictureUsers(ctx context.Context, exec bob.Executor, related ...*UserSetter) error {
if len(related) == 0 {
return nil
}
var err error
users1, err := insertFileProfilePictureUsers0(ctx, exec, related, file0)
if err != nil {
return err
}
file0.R.ProfilePictureUsers = append(file0.R.ProfilePictureUsers, users1...)
for _, rel := range users1 {
rel.R.ProfilePictureFile = file0
}
return nil
}
func (file0 *File) AttachProfilePictureUsers(ctx context.Context, exec bob.Executor, related ...*User) error {
if len(related) == 0 {
return nil
}
var err error
users1 := UserSlice(related)
_, err = attachFileProfilePictureUsers0(ctx, exec, len(related), users1, file0)
if err != nil {
return err
}
file0.R.ProfilePictureUsers = append(file0.R.ProfilePictureUsers, users1...)
for _, rel := range related {
rel.R.ProfilePictureFile = file0
}
return nil
}

View File

@ -1,12 +0,0 @@
package models
type File struct {
ID uint32 `gorm:"primaryKey"`
Name string
Data []byte
// User
UserID uint
User User
}

View File

@ -0,0 +1,732 @@
// Code generated by BobGen sql (devel). DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package models
import (
"context"
"database/sql"
"errors"
"fmt"
"io"
"time"
"github.com/aarondl/opt/omit"
"github.com/stephenafamo/bob"
"github.com/stephenafamo/bob/dialect/sqlite"
"github.com/stephenafamo/bob/dialect/sqlite/dialect"
"github.com/stephenafamo/bob/dialect/sqlite/dm"
"github.com/stephenafamo/bob/dialect/sqlite/sm"
"github.com/stephenafamo/bob/dialect/sqlite/um"
"github.com/stephenafamo/bob/expr"
"github.com/stephenafamo/bob/mods"
"github.com/stephenafamo/bob/orm"
)
// Item is an object representing the database table.
type Item struct {
ID int64 `db:"id,pk" `
Name string `db:"name" `
Added time.Time `db:"added" `
Description string `db:"description" `
Price float32 `db:"price" `
Quantity int64 `db:"quantity" `
UserID int64 `db:"user_id" `
R itemR `db:"-" `
}
// ItemSlice is an alias for a slice of pointers to Item.
// This should almost always be used instead of []*Item.
type ItemSlice []*Item
// Items contains methods to work with the item table
var Items = sqlite.NewTablex[*Item, ItemSlice, *ItemSetter]("", "item")
// ItemsQuery is a query on the item table
type ItemsQuery = *sqlite.ViewQuery[*Item, ItemSlice]
// itemR is where relationships are stored.
type itemR struct {
User *User // fk_item_0
}
type itemColumnNames struct {
ID string
Name string
Added string
Description string
Price string
Quantity string
UserID string
}
var ItemColumns = buildItemColumns("item")
type itemColumns struct {
tableAlias string
ID sqlite.Expression
Name sqlite.Expression
Added sqlite.Expression
Description sqlite.Expression
Price sqlite.Expression
Quantity sqlite.Expression
UserID sqlite.Expression
}
func (c itemColumns) Alias() string {
return c.tableAlias
}
func (itemColumns) AliasedAs(alias string) itemColumns {
return buildItemColumns(alias)
}
func buildItemColumns(alias string) itemColumns {
return itemColumns{
tableAlias: alias,
ID: sqlite.Quote(alias, "id"),
Name: sqlite.Quote(alias, "name"),
Added: sqlite.Quote(alias, "added"),
Description: sqlite.Quote(alias, "description"),
Price: sqlite.Quote(alias, "price"),
Quantity: sqlite.Quote(alias, "quantity"),
UserID: sqlite.Quote(alias, "user_id"),
}
}
type itemWhere[Q sqlite.Filterable] struct {
ID sqlite.WhereMod[Q, int64]
Name sqlite.WhereMod[Q, string]
Added sqlite.WhereMod[Q, time.Time]
Description sqlite.WhereMod[Q, string]
Price sqlite.WhereMod[Q, float32]
Quantity sqlite.WhereMod[Q, int64]
UserID sqlite.WhereMod[Q, int64]
}
func (itemWhere[Q]) AliasedAs(alias string) itemWhere[Q] {
return buildItemWhere[Q](buildItemColumns(alias))
}
func buildItemWhere[Q sqlite.Filterable](cols itemColumns) itemWhere[Q] {
return itemWhere[Q]{
ID: sqlite.Where[Q, int64](cols.ID),
Name: sqlite.Where[Q, string](cols.Name),
Added: sqlite.Where[Q, time.Time](cols.Added),
Description: sqlite.Where[Q, string](cols.Description),
Price: sqlite.Where[Q, float32](cols.Price),
Quantity: sqlite.Where[Q, int64](cols.Quantity),
UserID: sqlite.Where[Q, int64](cols.UserID),
}
}
var ItemErrors = &itemErrors{
ErrUniquePkMainItem: &UniqueConstraintError{s: "pk_main_item"},
}
type itemErrors struct {
ErrUniquePkMainItem *UniqueConstraintError
}
// ItemSetter is used for insert/upsert/update operations
// All values are optional, and do not have to be set
// Generated columns are not included
type ItemSetter struct {
ID omit.Val[int64] `db:"id,pk" `
Name omit.Val[string] `db:"name" `
Added omit.Val[time.Time] `db:"added" `
Description omit.Val[string] `db:"description" `
Price omit.Val[float32] `db:"price" `
Quantity omit.Val[int64] `db:"quantity" `
UserID omit.Val[int64] `db:"user_id" `
}
func (s ItemSetter) SetColumns() []string {
vals := make([]string, 0, 7)
if !s.ID.IsUnset() {
vals = append(vals, "id")
}
if !s.Name.IsUnset() {
vals = append(vals, "name")
}
if !s.Added.IsUnset() {
vals = append(vals, "added")
}
if !s.Description.IsUnset() {
vals = append(vals, "description")
}
if !s.Price.IsUnset() {
vals = append(vals, "price")
}
if !s.Quantity.IsUnset() {
vals = append(vals, "quantity")
}
if !s.UserID.IsUnset() {
vals = append(vals, "user_id")
}
return vals
}
func (s ItemSetter) Overwrite(t *Item) {
if !s.ID.IsUnset() {
t.ID, _ = s.ID.Get()
}
if !s.Name.IsUnset() {
t.Name, _ = s.Name.Get()
}
if !s.Added.IsUnset() {
t.Added, _ = s.Added.Get()
}
if !s.Description.IsUnset() {
t.Description, _ = s.Description.Get()
}
if !s.Price.IsUnset() {
t.Price, _ = s.Price.Get()
}
if !s.Quantity.IsUnset() {
t.Quantity, _ = s.Quantity.Get()
}
if !s.UserID.IsUnset() {
t.UserID, _ = s.UserID.Get()
}
}
func (s *ItemSetter) Apply(q *dialect.InsertQuery) {
q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) {
return Items.BeforeInsertHooks.RunHooks(ctx, exec, s)
})
if len(q.Table.Columns) == 0 {
q.Table.Columns = s.SetColumns()
}
q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
vals := make([]bob.Expression, 0, 7)
if !s.ID.IsUnset() {
vals = append(vals, sqlite.Arg(s.ID))
}
if !s.Name.IsUnset() {
vals = append(vals, sqlite.Arg(s.Name))
}
if !s.Added.IsUnset() {
vals = append(vals, sqlite.Arg(s.Added))
}
if !s.Description.IsUnset() {
vals = append(vals, sqlite.Arg(s.Description))
}
if !s.Price.IsUnset() {
vals = append(vals, sqlite.Arg(s.Price))
}
if !s.Quantity.IsUnset() {
vals = append(vals, sqlite.Arg(s.Quantity))
}
if !s.UserID.IsUnset() {
vals = append(vals, sqlite.Arg(s.UserID))
}
return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "")
}))
}
func (s ItemSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] {
return um.Set(s.Expressions()...)
}
func (s ItemSetter) Expressions(prefix ...string) []bob.Expression {
exprs := make([]bob.Expression, 0, 7)
if !s.ID.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
sqlite.Quote(append(prefix, "id")...),
sqlite.Arg(s.ID),
}})
}
if !s.Name.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
sqlite.Quote(append(prefix, "name")...),
sqlite.Arg(s.Name),
}})
}
if !s.Added.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
sqlite.Quote(append(prefix, "added")...),
sqlite.Arg(s.Added),
}})
}
if !s.Description.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
sqlite.Quote(append(prefix, "description")...),
sqlite.Arg(s.Description),
}})
}
if !s.Price.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
sqlite.Quote(append(prefix, "price")...),
sqlite.Arg(s.Price),
}})
}
if !s.Quantity.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
sqlite.Quote(append(prefix, "quantity")...),
sqlite.Arg(s.Quantity),
}})
}
if !s.UserID.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
sqlite.Quote(append(prefix, "user_id")...),
sqlite.Arg(s.UserID),
}})
}
return exprs
}
// FindItem retrieves a single record by primary key
// If cols is empty Find will return all columns.
func FindItem(ctx context.Context, exec bob.Executor, IDPK int64, cols ...string) (*Item, error) {
if len(cols) == 0 {
return Items.Query(
SelectWhere.Items.ID.EQ(IDPK),
).One(ctx, exec)
}
return Items.Query(
SelectWhere.Items.ID.EQ(IDPK),
sm.Columns(Items.Columns().Only(cols...)),
).One(ctx, exec)
}
// ItemExists checks the presence of a single record by primary key
func ItemExists(ctx context.Context, exec bob.Executor, IDPK int64) (bool, error) {
return Items.Query(
SelectWhere.Items.ID.EQ(IDPK),
).Exists(ctx, exec)
}
// AfterQueryHook is called after Item is retrieved from the database
func (o *Item) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error {
var err error
switch queryType {
case bob.QueryTypeSelect:
ctx, err = Items.AfterSelectHooks.RunHooks(ctx, exec, ItemSlice{o})
case bob.QueryTypeInsert:
ctx, err = Items.AfterInsertHooks.RunHooks(ctx, exec, ItemSlice{o})
case bob.QueryTypeUpdate:
ctx, err = Items.AfterUpdateHooks.RunHooks(ctx, exec, ItemSlice{o})
case bob.QueryTypeDelete:
ctx, err = Items.AfterDeleteHooks.RunHooks(ctx, exec, ItemSlice{o})
}
return err
}
// PrimaryKeyVals returns the primary key values of the Item
func (o *Item) PrimaryKeyVals() bob.Expression {
return sqlite.Arg(o.ID)
}
func (o *Item) pkEQ() dialect.Expression {
return sqlite.Quote("item", "id").EQ(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
return o.PrimaryKeyVals().WriteSQL(ctx, w, d, start)
}))
}
// Update uses an executor to update the Item
func (o *Item) Update(ctx context.Context, exec bob.Executor, s *ItemSetter) error {
v, err := Items.Update(s.UpdateMod(), um.Where(o.pkEQ())).One(ctx, exec)
if err != nil {
return err
}
o.R = v.R
*o = *v
return nil
}
// Delete deletes a single Item record with an executor
func (o *Item) Delete(ctx context.Context, exec bob.Executor) error {
_, err := Items.Delete(dm.Where(o.pkEQ())).Exec(ctx, exec)
return err
}
// Reload refreshes the Item using the executor
func (o *Item) Reload(ctx context.Context, exec bob.Executor) error {
o2, err := Items.Query(
SelectWhere.Items.ID.EQ(o.ID),
).One(ctx, exec)
if err != nil {
return err
}
o2.R = o.R
*o = *o2
return nil
}
// AfterQueryHook is called after ItemSlice is retrieved from the database
func (o ItemSlice) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error {
var err error
switch queryType {
case bob.QueryTypeSelect:
ctx, err = Items.AfterSelectHooks.RunHooks(ctx, exec, o)
case bob.QueryTypeInsert:
ctx, err = Items.AfterInsertHooks.RunHooks(ctx, exec, o)
case bob.QueryTypeUpdate:
ctx, err = Items.AfterUpdateHooks.RunHooks(ctx, exec, o)
case bob.QueryTypeDelete:
ctx, err = Items.AfterDeleteHooks.RunHooks(ctx, exec, o)
}
return err
}
func (o ItemSlice) pkIN() dialect.Expression {
if len(o) == 0 {
return sqlite.Raw("NULL")
}
return sqlite.Quote("item", "id").In(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
pkPairs := make([]bob.Expression, len(o))
for i, row := range o {
pkPairs[i] = row.PrimaryKeyVals()
}
return bob.ExpressSlice(ctx, w, d, start, pkPairs, "", ", ", "")
}))
}
// copyMatchingRows finds models in the given slice that have the same primary key
// then it first copies the existing relationships from the old model to the new model
// and then replaces the old model in the slice with the new model
func (o ItemSlice) copyMatchingRows(from ...*Item) {
for i, old := range o {
for _, new := range from {
if new.ID != old.ID {
continue
}
new.R = old.R
o[i] = new
break
}
}
}
// UpdateMod modifies an update query with "WHERE primary_key IN (o...)"
func (o ItemSlice) UpdateMod() bob.Mod[*dialect.UpdateQuery] {
return bob.ModFunc[*dialect.UpdateQuery](func(q *dialect.UpdateQuery) {
q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) {
return Items.BeforeUpdateHooks.RunHooks(ctx, exec, o)
})
q.AppendLoader(bob.LoaderFunc(func(ctx context.Context, exec bob.Executor, retrieved any) error {
var err error
switch retrieved := retrieved.(type) {
case *Item:
o.copyMatchingRows(retrieved)
case []*Item:
o.copyMatchingRows(retrieved...)
case ItemSlice:
o.copyMatchingRows(retrieved...)
default:
// If the retrieved value is not a Item or a slice of Item
// then run the AfterUpdateHooks on the slice
_, err = Items.AfterUpdateHooks.RunHooks(ctx, exec, o)
}
return err
}))
q.AppendWhere(o.pkIN())
})
}
// DeleteMod modifies an delete query with "WHERE primary_key IN (o...)"
func (o ItemSlice) DeleteMod() bob.Mod[*dialect.DeleteQuery] {
return bob.ModFunc[*dialect.DeleteQuery](func(q *dialect.DeleteQuery) {
q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) {
return Items.BeforeDeleteHooks.RunHooks(ctx, exec, o)
})
q.AppendLoader(bob.LoaderFunc(func(ctx context.Context, exec bob.Executor, retrieved any) error {
var err error
switch retrieved := retrieved.(type) {
case *Item:
o.copyMatchingRows(retrieved)
case []*Item:
o.copyMatchingRows(retrieved...)
case ItemSlice:
o.copyMatchingRows(retrieved...)
default:
// If the retrieved value is not a Item or a slice of Item
// then run the AfterDeleteHooks on the slice
_, err = Items.AfterDeleteHooks.RunHooks(ctx, exec, o)
}
return err
}))
q.AppendWhere(o.pkIN())
})
}
func (o ItemSlice) UpdateAll(ctx context.Context, exec bob.Executor, vals ItemSetter) error {
if len(o) == 0 {
return nil
}
_, err := Items.Update(vals.UpdateMod(), o.UpdateMod()).All(ctx, exec)
return err
}
func (o ItemSlice) DeleteAll(ctx context.Context, exec bob.Executor) error {
if len(o) == 0 {
return nil
}
_, err := Items.Delete(o.DeleteMod()).Exec(ctx, exec)
return err
}
func (o ItemSlice) ReloadAll(ctx context.Context, exec bob.Executor) error {
if len(o) == 0 {
return nil
}
o2, err := Items.Query(sm.Where(o.pkIN())).All(ctx, exec)
if err != nil {
return err
}
o.copyMatchingRows(o2...)
return nil
}
type itemJoins[Q dialect.Joinable] struct {
typ string
User func(context.Context) modAs[Q, userColumns]
}
func (j itemJoins[Q]) aliasedAs(alias string) itemJoins[Q] {
return buildItemJoins[Q](buildItemColumns(alias), j.typ)
}
func buildItemJoins[Q dialect.Joinable](cols itemColumns, typ string) itemJoins[Q] {
return itemJoins[Q]{
typ: typ,
User: itemsJoinUser[Q](cols, typ),
}
}
func itemsJoinUser[Q dialect.Joinable](from itemColumns, typ string) func(context.Context) modAs[Q, userColumns] {
return func(ctx context.Context) modAs[Q, userColumns] {
return modAs[Q, userColumns]{
c: UserColumns,
f: func(to userColumns) bob.Mod[Q] {
mods := make(mods.QueryMods[Q], 0, 1)
{
mods = append(mods, dialect.Join[Q](typ, Users.Name().As(to.Alias())).On(
to.ID.EQ(from.UserID),
))
}
return mods
},
}
}
}
// User starts a query for related objects on user
func (o *Item) User(mods ...bob.Mod[*dialect.SelectQuery]) UsersQuery {
return Users.Query(append(mods,
sm.Where(UserColumns.ID.EQ(sqlite.Arg(o.UserID))),
)...)
}
func (os ItemSlice) User(mods ...bob.Mod[*dialect.SelectQuery]) UsersQuery {
PKArgs := make([]bob.Expression, len(os))
for i, o := range os {
PKArgs[i] = sqlite.ArgGroup(o.UserID)
}
return Users.Query(append(mods,
sm.Where(sqlite.Group(UserColumns.ID).In(PKArgs...)),
)...)
}
func (o *Item) Preload(name string, retrieved any) error {
if o == nil {
return nil
}
switch name {
case "User":
rel, ok := retrieved.(*User)
if !ok {
return fmt.Errorf("item cannot load %T as %q", retrieved, name)
}
o.R.User = rel
if rel != nil {
rel.R.Items = ItemSlice{o}
}
return nil
default:
return fmt.Errorf("item has no relationship %q", name)
}
}
func PreloadItemUser(opts ...sqlite.PreloadOption) sqlite.Preloader {
return sqlite.Preload[*User, UserSlice](orm.Relationship{
Name: "User",
Sides: []orm.RelSide{
{
From: TableNames.Items,
To: TableNames.Users,
FromColumns: []string{
ColumnNames.Items.UserID,
},
ToColumns: []string{
ColumnNames.Users.ID,
},
},
},
}, Users.Columns().Names(), opts...)
}
func ThenLoadItemUser(queryMods ...bob.Mod[*dialect.SelectQuery]) sqlite.Loader {
return sqlite.Loader(func(ctx context.Context, exec bob.Executor, retrieved any) error {
loader, isLoader := retrieved.(interface {
LoadItemUser(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error
})
if !isLoader {
return fmt.Errorf("object %T cannot load ItemUser", retrieved)
}
err := loader.LoadItemUser(ctx, exec, queryMods...)
// Don't cause an issue due to missing relationships
if errors.Is(err, sql.ErrNoRows) {
return nil
}
return err
})
}
// LoadItemUser loads the item's User into the .R struct
func (o *Item) LoadItemUser(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error {
if o == nil {
return nil
}
// Reset the relationship
o.R.User = nil
related, err := o.User(mods...).One(ctx, exec)
if err != nil {
return err
}
related.R.Items = ItemSlice{o}
o.R.User = related
return nil
}
// LoadItemUser loads the item's User into the .R struct
func (os ItemSlice) LoadItemUser(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error {
if len(os) == 0 {
return nil
}
users, err := os.User(mods...).All(ctx, exec)
if err != nil {
return err
}
for _, o := range os {
for _, rel := range users {
if o.UserID != rel.ID {
continue
}
rel.R.Items = append(rel.R.Items, o)
o.R.User = rel
break
}
}
return nil
}
func attachItemUser0(ctx context.Context, exec bob.Executor, count int, item0 *Item, user1 *User) (*Item, error) {
setter := &ItemSetter{
UserID: omit.From(user1.ID),
}
err := item0.Update(ctx, exec, setter)
if err != nil {
return nil, fmt.Errorf("attachItemUser0: %w", err)
}
return item0, nil
}
func (item0 *Item) InsertUser(ctx context.Context, exec bob.Executor, related *UserSetter) error {
user1, err := Users.Insert(related).One(ctx, exec)
if err != nil {
return fmt.Errorf("inserting related objects: %w", err)
}
_, err = attachItemUser0(ctx, exec, 1, item0, user1)
if err != nil {
return err
}
item0.R.User = user1
user1.R.Items = append(user1.R.Items, item0)
return nil
}
func (item0 *Item) AttachUser(ctx context.Context, exec bob.Executor, user1 *User) error {
var err error
_, err = attachItemUser0(ctx, exec, 1, item0, user1)
if err != nil {
return err
}
item0.R.User = user1
user1.R.Items = append(user1.R.Items, item0)
return nil
}

View File

@ -1,33 +0,0 @@
package models
import (
"time"
itemv1 "github.com/spotdemo4/trevstack/server/internal/services/item/v1"
"google.golang.org/protobuf/types/known/timestamppb"
)
type Item struct {
ID uint32 `gorm:"primaryKey"`
Name string
Description string
Price float32
Quantity int
Added time.Time
// User
UserID uint
User User
}
func (i Item) ToConnectV1() *itemv1.Item {
return &itemv1.Item{
Id: &i.ID,
Name: i.Name,
Description: i.Description,
Price: i.Price,
Quantity: uint32(i.Quantity),
Added: timestamppb.New(i.Added),
}
}

View File

@ -0,0 +1,361 @@
// Code generated by BobGen sql (devel). DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package models
import (
"context"
"io"
"github.com/aarondl/opt/omit"
"github.com/stephenafamo/bob"
"github.com/stephenafamo/bob/dialect/sqlite"
"github.com/stephenafamo/bob/dialect/sqlite/dialect"
"github.com/stephenafamo/bob/dialect/sqlite/dm"
"github.com/stephenafamo/bob/dialect/sqlite/sm"
"github.com/stephenafamo/bob/dialect/sqlite/um"
"github.com/stephenafamo/bob/expr"
)
// SchemaMigration is an object representing the database table.
type SchemaMigration struct {
Version string `db:"version,pk" `
}
// SchemaMigrationSlice is an alias for a slice of pointers to SchemaMigration.
// This should almost always be used instead of []*SchemaMigration.
type SchemaMigrationSlice []*SchemaMigration
// SchemaMigrations contains methods to work with the schema_migrations table
var SchemaMigrations = sqlite.NewTablex[*SchemaMigration, SchemaMigrationSlice, *SchemaMigrationSetter]("", "schema_migrations")
// SchemaMigrationsQuery is a query on the schema_migrations table
type SchemaMigrationsQuery = *sqlite.ViewQuery[*SchemaMigration, SchemaMigrationSlice]
type schemaMigrationColumnNames struct {
Version string
}
var SchemaMigrationColumns = buildSchemaMigrationColumns("schema_migrations")
type schemaMigrationColumns struct {
tableAlias string
Version sqlite.Expression
}
func (c schemaMigrationColumns) Alias() string {
return c.tableAlias
}
func (schemaMigrationColumns) AliasedAs(alias string) schemaMigrationColumns {
return buildSchemaMigrationColumns(alias)
}
func buildSchemaMigrationColumns(alias string) schemaMigrationColumns {
return schemaMigrationColumns{
tableAlias: alias,
Version: sqlite.Quote(alias, "version"),
}
}
type schemaMigrationWhere[Q sqlite.Filterable] struct {
Version sqlite.WhereMod[Q, string]
}
func (schemaMigrationWhere[Q]) AliasedAs(alias string) schemaMigrationWhere[Q] {
return buildSchemaMigrationWhere[Q](buildSchemaMigrationColumns(alias))
}
func buildSchemaMigrationWhere[Q sqlite.Filterable](cols schemaMigrationColumns) schemaMigrationWhere[Q] {
return schemaMigrationWhere[Q]{
Version: sqlite.Where[Q, string](cols.Version),
}
}
var SchemaMigrationErrors = &schemaMigrationErrors{
ErrUniqueSqliteAutoindexSchemaMigrations1: &UniqueConstraintError{s: "sqlite_autoindex_schema_migrations_1"},
}
type schemaMigrationErrors struct {
ErrUniqueSqliteAutoindexSchemaMigrations1 *UniqueConstraintError
}
// SchemaMigrationSetter is used for insert/upsert/update operations
// All values are optional, and do not have to be set
// Generated columns are not included
type SchemaMigrationSetter struct {
Version omit.Val[string] `db:"version,pk" `
}
func (s SchemaMigrationSetter) SetColumns() []string {
vals := make([]string, 0, 1)
if !s.Version.IsUnset() {
vals = append(vals, "version")
}
return vals
}
func (s SchemaMigrationSetter) Overwrite(t *SchemaMigration) {
if !s.Version.IsUnset() {
t.Version, _ = s.Version.Get()
}
}
func (s *SchemaMigrationSetter) Apply(q *dialect.InsertQuery) {
q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) {
return SchemaMigrations.BeforeInsertHooks.RunHooks(ctx, exec, s)
})
if len(q.Table.Columns) == 0 {
q.Table.Columns = s.SetColumns()
}
q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
vals := make([]bob.Expression, 0, 1)
if !s.Version.IsUnset() {
vals = append(vals, sqlite.Arg(s.Version))
}
return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "")
}))
}
func (s SchemaMigrationSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] {
return um.Set(s.Expressions()...)
}
func (s SchemaMigrationSetter) Expressions(prefix ...string) []bob.Expression {
exprs := make([]bob.Expression, 0, 1)
if !s.Version.IsUnset() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
sqlite.Quote(append(prefix, "version")...),
sqlite.Arg(s.Version),
}})
}
return exprs
}
// FindSchemaMigration retrieves a single record by primary key
// If cols is empty Find will return all columns.
func FindSchemaMigration(ctx context.Context, exec bob.Executor, VersionPK string, cols ...string) (*SchemaMigration, error) {
if len(cols) == 0 {
return SchemaMigrations.Query(
SelectWhere.SchemaMigrations.Version.EQ(VersionPK),
).One(ctx, exec)
}
return SchemaMigrations.Query(
SelectWhere.SchemaMigrations.Version.EQ(VersionPK),
sm.Columns(SchemaMigrations.Columns().Only(cols...)),
).One(ctx, exec)
}
// SchemaMigrationExists checks the presence of a single record by primary key
func SchemaMigrationExists(ctx context.Context, exec bob.Executor, VersionPK string) (bool, error) {
return SchemaMigrations.Query(
SelectWhere.SchemaMigrations.Version.EQ(VersionPK),
).Exists(ctx, exec)
}
// AfterQueryHook is called after SchemaMigration is retrieved from the database
func (o *SchemaMigration) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error {
var err error
switch queryType {
case bob.QueryTypeSelect:
ctx, err = SchemaMigrations.AfterSelectHooks.RunHooks(ctx, exec, SchemaMigrationSlice{o})
case bob.QueryTypeInsert:
ctx, err = SchemaMigrations.AfterInsertHooks.RunHooks(ctx, exec, SchemaMigrationSlice{o})
case bob.QueryTypeUpdate:
ctx, err = SchemaMigrations.AfterUpdateHooks.RunHooks(ctx, exec, SchemaMigrationSlice{o})
case bob.QueryTypeDelete:
ctx, err = SchemaMigrations.AfterDeleteHooks.RunHooks(ctx, exec, SchemaMigrationSlice{o})
}
return err
}
// PrimaryKeyVals returns the primary key values of the SchemaMigration
func (o *SchemaMigration) PrimaryKeyVals() bob.Expression {
return sqlite.Arg(o.Version)
}
func (o *SchemaMigration) pkEQ() dialect.Expression {
return sqlite.Quote("schema_migrations", "version").EQ(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
return o.PrimaryKeyVals().WriteSQL(ctx, w, d, start)
}))
}
// Update uses an executor to update the SchemaMigration
func (o *SchemaMigration) Update(ctx context.Context, exec bob.Executor, s *SchemaMigrationSetter) error {
v, err := SchemaMigrations.Update(s.UpdateMod(), um.Where(o.pkEQ())).One(ctx, exec)
if err != nil {
return err
}
*o = *v
return nil
}
// Delete deletes a single SchemaMigration record with an executor
func (o *SchemaMigration) Delete(ctx context.Context, exec bob.Executor) error {
_, err := SchemaMigrations.Delete(dm.Where(o.pkEQ())).Exec(ctx, exec)
return err
}
// Reload refreshes the SchemaMigration using the executor
func (o *SchemaMigration) Reload(ctx context.Context, exec bob.Executor) error {
o2, err := SchemaMigrations.Query(
SelectWhere.SchemaMigrations.Version.EQ(o.Version),
).One(ctx, exec)
if err != nil {
return err
}
*o = *o2
return nil
}
// AfterQueryHook is called after SchemaMigrationSlice is retrieved from the database
func (o SchemaMigrationSlice) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error {
var err error
switch queryType {
case bob.QueryTypeSelect:
ctx, err = SchemaMigrations.AfterSelectHooks.RunHooks(ctx, exec, o)
case bob.QueryTypeInsert:
ctx, err = SchemaMigrations.AfterInsertHooks.RunHooks(ctx, exec, o)
case bob.QueryTypeUpdate:
ctx, err = SchemaMigrations.AfterUpdateHooks.RunHooks(ctx, exec, o)
case bob.QueryTypeDelete:
ctx, err = SchemaMigrations.AfterDeleteHooks.RunHooks(ctx, exec, o)
}
return err
}
func (o SchemaMigrationSlice) pkIN() dialect.Expression {
if len(o) == 0 {
return sqlite.Raw("NULL")
}
return sqlite.Quote("schema_migrations", "version").In(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
pkPairs := make([]bob.Expression, len(o))
for i, row := range o {
pkPairs[i] = row.PrimaryKeyVals()
}
return bob.ExpressSlice(ctx, w, d, start, pkPairs, "", ", ", "")
}))
}
// copyMatchingRows finds models in the given slice that have the same primary key
// then it first copies the existing relationships from the old model to the new model
// and then replaces the old model in the slice with the new model
func (o SchemaMigrationSlice) copyMatchingRows(from ...*SchemaMigration) {
for i, old := range o {
for _, new := range from {
if new.Version != old.Version {
continue
}
o[i] = new
break
}
}
}
// UpdateMod modifies an update query with "WHERE primary_key IN (o...)"
func (o SchemaMigrationSlice) UpdateMod() bob.Mod[*dialect.UpdateQuery] {
return bob.ModFunc[*dialect.UpdateQuery](func(q *dialect.UpdateQuery) {
q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) {
return SchemaMigrations.BeforeUpdateHooks.RunHooks(ctx, exec, o)
})
q.AppendLoader(bob.LoaderFunc(func(ctx context.Context, exec bob.Executor, retrieved any) error {
var err error
switch retrieved := retrieved.(type) {
case *SchemaMigration:
o.copyMatchingRows(retrieved)
case []*SchemaMigration:
o.copyMatchingRows(retrieved...)
case SchemaMigrationSlice:
o.copyMatchingRows(retrieved...)
default:
// If the retrieved value is not a SchemaMigration or a slice of SchemaMigration
// then run the AfterUpdateHooks on the slice
_, err = SchemaMigrations.AfterUpdateHooks.RunHooks(ctx, exec, o)
}
return err
}))
q.AppendWhere(o.pkIN())
})
}
// DeleteMod modifies an delete query with "WHERE primary_key IN (o...)"
func (o SchemaMigrationSlice) DeleteMod() bob.Mod[*dialect.DeleteQuery] {
return bob.ModFunc[*dialect.DeleteQuery](func(q *dialect.DeleteQuery) {
q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) {
return SchemaMigrations.BeforeDeleteHooks.RunHooks(ctx, exec, o)
})
q.AppendLoader(bob.LoaderFunc(func(ctx context.Context, exec bob.Executor, retrieved any) error {
var err error
switch retrieved := retrieved.(type) {
case *SchemaMigration:
o.copyMatchingRows(retrieved)
case []*SchemaMigration:
o.copyMatchingRows(retrieved...)
case SchemaMigrationSlice:
o.copyMatchingRows(retrieved...)
default:
// If the retrieved value is not a SchemaMigration or a slice of SchemaMigration
// then run the AfterDeleteHooks on the slice
_, err = SchemaMigrations.AfterDeleteHooks.RunHooks(ctx, exec, o)
}
return err
}))
q.AppendWhere(o.pkIN())
})
}
func (o SchemaMigrationSlice) UpdateAll(ctx context.Context, exec bob.Executor, vals SchemaMigrationSetter) error {
if len(o) == 0 {
return nil
}
_, err := SchemaMigrations.Update(vals.UpdateMod(), o.UpdateMod()).All(ctx, exec)
return err
}
func (o SchemaMigrationSlice) DeleteAll(ctx context.Context, exec bob.Executor) error {
if len(o) == 0 {
return nil
}
_, err := SchemaMigrations.Delete(o.DeleteMod()).Exec(ctx, exec)
return err
}
func (o SchemaMigrationSlice) ReloadAll(ctx context.Context, exec bob.Executor) error {
if len(o) == 0 {
return nil
}
o2, err := SchemaMigrations.Query(sm.Where(o.pkIN())).All(ctx, exec)
if err != nil {
return err
}
o.copyMatchingRows(o2...)
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -1,32 +0,0 @@
package models
import (
"fmt"
userv1 "github.com/spotdemo4/trevstack/server/internal/services/user/v1"
)
type User struct {
ID uint32 `gorm:"primaryKey"`
Username string
Password string
// Profile picture
ProfilePictureID *uint
ProfilePicture *File
}
func (u User) ToConnectV1() *userv1.User {
var ppid *string
if u.ProfilePicture != nil {
id := fmt.Sprintf("/file/%d", u.ProfilePicture.ID)
ppid = &id
}
return &userv1.User{
Id: u.ID,
Username: u.Username,
ProfilePicture: ppid,
}
}

View File

@ -24,11 +24,11 @@ const (
type Item struct { type Item struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Id *uint32 `protobuf:"varint,1,opt,name=id,proto3,oneof" json:"id,omitempty"` Id *int64 `protobuf:"varint,1,opt,name=id,proto3,oneof" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"`
Price float32 `protobuf:"fixed32,4,opt,name=price,proto3" json:"price,omitempty"` Price float32 `protobuf:"fixed32,4,opt,name=price,proto3" json:"price,omitempty"`
Quantity uint32 `protobuf:"varint,5,opt,name=quantity,proto3" json:"quantity,omitempty"` Quantity int32 `protobuf:"varint,5,opt,name=quantity,proto3" json:"quantity,omitempty"`
Added *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=added,proto3,oneof" json:"added,omitempty"` Added *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=added,proto3,oneof" json:"added,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@ -64,7 +64,7 @@ func (*Item) Descriptor() ([]byte, []int) {
return file_item_v1_item_proto_rawDescGZIP(), []int{0} return file_item_v1_item_proto_rawDescGZIP(), []int{0}
} }
func (x *Item) GetId() uint32 { func (x *Item) GetId() int64 {
if x != nil && x.Id != nil { if x != nil && x.Id != nil {
return *x.Id return *x.Id
} }
@ -92,7 +92,7 @@ func (x *Item) GetPrice() float32 {
return 0 return 0
} }
func (x *Item) GetQuantity() uint32 { func (x *Item) GetQuantity() int32 {
if x != nil { if x != nil {
return x.Quantity return x.Quantity
} }
@ -108,7 +108,7 @@ func (x *Item) GetAdded() *timestamppb.Timestamp {
type GetItemRequest struct { type GetItemRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -143,7 +143,7 @@ func (*GetItemRequest) Descriptor() ([]byte, []int) {
return file_item_v1_item_proto_rawDescGZIP(), []int{1} return file_item_v1_item_proto_rawDescGZIP(), []int{1}
} }
func (x *GetItemRequest) GetId() uint32 { func (x *GetItemRequest) GetId() int64 {
if x != nil { if x != nil {
return x.Id return x.Id
} }
@ -199,8 +199,8 @@ type GetItemsRequest struct {
Start *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=start,proto3,oneof" json:"start,omitempty"` Start *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=start,proto3,oneof" json:"start,omitempty"`
End *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=end,proto3,oneof" json:"end,omitempty"` End *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=end,proto3,oneof" json:"end,omitempty"`
Filter *string `protobuf:"bytes,3,opt,name=filter,proto3,oneof" json:"filter,omitempty"` Filter *string `protobuf:"bytes,3,opt,name=filter,proto3,oneof" json:"filter,omitempty"`
Limit *uint32 `protobuf:"varint,4,opt,name=limit,proto3,oneof" json:"limit,omitempty"` Limit *int32 `protobuf:"varint,4,opt,name=limit,proto3,oneof" json:"limit,omitempty"`
Offset *uint32 `protobuf:"varint,5,opt,name=offset,proto3,oneof" json:"offset,omitempty"` Offset *int32 `protobuf:"varint,5,opt,name=offset,proto3,oneof" json:"offset,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -256,14 +256,14 @@ func (x *GetItemsRequest) GetFilter() string {
return "" return ""
} }
func (x *GetItemsRequest) GetLimit() uint32 { func (x *GetItemsRequest) GetLimit() int32 {
if x != nil && x.Limit != nil { if x != nil && x.Limit != nil {
return *x.Limit return *x.Limit
} }
return 0 return 0
} }
func (x *GetItemsRequest) GetOffset() uint32 { func (x *GetItemsRequest) GetOffset() int32 {
if x != nil && x.Offset != nil { if x != nil && x.Offset != nil {
return *x.Offset return *x.Offset
} }
@ -273,7 +273,7 @@ func (x *GetItemsRequest) GetOffset() uint32 {
type GetItemsResponse struct { type GetItemsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Items []*Item `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` Items []*Item `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"`
Count uint64 `protobuf:"varint,2,opt,name=count,proto3" json:"count,omitempty"` Count int64 `protobuf:"varint,2,opt,name=count,proto3" json:"count,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -315,7 +315,7 @@ func (x *GetItemsResponse) GetItems() []*Item {
return nil return nil
} }
func (x *GetItemsResponse) GetCount() uint64 { func (x *GetItemsResponse) GetCount() int64 {
if x != nil { if x != nil {
return x.Count return x.Count
} }
@ -500,7 +500,7 @@ func (x *UpdateItemResponse) GetItem() *Item {
type DeleteItemRequest struct { type DeleteItemRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -535,7 +535,7 @@ func (*DeleteItemRequest) Descriptor() ([]byte, []int) {
return file_item_v1_item_proto_rawDescGZIP(), []int{9} return file_item_v1_item_proto_rawDescGZIP(), []int{9}
} }
func (x *DeleteItemRequest) GetId() uint32 { func (x *DeleteItemRequest) GetId() int64 {
if x != nil { if x != nil {
return x.Id return x.Id
} }
@ -586,20 +586,20 @@ var file_item_v1_item_proto_rawDesc = string([]byte{
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74,
0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xcb, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xcb,
0x01, 0x0a, 0x04, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x13, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x0a, 0x04, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x13, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x88, 0x01, 0x01, 0x12, 0x12, 0x0a, 0x04, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x88, 0x01, 0x01, 0x12, 0x12, 0x0a, 0x04,
0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65,
0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18,
0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69,
0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28,
0x02, 0x52, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x71, 0x75, 0x61, 0x6e, 0x02, 0x52, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x71, 0x75, 0x61, 0x6e,
0x74, 0x69, 0x74, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x71, 0x75, 0x61, 0x6e,
0x74, 0x69, 0x74, 0x79, 0x12, 0x35, 0x0a, 0x05, 0x61, 0x64, 0x64, 0x65, 0x64, 0x18, 0x06, 0x20, 0x74, 0x69, 0x74, 0x79, 0x12, 0x35, 0x0a, 0x05, 0x61, 0x64, 0x64, 0x65, 0x64, 0x18, 0x06, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48,
0x01, 0x52, 0x05, 0x61, 0x64, 0x64, 0x65, 0x64, 0x88, 0x01, 0x01, 0x42, 0x05, 0x0a, 0x03, 0x5f, 0x01, 0x52, 0x05, 0x61, 0x64, 0x64, 0x65, 0x64, 0x88, 0x01, 0x01, 0x42, 0x05, 0x0a, 0x03, 0x5f,
0x69, 0x64, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x61, 0x64, 0x64, 0x65, 0x64, 0x22, 0x20, 0x0a, 0x0e, 0x69, 0x64, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x61, 0x64, 0x64, 0x65, 0x64, 0x22, 0x20, 0x0a, 0x0e,
0x47, 0x65, 0x74, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x47, 0x65, 0x74, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e,
0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x22, 0x34, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x22, 0x34,
0x0a, 0x0f, 0x47, 0x65, 0x74, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x21, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x65, 0x12, 0x21, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x0d, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x04, 0x0d, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x04,
@ -613,9 +613,9 @@ var file_item_v1_item_proto_rawDesc = string([]byte{
0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x01, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x88, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x01, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x88,
0x01, 0x01, 0x12, 0x1b, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x01, 0x01, 0x12, 0x1b, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01,
0x28, 0x09, 0x48, 0x02, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x28, 0x09, 0x48, 0x02, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12,
0x19, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x03, 0x19, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x48, 0x03,
0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x88, 0x01, 0x01, 0x12, 0x1b, 0x0a, 0x06, 0x6f, 0x66, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x88, 0x01, 0x01, 0x12, 0x1b, 0x0a, 0x06, 0x6f, 0x66,
0x66, 0x73, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x04, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x48, 0x04, 0x52, 0x06, 0x6f, 0x66,
0x66, 0x73, 0x65, 0x74, 0x88, 0x01, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x66, 0x73, 0x65, 0x74, 0x88, 0x01, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x73, 0x74, 0x61, 0x72,
0x74, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x65, 0x6e, 0x64, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x66, 0x69, 0x74, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x65, 0x6e, 0x64, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x66, 0x69,
0x6c, 0x74, 0x65, 0x72, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x42, 0x09, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x42, 0x09,
@ -624,7 +624,7 @@ var file_item_v1_item_proto_rawDesc = string([]byte{
0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x69, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x69,
0x74, 0x65, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x74, 0x65, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65,
0x6d, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x6d, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
0x04, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x36, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x36, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61,
0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a,
0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x69, 0x74, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x69, 0x74,
0x65, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x65, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d,
@ -640,7 +640,7 @@ var file_item_v1_item_proto_rawDesc = string([]byte{
0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x69, 0x74, 0x65, 0x6d, 0x2e, 0x76, 0x31, 0x2e,
0x49, 0x74, 0x65, 0x6d, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x23, 0x0a, 0x11, 0x44, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x23, 0x0a, 0x11, 0x44, 0x65,
0x6c, 0x65, 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x22, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x69, 0x64, 0x22,
0x14, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x73, 0x14, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xeb, 0x02, 0x0a, 0x0b, 0x49, 0x74, 0x65, 0x6d, 0x53, 0x65, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0xeb, 0x02, 0x0a, 0x0b, 0x49, 0x74, 0x65, 0x6d, 0x53, 0x65,
0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x49, 0x74, 0x65, 0x6d, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x49, 0x74, 0x65, 0x6d,

View File

@ -23,9 +23,9 @@ const (
type User struct { type User struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Id uint32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"` Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"`
ProfilePicture *string `protobuf:"bytes,3,opt,name=profile_picture,json=profilePicture,proto3,oneof" json:"profile_picture,omitempty"` ProfilePictureId *int64 `protobuf:"varint,3,opt,name=profile_picture_id,json=profilePictureId,proto3,oneof" json:"profile_picture_id,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -60,7 +60,7 @@ func (*User) Descriptor() ([]byte, []int) {
return file_user_v1_user_proto_rawDescGZIP(), []int{0} return file_user_v1_user_proto_rawDescGZIP(), []int{0}
} }
func (x *User) GetId() uint32 { func (x *User) GetId() int64 {
if x != nil { if x != nil {
return x.Id return x.Id
} }
@ -74,11 +74,11 @@ func (x *User) GetUsername() string {
return "" return ""
} }
func (x *User) GetProfilePicture() string { func (x *User) GetProfilePictureId() int64 {
if x != nil && x.ProfilePicture != nil { if x != nil && x.ProfilePictureId != nil {
return *x.ProfilePicture return *x.ProfilePictureId
} }
return "" return 0
} }
type GetUserRequest struct { type GetUserRequest struct {
@ -461,81 +461,81 @@ var File_user_v1_user_proto protoreflect.FileDescriptor
var file_user_v1_user_proto_rawDesc = string([]byte{ var file_user_v1_user_proto_rawDesc = string([]byte{
0x0a, 0x12, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70, 0x0a, 0x12, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x22, 0x74, 0x0a, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x22, 0x7c, 0x0a,
0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0d, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x03, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d,
0x65, 0x12, 0x2c, 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x70, 0x69, 0x63, 0x65, 0x12, 0x31, 0x0a, 0x12, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x70, 0x69, 0x63,
0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0e, 0x70, 0x72, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52,
0x6f, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x88, 0x01, 0x01, 0x42, 0x10, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x49,
0x12, 0x0a, 0x10, 0x5f, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x70, 0x69, 0x63, 0x74, 0x64, 0x88, 0x01, 0x01, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65,
0x75, 0x72, 0x65, 0x22, 0x10, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x5f, 0x70, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x5f, 0x69, 0x64, 0x22, 0x10, 0x0a, 0x0e, 0x47,
0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x34, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x34, 0x0a,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x0f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31,
0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x88, 0x01, 0x0a, 0x15,
0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x6f, 0x6c, 0x64, 0x5f, 0x70, 0x61, 0x73,
0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6f, 0x6c, 0x64,
0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x65, 0x77, 0x5f,
0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b,
0x6e, 0x65, 0x77, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x63,
0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18,
0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x50, 0x61,
0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x3b, 0x0a, 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65,
0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x21, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x12, 0x21, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d,
0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75,
0x73, 0x65, 0x72, 0x22, 0x59, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x41, 0x50, 0x49, 0x4b, 0x65, 0x79, 0x73, 0x65, 0x72, 0x22, 0x88, 0x01, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a,
0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x0c, 0x6f, 0x6c, 0x64, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0b, 0x6f, 0x6c, 0x64, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x65, 0x77, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x65, 0x77, 0x50, 0x61, 0x73, 0x73, 0x77,
0x6f, 0x72, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x5f, 0x70, 0x6f, 0x72, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x5f, 0x70,
0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63,
0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x25, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x3b,
0x0a, 0x11, 0x47, 0x65, 0x74, 0x41, 0x50, 0x49, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x0a, 0x16, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72,
0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x4e, 0x0a, 0x1b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31,
0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x65, 0x71, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x59, 0x0a, 0x10, 0x47,
0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x74, 0x41, 0x50, 0x49, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x63,
0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x41, 0x0a, 0x1c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18,
0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x65, 0x73, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x50, 0x61,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x25, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x41, 0x50, 0x49,
0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b,
0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x32, 0xcf, 0x02, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x4e, 0x0a,
0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x55, 0x1b, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x69,
0x73, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09,
0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x75, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x53, 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x41, 0x0a,
0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x1e, 0x2e, 0x75, 0x73, 0x65, 0x1c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x69,
0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a,
0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x75, 0x73, 0x65, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x75, 0x73,
0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72,
0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x32, 0xcf, 0x02, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x09, 0x47, 0x65, 0x74, 0x41, 0x50, 0x49, 0x4b, 0x65, 0x79, 0x12, 0x19, 0x2e, 0x75, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x12, 0x17, 0x2e, 0x75, 0x73,
0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x50, 0x49, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47,
0x47, 0x65, 0x74, 0x41, 0x50, 0x49, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00,
0x65, 0x22, 0x00, 0x12, 0x65, 0x0a, 0x14, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x12, 0x53, 0x0a, 0x0e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f,
0x66, 0x69, 0x6c, 0x65, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x24, 0x2e, 0x75, 0x73, 0x72, 0x64, 0x12, 0x1e, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64,
0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65,
0x69, 0x6c, 0x65, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64,
0x74, 0x1a, 0x25, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x61, 0x74, 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x44, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x41, 0x50, 0x49, 0x4b,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x9d, 0x01, 0x0a, 0x0b, 0x63, 0x65, 0x79, 0x12, 0x19, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74,
0x6f, 0x6d, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x42, 0x09, 0x55, 0x73, 0x65, 0x72, 0x41, 0x50, 0x49, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e,
0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x46, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x50, 0x49, 0x4b, 0x65,
0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x70, 0x6f, 0x74, 0x64, 0x65, 0x6d, 0x6f, 0x34, 0x2f, 0x74, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x65, 0x0a, 0x14, 0x55,
0x65, 0x76, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x69, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x69, 0x63, 0x74,
0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x75, 0x72, 0x65, 0x12, 0x24, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70,
0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x75, 0x73, 0x65, 0x72, 0x76, 0x31, 0xa2, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x69, 0x63, 0x74, 0x75,
0x02, 0x03, 0x55, 0x58, 0x58, 0xaa, 0x02, 0x07, 0x55, 0x73, 0x65, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x75, 0x73, 0x65, 0x72,
0x02, 0x07, 0x55, 0x73, 0x65, 0x72, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x13, 0x55, 0x73, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c,
0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x65, 0x50, 0x69, 0x63, 0x74, 0x75, 0x72, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x02, 0x08, 0x55, 0x73, 0x65, 0x72, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x22, 0x00, 0x42, 0x9d, 0x01, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x2e,
0x6f, 0x33, 0x76, 0x31, 0x42, 0x09, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a,
0x46, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x70, 0x6f, 0x74,
0x64, 0x65, 0x6d, 0x6f, 0x34, 0x2f, 0x74, 0x72, 0x65, 0x76, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x2f,
0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f,
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f, 0x76, 0x31,
0x3b, 0x75, 0x73, 0x65, 0x72, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x55, 0x58, 0x58, 0xaa, 0x02, 0x07,
0x55, 0x73, 0x65, 0x72, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x07, 0x55, 0x73, 0x65, 0x72, 0x5c, 0x56,
0x31, 0xe2, 0x02, 0x13, 0x55, 0x73, 0x65, 0x72, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d,
0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x08, 0x55, 0x73, 0x65, 0x72, 0x3a, 0x3a,
0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}) })
var ( var (

View File

@ -1,97 +1,68 @@
// TrevStack HTTP Server
package main package main
import ( import (
"context" "context"
"embed"
"errors"
"fmt" "fmt"
"log" "log"
"log/slog"
"net/http" "net/http"
"net/url"
"os" "os"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
"time" "time"
connectcors "connectrpc.com/cors"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/rs/cors" "github.com/stephenafamo/bob"
"golang.org/x/net/http2" "golang.org/x/net/http2"
"golang.org/x/net/http2/h2c" "golang.org/x/net/http2/h2c"
"gorm.io/gorm"
"github.com/spotdemo4/trevstack/server/internal/database" "github.com/spotdemo4/trevstack/server/internal/database"
"github.com/spotdemo4/trevstack/server/internal/handlers"
"github.com/spotdemo4/trevstack/server/internal/handlers/client" "github.com/spotdemo4/trevstack/server/internal/handlers/client"
"github.com/spotdemo4/trevstack/server/internal/handlers/file"
"github.com/spotdemo4/trevstack/server/internal/handlers/item/v1"
"github.com/spotdemo4/trevstack/server/internal/handlers/user/v1"
"github.com/spotdemo4/trevstack/server/internal/interceptors"
) )
type env struct { var clientFS *embed.FS
DBType string var dbFS *embed.FS
DBUser string
DBPass string
DBHost string
DBPort string
DBName string
Port string
Key string
}
func main() { func main() {
err := godotenv.Load() logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
slog.SetDefault(logger)
// Get env
env, err := getEnv()
if err != nil { if err != nil {
log.Println("Failed to load .env file, using environment variables") log.Fatal(err.Error())
} }
// Get environment variables for server // Migrate database
env := env{ err = database.Migrate(env.DatabaseUrl, dbFS)
DBType: os.Getenv("DB_TYPE"), if err != nil {
DBUser: os.Getenv("DB_USER"), log.Fatal(err.Error())
DBPass: os.Getenv("DB_PASS"),
DBHost: os.Getenv("DB_HOST"),
DBPort: os.Getenv("DB_PORT"),
DBName: os.Getenv("DB_NAME"),
Port: os.Getenv("PORT"),
Key: os.Getenv("KEY"),
}
if env.Port == "" {
env.Port = "8080"
}
if env.Key == "" {
log.Fatal("KEY is required")
} }
// Get environment variables for database // Get database
db := &gorm.DB{} db := &bob.DB{}
switch env.DBType { switch env.DatabaseType {
case "postgres": case "postgres":
log.Println("Using Postgres") log.Println("Using Postgres")
if env.DBUser == "" { db, err = database.NewPostgresConnection(env.DatabaseUrl)
log.Fatal("DB_USER is required")
}
if env.DBPass == "" {
log.Fatal("DB_PASS is required")
}
if env.DBHost == "" {
log.Fatal("DB_HOST is required")
}
if env.DBPort == "" {
log.Fatal("DB_PORT is required")
}
if env.DBName == "" {
log.Fatal("DB_NAME is required")
}
db, err = database.NewPostgresConnection(env.DBUser, env.DBPass, env.DBHost, env.DBPort, env.DBName)
if err != nil { if err != nil {
log.Fatalf("failed to connect to postgres: %v", err) log.Fatalf("failed to connect to postgres: %v", err)
} }
case "sqlite": case "sqlite", "sqlite3":
log.Println("Using SQLite") log.Println("Using SQLite")
if env.DBName == "" { db, err = database.NewSQLiteConnection(env.DatabaseUrl)
log.Fatal("DB_NAME is required")
}
db, err = database.NewSQLiteConnection(env.DBName)
if err != nil { if err != nil {
log.Fatalf("failed to connect to sqlite: %v", err) log.Fatalf("failed to connect to sqlite: %v", err)
} }
@ -100,21 +71,16 @@ func main() {
log.Fatal("DB_TYPE must be either postgres or sqlite") log.Fatal("DB_TYPE must be either postgres or sqlite")
} }
// Init database
if err := database.Migrate(db); err != nil {
log.Fatalf("failed to migrate database: %v", err)
}
// Serve GRPC Handlers // Serve GRPC Handlers
api := http.NewServeMux() api := http.NewServeMux()
api.Handle(withCORS(handlers.NewAuthHandler(db, env.Key))) api.Handle(interceptors.WithCORS(user.NewAuthHandler(db, env.Key)))
api.Handle(withCORS(handlers.NewUserHandler(db, env.Key))) api.Handle(interceptors.WithCORS(user.NewHandler(db, env.Key)))
api.Handle(withCORS(handlers.NewItemHandler(db, env.Key))) api.Handle(interceptors.WithCORS(item.NewHandler(db, env.Key)))
// Serve web interface // Serve web interface
mux := http.NewServeMux() mux := http.NewServeMux()
mux.Handle("/", client.NewClientHandler(env.Key)) mux.Handle("/", client.NewClientHandler(env.Key, clientFS))
mux.Handle("/file/", handlers.NewFileHandler(db, env.Key)) mux.Handle("/file/", file.NewFileHandler(db, env.Key))
mux.Handle("/grpc/", http.StripPrefix("/grpc", api)) mux.Handle("/grpc/", http.StripPrefix("/grpc", api))
// Start server // Start server
@ -129,8 +95,7 @@ func main() {
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() { go func() {
sig := <-sigs sig := <-sigs
log.Printf("Received signal %s", sig) slog.Warn(fmt.Sprintf("Received signal %s, exiting", sig))
log.Println("Exiting")
// Close HTTP server // Close HTTP server
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
@ -140,10 +105,7 @@ func main() {
cancel() cancel()
// Close database connection // Close database connection
sqlDB, err := db.DB() // Get underlying SQL database db.Close()
if err == nil {
sqlDB.Close()
}
}() }()
if err := server.ListenAndServe(); err != nil { if err := server.ListenAndServe(); err != nil {
@ -151,13 +113,46 @@ func main() {
} }
} }
// withCORS adds CORS support to a Connect HTTP handler. type env struct {
func withCORS(pattern string, h http.Handler) (string, http.Handler) { Port string
middleware := cors.New(cors.Options{ Key string
AllowedOrigins: []string{"*"}, DatabaseType string
AllowedMethods: connectcors.AllowedMethods(), DatabaseUrl *url.URL
AllowedHeaders: connectcors.AllowedHeaders(), }
ExposedHeaders: connectcors.ExposedHeaders(),
}) func getEnv() (*env, error) {
return pattern, middleware.Handler(h) err := godotenv.Load()
if err != nil {
slog.Warn("Failed to load .env file, using environment variables")
}
// Create
env := env{
Port: os.Getenv("PORT"),
Key: os.Getenv("KEY"),
}
// Validate
if env.Port == "" {
env.Port = "8080"
}
if env.Key == "" {
return nil, errors.New("env 'key' not found")
}
// Validate DATABASE_URL
dbstr := os.Getenv("DATABASE_URL")
if dbstr == "" {
return nil, errors.New("env 'DATABASE_URL' not found")
}
dbsp := strings.Split(dbstr, ":")
dburl, err := url.Parse(dbstr)
if err != nil || len(dbsp) < 2 {
return nil, errors.New("env 'DATABASE_URL' formatted incorrectly")
}
env.DatabaseType = dbsp[0]
env.DatabaseUrl = dburl
return &env, nil
} }

20
server/prod.go Normal file
View File

@ -0,0 +1,20 @@
//go:build !dev
package main
import (
"embed"
"log"
)
//go:embed all:client
var cFS embed.FS
//go:embed db/migrations/*.sql
var dFS embed.FS
func init() {
log.Println("initializing for production")
clientFS = &cFS
dbFS = &dFS
}

27
server/revive.toml Normal file
View File

@ -0,0 +1,27 @@
ignoreGeneratedHeader = false
severity = "warning"
confidence = 0.8
errorCode = 0
warningCode = 0
[rule.blank-imports]
[rule.context-as-argument]
[rule.context-keys-type]
[rule.dot-imports]
[rule.error-return]
[rule.error-strings]
[rule.error-naming]
[rule.increment-decrement]
[rule.var-naming]
[rule.var-declaration]
[rule.range]
[rule.receiver-naming]
[rule.time-naming]
[rule.unexported-return]
[rule.indent-error-flow]
[rule.errorf]
[rule.empty-block]
[rule.superfluous-else]
[rule.unused-parameter]
[rule.unreachable-code]
[rule.redefines-builtin-id]