1 Commits
Author SHA1 Message Date
sgcommand 4bb3e1af58 docs: polish structured learnings README for issue #4
CI / build-and-test (push) Failing after -33m23s
CI / build-and-test (pull_request) Failing after -33m33s
2026-04-22 00:50:18 +02:00
2691 changed files with 5421 additions and 1119934 deletions
-39
View File
@@ -18,14 +18,6 @@ jobs:
with:
go-version: '1.26'
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Enable pnpm
run: corepack enable
- name: Cache Go modules
uses: actions/cache@v4
with:
@@ -39,37 +31,6 @@ jobs:
- name: Download dependencies
run: go mod download
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
- name: Install pnpm
run: npm install -g pnpm
- name: Build UI
run: |
cd ui
pnpm install --frozen-lockfile
pnpm run build
- name: Tidy modules
run: go mod tidy
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
- name: Install pnpm
run: npm install -g pnpm
- name: Build UI
run: |
cd ui
pnpm install --frozen-lockfile
pnpm run build
- name: Run tests
run: go test ./...
+1 -3
View File
@@ -33,7 +33,5 @@ bin/
OB1/
ui/node_modules/
ui/.svelte-kit/
internal/app/ui/dist/*
!internal/app/ui/dist/placeholder.txt
internal/app/ui/dist/
.codex
.worktrees/
-101
View File
@@ -1,101 +0,0 @@
## What This Project Is
AMCS (Avalon Memory Control Service) is an MCP server written in Go that provides memory, knowledge, and task management for AI agents. It exposes 56+ tools via HTTP/SSE MCP transport and includes a Svelte 5 admin UI embedded in the binary.
# Agent Rules
Keep your answers short. Question everything, never assume or guess. Ask the user if you are unsure about anything.
You must use the AMCS MCP system tools. Set Origin as the active project and always capture summaries of what was done as thoughts using the capture_thought tool. If amcs is not available write files to doc/llm/log/yyyymmdd_hh.md
When working on postgresql use the postgresql skill. On go/api/backend the go-skill and for the frontend the "svelte-architect" skill.
## Commands
### Build & Run
```bash
make build # Build server binary (outputs to bin/amcs-server), embeds UI
make build-cli # Build CLI binary (bin/amcs-cli)
make clean # Remove bin/
# Run locally (builds UI then starts server)
./scripts/run-local.sh configs/dev.yaml
# UI hot-reload dev server (proxy to backend on :8080)
make ui-dev # http://localhost:5173
```
### Testing
```bash
make test # All tests (svelte-check + go test ./...)
go test ./internal/tools -run TestFunctionName -v # Single test
go test ./internal/tools -v # Package tests
```
### Database
```bash
make migrate # Apply SQL migrations
make check-schema-drift # Verify no drift between DBML and SQL
```
## Architecture
### Key Pattern: Store → Tool → MCP Handler
Every tool domain follows the same three-layer pattern:
1. **Store** (`internal/store/`) — database access via BUN ORM + pgx. One file per domain (e.g., `thoughts.go`, `plans.go`).
2. **Tool** (`internal/tools/`) — MCP tool handler structs with a `Handle(ctx, req, input)` method. Input/output types are plain Go structs; JSON schemas are auto-generated from struct tags.
3. **Registration** (`internal/mcpserver/handlers.go`) — each domain has a `registerXxxTools(server, logger, ts)` function called from `NewHandlers`.
### Adding a New Tool Domain
Follow the checklist in `internal/tools/` (see `project_conventions.md` in memory, or look at any existing domain like `skills.go` as a reference):
- Define input/output structs with `jsonschema` tags
- Implement a struct with `Handle` method
- Add the struct to `ToolSet` in `mcpserver/handlers.go`
- Wire the store in `internal/store/db.go`
- Add a `registerXxxTools` call in `NewHandlers`
### AI Provider Architecture
Configured in YAML under `ai.providers`. Each role (embeddings, metadata extraction) supports a primary provider and a fallback chain. Providers are named references (litellm, ollama, openrouter) that resolve to OpenAI-compatible endpoints. Health tracking per provider with cooldown on transient failures.
### Error Handling
All tool errors return structured `mcperrors.RPCError` with:
- `data.type` — machine-readable category (`invalid_input`, `project_required`, `entity_not_found`, etc.)
- `data.field` / `data.detail` / `data.hint` — structured validation data
Use helpers in `internal/mcperrors/` rather than raw errors.
### Authentication
- API key via `x-brain-key` header (dev/simple deployments)
- OAuth 2.0 client credentials for production
- Session store tracks active project per client connection
### Database Schema
DBML files in `schema/` are the source of truth for the database schema. Never edit the generated SQL or Go models directly — always edit the DBML, then regenerate:
```bash
make generate-migrations # DBML → SQL migration files
make generate-models # DBML → Go model structs
make migrate # Apply pending migrations to the database
```
All primary keys are `bigserial int64` (migrated from UUID in migration 015).
### Frontend
Svelte 5 + Tailwind + Skeleton UI in `ui/`. Built via pnpm and embedded into the Go binary as a static filesystem. The Vite dev server proxies API calls to the backend on `:8080`.
The UI uses **resolvespec-js** for admin CRUD views. The backend API for the UI follows the **ResolveSpec** convention — resource endpoints, filtering, and pagination are structured according to that spec.
## Configuration
YAML config with schema `version: 2`. Key env var overrides:
- `DATABASE_URL` — PostgreSQL connection string
- `AMCS_LITELLM_API_KEY`, `AMCS_OPENROUTER_API_KEY` — AI provider keys
- `AMCS_SERVER_PORT` — defaults to 8080
See `configs/config.example.yaml` for the full schema. Config auto-migrates v1→v2 on startup.
-1
View File
@@ -1 +0,0 @@
Read the AGENTS.md file
+13 -58
View File
@@ -4,19 +4,14 @@ SERVER_BIN := $(BIN_DIR)/amcs-server
CMD_SERVER := ./cmd/amcs-server
BUILDINFO_PKG := git.warky.dev/wdevs/amcs/internal/buildinfo
UI_DIR := $(CURDIR)/ui
AMCS_UI_BACKEND ?= http://127.0.0.1:8080
PATCH_INCREMENT ?= 1
RELEASE_VERSION ?=
RELEASE_REMOTE ?= origin
VERSION_TAG ?= $(shell git describe --tags --exact-match 2>/dev/null || echo dev)
COMMIT_SHA ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
BUILD_DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
RELSPEC ?= $(shell command -v relspec 2>/dev/null || echo $(HOME)/go/bin/relspec)
#RELSPEC = /mnt/vault/relspecgo/build/relspec
SCHEMA_FILES := $(sort $(wildcard schema/*.dbml))
MERGE_TARGET_TMP := $(CURDIR)/.cache/schema.merge-target.dbml
GENERATED_SCHEMA_MIGRATION := migrations/020_generated_schema.sql
GENERATED_MODELS_DIR := internal/generatedmodels
PNPM ?= pnpm
LDFLAGS := -s -w \
-X $(BUILDINFO_PKG).Version=$(VERSION_TAG) \
@@ -24,27 +19,10 @@ LDFLAGS := -s -w \
-X $(BUILDINFO_PKG).Commit=$(COMMIT_SHA) \
-X $(BUILDINFO_PKG).BuildDate=$(BUILD_DATE)
.PHONY: all build clean migrate release-version release-build test generate-migrations generate-models check-schema-drift build-cli ui-install ui-build ui-dev ui-check help
.PHONY: all build clean migrate release-version test generate-migrations check-schema-drift build-cli ui-install ui-build ui-dev ui-check
all: build
help:
@echo "Available targets:"
@echo " build Build server binary (includes UI build)"
@echo " build-cli Build CLI binary"
@echo " test Run all tests (includes UI check)"
@echo " clean Remove build artifacts"
@echo " migrate Run database migrations"
@echo " release-version Tag and push a release (auto patch bump or RELEASE_VERSION=vX.Y.Z)"
@echo " release-build Build with a specific release tag (RELEASE_VERSION=vX.Y.Z)"
@echo " generate-migrations Generate SQL migration from DBML schema files"
@echo " generate-models Generate Go models from DBML schema"
@echo " check-schema-drift Verify generated migration matches current schema"
@echo " ui-install Install UI dependencies"
@echo " ui-build Build UI assets"
@echo " ui-dev Start UI dev server with local API proxy"
@echo " ui-check Run UI type checks"
build: ui-build
@mkdir -p $(BIN_DIR)
go build -ldflags "$(LDFLAGS)" -o $(SERVER_BIN) $(CMD_SERVER)
@@ -56,7 +34,7 @@ ui-build: ui-install
cd $(UI_DIR) && $(PNPM) run build
ui-dev: ui-install
cd $(UI_DIR) && VITE_API_URL=/api AMCS_UI_BACKEND=$(AMCS_UI_BACKEND) $(PNPM) run dev
cd $(UI_DIR) && $(PNPM) run dev
ui-check: ui-install
cd $(UI_DIR) && $(PNPM) run check
@@ -69,41 +47,22 @@ release-version:
@case "$(PATCH_INCREMENT)" in \
''|*[!0-9]*|0) echo "PATCH_INCREMENT must be a positive integer" >&2; exit 1 ;; \
esac
@if ! git diff --quiet || ! git diff --cached --quiet; then \
echo "Refusing to release from a dirty working tree. Commit or stash changes first." >&2; \
exit 1; \
fi
@next_tag="$(RELEASE_VERSION)"; \
if [ -z "$$next_tag" ]; then \
latest=$$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n 1); \
if [ -z "$$latest" ]; then latest="v0.0.0"; fi; \
version=$${latest#v}; \
major=$${version%%.*}; \
rest=$${version#*.}; \
minor=$${rest%%.*}; \
patch=$${rest##*.}; \
next_patch=$$((patch + $(PATCH_INCREMENT))); \
next_tag="v$$major.$$minor.$$next_patch"; \
fi; \
case "$$next_tag" in \
v[0-9]*.[0-9]*.[0-9]*) ;; \
*) echo "RELEASE_VERSION must look like vX.Y.Z (got '$$next_tag')" >&2; exit 1 ;; \
esac; \
@latest=$$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n 1); \
if [ -z "$$latest" ]; then latest="v0.0.0"; fi; \
version=$${latest#v}; \
major=$${version%%.*}; \
rest=$${version#*.}; \
minor=$${rest%%.*}; \
patch=$${rest##*.}; \
next_patch=$$((patch + $(PATCH_INCREMENT))); \
next_tag="v$$major.$$minor.$$next_patch"; \
if git rev-parse -q --verify "refs/tags/$$next_tag" >/dev/null; then \
echo "$$next_tag already exists" >&2; \
exit 1; \
fi; \
git tag -a "$$next_tag" -m "Release $$next_tag"; \
git push $(RELEASE_REMOTE) "$$next_tag"; \
$(MAKE) release-build RELEASE_VERSION="$$next_tag"; \
echo "Released $$next_tag"
release-build:
@case "$(RELEASE_VERSION)" in \
v[0-9]*.[0-9]*.[0-9]*) ;; \
*) echo "RELEASE_VERSION must look like vX.Y.Z" >&2; exit 1 ;; \
esac
@$(MAKE) build build-cli VERSION_TAG="$(RELEASE_VERSION)"
git push origin "$$next_tag"; \
echo "$$next_tag"
migrate:
./scripts/migrate.sh
@@ -119,10 +78,6 @@ generate-migrations:
@schema_list=$$(printf '%s\n' $(SCHEMA_FILES) | paste -sd, -); \
$(RELSPEC) merge --target dbml --target-path $(MERGE_TARGET_TMP) --source dbml --from-list "$$schema_list" --output pgsql --output-path $(GENERATED_SCHEMA_MIGRATION)
generate-models:
@test -n "$(SCHEMA_FILES)" || (echo "No DBML schema files found in schema/" >&2; exit 1)
@./scripts/generate-models.sh
check-schema-drift:
@test -f $(GENERATED_SCHEMA_MIGRATION) || (echo "$(GENERATED_SCHEMA_MIGRATION) is missing; run make generate-migrations" >&2; exit 1)
@command -v $(RELSPEC) >/dev/null 2>&1 || (echo "relspec not found; install git.warky.dev/wdevs/relspecgo/cmd/relspec@latest" >&2; exit 1)
+23 -88
View File
@@ -1,18 +1,26 @@
# AMCS Directory
# Avalon Memory Crystal Server (amcs)
This is the AMCS (Avalon Memory Control Service) directory.
![Avalon Memory Crystal](assets/avelonmemorycrystal.jpg)
## Purpose
A Go MCP server for capturing and retrieving thoughts, memory, and project context. Exposes tools over Streamable HTTP, backed by Postgres with pgvector for semantic search.
The AMCS directory is used to store configuration and code for the Avalon Memory Control Service, which handles...
The structured learnings feature adds a separate record type for distilled, reusable knowledge with provenance, verification, and actionability fields, while leaving thoughts as the original captured notes.
## Structure
## What it does
- `configs/` - Configuration files
- `scripts/` - Scripts for managing the system
- `assets/` - Asset files
- **Capture** thoughts with automatic embedding and metadata extraction
- **Search** thoughts semantically via vector similarity
- **Organise** thoughts into projects and retrieve full project context
- **Summarise** and recall memory across topics and time windows
- **Link** related thoughts and traverse relationships
## Next Steps
## Stack
- Go — MCP server over Streamable HTTP
- Postgres + pgvector — storage and vector search
- LiteLLM — primary hosted AI provider (embeddings + metadata extraction)
- OpenRouter — default upstream behind LiteLLM
- Ollama — supported local or self-hosted OpenAI-compatible provider
## Tools
@@ -31,9 +39,6 @@ The AMCS directory is used to store configuration and code for the Avalon Memory
| `get_project_context` | Recent + semantic context for a project; uses explicit `project` or the active session project |
| `set_active_project` | Set session project scope; requires a stateful MCP session |
| `get_active_project` | Get current session project |
| `add_learning` | Create a curated learning record distinct from raw thoughts |
| `get_learning` | Retrieve a structured learning by ID |
| `list_learnings` | List structured learnings by project/category/area/status/priority/tag/query |
| `summarize_thoughts` | LLM prose summary over a filtered set |
| `recall_context` | Semantic + recency context block for injection |
| `link_thoughts` | Create a typed relationship between thoughts |
@@ -65,58 +70,10 @@ The AMCS directory is used to store configuration and code for the Avalon Memory
| `add_project_guardrail` | Link a guardrail to a project; pass `project` if client is stateless |
| `remove_project_guardrail` | Unlink a guardrail from a project; pass `project` if client is stateless |
| `list_project_guardrails` | Guardrails for a project; pass `project` if client is stateless |
| `bootstrap_world_model` | Validate and activate a project, then load effective skills, guardrails, personas, and bounded context |
| `add_project_persona` | Link a persona to a project and optionally make it the default |
| `remove_project_persona` | Unlink a persona from a project |
| `set_default_project_persona` | Select the default persona for a project |
| `list_project_personas` | List project personas and identify the default |
| `get_version_info` | Build version, commit, and date |
| `describe_tools` | List all available MCP tools with names, descriptions, categories, and model-authored usage notes; call this at the start of a session to orient yourself |
| `annotate_tool` | Persist your own usage notes for a specific tool; notes are returned by `describe_tools` in future sessions |
## Webhook ingestion
External automation can create thoughts without speaking MCP by posting JSON to `POST /webhooks/thoughts`. The endpoint is protected by the same AMCS authentication middleware as MCP and file uploads, so pass one configured API key via `x-brain-key`, an authorization bearer token header, or another enabled auth method.
Example:
```bash
curl -X POST http://localhost:8080/webhooks/thoughts \
-H 'Content-Type: application/json' \
-H 'x-brain-key: <api-key>' \
-H 'Idempotency-Key: n8n-run-123' \
-d '{
"content": "External system observed build failure on main",
"project": "amcs",
"source": "n8n",
"type": "task",
"topics": ["ci", "webhook"],
"metadata": {"workflow": "ci-monitor", "run_id": "123"}
}'
```
Payload fields:
- `content` is required and becomes the thought content.
- `project` is optional; when present it must match an existing AMCS project.
- `source`, `type`, `topics`, `people`, `action_items`, and `dates_mentioned` are normalized into the standard thought metadata schema. Unknown `type` values fall back to `observation`.
- `metadata` or `source_metadata` may contain safe source-specific JSON values; unsupported values and overly deep objects are dropped rather than persisted.
- `idempotency_key` or the `Idempotency-Key` header can be supplied to make repeated webhook deliveries return the existing thought with `duplicate: true`.
- `external_id` is stored under `metadata.webhook.external_id` for source-side traceability.
Successful new ingestion returns `201` with the created thought. Duplicate idempotency-key delivery returns `200` and the previously created thought. Invalid JSON, missing content, missing/unknown projects, or unauthenticated requests are rejected before persistence. Metadata and embedding enrichment are queued after the thought is stored.
## Learnings
Learnings are curated, structured memory records for durable insights you want to keep distinct from raw thoughts. Use them for normalized lessons, decisions, and evidence-backed findings that should be easy to retrieve and review over time.
Compared with `capture_thought`, learnings are more explicit and reviewable: they include a required `summary`, optional `details`, and structured fields like `category`, `area`, `status`, `priority`, `confidence`, and `tags`, plus optional links to a `project`, `related_thought_id`, or `related_skill_id`.
Use:
- `add_learning` to create a curated learning.
- `get_learning` to fetch one by ID.
- `list_learnings` to filter curated learnings across project and status dimensions.
## Self-Documenting Tools
AMCS includes a built-in tool directory that models can read and annotate.
@@ -253,9 +210,9 @@ Use `get_version_info` to retrieve the runtime build metadata:
## Agent Skills and Guardrails
Skills and guardrails are reusable agent behaviour instructions and constraints that can be attached to projects. Projects can also expose personas and a default persona through the world model.
Skills and guardrails are reusable agent behaviour instructions and constraints that can be attached to projects.
**At the start of every project session, read `amcs://world-model/intro` and call `bootstrap_world_model` with an explicit project.** Apply its skills, guardrails, and default persona before continuing. If your MCP client does not preserve sessions across calls, keep passing `project` explicitly.
**At the start of every project session, always call `list_project_skills` and `list_project_guardrails` first.** Use the returned skills and guardrails to guide agent behaviour for that project. Only generate or create new skills/guardrails if none are returned. If your MCP client does not preserve sessions across calls, pass `project` explicitly instead of relying on `set_active_project`.
### Skills
@@ -280,12 +237,10 @@ Severity levels: `low`, `medium`, `high`, `critical`.
Link existing skills and guardrails to a project so they are automatically available when that project is active:
```json
{ "project": "my-project", "skill_id": 42, "override": false }
{ "project": "my-project", "guardrail_id": 17 }
{ "project": "my-project", "skill_id": "<uuid>" }
{ "project": "my-project", "guardrail_id": "<uuid>" }
```
Skills are additive by default. Set `override: true` on a project-skill or persona-skill link only when that skill must replace a lower-precedence skill with the same name. World-model precedence is project, selected persona, then runtime. Guardrails are additive and later layers cannot weaken them.
## Configuration
Config is YAML-driven. Copy `configs/config.example.yaml` and set:
@@ -456,7 +411,7 @@ Returns `{"file": {...}, "uri": "amcs://files/<id>"}`. Pass `thought_id`/`projec
### MCP resources
Stored files are also exposed as MCP resources at `amcs://files/{id}`. MCP clients can read raw binary content directly via `resources/read` without going through `load_file`. The startup guide is available at `amcs://world-model/intro`.
Stored files are also exposed as MCP resources at `amcs://files/{id}`. MCP clients can read raw binary content directly via `resources/read` without going through `load_file`.
### HTTP upload and download
@@ -510,7 +465,7 @@ metadata_retry:
include_archived: false
```
**Search fallback**: when no embeddings exist for the active model in scope, `search_thoughts`, `recall_context`, `get_project_context`, `summarize_thoughts`, and `related_thoughts` automatically fall back to Postgres full-text search so results are never silently empty. All five tools include a `retrieval_mode` field in their response (`"semantic"` or `"text"`) so callers can see which path was taken.
**Search fallback**: when no embeddings exist for the active model in scope, `search_thoughts`, `recall_context`, `get_project_context`, `summarize_thoughts`, and `related_thoughts` automatically fall back to Postgres full-text search so results are never silently empty.
## Client Setup
@@ -651,26 +606,6 @@ Run the SQL migrations against a local database with:
The web UI now lives in the top-level `ui/` module and is embedded into the Go binary at build time with `go:embed`.
### Admin UI deployment model
AMCS uses a **lightweight embedded SPA panel** model:
- the Svelte admin app is compiled to static assets
- assets are embedded in the server binary and served from `/`
- backend APIs (`/api/status`, `/api/rs/*`, admin action routes, OAuth endpoints) stay on the same origin
- auth is enforced server-side for all sensitive API routes
This keeps deployment simple (single binary/container) while preserving SPA ergonomics for operator workflows.
### UI stack baseline
The admin frontend baseline is:
- Svelte 5 for the app shell and pages
- ResolveSpec-backed APIs for data access
- `@warkypublic/svelix` for admin UX components (including `GridlerFull` and form controllers)
- `@warkypublic/artemis-kit` as the default JavaScript tooling dependency baseline in `ui/package.json`
**Use `pnpm` for all UI work in this repo.**
- `make build` — runs the real UI build first, then compiles the Go server
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

-3
View File
@@ -23,15 +23,12 @@ auth:
keys:
- id: "local-client"
value: "replace-me"
tenant_id: "local"
superadmin: true
description: "main local client key"
oauth:
clients:
- id: "oauth-client"
client_id: ""
client_secret: ""
superadmin: false
description: "optional OAuth client credentials"
database:
+2 -4
View File
@@ -21,14 +21,12 @@ auth:
keys:
- id: "local-client"
value: "replace-me"
tenant_id: "local"
superadmin: true
description: "main local client key"
oauth:
clients:
- id: "oauth-client"
client_id: "test_aab32200464910ab697efbd760e7ed2c"
client_secret: "test_135369559a422b4b93fcb534a4aed2c9"
client_id: ""
client_secret: ""
description: "used when auth.mode=oauth_client_credentials"
database:
-2
View File
@@ -21,8 +21,6 @@ auth:
keys:
- id: "local-client"
value: "replace-me"
tenant_id: "local"
superadmin: true
description: "main local client key"
oauth:
clients:
-20
View File
@@ -1,20 +0,0 @@
Completed personas UI reachability and fixed the missing backend ResolveSpec registrations that blocked it.
- Added Personas navigation/page wiring in the Svelte admin shell.
- Added a personas overview page with tabs for personas, parts, and traits.
- Expanded the persona inspector to load linked parts, traits, skills, guardrails, and arc state.
- Found that `/api/rs/public/agent_parts` and related persona routes were missing because `internal/app/resolvespec_admin.go` manually whitelists ResolveSpec models.
- Registered persona-related ResolveSpec models: `agent_personas`, `agent_parts`, `agent_traits`, persona join tables, arc tables, and `persona_arc`.
- Allowed ResolveSpec mutations for `agent_personas`, `agent_parts`, and `agent_traits`.
- Verified the `internal/app` package still compiles with `env GOCACHE=/tmp/amcs-go-cache go test -run '^$' ./internal/app`.
Follow-up:
- Automated ResolveSpec model registration generation with `relspec templ`.
- Added `scripts/templates/resolvespec_models.tmpl`.
- Updated `scripts/generate-models.sh` to generate `internal/app/resolvespec_models_generated.go`.
- Removed the handwritten `resolveSpecModels()` from `internal/app/resolvespec_admin.go`.
- Extended `scripts/patch-generated-models.sh` to fix current relspec output quirks:
- incorrect `persona_arc` primary-key cast
- unused `resolvespec_common` imports in join-table models
- Added focused tests covering persona entity presence and persona mutation allowlisting.
-15
View File
@@ -1,15 +0,0 @@
Fixed the Gitea build break caused by `go:embed` requiring `internal/app/ui/dist` to exist in a clean checkout.
Changes made:
- Added `internal/app/ui/dist/placeholder.txt` so the embedded UI directory is always present in source control.
- Updated `internal/mcpserver/server_test.go` to derive expected tool names from `BuildToolCatalog()` instead of a stale hard-coded list.
- Removed stale maintenance tool entries from `internal/mcpserver/server.go` because those tools are not currently registered.
Verification:
- `env GOCACHE=/tmp/go-build go test ./internal/mcpserver -run TestNewListsAllRegisteredTools -v` passes.
- A broader `go test ./internal/app ./cmd/amcs-server` compile check was started, but it did not finish before this log entry was written.
Migration follow-up:
- Fixed `migrations/020_generated_schema.sql` after PostgreSQL failed with `operator does not exist: name[] = text[]`.
- Root cause: `pg_attribute.attname` is type `name`, so `ARRAY(SELECT a.attname ...)` produced `name[]`, which was compared against `text[]` literals.
- Updated each repeated primary-key introspection block to use `SELECT a.attname::text`, keeping the existing `ARRAY[]::text[]` and `ARRAY['id']` / `ARRAY['persona_id']` comparisons valid.
-30
View File
@@ -1,30 +0,0 @@
# World Model Planning
Project: AMCS
AMCS already exposes MCP initialization instructions from `llm/memory.md`, active-project tools, project-linked skills and guardrails, persona manifests, project context, and MCP file resources.
Recommended design:
- Keep a concise mandatory world-model bootstrap protocol in MCP initialization instructions.
- Add a readable static resource at `amcs://world-model/intro` containing the fuller introduction and project-selection protocol.
- Add one `bootstrap_world_model` tool. It accepts an explicit project name or ID, validates it, sets it active when the transport is stateful, and returns one bounded snapshot containing project identity, linked skills, linked guardrails, linked persona manifests/default persona, and recent project context.
- Never silently select or create a project. Return structured `project_not_found` or `project_ambiguous` errors with candidate projects; stateless clients must keep passing the project explicitly.
- Add project-to-persona associations because the current schema only associates skills and guardrails with projects. Support one optional default persona plus additional available personas. Return manifests at bootstrap and load full persona content on demand.
- Merge skills additively by default. Store an explicit `override` flag on project-skill and persona-skill association rows, not on the reusable skill itself. An overriding skill replaces lower-precedence skills with the same stable skill key; unrelated skills remain additive.
- Resolve precedence deterministically: project skills form the base, the selected persona is applied next, and explicit runtime/session skills are applied last. At each layer, `override: true` replaces the accumulated entry with the same key while `override: false` adds or de-duplicates it.
- Include a snapshot version and generation timestamp so clients can cache and refresh the world model safely.
Delivery phases:
1. Fix the existing `list_project_skills` SQL failure: `skillSelectCols` leaves `created_at` and other columns unqualified in a join.
2. Define the bootstrap response contract, size limits, precedence rules, and structured errors.
3. Add project-persona schema/store/tool support through DBML generation.
4. Add the intro resource and initialization instructions.
5. Implement and register `bootstrap_world_model` using existing project, skill, guardrail, persona, and context stores.
6. Add MCP lifecycle, stateful/stateless, missing/ambiguous project, payload-bound, and resource tests.
7. Add admin UI management for project personas/default persona and update client documentation.
Confirmed decision: skills are additive unless their association has `override: true`. Guardrails remain additive, use the stricter severity when duplicates occur, and cannot be weakened by persona or runtime context.
AMCS MCP writes and subsequent verification hung during this planning session, so this local log is the required fallback rather than a confirmed remote thought/plan.
-19
View File
@@ -1,19 +0,0 @@
# Tenant identity management
The previous tenancy implementation used every API-key ID as its tenant key. This meant that two keys could not share data and old unscoped rows could not be reached by any key after tenancy was enabled.
Implemented a separate tenant mapping: configured and managed API keys now resolve to an assigned tenant ID, while unassigned configured keys retain their historical key-ID tenant boundary for compatibility. Added tenant, tenant-user, key-assignment, and managed-secret records, with a one-time, explicit legacy adoption endpoint that assigns only `NULL` tenant-key rows to a selected tenant.
The admin UI now has an Identity page for tenants, users, configured-key assignment, managed-key creation (secret shown once), disabling keys, and legacy adoption. Managed secrets are stored only as SHA-256 hashes. The AMCS MCP capture-thought tool was unavailable in this session, so this local log is the required fallback summary.
Configured keys also accept an optional `auth.keys[].tenant_id`. This supplies the initial tenant boundary at startup; an explicit assignment saved through the Identity UI overrides it.
Tenant-owned root records now reference `tenants(id)` through `tenant_id`; tenant scoped resources include skills, guardrails, personas, parts, traits, and character arcs. Tenant-selection in the UI is sent as `X-AMCS-Tenant-ID` for admin/ResolveSpec requests.
With explicit approval that tenancy data is disposable, the compatibility migration now renames the ownership column from `tenant_key` to `tenant_id` across the historical migration chain.
Tenant selection now centrally filters every tenant-owned ResolveSpec read, update, and delete in the admin UI, and injects the selected `tenant_id` on create requests.
Identity mutations now require a configured API key with `superadmin: true`; tenants themselves remain database-managed.
Fixed the admin sidebar tenant-scope selector flicker. Its focus handler reloaded the tenant list and disabled the native select while its picker was opening; tenants now load on sidebar mount only, so a selection remains open and usable. Validated with `pnpm check` (0 errors, 0 warnings).
-3
View File
@@ -1,3 +0,0 @@
# ResolveSpec array scanning
Diagnosed `/api/rs/public/agent_skills` failing to scan `tags text[]`. The generated Bun model correctly uses `sqltypes.SqlStringArray`, which implements `sql.Scanner`, but ResolveSpec's read path supplied a separate scan destination instead of scanning the query's registered Bun model. Updated the vendored ResolveSpec handler to call `ScanModel`, and for single-row reads to set the single record as the query model first. This preserves Bun's model field scanner for PostgreSQL arrays.
-3
View File
@@ -1,3 +0,0 @@
# Per-user tenancy progress review
Reviewed `docs/per-user-tenancy-plan.md` against the staged worktree. Identity, tenant mappings, schema/model migrations, and primary project/thought/file/catalog scoping are present. The plan's named hardening targets—learnings, plans, chat histories, thought-learning links, and project persona joins—still have no tenant helper predicates. Only auth/keyring tests are staged; the plan's cross-tenant store/tool and migration test matrix has not yet been added or run.
-161
View File
@@ -1,161 +0,0 @@
# Per-user tenancy implementation plan
## Scope and current state
Gitea issue #10 asks AMCS to isolate memories by user, tenant, or workspace. The current branch already carries the first implementation pass, so this plan records the intended model and the remaining hardening work another worker should verify or finish.
Observed code paths:
- Authentication enters through `internal/auth/middleware.go`. API-key auth uses the configured header, defaulting to `x-brain-key`; bearer tokens can resolve through OAuth `TokenStore` or API keyring; HTTP Basic resolves OAuth client credentials.
- `internal/auth/middleware.go` writes both `auth.key_id` and `tenancy.tenant_id` into the request context. The tenant key is intentionally opaque and currently equals the authenticated key id or OAuth client id.
- `internal/tenancy/tenancy.go` exposes `WithTenantKey` and `KeyFromContext` only; callers should not infer user semantics from the tenant string.
- Schema already has `tenant_id` on `projects`, `thoughts`, `stored_files`, `learnings`, `plans`, and `chat_histories` in `schema/*.dbml`.
- `internal/store/tenancy.go` provides helper functions for appending tenant predicates to SQL.
- Tenant scoping is already present in project, thought, and stored-file store paths. Some other project-owned domains still need explicit tenant enforcement.
## Tenant/user identity source
Use the authenticated principal id as the tenant boundary:
1. API key: resolve token through `auth.Keyring.Lookup`; use returned key id as `tenant_id`.
2. OAuth bearer token: resolve token through `TokenStore.Lookup`; use returned client id/key id as `tenant_id`.
3. OAuth Basic client credentials: resolve through `OAuthRegistry.Lookup`; use returned client id as `tenant_id`.
4. Unauthenticated requests must not get tenant context and must not reach protected MCP/API handlers.
Do not store raw API keys or bearer tokens in tenant columns. Store only stable configured key ids/client ids. Yes, it is less flashy than inventing an account service before breakfast, but it keeps the trust boundary small and auditable.
## Required schema/model changes
Source-of-truth DBML changes:
- Add nullable `tenant_id text` to all user-owned tables:
- `projects`
- `thoughts`
- `stored_files`
- `learnings`
- `plans`
- `chat_histories`
- Add tenant indexes:
- single-column `tenant_id` for direct filtering
- `(tenant_id, name)` unique on `projects`, replacing global `projects.name` uniqueness
- `(tenant_id, project_id)` on `thoughts` for common project-scoped memory lookups
- Regenerate SQL migrations and generated models from DBML; do not hand-edit generated Go models except as a temporary debugging step.
Important follow-up: `agent_skills`, `agent_guardrails`, `agent_personas`, `agent_parts`, traits, and arcs are currently global catalogs. Keep them global unless product requirements say skills/personas are private per tenant. Project join tables inherit protection through tenant-scoped project ids, but direct join queries must verify the project belongs to the tenant.
## Query scoping strategy
All request-facing store methods that touch tenant-owned rows must include tenant predicates whenever `tenancy.KeyFromContext(ctx)` returns a key.
Rules:
- Inserts must populate `tenant_id` from context.
- Gets/updates/deletes by id or guid must add `and tenant_id = $n`.
- Lists/searches must add `tenant_id = $n` before other filters.
- Joins must scope the tenant-owned root table. Example: project summaries should count only thoughts that belong to the same tenant as the project, not merely thoughts with the same `project_id`.
- Background maintenance jobs must either run per tenant, carry tenant context explicitly, or intentionally operate cross-tenant with an internal-only code path documented in the job.
Already-scoped paths to keep:
- `internal/store/projects.go`: create/get/list/touch projects.
- `internal/store/thoughts.go`: create/list/get/update/delete/archive/search/stats/embedding repair paths.
- `internal/store/files.go`: insert/get/list stored files.
Paths needing audit/hardening:
- `internal/store/learnings.go`: `CreateLearning`, `GetLearning`, and `ListLearnings` should populate/filter by tenant.
- `internal/store/plans.go`: `CreatePlan`, `GetPlan`, `GetPlanDetail`, `UpdatePlan`, `DeletePlan`, `ListPlans`, dependency/related-plan operations, and plan skill/guardrail joins should scope to tenant-owned plans.
- `internal/store/chat_histories.go`: chat history create/get/list/update paths should populate/filter by tenant.
- `internal/store/thought_learning_links.go`: links traverse tenant-owned thoughts/learnings; queries should join and scope both sides or at least the tenant-owned root.
- `internal/store/skills.go` and `internal/store/project_personas.go`: project-scoped joins should verify the project is visible to the current tenant before returning linked global catalog records.
- ResolveSpec/admin CRUD endpoints exposing generated models must not bypass tenant-aware store methods. If they use direct generic model access, add middleware-level filter injection or disable tenant-owned tables from generic admin writes.
## Migration and backfill
Migration approach for existing single-tenant installs:
1. Add nullable `tenant_id` columns first; do not make them `not null` initially because existing deployments have historical rows without a principal.
2. Backfill existing rows to a configured default tenant only if the instance enables multi-tenant mode or defines `auth.default_tenant_id`. Otherwise leave `NULL` rows visible only to unauthenticated/internal single-tenant contexts.
3. Replace global project-name uniqueness with `(tenant_id, name)` uniqueness. For PostgreSQL, preserve legacy `NULL` semantics carefully: multiple null-tenant projects with the same name may be possible unless a partial unique index is added for null tenant rows.
4. Add indexes concurrently where practical for production-sized tables.
5. Document that after enabling auth-backed tenancy, legacy null-tenant data is not visible to authenticated tenants unless backfilled.
Recommended config addition:
- `auth.default_tenant_id` or `tenancy.default_key` for one-time/self-hosted backfill and development.
- Optional `tenancy.mode: single|authenticated` so operators can keep current single-user behavior deliberately instead of discovering isolation by accident. An accident in auth is just a breach with better branding.
## Authorization checks
Authorization is row ownership by tenant key:
- A request may only see or mutate rows whose `tenant_id` equals the authenticated tenant key.
- Cross-tenant ids/gids should behave as not found, not forbidden, to avoid existence leaks.
- Tenant key is server-derived only. Ignore any client-provided `tenant_id` fields on tool/API inputs.
- Project id references in create/update operations must be validated against the same tenant before use. This prevents attaching a new thought/file/plan to another tenant's project id.
- Relationship operations must verify both endpoints are in the same tenant before creating links.
- Global catalogs can be read across tenants only if intentionally shared; project-specific associations must be tenant-checked through the project.
## Affected endpoints/services/UI surfaces
Backend/MCP tools:
- Project tools: create/get/list/set active project.
- Thought tools: capture, retrieve, update, delete/archive, semantic search, text search, stats, metadata and embedding repair queues.
- File tools and binary file upload/download APIs.
- Learning tools and thought-learning link tools.
- Plan/task tools including dependencies, related plans, skills, and guardrails.
- Chat history/session persistence tools.
- Persona/skill/guardrail project-association tools.
HTTP/API boundaries:
- MCP SSE and streamable HTTP handlers must run behind auth middleware when auth is configured.
- Any non-MCP REST endpoints under the admin/API server must either use tenant-aware stores or explicitly be internal/admin-only.
- ResolveSpec admin CRUD views need tenant filter injection or table-level access restrictions for tenant-owned models.
UI:
- Project selector/list should naturally show tenant-scoped projects.
- Admin tables for projects/thoughts/files/learnings/plans/chat histories must not show `tenant_id` as an editable field.
- If a tenant switcher is ever added, it must map to a server-side authenticated principal or admin impersonation path, not a client-side query parameter. Obviously.
## Test cases
Minimum test matrix:
1. Auth middleware propagates tenant key for API-key header auth.
2. Auth middleware propagates tenant key for bearer token via OAuth token store.
3. Auth middleware propagates tenant key for HTTP Basic OAuth client credentials.
4. Tenant A and tenant B can create projects with the same name; each only lists its own project.
5. Tenant A cannot get/update/delete/archive Tenant B's thought by guid or numeric id.
6. Semantic and text search return only same-tenant thoughts, including project-filtered searches.
7. Stored file get/list/download is scoped; a file id from another tenant returns not found.
8. Learning create/list/get is scoped, including links to thoughts.
9. Plan create/list/get/update/delete and dependency/related-plan operations are scoped.
10. Project skill/persona/guardrail association reads verify the project belongs to the tenant.
11. Background metadata/embedding retry queues do not cross tenants unless intentionally internal.
12. Migration test covers legacy null-tenant rows and backfill to default tenant.
13. ResolveSpec/admin API tests prove tenant-owned resources are filtered or unavailable without admin override.
## Assumptions and blockers
Assumptions:
- The first tenancy boundary is authenticated principal id, not human account, org, or workspace. This is consistent with the current auth system and avoids adding an account model prematurely.
- Null `tenant_id` remains the compatibility path for existing single-tenant/internal flows.
- Global skills/personas/guardrails remain shared catalogs until a separate product decision makes them tenant-private.
Blockers/decisions needed:
- Decide whether legacy rows should be backfilled automatically to a configured default tenant during migration or left null until an operator runs an explicit backfill.
- Decide whether ResolveSpec admin endpoints are trusted admin-only or must enforce tenant predicates like MCP tools.
- Decide whether future OAuth subjects should distinguish user id from client id; the current implementation only has client/key id available.
## Implementation order
1. Finish tenant scoping audits for learnings, plans, chat histories, thought-learning links, and project catalog joins.
2. Add tests proving cross-tenant invisibility for each store/tool domain before widening coverage. Yes, tests first; future us has enough enemies.
3. Add config/backfill migration behavior and document operator steps.
4. Lock down or filter ResolveSpec/admin tenant-owned model access.
5. Run `make test` and `make build`; include exact results in the issue/PR comment.
+9 -65
View File
@@ -3,85 +3,29 @@ module git.warky.dev/wdevs/amcs
go 1.26.1
require (
git.warky.dev/wdevs/relspecgo v1.0.62
github.com/bitechdev/ResolveSpec v1.1.27
github.com/google/jsonschema-go v0.4.3
github.com/google/jsonschema-go v0.4.2
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.9.2
github.com/jackc/pgx/v5 v5.9.1
github.com/modelcontextprotocol/go-sdk v1.4.1
github.com/pgvector/pgvector-go v0.3.0
github.com/spf13/cobra v1.10.2
github.com/uptrace/bun v1.2.18
github.com/uptrace/bun/dialect/pgdialect v1.2.18
github.com/uptrace/bun/driver/pgdriver v1.1.12
github.com/uptrace/bunrouter v1.0.23
golang.org/x/sync v0.20.0
golang.org/x/sync v0.17.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/getsentry/sentry-go v0.46.2 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // 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-sqlite3 v1.14.44 // indirect
github.com/microsoft/go-mssqldb v1.10.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/redis/go-redis/v9 v9.19.0 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/segmentio/asm v1.1.3 // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tidwall/gjson v1.19.0 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/uptrace/bun/dialect/mssqldialect v1.2.18 // indirect
github.com/uptrace/bun/dialect/sqlitedialect v1.2.18 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.28.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/text v0.37.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gorm.io/driver/postgres v1.6.0 // indirect
gorm.io/driver/sqlite v1.6.0 // indirect
gorm.io/driver/sqlserver v1.6.3 // indirect
gorm.io/gorm v1.31.1 // indirect
mellium.im/sasl v0.3.1 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.29.0 // indirect
)
+32 -414
View File
@@ -1,315 +1,78 @@
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
entgo.io/ent v0.14.3 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ=
entgo.io/ent v0.14.3/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM=
git.warky.dev/wdevs/relspecgo v1.0.62 h1:byBe2IlcwQRKsV5qXHfGITtfvGZewHlNstP+O8BGnns=
git.warky.dev/wdevs/relspecgo v1.0.62/go.mod h1:JpZBvui9dYc/QcD5TZ1wKab+4bUvPw5/rOLOA/h2F6A=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bitechdev/ResolveSpec v1.1.27 h1:qugBwR3Qoy4tNKhAyJsFszZE+kRR0gzl5w42XE3/scY=
github.com/bitechdev/ResolveSpec v1.1.27/go.mod h1:GF51sMRCWbAyri2WNae3IZAFM/2s6DG6i3eTTrobbVs=
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c h1:6Gpm9YYUEQx2T9zMsYolQhr6sjwwGtFitSA0pQsa7a8=
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
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/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/getsentry/sentry-go v0.46.2 h1:1jhYwrKGa3sIpo/y5iDNXS5wDoT7I1KNzMHrnK6ojns=
github.com/getsentry/sentry-go v0.46.2/go.mod h1:evVbw2qotNUdYG8KxXbAdjOQWWvWIwKxpjdZZIvcIPw=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-pg/pg/v10 v10.11.0 h1:CMKJqLgTrfpE/aOVeLdybezR2om071Vh38OLZjsyMI0=
github.com/go-pg/pg/v10 v10.11.0/go.mod h1:4BpHRoxE61y4Onpof3x1a2SQvi9c+q1dJnrNdMjsroA=
github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU=
github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
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/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
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.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
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/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
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/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
github.com/microsoft/go-mssqldb v1.10.0 h1:pHEt+Qz6YFPWqREq10mqSE524QQo+/QremwTCQht7TY=
github.com/microsoft/go-mssqldb v1.10.0/go.mod h1:mnG7lGa9iYJbzJqGCXyuQCegStKMr3kogDLD6+bmggg=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc=
github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pgvector/pgvector-go v0.3.0 h1:Ij+Yt78R//uYqs3Zk35evZFvr+G0blW0OUN+Q2D1RWc=
github.com/pgvector/pgvector-go v0.3.0/go.mod h1:duFy+PXWfW7QQd5ibqutBO4GxLsUZ9RVXhFZGIBsWSA=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k=
github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
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/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/uptrace/bun v1.2.18 h1:3HnRcMfS6OBPMG1eSOzlbFJ/X/AyMEJb7rMxE6VQvDU=
github.com/uptrace/bun v1.2.18/go.mod h1:wNltaKJk4JtOt4SG5I5zmA7v0/Mzjh1+/S906Rayd3Y=
github.com/uptrace/bun/dialect/mssqldialect v1.2.18 h1:nYzHoyJKJlIyl5i95Exi8ZTK8ooKWG+o3z3f404d/yQ=
github.com/uptrace/bun/dialect/mssqldialect v1.2.18/go.mod h1:Su45Je7z66sfeZ3d1ZsnOQEK8xfzGgaMzBvtoE8yFhk=
github.com/uptrace/bun/dialect/pgdialect v1.2.18 h1:IZ6nM2+OYrL8lkEAy7UkSEZvoa3vluTAUlZfPtlRB2k=
github.com/uptrace/bun/dialect/pgdialect v1.2.18/go.mod h1:Tqdf4QP1okrGYpXfodXvCOK6Ob1OOTwSaoAzCgBB3IU=
github.com/uptrace/bun/dialect/sqlitedialect v1.2.18 h1:Z33SY/U++XK9uGWqS4h8OZVxfCXguIG+sU9cYq2PGFQ=
github.com/uptrace/bun/dialect/sqlitedialect v1.2.18/go.mod h1:1MVOS/Ncy4FZbkJcgUFH6OqYoQinYNjkEwsmNQEXz2A=
github.com/uptrace/bun v1.1.12 h1:sOjDVHxNTuM6dNGaba0wUuz7KvDE1BmNu9Gqs2gJSXQ=
github.com/uptrace/bun v1.1.12/go.mod h1:NPG6JGULBeQ9IU6yHp7YGELRa5Agmd7ATZdz4tGZ6z0=
github.com/uptrace/bun/dialect/pgdialect v1.1.12 h1:m/CM1UfOkoBTglGO5CUTKnIKKOApOYxkcP2qn0F9tJk=
github.com/uptrace/bun/dialect/pgdialect v1.1.12/go.mod h1:Ij6WIxQILxLlL2frUBxUBOZJtLElD2QQNDcu/PWDHTc=
github.com/uptrace/bun/driver/pgdriver v1.1.12 h1:3rRWB1GK0psTJrHwxzNfEij2MLibggiLdTqjTtfHc1w=
github.com/uptrace/bun/driver/pgdriver v1.1.12/go.mod h1:ssYUP+qwSEgeDDS1xm2XBip9el1y9Mi5mTAvLoiADLM=
github.com/uptrace/bun/driver/sqliteshim v1.2.16 h1:M6Dh5kkDWFbUWBrOsIE1g1zdZ5JbSytTD4piFRBOUAI=
github.com/uptrace/bun/driver/sqliteshim v1.2.16/go.mod h1:iKdJ06P3XS+pwKcONjSIK07bbhksH3lWsw3mpfr0+bY=
github.com/uptrace/bunrouter v1.0.23 h1:Bi7NKw3uCQkcA/GUCtDNPq5LE5UdR9pe+UyWbjHB/wU=
github.com/uptrace/bunrouter v1.0.23/go.mod h1:O3jAcl+5qgnF+ejhgkmbceEk0E/mqaK+ADOocdNpY8M=
github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94=
github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
@@ -318,173 +81,28 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/driver/sqlserver v1.6.3 h1:UR+nWCuphPnq7UxnL57PSrlYjuvs+sf1N59GgFX7uAI=
gorm.io/driver/sqlserver v1.6.3/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo=
mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw=
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/sqlite v1.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w=
modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
-79
View File
@@ -1,79 +0,0 @@
package app
import (
"encoding/json"
"log/slog"
"net/http"
"git.warky.dev/wdevs/amcs/internal/tools"
)
type adminActions struct {
backfill *tools.BackfillTool
retry *tools.EnrichmentRetryer
logger *slog.Logger
}
func newAdminActions(backfill *tools.BackfillTool, retry *tools.EnrichmentRetryer, logger *slog.Logger) *adminActions {
return &adminActions{
backfill: backfill,
retry: retry,
logger: logger,
}
}
func (a *adminActions) backfillHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.Header().Set("Allow", http.MethodPost)
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var in tools.BackfillInput
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
_, out, err := a.backfill.Handle(r.Context(), nil, in)
if err != nil {
if a.logger != nil {
a.logger.Warn("admin backfill failed", slog.String("error", err.Error()))
}
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(out)
})
}
func (a *adminActions) retryMetadataHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.Header().Set("Allow", http.MethodPost)
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var in tools.RetryEnrichmentInput
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
_, out, err := a.retry.Handle(r.Context(), nil, in)
if err != nil {
if a.logger != nil {
a.logger.Warn("admin metadata retry failed", slog.String("error", err.Error()))
}
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(out)
})
}
-372
View File
@@ -1,372 +0,0 @@
package app
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"strings"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"git.warky.dev/wdevs/amcs/internal/auth"
)
type identityAdmin struct {
pool *pgxpool.Pool
keyring *auth.Keyring
oauthRegistry *auth.OAuthRegistry
logger *slog.Logger
}
func newIdentityAdmin(pool *pgxpool.Pool, keyring *auth.Keyring, oauthRegistry *auth.OAuthRegistry, logger *slog.Logger) *identityAdmin {
return &identityAdmin{pool: pool, keyring: keyring, oauthRegistry: oauthRegistry, logger: logger}
}
func (a *identityAdmin) isSuperadmin(keyID string) bool {
if a.keyring != nil && a.keyring.IsSuperadmin(keyID) {
return true
}
return a.oauthRegistry != nil && a.oauthRegistry.IsSuperadmin(keyID)
}
func loadIdentityKeyring(ctx context.Context, pool *pgxpool.Pool, keyring *auth.Keyring) error {
if keyring == nil {
return nil
}
rows, err := pool.Query(ctx, `select a.key_id, a.tenant_id, a.enabled, coalesce(m.secret_hash, '') from api_key_assignments a left join managed_api_keys m on m.key_id = a.key_id`)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var keyID, tenantID, hash string
var enabled bool
if err := rows.Scan(&keyID, &tenantID, &enabled, &hash); err != nil {
return err
}
keyring.AssignTenant(keyID, tenantID)
if hash != "" {
keyring.AddManaged(keyID, hash, enabled)
}
}
return rows.Err()
}
// ensureConfiguredTenants makes a tenant referenced in static YAML visible to
// the admin UI as well as to the authentication middleware.
func ensureConfiguredTenants(ctx context.Context, pool *pgxpool.Pool, keyring *auth.Keyring) error {
if keyring == nil {
return nil
}
for _, key := range keyring.ConfiguredKeys() {
tenantID := strings.TrimSpace(key.TenantID)
if tenantID == "" {
continue
}
if _, err := pool.Exec(ctx, `insert into tenants (id, name) values ($1, $1) on conflict (id) do nothing`, tenantID); err != nil {
return err
}
}
return nil
}
func (a *identityAdmin) handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
keyID, ok := auth.KeyIDFromContext(r.Context())
if !ok || !a.isSuperadmin(keyID) {
writeJSON(w, http.StatusForbidden, map[string]string{"error": "superadmin API key required"})
return
}
}
path := strings.TrimPrefix(r.URL.Path, "/api/admin/identity")
switch {
case r.Method == http.MethodGet && path == "":
a.list(w, r)
case r.Method == http.MethodPost && path == "/tenants":
a.createTenant(w, r)
case r.Method == http.MethodPost && path == "/users":
a.createUser(w, r)
case r.Method == http.MethodPost && path == "/keys":
a.createKey(w, r)
case r.Method == http.MethodPatch && strings.HasPrefix(path, "/keys/"):
a.updateKey(w, r, strings.TrimPrefix(path, "/keys/"))
case r.Method == http.MethodPost && strings.HasPrefix(path, "/tenants/") && strings.HasSuffix(path, "/adopt-legacy"):
a.adoptLegacy(w, r, strings.TrimSuffix(strings.TrimPrefix(path, "/tenants/"), "/adopt-legacy"))
default:
http.NotFound(w, r)
}
})
}
type tenantDTO struct {
ID string `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
}
type userDTO struct {
ID string `json:"id"`
TenantID string `json:"tenant_id"`
Name string `json:"name"`
Email *string `json:"email,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
type keyDTO struct {
ID string `json:"id"`
TenantID string `json:"tenant_id"`
UserID *string `json:"user_id,omitempty"`
Description string `json:"description"`
Source string `json:"source"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"created_at"`
}
func (a *identityAdmin) list(w http.ResponseWriter, r *http.Request) {
result := struct {
Tenants []tenantDTO `json:"tenants"`
Users []userDTO `json:"users"`
Keys []keyDTO `json:"keys"`
}{Tenants: []tenantDTO{}, Users: []userDTO{}, Keys: []keyDTO{}}
rows, err := a.pool.Query(r.Context(), `select id, name, created_at from tenants order by name`)
if err != nil {
identityError(w, err)
return
}
defer rows.Close()
for rows.Next() {
var x tenantDTO
if err := rows.Scan(&x.ID, &x.Name, &x.CreatedAt); err != nil {
identityError(w, err)
return
}
result.Tenants = append(result.Tenants, x)
}
rows, err = a.pool.Query(r.Context(), `select id, tenant_id, name, email, created_at from tenant_users order by name`)
if err != nil {
identityError(w, err)
return
}
defer rows.Close()
for rows.Next() {
var x userDTO
if err := rows.Scan(&x.ID, &x.TenantID, &x.Name, &x.Email, &x.CreatedAt); err != nil {
identityError(w, err)
return
}
result.Users = append(result.Users, x)
}
configured := make(map[string]authKey)
if a.keyring != nil {
for _, key := range a.keyring.ConfiguredKeys() {
configured[key.ID] = authKey{description: key.Description}
}
}
rows, err = a.pool.Query(r.Context(), `select key_id, tenant_id, user_id, description, source, enabled, created_at from api_key_assignments order by key_id`)
if err != nil {
identityError(w, err)
return
}
defer rows.Close()
for rows.Next() {
var x keyDTO
if err := rows.Scan(&x.ID, &x.TenantID, &x.UserID, &x.Description, &x.Source, &x.Enabled, &x.CreatedAt); err != nil {
identityError(w, err)
return
}
result.Keys = append(result.Keys, x)
delete(configured, x.ID)
}
for id, key := range configured {
result.Keys = append(result.Keys, keyDTO{ID: id, Description: key.description, Source: "configured", Enabled: true})
}
writeJSON(w, http.StatusOK, result)
}
type authKey struct{ description string }
func (a *identityAdmin) createTenant(w http.ResponseWriter, r *http.Request) {
var body struct {
Name string `json:"name"`
}
if !decodeJSON(w, r, &body) {
return
}
body.Name = strings.TrimSpace(body.Name)
if body.Name == "" {
badRequest(w, "name is required")
return
}
x := tenantDTO{ID: newIdentityID(), Name: body.Name}
err := a.pool.QueryRow(r.Context(), `insert into tenants (id,name) values ($1,$2) returning created_at`, x.ID, x.Name).Scan(&x.CreatedAt)
if err != nil {
identityError(w, err)
return
}
writeJSON(w, http.StatusCreated, x)
}
func (a *identityAdmin) createUser(w http.ResponseWriter, r *http.Request) {
var body struct {
TenantID, Name string
Email *string `json:"email"`
}
if !decodeJSON(w, r, &body) {
return
}
body.TenantID = strings.TrimSpace(body.TenantID)
body.Name = strings.TrimSpace(body.Name)
if body.TenantID == "" || body.Name == "" {
badRequest(w, "tenant_id and name are required")
return
}
x := userDTO{ID: newIdentityID(), TenantID: body.TenantID, Name: body.Name, Email: body.Email}
err := a.pool.QueryRow(r.Context(), `insert into tenant_users (id,tenant_id,name,email) values ($1,$2,$3,$4) returning created_at`, x.ID, x.TenantID, x.Name, x.Email).Scan(&x.CreatedAt)
if err != nil {
identityError(w, err)
return
}
writeJSON(w, http.StatusCreated, x)
}
func (a *identityAdmin) createKey(w http.ResponseWriter, r *http.Request) {
var body struct {
TenantID string `json:"tenant_id"`
UserID *string `json:"user_id"`
Description string `json:"description"`
}
if !decodeJSON(w, r, &body) {
return
}
body.TenantID = strings.TrimSpace(body.TenantID)
if body.TenantID == "" {
badRequest(w, "tenant_id is required")
return
}
secret, hash, err := auth.GenerateSecret()
if err != nil {
identityError(w, err)
return
}
x := keyDTO{ID: newIdentityID(), TenantID: body.TenantID, UserID: body.UserID, Description: strings.TrimSpace(body.Description), Source: "managed", Enabled: true}
tx, err := a.pool.Begin(r.Context())
if err != nil {
identityError(w, err)
return
}
defer tx.Rollback(r.Context())
if err = tx.QueryRow(r.Context(), `insert into api_key_assignments (key_id,tenant_id,user_id,description,source,enabled) values ($1,$2,$3,$4,'managed',true) returning created_at`, x.ID, x.TenantID, x.UserID, x.Description).Scan(&x.CreatedAt); err == nil {
_, err = tx.Exec(r.Context(), `insert into managed_api_keys (key_id,secret_hash) values ($1,$2)`, x.ID, hash)
}
if err == nil {
err = tx.Commit(r.Context())
}
if err != nil {
identityError(w, err)
return
}
a.keyring.AddManaged(x.ID, hash, true)
a.keyring.AssignTenant(x.ID, x.TenantID)
writeJSON(w, http.StatusCreated, struct {
Key keyDTO `json:"key"`
Secret string `json:"secret"`
}{x, secret})
}
func (a *identityAdmin) updateKey(w http.ResponseWriter, r *http.Request, keyID string) {
var body struct {
TenantID string `json:"tenant_id"`
UserID *string `json:"user_id"`
Description *string `json:"description"`
Enabled *bool `json:"enabled"`
}
if !decodeJSON(w, r, &body) {
return
}
body.TenantID = strings.TrimSpace(body.TenantID)
if body.TenantID == "" {
badRequest(w, "tenant_id is required")
return
}
if !a.keyring.IsConfigured(keyID) {
var exists bool
if err := a.pool.QueryRow(r.Context(), `select exists(select 1 from managed_api_keys where key_id=$1)`, keyID).Scan(&exists); err != nil {
identityError(w, err)
return
}
if !exists {
badRequest(w, "unknown key id")
return
}
}
var x keyDTO
err := a.pool.QueryRow(r.Context(), `insert into api_key_assignments (key_id,tenant_id,user_id,description,source,enabled) values ($1,$2,$3,coalesce($4,''),case when $6 then 'configured' else 'managed' end,coalesce($5,true)) on conflict (key_id) do update set tenant_id=excluded.tenant_id,user_id=excluded.user_id,description=coalesce($4,api_key_assignments.description),enabled=coalesce($5,api_key_assignments.enabled) returning key_id,tenant_id,user_id,description,source,enabled,created_at`, keyID, body.TenantID, body.UserID, body.Description, body.Enabled, a.keyring.IsConfigured(keyID)).Scan(&x.ID, &x.TenantID, &x.UserID, &x.Description, &x.Source, &x.Enabled, &x.CreatedAt)
if err != nil {
identityError(w, err)
return
}
a.keyring.AssignTenant(x.ID, x.TenantID)
a.keyring.SetManagedEnabled(x.ID, x.Enabled)
writeJSON(w, http.StatusOK, x)
}
func (a *identityAdmin) adoptLegacy(w http.ResponseWriter, r *http.Request, tenantID string) {
tenantID = strings.TrimSpace(tenantID)
if tenantID == "" {
badRequest(w, "tenant id is required")
return
}
tables := []string{"projects", "thoughts", "stored_files", "learnings", "plans", "chat_histories"}
tx, err := a.pool.Begin(r.Context())
if err != nil {
identityError(w, err)
return
}
defer tx.Rollback(r.Context())
var exists bool
if err = tx.QueryRow(r.Context(), `select exists(select 1 from tenants where id=$1)`, tenantID).Scan(&exists); err == nil && !exists {
badRequest(w, "tenant does not exist")
return
}
for _, table := range tables {
if _, err = tx.Exec(r.Context(), fmt.Sprintf("update %s set tenant_id=$1 where tenant_id is null", table), tenantID); err != nil {
identityError(w, err)
return
}
}
if err = tx.Commit(r.Context()); err != nil {
identityError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
func newIdentityID() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return hex.EncodeToString(b)
}
func decodeJSON(w http.ResponseWriter, r *http.Request, v any) bool {
defer r.Body.Close()
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
badRequest(w, "invalid JSON")
return false
}
return true
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
func badRequest(w http.ResponseWriter, message string) {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": message})
}
func identityError(w http.ResponseWriter, err error) {
if a, ok := err.(interface{ SQLState() string }); ok && a.SQLState() == "23505" {
badRequest(w, "that value already exists")
return
}
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "identity operation failed"})
}
-268
View File
@@ -1,268 +0,0 @@
package app
// Legacy admin handlers retired in favor of ResolveSpec-backed routes.
import (
"encoding/json"
"log/slog"
"net/http"
"strconv"
"strings"
"git.warky.dev/wdevs/amcs/internal/store"
ext "git.warky.dev/wdevs/amcs/internal/types"
"github.com/google/uuid"
)
type adminHandlers struct {
db *store.DB
logger *slog.Logger
}
func newAdminHandlers(db *store.DB, logger *slog.Logger) *adminHandlers {
return &adminHandlers{db: db, logger: logger}
}
func (h *adminHandlers) register(mux *http.ServeMux, middleware func(http.Handler) http.Handler) {
handle := func(pattern string, fn http.HandlerFunc) {
mux.Handle(pattern, middleware(fn))
}
handle("GET /api/admin/projects", h.listProjects)
handle("POST /api/admin/projects", h.createProject)
handle("GET /api/admin/thoughts", h.listThoughts)
handle("GET /api/admin/thoughts/{id}", h.getThought)
handle("DELETE /api/admin/thoughts/{id}", h.deleteThought)
handle("POST /api/admin/thoughts/{id}/archive", h.archiveThought)
handle("GET /api/admin/skills", h.listSkills)
handle("DELETE /api/admin/skills/{id}", h.deleteSkill)
handle("GET /api/admin/guardrails", h.listGuardrails)
handle("DELETE /api/admin/guardrails/{id}", h.deleteGuardrail)
handle("GET /api/admin/files", h.listFiles)
handle("GET /api/admin/stats", h.stats)
}
// --- Projects ---
func (h *adminHandlers) listProjects(w http.ResponseWriter, r *http.Request) {
projects, err := h.db.ListProjects(r.Context())
if err != nil {
h.internalError(w, "list projects", err)
return
}
writeJSON(w, projects)
}
func (h *adminHandlers) createProject(w http.ResponseWriter, r *http.Request) {
var body struct {
Name string `json:"name"`
Description string `json:"description"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
if strings.TrimSpace(body.Name) == "" {
http.Error(w, "name is required", http.StatusBadRequest)
return
}
project, err := h.db.CreateProject(r.Context(), body.Name, body.Description)
if err != nil {
h.internalError(w, "create project", err)
return
}
w.WriteHeader(http.StatusCreated)
writeJSON(w, project)
}
// --- Thoughts ---
func (h *adminHandlers) listThoughts(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
limit := 50
if l := q.Get("limit"); l != "" {
if n, err := strconv.Atoi(l); err == nil && n > 0 {
limit = min(n, 200)
}
}
query := strings.TrimSpace(q.Get("q"))
includeArchived := q.Get("include_archived") == "true"
var projectID *uuid.UUID
if pid := q.Get("project_id"); pid != "" {
if id, err := uuid.Parse(pid); err == nil {
projectID = &id
}
}
if query != "" {
results, err := h.db.SearchThoughtsText(r.Context(), query, limit, projectID, nil)
if err != nil {
h.internalError(w, "search thoughts", err)
return
}
writeJSON(w, results)
return
}
thoughts, err := h.db.ListThoughts(r.Context(), ext.ListFilter{
Limit: limit,
ProjectID: projectID,
IncludeArchived: includeArchived,
})
if err != nil {
h.internalError(w, "list thoughts", err)
return
}
writeJSON(w, thoughts)
}
func (h *adminHandlers) getThought(w http.ResponseWriter, r *http.Request) {
id, ok := parseUUID(w, r.PathValue("id"))
if !ok {
return
}
thought, err := h.db.GetThought(r.Context(), id)
if err != nil {
h.internalError(w, "get thought", err)
return
}
writeJSON(w, thought)
}
func (h *adminHandlers) deleteThought(w http.ResponseWriter, r *http.Request) {
id, ok := parseUUID(w, r.PathValue("id"))
if !ok {
return
}
if err := h.db.DeleteThought(r.Context(), id); err != nil {
h.internalError(w, "delete thought", err)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *adminHandlers) archiveThought(w http.ResponseWriter, r *http.Request) {
id, ok := parseUUID(w, r.PathValue("id"))
if !ok {
return
}
if err := h.db.ArchiveThought(r.Context(), id); err != nil {
h.internalError(w, "archive thought", err)
return
}
w.WriteHeader(http.StatusNoContent)
}
// --- Skills ---
func (h *adminHandlers) listSkills(w http.ResponseWriter, r *http.Request) {
tag := r.URL.Query().Get("tag")
skills, err := h.db.ListSkills(r.Context(), tag)
if err != nil {
h.internalError(w, "list skills", err)
return
}
writeJSON(w, skills)
}
func (h *adminHandlers) deleteSkill(w http.ResponseWriter, r *http.Request) {
id, ok := parseUUID(w, r.PathValue("id"))
if !ok {
return
}
if err := h.db.RemoveSkill(r.Context(), id); err != nil {
h.internalError(w, "delete skill", err)
return
}
w.WriteHeader(http.StatusNoContent)
}
// --- Guardrails ---
func (h *adminHandlers) listGuardrails(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
guardrails, err := h.db.ListGuardrails(r.Context(), q.Get("tag"), q.Get("severity"))
if err != nil {
h.internalError(w, "list guardrails", err)
return
}
writeJSON(w, guardrails)
}
func (h *adminHandlers) deleteGuardrail(w http.ResponseWriter, r *http.Request) {
id, ok := parseUUID(w, r.PathValue("id"))
if !ok {
return
}
if err := h.db.RemoveGuardrail(r.Context(), id); err != nil {
h.internalError(w, "delete guardrail", err)
return
}
w.WriteHeader(http.StatusNoContent)
}
// --- Files ---
func (h *adminHandlers) listFiles(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
limit := 100
if l := q.Get("limit"); l != "" {
if n, err := strconv.Atoi(l); err == nil && n > 0 {
limit = min(n, 500)
}
}
filter := ext.StoredFileFilter{Limit: limit}
if pid := q.Get("project_id"); pid != "" {
if id, err := uuid.Parse(pid); err == nil {
filter.ProjectID = &id
}
}
if tid := q.Get("thought_id"); tid != "" {
if id, err := uuid.Parse(tid); err == nil {
filter.ThoughtID = &id
}
}
filter.Kind = q.Get("kind")
files, err := h.db.ListStoredFiles(r.Context(), filter)
if err != nil {
h.internalError(w, "list files", err)
return
}
writeJSON(w, files)
}
// --- Stats ---
func (h *adminHandlers) stats(w http.ResponseWriter, r *http.Request) {
stats, err := h.db.Stats(r.Context())
if err != nil {
h.internalError(w, "stats", err)
return
}
writeJSON(w, stats)
}
// --- Helpers ---
func (h *adminHandlers) internalError(w http.ResponseWriter, op string, err error) {
h.logger.Error("admin handler error", slog.String("op", op), slog.String("error", err.Error()))
http.Error(w, "internal server error", http.StatusInternalServerError)
}
func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(v)
}
func parseUUID(w http.ResponseWriter, s string) (uuid.UUID, bool) {
id, err := uuid.Parse(s)
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return uuid.UUID{}, false
}
return id, true
}
+35 -60
View File
@@ -91,24 +91,16 @@ func Run(ctx context.Context, configPath string) error {
if err != nil {
return err
}
} else {
keyring = auth.NewManagedKeyring()
}
if err := ensureConfiguredTenants(ctx, db.Pool(), keyring); err != nil {
return fmt.Errorf("create configured tenants: %w", err)
}
if err := loadIdentityKeyring(ctx, db.Pool(), keyring); err != nil {
return fmt.Errorf("load identity key assignments: %w", err)
}
tokenStore = auth.NewTokenStore(0)
if len(cfg.Auth.OAuth.Clients) > 0 {
oauthRegistry, err = auth.NewOAuthRegistry(cfg.Auth.OAuth.Clients)
if err != nil {
return err
}
tokenStore = auth.NewTokenStore(0)
}
authCodes := auth.NewAuthCodeStore()
dynClients := auth.NewPostgresClientStore(db.Pool())
dynClients := auth.NewDynamicClientStore()
activeProjects := session.NewActiveProjects()
logger.Info("ai providers initialised",
@@ -191,47 +183,38 @@ func Run(ctx context.Context, configPath string) error {
}
}
func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *store.DB, embeddings *ai.EmbeddingRunner, metadata *ai.MetadataRunner, bgEmbeddings *ai.EmbeddingRunner, bgMetadata *ai.MetadataRunner, keyring *auth.Keyring, oauthRegistry *auth.OAuthRegistry, tokenStore *auth.TokenStore, authCodes *auth.AuthCodeStore, dynClients auth.ClientStore, activeProjects *session.ActiveProjects) (http.Handler, error) {
func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *store.DB, embeddings *ai.EmbeddingRunner, metadata *ai.MetadataRunner, bgEmbeddings *ai.EmbeddingRunner, bgMetadata *ai.MetadataRunner, keyring *auth.Keyring, oauthRegistry *auth.OAuthRegistry, tokenStore *auth.TokenStore, authCodes *auth.AuthCodeStore, dynClients *auth.DynamicClientStore, activeProjects *session.ActiveProjects) (http.Handler, error) {
mux := http.NewServeMux()
accessTracker := auth.NewAccessTracker()
oauthEnabled := oauthRegistry != nil
oauthEnabled := oauthRegistry != nil && tokenStore != nil
authMiddleware := auth.Middleware(cfg.Auth, keyring, oauthRegistry, tokenStore, accessTracker, logger)
filesTool := tools.NewFilesTool(db, activeProjects)
enrichmentRetryer := tools.NewEnrichmentRetryer(context.Background(), db, bgMetadata, cfg.Capture, cfg.AI.Metadata.Timeout, activeProjects, logger)
backfillTool := tools.NewBackfillTool(db, bgEmbeddings, activeProjects, logger)
adminActions := newAdminActions(backfillTool, enrichmentRetryer, logger)
identityAdmin := newIdentityAdmin(db.Pool(), keyring, oauthRegistry, logger)
toolSet := mcpserver.ToolSet{
Capture: tools.NewCaptureTool(db, embeddings, cfg.Capture, activeProjects, enrichmentRetryer, backfillTool),
Search: tools.NewSearchTool(db, embeddings, cfg.Search, activeProjects),
List: tools.NewListTool(db, cfg.Search, activeProjects),
Stats: tools.NewStatsTool(db),
Get: tools.NewGetTool(db),
Update: tools.NewUpdateTool(db, embeddings, metadata, cfg.Capture, logger),
Delete: tools.NewDeleteTool(db),
Archive: tools.NewArchiveTool(db),
DuplicateAudit: tools.NewDuplicateAuditTool(db, cfg.Search, activeProjects),
Projects: tools.NewProjectsTool(db, activeProjects),
Version: tools.NewVersionTool(cfg.MCP.ServerName, info),
Learnings: tools.NewLearningsTool(db, activeProjects, cfg.Search),
Plans: tools.NewPlansTool(db, activeProjects, cfg.Search),
ProjectPersonas: tools.NewProjectPersonasTool(db, activeProjects),
WorldModel: tools.NewWorldModelTool(db, activeProjects),
ThoughtLearningLinks: tools.NewThoughtLearningLinksTool(db),
Context: tools.NewContextTool(db, embeddings, cfg.Search, activeProjects),
Recall: tools.NewRecallTool(db, embeddings, cfg.Search, activeProjects),
Summarize: tools.NewSummarizeTool(db, embeddings, metadata, cfg.Search, activeProjects),
Links: tools.NewLinksTool(db, embeddings, cfg.Search),
Files: filesTool,
Backfill: backfillTool,
Reparse: tools.NewReparseMetadataTool(db, bgMetadata, cfg.Capture, activeProjects, logger),
RetryMetadata: tools.NewRetryEnrichmentTool(enrichmentRetryer),
//Maintenance: tools.NewMaintenanceTool(db),
Skills: tools.NewSkillsTool(db, activeProjects),
Personas: tools.NewAgentPersonasTool(db),
ChatHistory: tools.NewChatHistoryTool(db, activeProjects),
Describe: tools.NewDescribeTool(db, mcpserver.BuildToolCatalog()),
Capture: tools.NewCaptureTool(db, embeddings, cfg.Capture, activeProjects, enrichmentRetryer, backfillTool),
Search: tools.NewSearchTool(db, embeddings, cfg.Search, activeProjects),
List: tools.NewListTool(db, cfg.Search, activeProjects),
Stats: tools.NewStatsTool(db),
Get: tools.NewGetTool(db),
Update: tools.NewUpdateTool(db, embeddings, metadata, cfg.Capture, logger),
Delete: tools.NewDeleteTool(db),
Archive: tools.NewArchiveTool(db),
Projects: tools.NewProjectsTool(db, activeProjects),
Version: tools.NewVersionTool(cfg.MCP.ServerName, info),
Context: tools.NewContextTool(db, embeddings, cfg.Search, activeProjects),
Recall: tools.NewRecallTool(db, embeddings, cfg.Search, activeProjects),
Summarize: tools.NewSummarizeTool(db, embeddings, metadata, cfg.Search, activeProjects),
Links: tools.NewLinksTool(db, embeddings, cfg.Search),
Files: filesTool,
Backfill: backfillTool,
Reparse: tools.NewReparseMetadataTool(db, bgMetadata, cfg.Capture, activeProjects, logger),
RetryMetadata: tools.NewRetryEnrichmentTool(enrichmentRetryer),
Maintenance: tools.NewMaintenanceTool(db),
Skills: tools.NewSkillsTool(db, activeProjects),
ChatHistory: tools.NewChatHistoryTool(db, activeProjects),
Describe: tools.NewDescribeTool(db, mcpserver.BuildToolCatalog()),
}
mcpHandlers, err := mcpserver.NewHandlers(cfg.MCP, logger, toolSet, activeProjects.Clear)
@@ -243,29 +226,21 @@ func routes(logger *slog.Logger, cfg *config.Config, info buildinfo.Info, db *st
mux.Handle(cfg.MCP.SSEPath, authMiddleware(mcpHandlers.SSE))
logger.Info("SSE transport enabled", slog.String("sse_path", cfg.MCP.SSEPath))
}
if err := registerResolveSpecAdminRoutes(mux, db, authMiddleware, logger); err != nil {
return nil, fmt.Errorf("setup resolvespec admin routes: %w", err)
}
mux.Handle("/files", authMiddleware(fileHandler(filesTool)))
mux.Handle("/files/{id}", authMiddleware(fileHandler(filesTool)))
mux.Handle("/webhooks/thoughts", authMiddleware(newWebhookThoughtHandler(db, embeddings, cfg.Capture, enrichmentRetryer, backfillTool)))
mux.HandleFunc("/.well-known/oauth-authorization-server", oauthMetadataHandler())
mux.HandleFunc("/api/oauth/register", oauthRegisterHandler(dynClients, logger))
mux.HandleFunc("/api/oauth/authorize", oauthAuthorizeHandler(dynClients, authCodes, logger))
mux.HandleFunc("/api/oauth/token", oauthTokenHandler(oauthRegistry, tokenStore, authCodes, logger))
mux.Handle("/api/admin/actions/backfill", authMiddleware(adminActions.backfillHandler()))
mux.Handle("/api/admin/actions/retry-metadata", authMiddleware(adminActions.retryMetadataHandler()))
mux.Handle("/api/admin/identity", authMiddleware(identityAdmin.handler()))
mux.Handle("/api/admin/identity/", authMiddleware(identityAdmin.handler()))
if oauthEnabled {
mux.HandleFunc("/.well-known/oauth-authorization-server", oauthMetadataHandler())
mux.HandleFunc("/oauth-authorization-server", oauthMetadataHandler())
mux.HandleFunc("/oauth/register", oauthRegisterHandler(dynClients, logger))
mux.HandleFunc("/authorize", oauthAuthorizeHandler(dynClients, authCodes, logger))
mux.HandleFunc("/oauth/authorize", oauthAuthorizeHandler(dynClients, authCodes, logger))
mux.HandleFunc("/oauth/token", oauthTokenHandler(oauthRegistry, tokenStore, authCodes, logger))
}
mux.HandleFunc("/favicon.ico", serveFavicon)
mux.HandleFunc("/images/project.jpg", serveHomeImage)
mux.HandleFunc("/images/icon.png", serveIcon)
mux.HandleFunc("/llm", serveLLMInstructions)
mux.HandleFunc("/llms.txt", serveLLMSTXT)
mux.HandleFunc("/.well-known/llms.txt", serveLLMSTXT)
mux.HandleFunc("/robots.txt", serveRobotsTXT)
mux.Handle("/api/status", authMiddleware(statusAPIHandler(info, accessTracker, oauthEnabled)))
mux.HandleFunc("/status", publicStatusHandler(accessTracker))
mux.HandleFunc("/api/status", statusAPIHandler(info, accessTracker, oauthEnabled))
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
-73
View File
@@ -1,9 +1,7 @@
package app
import (
"fmt"
"net/http"
"strings"
amcsllm "git.warky.dev/wdevs/amcs/llm"
)
@@ -22,74 +20,3 @@ func serveLLMInstructions(w http.ResponseWriter, r *http.Request) {
}
_, _ = w.Write(amcsllm.MemoryInstructions)
}
func serveRobotsTXT(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/robots.txt" {
http.NotFound(w, r)
return
}
if r.Method != http.MethodGet && r.Method != http.MethodHead {
w.Header().Set("Allow", "GET, HEAD")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Cache-Control", "public, max-age=300")
w.WriteHeader(http.StatusOK)
if r.Method == http.MethodHead {
return
}
body := fmt.Sprintf("User-agent: *\nAllow: /\n\n# LLM-friendly docs\nLLM: %s/llm\nLLMS: %s/llms.txt\n", requestBaseURL(r), requestBaseURL(r))
_, _ = w.Write([]byte(body))
}
func serveLLMSTXT(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/llms.txt" && r.URL.Path != "/.well-known/llms.txt" {
http.NotFound(w, r)
return
}
if r.Method != http.MethodGet && r.Method != http.MethodHead {
w.Header().Set("Allow", "GET, HEAD")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Cache-Control", "public, max-age=300")
w.WriteHeader(http.StatusOK)
if r.Method == http.MethodHead {
return
}
base := requestBaseURL(r)
body := fmt.Sprintf(
"# AMCS\n\n> A memory server for AI assistants (MCP tools, semantic retrieval, and structured project memory).\n\n## Endpoints\n- %s/llm\n- %s/status\n- %s/mcp\n- %s/.well-known/oauth-authorization-server\n",
base,
base,
base,
base,
)
_, _ = w.Write([]byte(body))
}
func requestBaseURL(r *http.Request) string {
scheme := "http"
if r != nil && r.TLS != nil {
scheme = "https"
}
if r != nil {
if proto := strings.TrimSpace(r.Header.Get("X-Forwarded-Proto")); proto != "" {
scheme = proto
}
}
host := "localhost"
if r != nil {
if v := strings.TrimSpace(r.Host); v != "" {
host = v
}
}
return scheme + "://" + host
}
-68
View File
@@ -3,7 +3,6 @@ package app
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
amcsllm "git.warky.dev/wdevs/amcs/llm"
@@ -30,70 +29,3 @@ func TestServeLLMInstructions(t *testing.T) {
t.Fatalf("body = %q, want embedded instructions", body)
}
}
func TestServeRobotsTXT(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/robots.txt", nil)
req.Host = "amcs.example.com"
req.Header.Set("X-Forwarded-Proto", "https")
rec := httptest.NewRecorder()
serveRobotsTXT(rec, req)
res := rec.Result()
defer func() {
_ = res.Body.Close()
}()
if res.StatusCode != http.StatusOK {
t.Fatalf("status = %d, want %d", res.StatusCode, http.StatusOK)
}
if got := res.Header.Get("Content-Type"); got != "text/plain; charset=utf-8" {
t.Fatalf("content-type = %q, want %q", got, "text/plain; charset=utf-8")
}
body := rec.Body.String()
if !strings.Contains(body, "LLM: https://amcs.example.com/llm") {
t.Fatalf("body = %q, want LLM link", body)
}
if !strings.Contains(body, "LLMS: https://amcs.example.com/llms.txt") {
t.Fatalf("body = %q, want LLMS link", body)
}
}
func TestServeLLMSTXT(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/llms.txt", nil)
req.Host = "amcs.example.com"
req.Header.Set("X-Forwarded-Proto", "https")
rec := httptest.NewRecorder()
serveLLMSTXT(rec, req)
res := rec.Result()
defer func() {
_ = res.Body.Close()
}()
if res.StatusCode != http.StatusOK {
t.Fatalf("status = %d, want %d", res.StatusCode, http.StatusOK)
}
if got := res.Header.Get("Content-Type"); got != "text/plain; charset=utf-8" {
t.Fatalf("content-type = %q, want %q", got, "text/plain; charset=utf-8")
}
body := rec.Body.String()
if !strings.Contains(body, "https://amcs.example.com/llm") {
t.Fatalf("body = %q, want /llm link", body)
}
if !strings.Contains(body, "https://amcs.example.com/.well-known/oauth-authorization-server") {
t.Fatalf("body = %q, want oauth discovery link", body)
}
}
func TestServeLLMSTXTWellKnownPath(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/.well-known/llms.txt", nil)
rec := httptest.NewRecorder()
serveLLMSTXT(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
}
}
+8 -12
View File
@@ -67,9 +67,9 @@ func oauthMetadataHandler() http.HandlerFunc {
base := serverBaseURL(r)
meta := oauthServerMetadata{
Issuer: base,
AuthorizationEndpoint: base + "/api/oauth/authorize",
TokenEndpoint: base + "/api/oauth/token",
RegistrationEndpoint: base + "/api/oauth/register",
AuthorizationEndpoint: base + "/authorize",
TokenEndpoint: base + "/oauth/token",
RegistrationEndpoint: base + "/oauth/register",
ScopesSupported: []string{"mcp"},
ResponseTypesSupported: []string{"code"},
GrantTypesSupported: []string{"authorization_code", "client_credentials"},
@@ -83,7 +83,7 @@ func oauthMetadataHandler() http.HandlerFunc {
// oauthRegisterHandler serves POST /oauth/register per RFC 7591
// (OAuth 2.0 Dynamic Client Registration).
func oauthRegisterHandler(dynClients auth.ClientStore, log *slog.Logger) http.HandlerFunc {
func oauthRegisterHandler(dynClients *auth.DynamicClientStore, log *slog.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.Header().Set("Allow", http.MethodPost)
@@ -130,7 +130,7 @@ func oauthRegisterHandler(dynClients auth.ClientStore, log *slog.Logger) http.Ha
// oauthAuthorizeHandler serves GET and POST /oauth/authorize.
// GET shows an approval page; POST processes the user's approve/deny action.
func oauthAuthorizeHandler(dynClients auth.ClientStore, authCodes *auth.AuthCodeStore, log *slog.Logger) http.HandlerFunc {
func oauthAuthorizeHandler(dynClients *auth.DynamicClientStore, authCodes *auth.AuthCodeStore, log *slog.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
@@ -144,7 +144,7 @@ func oauthAuthorizeHandler(dynClients auth.ClientStore, authCodes *auth.AuthCode
}
}
func handleAuthorizeGET(w http.ResponseWriter, r *http.Request, dynClients auth.ClientStore) {
func handleAuthorizeGET(w http.ResponseWriter, r *http.Request, dynClients *auth.DynamicClientStore) {
q := r.URL.Query()
clientID := q.Get("client_id")
redirectURI := q.Get("redirect_uri")
@@ -178,7 +178,7 @@ func handleAuthorizeGET(w http.ResponseWriter, r *http.Request, dynClients auth.
serveAuthorizePage(w, client.ClientName, clientID, redirectURI, state, codeChallenge, codeChallengeMethod, scope)
}
func handleAuthorizePOST(w http.ResponseWriter, r *http.Request, dynClients auth.ClientStore, authCodes *auth.AuthCodeStore, log *slog.Logger) {
func handleAuthorizePOST(w http.ResponseWriter, r *http.Request, dynClients *auth.DynamicClientStore, authCodes *auth.AuthCodeStore, log *slog.Logger) {
if err := r.ParseForm(); err != nil {
http.Error(w, "invalid form", http.StatusBadRequest)
return
@@ -244,10 +244,6 @@ func oauthTokenHandler(oauthRegistry *auth.OAuthRegistry, tokenStore *auth.Token
switch r.FormValue("grant_type") {
case "client_credentials":
if oauthRegistry == nil {
writeTokenError(w, "unsupported_grant_type", http.StatusBadRequest)
return
}
handleClientCredentials(w, r, oauthRegistry, tokenStore, log)
case "authorization_code":
handleAuthorizationCode(w, r, authCodes, tokenStore, log)
@@ -338,7 +334,7 @@ button{padding:.5rem 1.2rem;margin-right:.5rem;cursor:pointer;font-size:1rem}
<body>
<h2>Authorize Access</h2>
<p><strong>%s</strong> is requesting access to this AMCS server.</p>
<form method=POST action=/api/oauth/authorize>
<form method=POST action=/oauth/authorize>
<input type=hidden name=client_id value="%s">
<input type=hidden name=redirect_uri value="%s">
<input type=hidden name=state value="%s">
-155
View File
@@ -1,155 +0,0 @@
package app
import (
"fmt"
"log/slog"
"net/http"
"strings"
"github.com/bitechdev/ResolveSpec/pkg/resolvespec"
"github.com/uptrace/bunrouter"
"git.warky.dev/wdevs/amcs/internal/store"
"git.warky.dev/wdevs/amcs/internal/tenancy"
)
func registerResolveSpecAdminRoutes(mux *http.ServeMux, db *store.DB, middleware func(http.Handler) http.Handler, logger *slog.Logger) error {
rs := resolvespec.NewHandlerWithBun(db.Bun())
registerResolveSpecGuards(rs)
for _, model := range resolveSpecModels() {
if err := rs.RegisterModel(model.schema, model.entity, model.model); err != nil {
return fmt.Errorf("register resolvespec model %s.%s: %w", model.schema, model.entity, err)
}
}
rsRouter := bunrouter.New()
resolvespec.SetupBunRouterRoutes(rsRouter, rs, nil)
rsMount := http.StripPrefix("/api/rs", rsRouter)
protectedRSMount := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" && strings.HasSuffix(r.URL.Path, "/") {
trimmed := strings.TrimRight(r.URL.Path, "/")
if trimmed == "" {
trimmed = "/"
}
clone := r.Clone(r.Context())
clone.URL.Path = trimmed
if clone.URL.RawPath != "" {
clone.URL.RawPath = strings.TrimRight(clone.URL.RawPath, "/")
if clone.URL.RawPath == "" {
clone.URL.RawPath = "/"
}
}
r = clone
}
if r.Method == http.MethodOptions {
rsMount.ServeHTTP(w, r)
return
}
middleware(http.HandlerFunc(func(w http.ResponseWriter, authenticated *http.Request) {
if tenantID := strings.TrimSpace(authenticated.Header.Get("X-AMCS-Tenant-ID")); tenantID != "" {
authenticated = authenticated.WithContext(tenancy.WithTenantKey(authenticated.Context(), tenantID))
}
rsMount.ServeHTTP(w, authenticated)
})).ServeHTTP(w, r)
})
mux.Handle("/api/rs/", protectedRSMount)
mux.Handle("/api/rs", http.RedirectHandler("/api/rs/openapi", http.StatusTemporaryRedirect))
if logger != nil {
logger.Info("resolvespec admin api enabled",
slog.String("prefix", "/api/rs"),
slog.Int("models", len(resolveSpecModels())),
)
}
return nil
}
func registerResolveSpecGuards(rs *resolvespec.Handler) {
mutableByEntity := map[string]map[string]struct{}{
"projects": {
"create": {},
"update": {},
"delete": {},
},
"thoughts": {
"create": {},
"update": {},
"delete": {},
},
"plans": {
"create": {},
"update": {},
"delete": {},
},
"learnings": {
"create": {},
"update": {},
"delete": {},
},
"agent_personas": {
"create": {},
"update": {},
"delete": {},
},
"agent_parts": {
"create": {},
"update": {},
"delete": {},
},
"agent_traits": {
"create": {},
"update": {},
"delete": {},
},
"agent_skills": {
"create": {},
"update": {},
"delete": {},
},
"agent_guardrails": {
"create": {},
"update": {},
"delete": {},
},
"stored_files": {
"update": {},
"delete": {},
},
}
rs.Hooks().Register(resolvespec.BeforeHandle, func(hookCtx *resolvespec.HookContext) error {
switch hookCtx.Operation {
case "read", "meta":
return nil
case "create", "update", "delete":
allowedOps, ok := mutableByEntity[hookCtx.Entity]
if !ok {
hookCtx.Abort = true
hookCtx.AbortCode = http.StatusForbidden
hookCtx.AbortMessage = fmt.Sprintf("operation %q is not allowed for %s.%s", hookCtx.Operation, hookCtx.Schema, hookCtx.Entity)
return fmt.Errorf("forbidden operation")
}
if _, ok := allowedOps[hookCtx.Operation]; !ok {
hookCtx.Abort = true
hookCtx.AbortCode = http.StatusForbidden
hookCtx.AbortMessage = fmt.Sprintf("operation %q is not allowed for %s.%s", hookCtx.Operation, hookCtx.Schema, hookCtx.Entity)
return fmt.Errorf("forbidden operation")
}
return nil
default:
hookCtx.Abort = true
hookCtx.AbortCode = http.StatusBadRequest
hookCtx.AbortMessage = fmt.Sprintf("unsupported operation %q", hookCtx.Operation)
return fmt.Errorf("unsupported operation")
}
})
}
type resolveSpecModel struct {
schema string
entity string
model any
}
-206
View File
@@ -1,206 +0,0 @@
package app
import (
"io"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.warky.dev/wdevs/amcs/internal/auth"
"git.warky.dev/wdevs/amcs/internal/config"
"github.com/bitechdev/ResolveSpec/pkg/resolvespec"
)
func TestResolveSpecAuthRequiresValidCredentials(t *testing.T) {
keyring, err := auth.NewKeyring([]config.APIKey{{ID: "operator", Value: "secret"}})
if err != nil {
t.Fatalf("NewKeyring() error = %v", err)
}
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
protected := auth.Middleware(config.AuthConfig{}, keyring, nil, nil, nil, logger)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/rs/public/projects" {
t.Fatalf("path = %q, want /api/rs/public/projects", r.URL.Path)
}
w.WriteHeader(http.StatusNoContent)
}))
t.Run("missing credentials are rejected", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/rs/public/projects", strings.NewReader(`{"operation":"read"}`))
rec := httptest.NewRecorder()
protected.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusUnauthorized)
}
})
t.Run("valid API key is accepted", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/rs/public/projects", strings.NewReader(`{"operation":"read"}`))
req.Header.Set("x-brain-key", "secret")
rec := httptest.NewRecorder()
protected.ServeHTTP(rec, req)
if rec.Code != http.StatusNoContent {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNoContent)
}
})
}
func TestResolveSpecGuardAllowsSupportedMutations(t *testing.T) {
rs := resolvespec.NewHandler(nil, nil)
registerResolveSpecGuards(rs)
cases := []struct {
name string
entity string
operation string
}{
{name: "learnings read", entity: "learnings", operation: "read"},
{name: "projects create", entity: "projects", operation: "create"},
{name: "projects update", entity: "projects", operation: "update"},
{name: "projects delete", entity: "projects", operation: "delete"},
{name: "plans create", entity: "plans", operation: "create"},
{name: "plans update", entity: "plans", operation: "update"},
{name: "plans delete", entity: "plans", operation: "delete"},
{name: "learnings create", entity: "learnings", operation: "create"},
{name: "learnings update", entity: "learnings", operation: "update"},
{name: "learnings delete", entity: "learnings", operation: "delete"},
{name: "thoughts create", entity: "thoughts", operation: "create"},
{name: "thoughts update", entity: "thoughts", operation: "update"},
{name: "thoughts delete", entity: "thoughts", operation: "delete"},
{name: "agent_skills create", entity: "agent_skills", operation: "create"},
{name: "agent_skills update", entity: "agent_skills", operation: "update"},
{name: "agent_skills delete", entity: "agent_skills", operation: "delete"},
{name: "agent_guardrails create", entity: "agent_guardrails", operation: "create"},
{name: "agent_guardrails update", entity: "agent_guardrails", operation: "update"},
{name: "agent_guardrails delete", entity: "agent_guardrails", operation: "delete"},
{name: "agent_personas create", entity: "agent_personas", operation: "create"},
{name: "agent_personas update", entity: "agent_personas", operation: "update"},
{name: "agent_personas delete", entity: "agent_personas", operation: "delete"},
{name: "agent_parts create", entity: "agent_parts", operation: "create"},
{name: "agent_parts update", entity: "agent_parts", operation: "update"},
{name: "agent_parts delete", entity: "agent_parts", operation: "delete"},
{name: "agent_traits create", entity: "agent_traits", operation: "create"},
{name: "agent_traits update", entity: "agent_traits", operation: "update"},
{name: "agent_traits delete", entity: "agent_traits", operation: "delete"},
{name: "stored_files update", entity: "stored_files", operation: "update"},
{name: "stored_files delete", entity: "stored_files", operation: "delete"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
hookCtx := &resolvespec.HookContext{
Schema: "public",
Entity: tc.entity,
Operation: tc.operation,
}
err := rs.Hooks().Execute(resolvespec.BeforeHandle, hookCtx)
if err != nil {
t.Fatalf("Execute() error = %v, want nil", err)
}
if hookCtx.Abort {
t.Fatalf("Abort = true, want false (code=%d message=%q)", hookCtx.AbortCode, hookCtx.AbortMessage)
}
})
}
}
func TestResolveSpecGuardBlocksUnsupportedMutations(t *testing.T) {
rs := resolvespec.NewHandler(nil, nil)
registerResolveSpecGuards(rs)
cases := []struct {
name string
entity string
operation string
wantCode int
wantMessageIn string
}{
{
name: "mutations blocked for non-allowlisted operation",
entity: "stored_files",
operation: "create",
wantCode: http.StatusForbidden,
wantMessageIn: `operation "create" is not allowed for public.stored_files`,
},
{
name: "mutations blocked for non-allowlisted entity",
entity: "maintenance_logs",
operation: "delete",
wantCode: http.StatusForbidden,
wantMessageIn: `operation "delete" is not allowed for public.maintenance_logs`,
},
{
name: "unknown operation is rejected",
entity: "projects",
operation: "scan",
wantCode: http.StatusBadRequest,
wantMessageIn: `unsupported operation "scan"`,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
hookCtx := &resolvespec.HookContext{
Schema: "public",
Entity: tc.entity,
Operation: tc.operation,
}
err := rs.Hooks().Execute(resolvespec.BeforeHandle, hookCtx)
if err == nil {
t.Fatal("Execute() error = nil, want non-nil")
}
if !hookCtx.Abort {
t.Fatal("Abort = false, want true")
}
if hookCtx.AbortCode != tc.wantCode {
t.Fatalf("AbortCode = %d, want %d", hookCtx.AbortCode, tc.wantCode)
}
if !strings.Contains(hookCtx.AbortMessage, tc.wantMessageIn) {
t.Fatalf("AbortMessage = %q, want substring %q", hookCtx.AbortMessage, tc.wantMessageIn)
}
})
}
}
func TestResolveSpecModelsIncludeLearnings(t *testing.T) {
models := resolveSpecModels()
for _, model := range models {
if model.schema == "public" && model.entity == "learnings" {
return
}
}
t.Fatal("resolveSpecModels() missing public.learnings")
}
func TestResolveSpecModelsIncludePersonaEntities(t *testing.T) {
models := resolveSpecModels()
required := map[string]bool{
"agent_personas": false,
"agent_parts": false,
"agent_traits": false,
}
for _, model := range models {
if model.schema != "public" {
continue
}
if _, ok := required[model.entity]; ok {
required[model.entity] = true
}
}
for entity, found := range required {
if !found {
t.Fatalf("resolveSpecModels() missing public.%s", entity)
}
}
}
@@ -1,44 +0,0 @@
// Code generated by relspec templ. DO NOT EDIT.
package app
import "git.warky.dev/wdevs/amcs/internal/generatedmodels"
func resolveSpecModels() []resolveSpecModel {
return []resolveSpecModel{
{schema: "public", entity: "agent_guardrails", model: generatedmodels.ModelPublicAgentGuardrails{}},
{schema: "public", entity: "agent_parts", model: generatedmodels.ModelPublicAgentParts{}},
{schema: "public", entity: "agent_persona_guardrails", model: generatedmodels.ModelPublicAgentPersonaGuardrails{}},
{schema: "public", entity: "agent_persona_parts", model: generatedmodels.ModelPublicAgentPersonaParts{}},
{schema: "public", entity: "agent_persona_skills", model: generatedmodels.ModelPublicAgentPersonaSkills{}},
{schema: "public", entity: "agent_persona_traits", model: generatedmodels.ModelPublicAgentPersonaTraits{}},
{schema: "public", entity: "agent_personas", model: generatedmodels.ModelPublicAgentPersonas{}},
{schema: "public", entity: "agent_skills", model: generatedmodels.ModelPublicAgentSkills{}},
{schema: "public", entity: "agent_traits", model: generatedmodels.ModelPublicAgentTraits{}},
{schema: "public", entity: "api_key_assignments", model: generatedmodels.ModelPublicAPIKeyAssignments{}},
{schema: "public", entity: "arc_stage_parts", model: generatedmodels.ModelPublicArcStageParts{}},
{schema: "public", entity: "arc_stages", model: generatedmodels.ModelPublicArcStages{}},
{schema: "public", entity: "character_arcs", model: generatedmodels.ModelPublicCharacterArcs{}},
{schema: "public", entity: "chat_histories", model: generatedmodels.ModelPublicChatHistories{}},
{schema: "public", entity: "embeddings", model: generatedmodels.ModelPublicEmbeddings{}},
{schema: "public", entity: "learnings", model: generatedmodels.ModelPublicLearnings{}},
{schema: "public", entity: "managed_api_keys", model: generatedmodels.ModelPublicManagedAPIKeys{}},
{schema: "public", entity: "oauth_clients", model: generatedmodels.ModelPublicOauthClients{}},
{schema: "public", entity: "persona_arc", model: generatedmodels.ModelPublicPersonaArc{}},
{schema: "public", entity: "plan_dependencies", model: generatedmodels.ModelPublicPlanDependencies{}},
{schema: "public", entity: "plan_guardrails", model: generatedmodels.ModelPublicPlanGuardrails{}},
{schema: "public", entity: "plan_related_plans", model: generatedmodels.ModelPublicPlanRelatedPlans{}},
{schema: "public", entity: "plan_skills", model: generatedmodels.ModelPublicPlanSkills{}},
{schema: "public", entity: "plans", model: generatedmodels.ModelPublicPlans{}},
{schema: "public", entity: "project_guardrails", model: generatedmodels.ModelPublicProjectGuardrails{}},
{schema: "public", entity: "project_personas", model: generatedmodels.ModelPublicProjectPersonas{}},
{schema: "public", entity: "project_skills", model: generatedmodels.ModelPublicProjectSkills{}},
{schema: "public", entity: "projects", model: generatedmodels.ModelPublicProjects{}},
{schema: "public", entity: "stored_files", model: generatedmodels.ModelPublicStoredFiles{}},
{schema: "public", entity: "tenant_users", model: generatedmodels.ModelPublicTenantUsers{}},
{schema: "public", entity: "tenants", model: generatedmodels.ModelPublicTenants{}},
{schema: "public", entity: "thought_learning_links", model: generatedmodels.ModelPublicThoughtLearningLinks{}},
{schema: "public", entity: "thought_links", model: generatedmodels.ModelPublicThoughtLinks{}},
{schema: "public", entity: "thoughts", model: generatedmodels.ModelPublicThoughts{}},
{schema: "public", entity: "tool_annotations", model: generatedmodels.ModelPublicToolAnnotations{}},
}
}
+3 -57
View File
@@ -25,25 +25,11 @@ type statusAPIResponse struct {
TotalKnown int `json:"total_known"`
ConnectedWindow string `json:"connected_window"`
Entries []auth.AccessSnapshot `json:"entries"`
Metrics auth.AccessMetrics `json:"metrics"`
OAuthEnabled bool `json:"oauth_enabled"`
}
type publicClientStatus struct {
KeyID string `json:"key_id"`
RequestCount int `json:"request_count"`
LastAccessedAt time.Time `json:"last_accessed_at"`
}
type publicStatusResponse struct {
ConnectedCount int `json:"connected_count"`
ConnectedWindow string `json:"connected_window"`
Entries []publicClientStatus `json:"entries"`
}
func statusSnapshot(info buildinfo.Info, tracker *auth.AccessTracker, oauthEnabled bool, now time.Time) statusAPIResponse {
entries := tracker.Snapshot()
metrics := tracker.Metrics(20)
return statusAPIResponse{
Title: "Avelon Memory Crystal Server (AMCS)",
Description: "AMCS is a memory server that captures, links, and retrieves structured project thoughts for AI assistants using semantic search, summaries, and MCP tools.",
@@ -53,8 +39,7 @@ func statusSnapshot(info buildinfo.Info, tracker *auth.AccessTracker, oauthEnabl
ConnectedCount: tracker.ConnectedCount(now, connectedWindow),
TotalKnown: len(entries),
ConnectedWindow: "last 10 minutes",
Entries: nil,
Metrics: metrics,
Entries: entries,
OAuthEnabled: oauthEnabled,
}
}
@@ -88,47 +73,6 @@ func statusAPIHandler(info buildinfo.Info, tracker *auth.AccessTracker, oauthEna
}
}
func publicStatusHandler(tracker *auth.AccessTracker) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/status" {
http.NotFound(w, r)
return
}
if r.Method != http.MethodGet && r.Method != http.MethodHead {
w.Header().Set("Allow", "GET, HEAD")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
now := time.Now()
cutoff := now.UTC().Add(-connectedWindow)
snapshot := tracker.Snapshot()
entries := make([]publicClientStatus, 0, len(snapshot))
for _, item := range snapshot {
if item.LastAccessedAt.Before(cutoff) {
continue
}
entries = append(entries, publicClientStatus{
KeyID: item.KeyID,
RequestCount: item.RequestCount,
LastAccessedAt: item.LastAccessedAt,
})
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
if r.Method == http.MethodHead {
return
}
_ = json.NewEncoder(w).Encode(publicStatusResponse{
ConnectedCount: len(entries),
ConnectedWindow: "last 10 minutes",
Entries: entries,
})
}
}
func homeHandler(_ buildinfo.Info, _ *auth.AccessTracker, _ bool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
@@ -146,6 +90,8 @@ func homeHandler(_ buildinfo.Info, _ *auth.AccessTracker, _ bool) http.HandlerFu
if serveUIAsset(w, r, requestPath) {
return
}
http.NotFound(w, r)
return
}
serveUIIndex(w, r)
+5 -69
View File
@@ -33,7 +33,7 @@ func TestStatusSnapshotHidesOAuthLinkWhenDisabled(t *testing.T) {
func TestStatusSnapshotShowsTrackedAccess(t *testing.T) {
tracker := auth.NewAccessTracker()
now := time.Date(2026, 4, 4, 12, 0, 0, 0, time.UTC)
tracker.Record("client-a", "/files", "127.0.0.1:1234", "tester", "list_projects", now)
tracker.Record("client-a", "/files", "127.0.0.1:1234", "tester", now)
snapshot := statusSnapshot(buildinfo.Info{Version: "v1.2.3"}, tracker, true, now)
@@ -43,29 +43,11 @@ func TestStatusSnapshotShowsTrackedAccess(t *testing.T) {
if snapshot.ConnectedCount != 1 {
t.Fatalf("ConnectedCount = %d, want 1", snapshot.ConnectedCount)
}
if len(snapshot.Entries) != 0 {
t.Fatalf("len(Entries) = %d, want 0 for counts-only status", len(snapshot.Entries))
if len(snapshot.Entries) != 1 {
t.Fatalf("len(Entries) = %d, want 1", len(snapshot.Entries))
}
if snapshot.Metrics.TotalRequests != 1 {
t.Fatalf("Metrics.TotalRequests = %d, want 1", snapshot.Metrics.TotalRequests)
}
if snapshot.Metrics.UniqueIPs != 1 {
t.Fatalf("Metrics.UniqueIPs = %d, want 1", snapshot.Metrics.UniqueIPs)
}
if snapshot.Metrics.UniqueAgents != 1 {
t.Fatalf("Metrics.UniqueAgents = %d, want 1", snapshot.Metrics.UniqueAgents)
}
if snapshot.Metrics.UniqueTools != 1 {
t.Fatalf("Metrics.UniqueTools = %d, want 1", snapshot.Metrics.UniqueTools)
}
if len(snapshot.Metrics.TopIPs) != 1 || len(snapshot.Metrics.TopAgents) != 1 || len(snapshot.Metrics.TopTools) != 1 {
t.Fatalf("Top breakdowns not populated: %+v", snapshot.Metrics)
}
if len(snapshot.Metrics.RecentLog) != 1 {
t.Fatalf("RecentLog len = %d, want 1", len(snapshot.Metrics.RecentLog))
}
if snapshot.Metrics.RecentLog[0].Tool != "list_projects" {
t.Fatalf("RecentLog[0].Tool = %q, want %q", snapshot.Metrics.RecentLog[0].Tool, "list_projects")
if snapshot.Entries[0].KeyID != "client-a" || snapshot.Entries[0].LastPath != "/files" {
t.Fatalf("entry = %+v, want keyID client-a and path /files", snapshot.Entries[0])
}
}
@@ -92,52 +74,6 @@ func TestStatusAPIHandlerReturnsJSON(t *testing.T) {
}
}
func TestStatusAPIHandlerRejectsStatusPath(t *testing.T) {
handler := statusAPIHandler(buildinfo.Info{Version: "v1"}, auth.NewAccessTracker(), true)
req := httptest.NewRequest(http.MethodGet, "/status", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound)
}
}
func TestPublicStatusHandlerReturnsConnectedClientsOnly(t *testing.T) {
tracker := auth.NewAccessTracker()
now := time.Now().UTC()
tracker.Record("recent-client", "/mcp", "127.0.0.1:1234", "tester", "list_projects", now.Add(-2*time.Minute))
tracker.Record("stale-client", "/mcp", "127.0.0.1:9999", "tester", "list_projects", now.Add(-30*time.Minute))
handler := publicStatusHandler(tracker)
req := httptest.NewRequest(http.MethodGet, "/status", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
}
var payload publicStatusResponse
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if payload.ConnectedCount != 1 {
t.Fatalf("ConnectedCount = %d, want 1", payload.ConnectedCount)
}
if len(payload.Entries) != 1 {
t.Fatalf("len(Entries) = %d, want 1", len(payload.Entries))
}
if payload.Entries[0].KeyID != "recent-client" {
t.Fatalf("Entries[0].KeyID = %q, want %q", payload.Entries[0].KeyID, "recent-client")
}
if payload.Entries[0].LastAccessedAt.Before(now.Add(-11 * time.Minute)) {
t.Fatalf("Entries[0].LastAccessedAt = %v, expected recent timestamp", payload.Entries[0].LastAccessedAt)
}
}
func TestHomeHandlerAllowsHead(t *testing.T) {
handler := homeHandler(buildinfo.Info{Version: "v1"}, auth.NewAccessTracker(), false)
req := httptest.NewRequest(http.MethodHead, "/", nil)
+2 -2
View File
@@ -7,8 +7,8 @@ import (
var (
//go:embed ui/dist
uiFiles embed.FS
uiDistFS fs.FS
uiFiles embed.FS
uiDistFS fs.FS
indexHTML []byte
)
-214
View File
@@ -1,214 +0,0 @@
package app
import (
"encoding/json"
"errors"
"net/http"
"strings"
"time"
"git.warky.dev/wdevs/amcs/internal/ai"
"git.warky.dev/wdevs/amcs/internal/config"
"git.warky.dev/wdevs/amcs/internal/metadata"
"git.warky.dev/wdevs/amcs/internal/store"
"git.warky.dev/wdevs/amcs/internal/tools"
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
)
const maxWebhookBodyBytes = 1 << 20
type webhookThoughtRequest struct {
Content string `json:"content"`
Project string `json:"project,omitempty"`
Source string `json:"source,omitempty"`
Type string `json:"type,omitempty"`
Topics []string `json:"topics,omitempty"`
People []string `json:"people,omitempty"`
ActionItems []string `json:"action_items,omitempty"`
DatesMentioned []string `json:"dates_mentioned,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
SourceMetadata map[string]any `json:"source_metadata,omitempty"`
IDempotencyKey string `json:"idempotency_key,omitempty"`
ExternalID string `json:"external_id,omitempty"`
}
type webhookThoughtResponse struct {
Thought thoughttypes.Thought `json:"thought"`
Duplicate bool `json:"duplicate"`
WebhookMeta thoughttypes.WebhookMetadata `json:"webhook"`
}
type webhookThoughtHandler struct {
store *store.DB
embeddings *ai.EmbeddingRunner
capture config.CaptureConfig
retryer tools.MetadataQueuer
embedRetryer tools.EmbeddingQueuer
}
func newWebhookThoughtHandler(db *store.DB, embeddings *ai.EmbeddingRunner, capture config.CaptureConfig, retryer tools.MetadataQueuer, embedRetryer tools.EmbeddingQueuer) http.Handler {
return &webhookThoughtHandler{store: db, embeddings: embeddings, capture: capture, retryer: retryer, embedRetryer: embedRetryer}
}
func (h *webhookThoughtHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/webhooks/thoughts" {
http.NotFound(w, r)
return
}
if r.Method != http.MethodPost {
w.Header().Set("Allow", http.MethodPost)
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
r.Body = http.MaxBytesReader(w, r.Body, maxWebhookBodyBytes)
in, err := parseWebhookThoughtRequest(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
webhookMeta := buildWebhookMetadata(in, r.Header.Get("Idempotency-Key"), time.Now().UTC())
if webhookMeta.IDempotencyKey != "" {
if existing, err := h.store.GetThoughtByWebhookIDempotencyKey(r.Context(), webhookMeta.IDempotencyKey); err == nil {
writeWebhookThoughtResponse(w, http.StatusOK, webhookThoughtResponse{Thought: existing, Duplicate: true, WebhookMeta: webhookMeta})
return
}
}
projectID, err := h.resolveWebhookProject(r, in.Project)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
thought := thoughttypes.Thought{
Content: strings.TrimSpace(in.Content),
Metadata: normalizeWebhookThoughtMetadata(in, webhookMeta, h.capture),
ProjectID: projectID,
}
created, err := h.store.InsertThought(r.Context(), thought, h.embeddings.PrimaryModel())
if err != nil {
http.Error(w, "insert thought: "+err.Error(), http.StatusInternalServerError)
return
}
if projectID != nil {
_ = h.store.TouchProject(r.Context(), *projectID)
}
if h.retryer != nil {
h.retryer.QueueThought(created.ID)
}
if h.embedRetryer != nil {
h.embedRetryer.QueueThought(r.Context(), created.ID, created.Content)
}
writeWebhookThoughtResponse(w, http.StatusCreated, webhookThoughtResponse{Thought: created, WebhookMeta: webhookMeta})
}
func parseWebhookThoughtRequest(r *http.Request) (webhookThoughtRequest, error) {
if !strings.Contains(r.Header.Get("Content-Type"), "application/json") {
return webhookThoughtRequest{}, errors.New("webhook requires application/json")
}
defer r.Body.Close()
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
var in webhookThoughtRequest
if err := decoder.Decode(&in); err != nil {
return webhookThoughtRequest{}, err
}
if strings.TrimSpace(in.Content) == "" {
return webhookThoughtRequest{}, errors.New("content is required")
}
return in, nil
}
func (h *webhookThoughtHandler) resolveWebhookProject(r *http.Request, projectName string) (*int64, error) {
projectName = strings.TrimSpace(projectName)
if projectName == "" {
return nil, nil
}
project, err := h.store.GetProject(r.Context(), projectName)
if err != nil {
return nil, err
}
return &project.NumericID, nil
}
func buildWebhookMetadata(in webhookThoughtRequest, headerKey string, now time.Time) thoughttypes.WebhookMetadata {
sourceMetadata := in.SourceMetadata
if len(sourceMetadata) == 0 {
sourceMetadata = in.Metadata
}
return thoughttypes.WebhookMetadata{
ReceivedAt: now.Format(time.RFC3339),
IDempotencyKey: firstNonEmpty(in.IDempotencyKey, headerKey),
ExternalID: strings.TrimSpace(in.ExternalID),
SourceMetadata: sanitizeWebhookMetadata(sourceMetadata),
}
}
func normalizeWebhookThoughtMetadata(in webhookThoughtRequest, webhookMeta thoughttypes.WebhookMetadata, capture config.CaptureConfig) thoughttypes.ThoughtMetadata {
return metadata.Normalize(thoughttypes.ThoughtMetadata{
People: in.People,
ActionItems: in.ActionItems,
DatesMentioned: in.DatesMentioned,
Topics: in.Topics,
Type: in.Type,
Source: firstNonEmpty(in.Source, "webhook"),
Webhook: &webhookMeta,
}, capture)
}
func sanitizeWebhookMetadata(in map[string]any) map[string]any {
if len(in) == 0 {
return nil
}
out := make(map[string]any, len(in))
for key, value := range in {
key = strings.TrimSpace(key)
if key == "" {
continue
}
if sanitized, ok := sanitizeWebhookMetadataValue(value, 0); ok {
out[key] = sanitized
}
}
if len(out) == 0 {
return nil
}
return out
}
func sanitizeWebhookMetadataValue(value any, depth int) (any, bool) {
if depth > 3 {
return nil, false
}
switch v := value.(type) {
case nil, bool, float64, string:
return v, true
case []any:
if len(v) > 50 {
v = v[:50]
}
out := make([]any, 0, len(v))
for _, item := range v {
if sanitized, ok := sanitizeWebhookMetadataValue(item, depth+1); ok {
out = append(out, sanitized)
}
}
return out, true
case map[string]any:
if len(v) > 50 {
return nil, false
}
return sanitizeWebhookMetadata(v), true
default:
return nil, false
}
}
func writeWebhookThoughtResponse(w http.ResponseWriter, status int, out webhookThoughtResponse) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(out)
}
-86
View File
@@ -1,86 +0,0 @@
package app
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"git.warky.dev/wdevs/amcs/internal/config"
)
func TestParseWebhookThoughtRequestRequiresJSON(t *testing.T) {
req := httptestRequest("text/plain", `{"content":"hello"}`)
_, err := parseWebhookThoughtRequest(req)
if err == nil {
t.Fatal("expected error for non-json content type")
}
}
func TestParseWebhookThoughtRequestRequiresContent(t *testing.T) {
req := httptestRequest("application/json", `{"source":"n8n"}`)
_, err := parseWebhookThoughtRequest(req)
if err == nil || !strings.Contains(err.Error(), "content is required") {
t.Fatalf("error = %v, want content required", err)
}
}
func TestBuildWebhookMetadataUsesHeaderIdempotencyAndSanitizesMetadata(t *testing.T) {
now := time.Date(2026, 7, 15, 4, 0, 0, 0, time.UTC)
got := buildWebhookMetadata(webhookThoughtRequest{
ExternalID: " ext-1 ",
Metadata: map[string]any{
"service": "n8n",
"unsafe": struct{}{},
"nested": map[string]any{"ok": true},
},
}, " key-1 ", now)
if got.IDempotencyKey != "key-1" {
t.Fatalf("IDempotencyKey = %q, want key-1", got.IDempotencyKey)
}
if got.ExternalID != "ext-1" {
t.Fatalf("ExternalID = %q, want ext-1", got.ExternalID)
}
if got.ReceivedAt != "2026-07-15T04:00:00Z" {
t.Fatalf("ReceivedAt = %q", got.ReceivedAt)
}
if _, ok := got.SourceMetadata["unsafe"]; ok {
t.Fatal("unsafe metadata value was not removed")
}
if got.SourceMetadata["service"] != "n8n" {
t.Fatalf("service metadata = %#v", got.SourceMetadata["service"])
}
}
func TestNormalizeWebhookThoughtMetadata(t *testing.T) {
webhookMeta := buildWebhookMetadata(webhookThoughtRequest{IDempotencyKey: "abc"}, "", time.Date(2026, 7, 15, 4, 0, 0, 0, time.UTC))
got := normalizeWebhookThoughtMetadata(webhookThoughtRequest{
Source: "github",
Type: "task",
Topics: []string{"ci", "ci", ""},
People: []string{" Sam "},
}, webhookMeta, config.CaptureConfig{})
if got.Source != "github" {
t.Fatalf("Source = %q, want github", got.Source)
}
if got.Type != "task" {
t.Fatalf("Type = %q, want task", got.Type)
}
if len(got.Topics) != 1 || got.Topics[0] != "ci" {
t.Fatalf("Topics = %#v, want [ci]", got.Topics)
}
if got.Webhook == nil || got.Webhook.IDempotencyKey != "abc" {
t.Fatalf("Webhook = %#v, want idempotency key abc", got.Webhook)
}
}
func httptestRequest(contentType, body string) *http.Request {
req := httptest.NewRequest(http.MethodPost, "/webhooks/thoughts", strings.NewReader(body))
req.Header.Set("Content-Type", contentType)
return req
}
+5 -128
View File
@@ -1,9 +1,7 @@
package auth
import (
"net"
"sort"
"strings"
"sync"
"time"
)
@@ -17,38 +15,16 @@ type AccessSnapshot struct {
LastAccessedAt time.Time `json:"last_accessed_at"`
}
const maxRecentLog = 100
type AccessLogEntry struct {
Timestamp time.Time `json:"timestamp"`
KeyID string `json:"key_id"`
IP string `json:"ip"`
UserAgent string `json:"user_agent"`
Tool string `json:"tool"`
Path string `json:"path"`
}
type AccessTracker struct {
mu sync.RWMutex
entries map[string]AccessSnapshot
ipCounts map[string]int
agentCounts map[string]int
toolCounts map[string]int
recentLog []AccessLogEntry
totalRequests int
mu sync.RWMutex
entries map[string]AccessSnapshot
}
func NewAccessTracker() *AccessTracker {
return &AccessTracker{
entries: make(map[string]AccessSnapshot),
ipCounts: make(map[string]int),
agentCounts: make(map[string]int),
toolCounts: make(map[string]int),
recentLog: make([]AccessLogEntry, 0, maxRecentLog),
}
return &AccessTracker{entries: make(map[string]AccessSnapshot)}
}
func (t *AccessTracker) Record(keyID, path, remoteAddr, userAgent, toolName string, now time.Time) {
func (t *AccessTracker) Record(keyID, path, remoteAddr, userAgent string, now time.Time) {
if t == nil || keyID == "" {
return
}
@@ -56,52 +32,14 @@ func (t *AccessTracker) Record(keyID, path, remoteAddr, userAgent, toolName stri
t.mu.Lock()
defer t.mu.Unlock()
normalizedRemoteAddr := normalizeRemoteAddr(remoteAddr)
entry := t.entries[keyID]
entry.KeyID = keyID
entry.LastPath = path
entry.RemoteAddr = normalizedRemoteAddr
entry.RemoteAddr = remoteAddr
entry.UserAgent = userAgent
entry.LastAccessedAt = now.UTC()
entry.RequestCount++
t.entries[keyID] = entry
t.totalRequests++
if normalizedRemoteAddr != "" {
t.ipCounts[normalizedRemoteAddr]++
}
if userAgent != "" {
t.agentCounts[userAgent]++
}
if tool := strings.TrimSpace(toolName); tool != "" {
t.toolCounts[tool]++
}
logEntry := AccessLogEntry{
Timestamp: now.UTC(),
KeyID: keyID,
IP: normalizedRemoteAddr,
UserAgent: userAgent,
Tool: strings.TrimSpace(toolName),
Path: path,
}
t.recentLog = append([]AccessLogEntry{logEntry}, t.recentLog...)
if len(t.recentLog) > maxRecentLog {
t.recentLog = t.recentLog[:maxRecentLog]
}
}
func normalizeRemoteAddr(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return ""
}
host, _, err := net.SplitHostPort(trimmed)
if err == nil {
return host
}
return trimmed
}
func (t *AccessTracker) Snapshot() []AccessSnapshot {
@@ -141,64 +79,3 @@ func (t *AccessTracker) ConnectedCount(now time.Time, window time.Duration) int
}
return count
}
type RequestAggregate struct {
Key string `json:"key"`
RequestCount int `json:"request_count"`
}
type AccessMetrics struct {
TotalRequests int `json:"total_requests"`
UniquePrincipals int `json:"unique_principals"`
UniqueIPs int `json:"unique_ips"`
UniqueAgents int `json:"unique_agents"`
UniqueTools int `json:"unique_tools"`
TopIPs []RequestAggregate `json:"top_ips"`
TopAgents []RequestAggregate `json:"top_agents"`
TopTools []RequestAggregate `json:"top_tools"`
RecentLog []AccessLogEntry `json:"recent_log"`
}
func (t *AccessTracker) Metrics(topN int) AccessMetrics {
if t == nil {
return AccessMetrics{}
}
if topN <= 0 {
topN = 10
}
t.mu.RLock()
defer t.mu.RUnlock()
log := make([]AccessLogEntry, len(t.recentLog))
copy(log, t.recentLog)
return AccessMetrics{
TotalRequests: t.totalRequests,
UniquePrincipals: len(t.entries),
UniqueIPs: len(t.ipCounts),
UniqueAgents: len(t.agentCounts),
UniqueTools: len(t.toolCounts),
TopIPs: topAggregates(t.ipCounts, topN),
TopAgents: topAggregates(t.agentCounts, topN),
TopTools: topAggregates(t.toolCounts, topN),
RecentLog: log,
}
}
func topAggregates(items map[string]int, topN int) []RequestAggregate {
out := make([]RequestAggregate, 0, len(items))
for key, count := range items {
out = append(out, RequestAggregate{Key: key, RequestCount: count})
}
sort.Slice(out, func(i, j int) bool {
if out[i].RequestCount == out[j].RequestCount {
return out[i].Key < out[j].Key
}
return out[i].RequestCount > out[j].RequestCount
})
if len(out) > topN {
out = out[:topN]
}
return out
}
+5 -56
View File
@@ -10,9 +10,9 @@ func TestAccessTrackerRecordAndSnapshot(t *testing.T) {
older := time.Date(2026, 4, 4, 10, 0, 0, 0, time.UTC)
newer := older.Add(2 * time.Minute)
tracker.Record("client-a", "/files", "10.0.0.1:1234", "agent-a", "", older)
tracker.Record("client-b", "/mcp", "10.0.0.2:1234", "agent-b", "list_projects", newer)
tracker.Record("client-a", "/files/1", "10.0.0.1:1234", "agent-a2", "", newer.Add(30*time.Second))
tracker.Record("client-a", "/files", "10.0.0.1:1234", "agent-a", older)
tracker.Record("client-b", "/mcp", "10.0.0.2:1234", "agent-b", newer)
tracker.Record("client-a", "/files/1", "10.0.0.1:1234", "agent-a2", newer.Add(30*time.Second))
snap := tracker.Snapshot()
if len(snap) != 2 {
@@ -30,67 +30,16 @@ func TestAccessTrackerRecordAndSnapshot(t *testing.T) {
if snap[0].UserAgent != "agent-a2" {
t.Fatalf("snapshot[0].UserAgent = %q, want agent-a2", snap[0].UserAgent)
}
if snap[0].RemoteAddr != "10.0.0.1" {
t.Fatalf("snapshot[0].RemoteAddr = %q, want 10.0.0.1", snap[0].RemoteAddr)
}
}
func TestAccessTrackerConnectedCount(t *testing.T) {
tracker := NewAccessTracker()
now := time.Date(2026, 4, 4, 12, 0, 0, 0, time.UTC)
tracker.Record("recent", "/mcp", "", "", "", now.Add(-2*time.Minute))
tracker.Record("stale", "/mcp", "", "", "", now.Add(-11*time.Minute))
tracker.Record("recent", "/mcp", "", "", now.Add(-2*time.Minute))
tracker.Record("stale", "/mcp", "", "", now.Add(-11*time.Minute))
if got := tracker.ConnectedCount(now, 10*time.Minute); got != 1 {
t.Fatalf("ConnectedCount() = %d, want 1", got)
}
}
func TestAccessTrackerMetrics(t *testing.T) {
tracker := NewAccessTracker()
now := time.Date(2026, 4, 4, 12, 0, 0, 0, time.UTC)
tracker.Record("client-a", "/mcp", "10.0.0.1:1234", "agent-a", "list_projects", now)
tracker.Record("client-a", "/mcp", "10.0.0.1:1234", "agent-a", "list_projects", now.Add(1*time.Second))
tracker.Record("client-b", "/files", "10.0.0.2:5678", "agent-b", "", now.Add(2*time.Second))
tracker.Record("client-c", "/files", "10.0.0.2:5678", "agent-b", "search_thoughts", now.Add(3*time.Second))
metrics := tracker.Metrics(5)
if metrics.TotalRequests != 4 {
t.Fatalf("TotalRequests = %d, want 4", metrics.TotalRequests)
}
if metrics.UniquePrincipals != 3 {
t.Fatalf("UniquePrincipals = %d, want 3", metrics.UniquePrincipals)
}
if metrics.UniqueIPs != 2 {
t.Fatalf("UniqueIPs = %d, want 2", metrics.UniqueIPs)
}
if metrics.UniqueAgents != 2 {
t.Fatalf("UniqueAgents = %d, want 2", metrics.UniqueAgents)
}
if metrics.UniqueTools != 2 {
t.Fatalf("UniqueTools = %d, want 2", metrics.UniqueTools)
}
if len(metrics.TopIPs) != 2 {
t.Fatalf("len(TopIPs) = %d, want 2", len(metrics.TopIPs))
}
if metrics.TopIPs[0].RequestCount != 2 || metrics.TopIPs[1].RequestCount != 2 {
t.Fatalf("TopIPs counts = %+v, want both counts to be 2", metrics.TopIPs)
}
if metrics.TopIPs[0].Key != "10.0.0.1" && metrics.TopIPs[0].Key != "10.0.0.2" {
t.Fatalf("TopIPs[0].Key = %q, want normalized IP", metrics.TopIPs[0].Key)
}
if len(metrics.TopAgents) != 2 {
t.Fatalf("len(TopAgents) = %d, want 2", len(metrics.TopAgents))
}
if metrics.TopAgents[0].RequestCount != 2 || metrics.TopAgents[1].RequestCount != 2 {
t.Fatalf("TopAgents counts = %+v, want both counts to be 2", metrics.TopAgents)
}
if len(metrics.TopTools) != 2 {
t.Fatalf("len(TopTools) = %d, want 2", len(metrics.TopTools))
}
if metrics.TopTools[0].Key != "list_projects" || metrics.TopTools[0].RequestCount != 2 {
t.Fatalf("TopTools[0] = %+v, want list_projects with count 2", metrics.TopTools[0])
}
}
-6
View File
@@ -26,12 +26,6 @@ func (c *DynamicClient) HasRedirectURI(uri string) bool {
return false
}
// ClientStore is the interface implemented by both DynamicClientStore and PostgresClientStore.
type ClientStore interface {
Register(name string, redirectURIs []string) (DynamicClient, error)
Lookup(clientID string) (DynamicClient, bool)
}
// DynamicClientStore holds dynamically registered OAuth clients in memory.
type DynamicClientStore struct {
mu sync.RWMutex
+2 -113
View File
@@ -1,27 +1,14 @@
package auth
import (
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"fmt"
"strings"
"sync"
"git.warky.dev/wdevs/amcs/internal/config"
)
type Keyring struct {
mu sync.RWMutex
keys []config.APIKey
tenantsByKeyID map[string]string
managed map[string]managedKey
}
type managedKey struct {
secretHash string
enabled bool
keys []config.APIKey
}
func NewKeyring(keys []config.APIKey) (*Keyring, error) {
@@ -29,112 +16,14 @@ func NewKeyring(keys []config.APIKey) (*Keyring, error) {
return nil, fmt.Errorf("keyring requires at least one key")
}
tenantsByKeyID := make(map[string]string)
for _, key := range keys {
if tenantID := strings.TrimSpace(key.TenantID); tenantID != "" {
tenantsByKeyID[key.ID] = tenantID
}
}
return &Keyring{keys: append([]config.APIKey(nil), keys...), tenantsByKeyID: tenantsByKeyID, managed: make(map[string]managedKey)}, nil
}
// NewManagedKeyring is used when API credentials are administered in the
// database rather than supplied through static configuration.
func NewManagedKeyring() *Keyring {
return &Keyring{tenantsByKeyID: make(map[string]string), managed: make(map[string]managedKey)}
return &Keyring{keys: append([]config.APIKey(nil), keys...)}, nil
}
func (k *Keyring) Lookup(value string) (string, bool) {
k.mu.RLock()
defer k.mu.RUnlock()
for _, key := range k.keys {
if subtle.ConstantTimeCompare([]byte(key.Value), []byte(value)) == 1 {
return key.ID, true
}
}
hash := secretHash(value)
for keyID, key := range k.managed {
if key.enabled && subtle.ConstantTimeCompare([]byte(key.secretHash), []byte(hash)) == 1 {
return keyID, true
}
}
return "", false
}
// TenantForKey returns the tenant boundary assigned to keyID. Unassigned
// configured keys retain the historical key-ID boundary for compatibility.
func (k *Keyring) TenantForKey(keyID string) string {
k.mu.RLock()
defer k.mu.RUnlock()
if tenantID := k.tenantsByKeyID[keyID]; tenantID != "" {
return tenantID
}
return keyID
}
func (k *Keyring) AssignTenant(keyID, tenantID string) {
k.mu.Lock()
defer k.mu.Unlock()
if tenantID == "" {
delete(k.tenantsByKeyID, keyID)
return
}
k.tenantsByKeyID[keyID] = tenantID
}
func (k *Keyring) AddManaged(keyID, secretHash string, enabled bool) {
k.mu.Lock()
defer k.mu.Unlock()
k.managed[keyID] = managedKey{secretHash: secretHash, enabled: enabled}
}
func (k *Keyring) ConfiguredKeys() []config.APIKey {
k.mu.RLock()
defer k.mu.RUnlock()
return append([]config.APIKey(nil), k.keys...)
}
func (k *Keyring) IsConfigured(keyID string) bool {
k.mu.RLock()
defer k.mu.RUnlock()
for _, key := range k.keys {
if key.ID == keyID {
return true
}
}
return false
}
func (k *Keyring) IsSuperadmin(keyID string) bool {
k.mu.RLock()
defer k.mu.RUnlock()
for _, key := range k.keys {
if key.ID == keyID {
return key.Superadmin
}
}
return false
}
func (k *Keyring) SetManagedEnabled(keyID string, enabled bool) {
k.mu.Lock()
defer k.mu.Unlock()
if key, ok := k.managed[keyID]; ok {
key.enabled = enabled
k.managed[keyID] = key
}
}
func GenerateSecret() (string, string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", "", err
}
secret := "amcs_" + hex.EncodeToString(b)
return secret, secretHash(secret), nil
}
func secretHash(secret string) string {
sum := sha256.Sum256([]byte(secret))
return hex.EncodeToString(sum[:])
}
-70
View File
@@ -1,8 +1,6 @@
package auth
import (
"bytes"
"encoding/json"
"io"
"log/slog"
"net/http"
@@ -10,7 +8,6 @@ import (
"testing"
"git.warky.dev/wdevs/amcs/internal/config"
"git.warky.dev/wdevs/amcs/internal/observability"
)
func testLogger() *slog.Logger {
@@ -36,26 +33,6 @@ func TestNewKeyringAndLookup(t *testing.T) {
}
}
func TestConfiguredKeyUsesTenantID(t *testing.T) {
keyring, err := NewKeyring([]config.APIKey{{ID: "agent-key", Value: "secret", TenantID: "acme"}})
if err != nil {
t.Fatalf("NewKeyring() error = %v", err)
}
if got := keyring.TenantForKey("agent-key"); got != "acme" {
t.Fatalf("TenantForKey() = %q, want acme", got)
}
}
func TestConfiguredKeySuperadmin(t *testing.T) {
keyring, err := NewKeyring([]config.APIKey{{ID: "operator", Value: "secret", Superadmin: true}})
if err != nil {
t.Fatalf("NewKeyring() error = %v", err)
}
if !keyring.IsSuperadmin("operator") || keyring.IsSuperadmin("missing") {
t.Fatal("IsSuperadmin() did not return the configured role")
}
}
func TestMiddlewareAllowsHeaderAuthAndSetsContext(t *testing.T) {
keyring, err := NewKeyring([]config.APIKey{{ID: "client-a", Value: "secret"}})
if err != nil {
@@ -211,50 +188,3 @@ func TestMiddlewareRecordsForwardedRemoteAddr(t *testing.T) {
t.Fatalf("snapshot remote_addr = %q, want %q", snap[0].RemoteAddr, "203.0.113.99")
}
}
func TestMiddlewareRecordsMCPToolUsage(t *testing.T) {
keyring, err := NewKeyring([]config.APIKey{{ID: "client-a", Value: "secret"}})
if err != nil {
t.Fatalf("NewKeyring() error = %v", err)
}
tracker := NewAccessTracker()
logger := testLogger()
authenticated := Middleware(config.AuthConfig{HeaderName: "x-brain-key"}, keyring, nil, nil, tracker, logger)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
handler := observability.AccessLog(logger)(authenticated)
payload := map[string]any{
"jsonrpc": "2.0",
"id": "1",
"method": "tools/call",
"params": map[string]any{
"name": "list_projects",
},
}
body, err := json.Marshal(payload)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/mcp", bytes.NewReader(body))
req.Header.Set("x-brain-key", "secret")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusNoContent {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNoContent)
}
metrics := tracker.Metrics(10)
if metrics.UniqueTools != 1 {
t.Fatalf("UniqueTools = %d, want 1", metrics.UniqueTools)
}
if len(metrics.TopTools) != 1 {
t.Fatalf("len(TopTools) = %d, want 1", len(metrics.TopTools))
}
if metrics.TopTools[0].Key != "list_projects" || metrics.TopTools[0].RequestCount != 1 {
t.Fatalf("TopTools[0] = %+v, want list_projects with count 1", metrics.TopTools[0])
}
}
+6 -41
View File
@@ -9,31 +9,13 @@ import (
"time"
"git.warky.dev/wdevs/amcs/internal/config"
"git.warky.dev/wdevs/amcs/internal/observability"
"git.warky.dev/wdevs/amcs/internal/requestip"
"git.warky.dev/wdevs/amcs/internal/tenancy"
)
type contextKey string
const keyIDContextKey contextKey = "auth.key_id"
// wwwAuthenticate returns the value for a WWW-Authenticate header.
// It advertises Bearer and, when a public URL is known, the OAuth metadata URL per RFC 9728.
func wwwAuthenticate(r *http.Request, publicURL string) string {
base := publicURL
if base == "" {
scheme := "https"
if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
scheme = strings.ToLower(proto)
} else if r.TLS == nil {
scheme = "http"
}
base = scheme + "://" + r.Host
}
return `Bearer resource_metadata="` + base + `/.well-known/oauth-authorization-server"`
}
func Middleware(cfg config.AuthConfig, keyring *Keyring, oauthRegistry *OAuthRegistry, tokenStore *TokenStore, tracker *AccessTracker, log *slog.Logger) func(http.Handler) http.Handler {
headerName := cfg.HeaderName
if headerName == "" {
@@ -41,24 +23,9 @@ func Middleware(cfg config.AuthConfig, keyring *Keyring, oauthRegistry *OAuthReg
}
recordAccess := func(r *http.Request, keyID string) {
if tracker != nil {
tracker.Record(
keyID,
r.URL.Path,
requestip.FromRequest(r),
r.UserAgent(),
observability.MCPToolFromContext(r.Context()),
time.Now(),
)
tracker.Record(keyID, r.URL.Path, requestip.FromRequest(r), r.UserAgent(), time.Now())
}
}
withTenant := func(ctx context.Context, keyID string) context.Context {
ctx = context.WithValue(ctx, keyIDContextKey, keyID)
tenantID := keyID
if keyring != nil {
tenantID = keyring.TenantForKey(keyID)
}
return tenancy.WithTenantKey(ctx, tenantID)
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
remoteAddr := requestip.FromRequest(r)
@@ -72,7 +39,7 @@ func Middleware(cfg config.AuthConfig, keyring *Keyring, oauthRegistry *OAuthReg
return
}
recordAccess(r, keyID)
next.ServeHTTP(w, r.WithContext(withTenant(r.Context(), keyID)))
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), keyIDContextKey, keyID)))
return
}
}
@@ -82,19 +49,18 @@ func Middleware(cfg config.AuthConfig, keyring *Keyring, oauthRegistry *OAuthReg
if tokenStore != nil {
if keyID, ok := tokenStore.Lookup(bearer); ok {
recordAccess(r, keyID)
next.ServeHTTP(w, r.WithContext(withTenant(r.Context(), keyID)))
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), keyIDContextKey, keyID)))
return
}
}
if keyring != nil {
if keyID, ok := keyring.Lookup(bearer); ok {
recordAccess(r, keyID)
next.ServeHTTP(w, r.WithContext(withTenant(r.Context(), keyID)))
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), keyIDContextKey, keyID)))
return
}
}
log.Warn("bearer token rejected", slog.String("remote_addr", remoteAddr))
w.Header().Set("WWW-Authenticate", wwwAuthenticate(r, "")+`, error="invalid_token"`)
http.Error(w, "invalid token or API key", http.StatusUnauthorized)
return
}
@@ -112,7 +78,7 @@ func Middleware(cfg config.AuthConfig, keyring *Keyring, oauthRegistry *OAuthReg
return
}
recordAccess(r, keyID)
next.ServeHTTP(w, r.WithContext(withTenant(r.Context(), keyID)))
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), keyIDContextKey, keyID)))
return
}
@@ -126,12 +92,11 @@ func Middleware(cfg config.AuthConfig, keyring *Keyring, oauthRegistry *OAuthReg
return
}
recordAccess(r, keyID)
next.ServeHTTP(w, r.WithContext(withTenant(r.Context(), keyID)))
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), keyIDContextKey, keyID)))
return
}
}
w.Header().Set("WWW-Authenticate", wwwAuthenticate(r, ""))
http.Error(w, "authentication required", http.StatusUnauthorized)
})
}
-15
View File
@@ -31,18 +31,3 @@ func (o *OAuthRegistry) Lookup(clientID string, clientSecret string) (string, bo
}
return "", false
}
// IsSuperadmin reports whether keyID (as returned by Lookup) belongs to an
// OAuth client configured with superadmin: true.
func (o *OAuthRegistry) IsSuperadmin(keyID string) bool {
for _, client := range o.clients {
id := client.ID
if id == "" {
id = client.ClientID
}
if id == keyID {
return client.Superadmin
}
}
return false
}
-20
View File
@@ -32,26 +32,6 @@ func TestNewOAuthRegistryAndLookup(t *testing.T) {
}
}
func TestOAuthRegistryIsSuperadmin(t *testing.T) {
registry, err := NewOAuthRegistry([]config.OAuthClient{
{ID: "oauth-admin", ClientID: "admin-id", ClientSecret: "admin-secret", Superadmin: true},
{ID: "oauth-client", ClientID: "client-id", ClientSecret: "client-secret"},
})
if err != nil {
t.Fatalf("NewOAuthRegistry() error = %v", err)
}
if !registry.IsSuperadmin("oauth-admin") {
t.Fatal("IsSuperadmin(oauth-admin) = false, want true")
}
if registry.IsSuperadmin("oauth-client") {
t.Fatal("IsSuperadmin(oauth-client) = true, want false")
}
if registry.IsSuperadmin("unknown") {
t.Fatal("IsSuperadmin(unknown) = true, want false")
}
}
func TestMiddlewareAllowsOAuthBasicAuthAndSetsContext(t *testing.T) {
oauthRegistry, err := NewOAuthRegistry([]config.OAuthClient{{
ID: "oauth-client",
-50
View File
@@ -1,50 +0,0 @@
package auth
import (
"context"
"crypto/rand"
"fmt"
"github.com/jackc/pgx/v5/pgxpool"
)
// PostgresClientStore persists dynamically registered OAuth clients (RFC 7591) in PostgreSQL.
type PostgresClientStore struct {
pool *pgxpool.Pool
}
func NewPostgresClientStore(pool *pgxpool.Pool) *PostgresClientStore {
return &PostgresClientStore{pool: pool}
}
func (s *PostgresClientStore) Register(name string, redirectURIs []string) (DynamicClient, error) {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return DynamicClient{}, err
}
clientID := fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
var client DynamicClient
row := s.pool.QueryRow(context.Background(), `
insert into oauth_clients (client_id, client_name, redirect_uris)
values ($1, $2, $3)
returning client_id, client_name, redirect_uris, created_at
`, clientID, name, redirectURIs)
if err := row.Scan(&client.ClientID, &client.ClientName, &client.RedirectURIs, &client.CreatedAt); err != nil {
return DynamicClient{}, fmt.Errorf("register oauth client: %w", err)
}
return client, nil
}
func (s *PostgresClientStore) Lookup(clientID string) (DynamicClient, bool) {
var client DynamicClient
row := s.pool.QueryRow(context.Background(), `
select client_id, client_name, redirect_uris, created_at
from oauth_clients
where client_id = $1
`, clientID)
if err := row.Scan(&client.ClientID, &client.ClientName, &client.RedirectURIs, &client.CreatedAt); err != nil {
return DynamicClient{}, false
}
return client, true
}
-44
View File
@@ -1,44 +0,0 @@
package auth
import (
"log/slog"
"net/http"
"net/http/httptest"
"testing"
"git.warky.dev/wdevs/amcs/internal/config"
"git.warky.dev/wdevs/amcs/internal/tenancy"
)
func TestMiddlewareAddsTenantKeyToContext(t *testing.T) {
keyring, err := NewKeyring([]config.APIKey{{ID: "user-a", Value: "secret-a"}})
if err != nil {
t.Fatalf("NewKeyring error = %v", err)
}
var gotKeyID, gotTenant string
handler := Middleware(config.AuthConfig{}, keyring, nil, nil, nil, slog.Default())(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var ok bool
gotKeyID, ok = KeyIDFromContext(r.Context())
if !ok {
t.Fatal("KeyIDFromContext ok = false")
}
gotTenant, ok = tenancy.KeyFromContext(r.Context())
if !ok {
t.Fatal("tenancy.KeyFromContext ok = false")
}
w.WriteHeader(http.StatusNoContent)
}))
req := httptest.NewRequest(http.MethodPost, "/mcp", nil)
req.Header.Set("x-brain-key", "secret-a")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusNoContent {
t.Fatalf("status = %d, want %d", rr.Code, http.StatusNoContent)
}
if gotKeyID != "user-a" || gotTenant != "user-a" {
t.Fatalf("keyID=%q tenant=%q, want user-a/user-a", gotKeyID, gotTenant)
}
}
-3
View File
@@ -53,8 +53,6 @@ type AuthConfig struct {
type APIKey struct {
ID string `yaml:"id"`
Value string `yaml:"value"`
TenantID string `yaml:"tenant_id"`
Superadmin bool `yaml:"superadmin"`
Description string `yaml:"description"`
}
@@ -66,7 +64,6 @@ type OAuthClient struct {
ID string `yaml:"id"`
ClientID string `yaml:"client_id"`
ClientSecret string `yaml:"client_secret"`
Superadmin bool `yaml:"superadmin"`
Description string `yaml:"description"`
}
@@ -1,71 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicAgentGuardrails struct {
bun.BaseModel `bun:"table:public.agent_guardrails,alias:agent_guardrails"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
Content sql_types.SqlString `bun:"content,type:text,notnull," json:"content"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
Severity sql_types.SqlString `bun:"severity,type:text,default:'medium',notnull," json:"severity"`
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
RelGuardrailIDPublicAgentPersonaGuardrails []*ModelPublicAgentPersonaGuardrails `bun:"rel:has-many,join:id=guardrail_id" json:"relguardrailidpublicagentpersonaguardrails,omitempty"` // Has many ModelPublicAgentPersonaGuardrails
RelGuardrailIDPublicPlanGuardrails []*ModelPublicPlanGuardrails `bun:"rel:has-many,join:id=guardrail_id" json:"relguardrailidpublicplanguardrails,omitempty"` // Has many ModelPublicPlanGuardrails
RelGuardrailIDPublicProjectGuardrails []*ModelPublicProjectGuardrails `bun:"rel:has-many,join:id=guardrail_id" json:"relguardrailidpublicprojectguardrails,omitempty"` // Has many ModelPublicProjectGuardrails
}
// TableName returns the table name for ModelPublicAgentGuardrails
func (m ModelPublicAgentGuardrails) TableName() string {
return "public.agent_guardrails"
}
// TableNameOnly returns the table name without schema for ModelPublicAgentGuardrails
func (m ModelPublicAgentGuardrails) TableNameOnly() string {
return "agent_guardrails"
}
// SchemaName returns the schema name for ModelPublicAgentGuardrails
func (m ModelPublicAgentGuardrails) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicAgentGuardrails) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicAgentGuardrails) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicAgentGuardrails) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicAgentGuardrails) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicAgentGuardrails) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicAgentGuardrails) GetPrefix() string {
return "AGG"
}
@@ -1,71 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicAgentParts struct {
bun.BaseModel `bun:"table:public.agent_parts,alias:agent_parts"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
Content sql_types.SqlString `bun:"content,type:text,default:'',notnull," json:"content"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
PartType sql_types.SqlString `bun:"part_type,type:text,notnull," json:"part_type"`
Summary sql_types.SqlString `bun:"summary,type:text,notnull," json:"summary"`
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
RelPartIDPublicAgentPersonaParts []*ModelPublicAgentPersonaParts `bun:"rel:has-many,join:id=part_id" json:"relpartidpublicagentpersonaparts,omitempty"` // Has many ModelPublicAgentPersonaParts
RelPartIDPublicArcStageParts []*ModelPublicArcStageParts `bun:"rel:has-many,join:id=part_id" json:"relpartidpublicarcstageparts,omitempty"` // Has many ModelPublicArcStageParts
}
// TableName returns the table name for ModelPublicAgentParts
func (m ModelPublicAgentParts) TableName() string {
return "public.agent_parts"
}
// TableNameOnly returns the table name without schema for ModelPublicAgentParts
func (m ModelPublicAgentParts) TableNameOnly() string {
return "agent_parts"
}
// SchemaName returns the schema name for ModelPublicAgentParts
func (m ModelPublicAgentParts) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicAgentParts) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicAgentParts) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicAgentParts) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicAgentParts) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicAgentParts) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicAgentParts) GetPrefix() string {
return "APG"
}
@@ -1,62 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicAgentPersonaGuardrails struct {
bun.BaseModel `bun:"table:public.agent_persona_guardrails,alias:agent_persona_guardrails"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
GuardrailID int64 `bun:"guardrail_id,type:bigint,notnull," json:"guardrail_id"`
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
RelGuardrailID *ModelPublicAgentGuardrails `bun:"rel:has-one,join:guardrail_id=id" json:"relguardrailid,omitempty"` // Has one ModelPublicAgentGuardrails
RelPersonaID *ModelPublicAgentPersonas `bun:"rel:has-one,join:persona_id=id" json:"relpersonaid,omitempty"` // Has one ModelPublicAgentPersonas
}
// TableName returns the table name for ModelPublicAgentPersonaGuardrails
func (m ModelPublicAgentPersonaGuardrails) TableName() string {
return "public.agent_persona_guardrails"
}
// TableNameOnly returns the table name without schema for ModelPublicAgentPersonaGuardrails
func (m ModelPublicAgentPersonaGuardrails) TableNameOnly() string {
return "agent_persona_guardrails"
}
// SchemaName returns the schema name for ModelPublicAgentPersonaGuardrails
func (m ModelPublicAgentPersonaGuardrails) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicAgentPersonaGuardrails) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicAgentPersonaGuardrails) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicAgentPersonaGuardrails) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicAgentPersonaGuardrails) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicAgentPersonaGuardrails) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicAgentPersonaGuardrails) GetPrefix() string {
return "APG"
}
@@ -1,64 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicAgentPersonaParts struct {
bun.BaseModel `bun:"table:public.agent_persona_parts,alias:agent_persona_parts"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
PartID int64 `bun:"part_id,type:bigint,notnull," json:"part_id"`
PartOrder int32 `bun:"part_order,type:int,default:0,notnull," json:"part_order"`
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
Priority int32 `bun:"priority,type:int,default:0,notnull," json:"priority"`
RelPartID *ModelPublicAgentParts `bun:"rel:has-one,join:part_id=id" json:"relpartid,omitempty"` // Has one ModelPublicAgentParts
RelPersonaID *ModelPublicAgentPersonas `bun:"rel:has-one,join:persona_id=id" json:"relpersonaid,omitempty"` // Has one ModelPublicAgentPersonas
}
// TableName returns the table name for ModelPublicAgentPersonaParts
func (m ModelPublicAgentPersonaParts) TableName() string {
return "public.agent_persona_parts"
}
// TableNameOnly returns the table name without schema for ModelPublicAgentPersonaParts
func (m ModelPublicAgentPersonaParts) TableNameOnly() string {
return "agent_persona_parts"
}
// SchemaName returns the schema name for ModelPublicAgentPersonaParts
func (m ModelPublicAgentPersonaParts) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicAgentPersonaParts) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicAgentPersonaParts) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicAgentPersonaParts) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicAgentPersonaParts) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicAgentPersonaParts) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicAgentPersonaParts) GetPrefix() string {
return "APP"
}
@@ -1,63 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicAgentPersonaSkills struct {
bun.BaseModel `bun:"table:public.agent_persona_skills,alias:agent_persona_skills"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
Override bool `bun:"override,type:boolean,default:false,notnull," json:"override"`
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
SkillID int64 `bun:"skill_id,type:bigint,notnull," json:"skill_id"`
RelPersonaID *ModelPublicAgentPersonas `bun:"rel:has-one,join:persona_id=id" json:"relpersonaid,omitempty"` // Has one ModelPublicAgentPersonas
RelSkillID *ModelPublicAgentSkills `bun:"rel:has-one,join:skill_id=id" json:"relskillid,omitempty"` // Has one ModelPublicAgentSkills
}
// TableName returns the table name for ModelPublicAgentPersonaSkills
func (m ModelPublicAgentPersonaSkills) TableName() string {
return "public.agent_persona_skills"
}
// TableNameOnly returns the table name without schema for ModelPublicAgentPersonaSkills
func (m ModelPublicAgentPersonaSkills) TableNameOnly() string {
return "agent_persona_skills"
}
// SchemaName returns the schema name for ModelPublicAgentPersonaSkills
func (m ModelPublicAgentPersonaSkills) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicAgentPersonaSkills) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicAgentPersonaSkills) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicAgentPersonaSkills) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicAgentPersonaSkills) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicAgentPersonaSkills) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicAgentPersonaSkills) GetPrefix() string {
return "APS"
}
@@ -1,62 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicAgentPersonaTraits struct {
bun.BaseModel `bun:"table:public.agent_persona_traits,alias:agent_persona_traits"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
TraitID int64 `bun:"trait_id,type:bigint,notnull," json:"trait_id"`
RelPersonaID *ModelPublicAgentPersonas `bun:"rel:has-one,join:persona_id=id" json:"relpersonaid,omitempty"` // Has one ModelPublicAgentPersonas
RelTraitID *ModelPublicAgentTraits `bun:"rel:has-one,join:trait_id=id" json:"reltraitid,omitempty"` // Has one ModelPublicAgentTraits
}
// TableName returns the table name for ModelPublicAgentPersonaTraits
func (m ModelPublicAgentPersonaTraits) TableName() string {
return "public.agent_persona_traits"
}
// TableNameOnly returns the table name without schema for ModelPublicAgentPersonaTraits
func (m ModelPublicAgentPersonaTraits) TableNameOnly() string {
return "agent_persona_traits"
}
// SchemaName returns the schema name for ModelPublicAgentPersonaTraits
func (m ModelPublicAgentPersonaTraits) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicAgentPersonaTraits) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicAgentPersonaTraits) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicAgentPersonaTraits) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicAgentPersonaTraits) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicAgentPersonaTraits) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicAgentPersonaTraits) GetPrefix() string {
return "APT"
}
@@ -1,77 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicAgentPersonas struct {
bun.BaseModel `bun:"table:public.agent_personas,alias:agent_personas"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
CompiledAt sql_types.SqlTimeStamp `bun:"compiled_at,type:timestamptz,nullzero," json:"compiled_at"`
CompiledDetail sql_types.SqlString `bun:"compiled_detail,type:text,default:'',notnull," json:"compiled_detail"`
CompiledSummary sql_types.SqlString `bun:"compiled_summary,type:text,default:'',notnull," json:"compiled_summary"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
Detail sql_types.SqlString `bun:"detail,type:text,default:'',notnull," json:"detail"`
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
Summary sql_types.SqlString `bun:"summary,type:text,notnull," json:"summary"`
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
RelPersonaIDPublicAgentPersonaParts []*ModelPublicAgentPersonaParts `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicagentpersonaparts,omitempty"` // Has many ModelPublicAgentPersonaParts
RelPersonaIDPublicAgentPersonaSkills []*ModelPublicAgentPersonaSkills `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicagentpersonaskills,omitempty"` // Has many ModelPublicAgentPersonaSkills
RelPersonaIDPublicProjectPersonas []*ModelPublicProjectPersonas `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicprojectpersonas,omitempty"` // Has many ModelPublicProjectPersonas
RelPersonaIDPublicAgentPersonaGuardrails []*ModelPublicAgentPersonaGuardrails `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicagentpersonaguardrails,omitempty"` // Has many ModelPublicAgentPersonaGuardrails
RelPersonaIDPublicAgentPersonaTraits []*ModelPublicAgentPersonaTraits `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicagentpersonatraits,omitempty"` // Has many ModelPublicAgentPersonaTraits
RelPersonaIDPublicPersonaArcs []*ModelPublicPersonaArc `bun:"rel:has-many,join:id=persona_id" json:"relpersonaidpublicpersonaarcs,omitempty"` // Has many ModelPublicPersonaArc
}
// TableName returns the table name for ModelPublicAgentPersonas
func (m ModelPublicAgentPersonas) TableName() string {
return "public.agent_personas"
}
// TableNameOnly returns the table name without schema for ModelPublicAgentPersonas
func (m ModelPublicAgentPersonas) TableNameOnly() string {
return "agent_personas"
}
// SchemaName returns the schema name for ModelPublicAgentPersonas
func (m ModelPublicAgentPersonas) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicAgentPersonas) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicAgentPersonas) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicAgentPersonas) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicAgentPersonas) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicAgentPersonas) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicAgentPersonas) GetPrefix() string {
return "APG"
}
@@ -1,76 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicAgentSkills struct {
bun.BaseModel `bun:"table:public.agent_skills,alias:agent_skills"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
Content sql_types.SqlString `bun:"content,type:text,notnull," json:"content"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
DomainTags sql_types.SqlStringArray `bun:"domain_tags,type:text[],default:'{}',notnull," json:"domain_tags"`
FrameworkTags sql_types.SqlStringArray `bun:"framework_tags,type:text[],default:'{}',notnull," json:"framework_tags"`
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
LanguageTags sql_types.SqlStringArray `bun:"language_tags,type:text[],default:'{}',notnull," json:"language_tags"`
LibraryTags sql_types.SqlStringArray `bun:"library_tags,type:text[],default:'{}',notnull," json:"library_tags"`
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
Tags sql_types.SqlStringArray `bun:"tags,array,type:text[],default:'{}',notnull," json:"tags"`
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
RelSkillIDPublicAgentPersonaSkills []*ModelPublicAgentPersonaSkills `bun:"rel:has-many,join:id=skill_id" json:"relskillidpublicagentpersonaskills,omitempty"` // Has many ModelPublicAgentPersonaSkills
RelRelatedSkillIDPublicLearnings []*ModelPublicLearnings `bun:"rel:has-many,join:id=related_skill_id" json:"relrelatedskillidpubliclearnings,omitempty"` // Has many ModelPublicLearnings
RelSkillIDPublicPlanSkills []*ModelPublicPlanSkills `bun:"rel:has-many,join:id=skill_id" json:"relskillidpublicplanskills,omitempty"` // Has many ModelPublicPlanSkills
RelSkillIDPublicProjectSkills []*ModelPublicProjectSkills `bun:"rel:has-many,join:id=skill_id" json:"relskillidpublicprojectskills,omitempty"` // Has many ModelPublicProjectSkills
}
// TableName returns the table name for ModelPublicAgentSkills
func (m ModelPublicAgentSkills) TableName() string {
return "public.agent_skills"
}
// TableNameOnly returns the table name without schema for ModelPublicAgentSkills
func (m ModelPublicAgentSkills) TableNameOnly() string {
return "agent_skills"
}
// SchemaName returns the schema name for ModelPublicAgentSkills
func (m ModelPublicAgentSkills) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicAgentSkills) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicAgentSkills) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicAgentSkills) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicAgentSkills) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicAgentSkills) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicAgentSkills) GetPrefix() string {
return "ASG"
}
@@ -1,69 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicAgentTraits struct {
bun.BaseModel `bun:"table:public.agent_traits,alias:agent_traits"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
Instruction sql_types.SqlString `bun:"instruction,type:text,default:'',notnull," json:"instruction"`
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
TraitType sql_types.SqlString `bun:"trait_type,type:text,notnull," json:"trait_type"`
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
RelTraitIDPublicAgentPersonaTraits []*ModelPublicAgentPersonaTraits `bun:"rel:has-many,join:id=trait_id" json:"reltraitidpublicagentpersonatraits,omitempty"` // Has many ModelPublicAgentPersonaTraits
}
// TableName returns the table name for ModelPublicAgentTraits
func (m ModelPublicAgentTraits) TableName() string {
return "public.agent_traits"
}
// TableNameOnly returns the table name without schema for ModelPublicAgentTraits
func (m ModelPublicAgentTraits) TableNameOnly() string {
return "agent_traits"
}
// SchemaName returns the schema name for ModelPublicAgentTraits
func (m ModelPublicAgentTraits) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicAgentTraits) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicAgentTraits) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicAgentTraits) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicAgentTraits) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicAgentTraits) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicAgentTraits) GetPrefix() string {
return "ATG"
}
@@ -1,66 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicAPIKeyAssignments struct {
bun.BaseModel `bun:"table:public.api_key_assignments,alias:api_key_assignments"`
KeyID sql_types.SqlString `bun:"key_id,type:text,pk," json:"key_id"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
Enabled bool `bun:"enabled,type:boolean,default:true,notnull," json:"enabled"`
Source sql_types.SqlString `bun:"source,type:text,notnull," json:"source"`
TenantID sql_types.SqlString `bun:"tenant_id,type:text,notnull," json:"tenant_id"`
UserID sql_types.SqlString `bun:"user_id,type:text,nullzero," json:"user_id"`
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
RelUserID *ModelPublicTenantUsers `bun:"rel:has-one,join:user_id=id" json:"reluserid,omitempty"` // Has one ModelPublicTenantUsers
RelKeyIDPublicManagedAPIKeys []*ModelPublicManagedAPIKeys `bun:"rel:has-many,join:key_id=key_id" json:"relkeyidpublicmanagedapikeys,omitempty"` // Has many ModelPublicManagedAPIKeys
}
// TableName returns the table name for ModelPublicAPIKeyAssignments
func (m ModelPublicAPIKeyAssignments) TableName() string {
return "public.api_key_assignments"
}
// TableNameOnly returns the table name without schema for ModelPublicAPIKeyAssignments
func (m ModelPublicAPIKeyAssignments) TableNameOnly() string {
return "api_key_assignments"
}
// SchemaName returns the schema name for ModelPublicAPIKeyAssignments
func (m ModelPublicAPIKeyAssignments) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicAPIKeyAssignments) GetID() string {
return m.KeyID.String()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicAPIKeyAssignments) GetIDStr() string {
return m.KeyID.String()
}
// SetID sets the primary key value
func (m ModelPublicAPIKeyAssignments) SetID(newid string) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicAPIKeyAssignments) UpdateID(newid string) {
m.KeyID.FromString(newid)
}
// GetIDName returns the name of the primary key column
func (m ModelPublicAPIKeyAssignments) GetIDName() string {
return "key_id"
}
// GetPrefix returns the table prefix
func (m ModelPublicAPIKeyAssignments) GetPrefix() string {
return "AKA"
}
@@ -1,62 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicArcStageParts struct {
bun.BaseModel `bun:"table:public.arc_stage_parts,alias:arc_stage_parts"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
PartID int64 `bun:"part_id,type:bigint,notnull," json:"part_id"`
StageID int64 `bun:"stage_id,type:bigint,notnull," json:"stage_id"`
RelPartID *ModelPublicAgentParts `bun:"rel:has-one,join:part_id=id" json:"relpartid,omitempty"` // Has one ModelPublicAgentParts
RelStageID *ModelPublicArcStages `bun:"rel:has-one,join:stage_id=id" json:"relstageid,omitempty"` // Has one ModelPublicArcStages
}
// TableName returns the table name for ModelPublicArcStageParts
func (m ModelPublicArcStageParts) TableName() string {
return "public.arc_stage_parts"
}
// TableNameOnly returns the table name without schema for ModelPublicArcStageParts
func (m ModelPublicArcStageParts) TableNameOnly() string {
return "arc_stage_parts"
}
// SchemaName returns the schema name for ModelPublicArcStageParts
func (m ModelPublicArcStageParts) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicArcStageParts) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicArcStageParts) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicArcStageParts) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicArcStageParts) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicArcStageParts) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicArcStageParts) GetPrefix() string {
return "ASP"
}
@@ -1,67 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicArcStages struct {
bun.BaseModel `bun:"table:public.arc_stages,alias:arc_stages"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
ArcID int64 `bun:"arc_id,type:bigint,notnull," json:"arc_id"`
Condition sql_types.SqlString `bun:"condition,type:text,default:'',notnull," json:"condition"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
StageOrder int32 `bun:"stage_order,type:int,default:0,notnull," json:"stage_order"`
RelArcID *ModelPublicCharacterArcs `bun:"rel:has-one,join:arc_id=id" json:"relarcid,omitempty"` // Has one ModelPublicCharacterArcs
RelStageIDPublicArcStageParts []*ModelPublicArcStageParts `bun:"rel:has-many,join:id=stage_id" json:"relstageidpublicarcstageparts,omitempty"` // Has many ModelPublicArcStageParts
RelCurrentStageIDPublicPersonaArcs []*ModelPublicPersonaArc `bun:"rel:has-many,join:id=current_stage_id" json:"relcurrentstageidpublicpersonaarcs,omitempty"` // Has many ModelPublicPersonaArc
}
// TableName returns the table name for ModelPublicArcStages
func (m ModelPublicArcStages) TableName() string {
return "public.arc_stages"
}
// TableNameOnly returns the table name without schema for ModelPublicArcStages
func (m ModelPublicArcStages) TableNameOnly() string {
return "arc_stages"
}
// SchemaName returns the schema name for ModelPublicArcStages
func (m ModelPublicArcStages) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicArcStages) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicArcStages) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicArcStages) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicArcStages) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicArcStages) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicArcStages) GetPrefix() string {
return "ASR"
}
@@ -1,67 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicCharacterArcs struct {
bun.BaseModel `bun:"table:public.character_arcs,alias:character_arcs"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
Summary sql_types.SqlString `bun:"summary,type:text,default:'',notnull," json:"summary"`
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
RelArcIDPublicArcStages []*ModelPublicArcStages `bun:"rel:has-many,join:id=arc_id" json:"relarcidpublicarcstages,omitempty"` // Has many ModelPublicArcStages
RelArcIDPublicPersonaArcs []*ModelPublicPersonaArc `bun:"rel:has-many,join:id=arc_id" json:"relarcidpublicpersonaarcs,omitempty"` // Has many ModelPublicPersonaArc
}
// TableName returns the table name for ModelPublicCharacterArcs
func (m ModelPublicCharacterArcs) TableName() string {
return "public.character_arcs"
}
// TableNameOnly returns the table name without schema for ModelPublicCharacterArcs
func (m ModelPublicCharacterArcs) TableNameOnly() string {
return "character_arcs"
}
// SchemaName returns the schema name for ModelPublicCharacterArcs
func (m ModelPublicCharacterArcs) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicCharacterArcs) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicCharacterArcs) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicCharacterArcs) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicCharacterArcs) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicCharacterArcs) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicCharacterArcs) GetPrefix() string {
return "CAH"
}
@@ -1,72 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicChatHistories struct {
bun.BaseModel `bun:"table:public.chat_histories,alias:chat_histories"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
AgentID sql_types.SqlString `bun:"agent_id,type:text,nullzero," json:"agent_id"`
Channel sql_types.SqlString `bun:"channel,type:text,nullzero," json:"channel"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
Messages sql_types.SqlJSONB `bun:"messages,type:jsonb,default:'[]',notnull," json:"messages"`
Metadata sql_types.SqlJSONB `bun:"metadata,type:jsonb,default:'{}',notnull," json:"metadata"`
ProjectID sql_types.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
SessionID sql_types.SqlString `bun:"session_id,type:text,notnull," json:"session_id"`
Summary sql_types.SqlString `bun:"summary,type:text,nullzero," json:"summary"`
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
Title sql_types.SqlString `bun:"title,type:text,nullzero," json:"title"`
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
}
// TableName returns the table name for ModelPublicChatHistories
func (m ModelPublicChatHistories) TableName() string {
return "public.chat_histories"
}
// TableNameOnly returns the table name without schema for ModelPublicChatHistories
func (m ModelPublicChatHistories) TableNameOnly() string {
return "chat_histories"
}
// SchemaName returns the schema name for ModelPublicChatHistories
func (m ModelPublicChatHistories) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicChatHistories) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicChatHistories) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicChatHistories) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicChatHistories) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicChatHistories) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicChatHistories) GetPrefix() string {
return "CHH"
}
@@ -1,66 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicEmbeddings struct {
bun.BaseModel `bun:"table:public.embeddings,alias:embeddings"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),nullzero," json:"created_at"`
Dim int32 `bun:"dim,type:int,notnull," json:"dim"`
Embedding sql_types.SqlVector `bun:"embedding,type:vector,notnull," json:"embedding"`
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
Model sql_types.SqlString `bun:"model,type:text,notnull,unique:uidx_embeddings_thought_id_model," json:"model"`
ThoughtID int64 `bun:"thought_id,type:bigint,notnull,unique:uidx_embeddings_thought_id_model," json:"thought_id"`
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),nullzero," json:"updated_at"`
RelThoughtID *ModelPublicThoughts `bun:"rel:has-one,join:thought_id=id" json:"relthoughtid,omitempty"` // Has one ModelPublicThoughts
}
// TableName returns the table name for ModelPublicEmbeddings
func (m ModelPublicEmbeddings) TableName() string {
return "public.embeddings"
}
// TableNameOnly returns the table name without schema for ModelPublicEmbeddings
func (m ModelPublicEmbeddings) TableNameOnly() string {
return "embeddings"
}
// SchemaName returns the schema name for ModelPublicEmbeddings
func (m ModelPublicEmbeddings) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicEmbeddings) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicEmbeddings) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicEmbeddings) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicEmbeddings) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicEmbeddings) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicEmbeddings) GetPrefix() string {
return "EMB"
}
@@ -1,87 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicLearnings struct {
bun.BaseModel `bun:"table:public.learnings,alias:learnings"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
ActionRequired bool `bun:"action_required,type:boolean,default:false,notnull," json:"action_required"`
Area sql_types.SqlString `bun:"area,type:text,default:'other',notnull," json:"area"`
Category sql_types.SqlString `bun:"category,type:text,default:'insight',notnull," json:"category"`
Confidence sql_types.SqlString `bun:"confidence,type:text,default:'hypothesis',notnull," json:"confidence"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
Details sql_types.SqlString `bun:"details,type:text,default:'',notnull," json:"details"`
DuplicateOfLearningID sql_types.SqlInt64 `bun:"duplicate_of_learning_id,type:bigint,nullzero," json:"duplicate_of_learning_id"`
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
Priority sql_types.SqlString `bun:"priority,type:text,default:'medium',notnull," json:"priority"`
ProjectID sql_types.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
RelatedSkillID sql_types.SqlInt64 `bun:"related_skill_id,type:bigint,nullzero," json:"related_skill_id"`
RelatedThoughtID sql_types.SqlInt64 `bun:"related_thought_id,type:bigint,nullzero," json:"related_thought_id"`
ReviewedAt sql_types.SqlTimeStamp `bun:"reviewed_at,type:timestamptz,nullzero," json:"reviewed_at"`
ReviewedBy sql_types.SqlString `bun:"reviewed_by,type:text,nullzero," json:"reviewed_by"`
SourceRef sql_types.SqlString `bun:"source_ref,type:text,nullzero," json:"source_ref"`
SourceType sql_types.SqlString `bun:"source_type,type:text,nullzero," json:"source_type"`
Status sql_types.SqlString `bun:"status,type:text,default:'pending',notnull," json:"status"`
Summary sql_types.SqlString `bun:"summary,type:text,notnull," json:"summary"`
SupersedesLearningID sql_types.SqlInt64 `bun:"supersedes_learning_id,type:bigint,nullzero," json:"supersedes_learning_id"`
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
RelDuplicateOfLearningID *ModelPublicLearnings `bun:"rel:has-one,join:duplicate_of_learning_id=id" json:"relduplicateoflearningid,omitempty"` // Has one ModelPublicLearnings
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
RelRelatedSkillID *ModelPublicAgentSkills `bun:"rel:has-one,join:related_skill_id=id" json:"relrelatedskillid,omitempty"` // Has one ModelPublicAgentSkills
RelRelatedThoughtID *ModelPublicThoughts `bun:"rel:has-one,join:related_thought_id=id" json:"relrelatedthoughtid,omitempty"` // Has one ModelPublicThoughts
RelSupersedesLearningID *ModelPublicLearnings `bun:"rel:has-one,join:supersedes_learning_id=id" json:"relsupersedeslearningid,omitempty"` // Has one ModelPublicLearnings
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
RelLearningIDPublicThoughtLearningLinks []*ModelPublicThoughtLearningLinks `bun:"rel:has-many,join:id=learning_id" json:"rellearningidpublicthoughtlearninglinks,omitempty"` // Has many ModelPublicThoughtLearningLinks
}
// TableName returns the table name for ModelPublicLearnings
func (m ModelPublicLearnings) TableName() string {
return "public.learnings"
}
// TableNameOnly returns the table name without schema for ModelPublicLearnings
func (m ModelPublicLearnings) TableNameOnly() string {
return "learnings"
}
// SchemaName returns the schema name for ModelPublicLearnings
func (m ModelPublicLearnings) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicLearnings) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicLearnings) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicLearnings) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicLearnings) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicLearnings) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicLearnings) GetPrefix() string {
return "LEA"
}
@@ -1,59 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicManagedAPIKeys struct {
bun.BaseModel `bun:"table:public.managed_api_keys,alias:managed_api_keys"`
KeyID sql_types.SqlString `bun:"key_id,type:text,pk," json:"key_id"`
SecretHash sql_types.SqlString `bun:"secret_hash,type:text,notnull," json:"secret_hash"`
RelKeyID *ModelPublicAPIKeyAssignments `bun:"rel:has-one,join:key_id=key_id" json:"relkeyid,omitempty"` // Has one ModelPublicAPIKeyAssignments
}
// TableName returns the table name for ModelPublicManagedAPIKeys
func (m ModelPublicManagedAPIKeys) TableName() string {
return "public.managed_api_keys"
}
// TableNameOnly returns the table name without schema for ModelPublicManagedAPIKeys
func (m ModelPublicManagedAPIKeys) TableNameOnly() string {
return "managed_api_keys"
}
// SchemaName returns the schema name for ModelPublicManagedAPIKeys
func (m ModelPublicManagedAPIKeys) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicManagedAPIKeys) GetID() string {
return m.KeyID.String()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicManagedAPIKeys) GetIDStr() string {
return m.KeyID.String()
}
// SetID sets the primary key value
func (m ModelPublicManagedAPIKeys) SetID(newid string) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicManagedAPIKeys) UpdateID(newid string) {
m.KeyID.FromString(newid)
}
// GetIDName returns the name of the primary key column
func (m ModelPublicManagedAPIKeys) GetIDName() string {
return "key_id"
}
// GetPrefix returns the table prefix
func (m ModelPublicManagedAPIKeys) GetPrefix() string {
return "MAK"
}
@@ -1,62 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicOauthClients struct {
bun.BaseModel `bun:"table:public.oauth_clients,alias:oauth_clients"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
ClientID sql_types.SqlString `bun:"client_id,type:text,notnull," json:"client_id"`
ClientName sql_types.SqlString `bun:"client_name,type:text,default:'',notnull," json:"client_name"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
RedirectUris sql_types.SqlStringArray `bun:"redirect_uris,type:text[],default:'{}',notnull," json:"redirect_uris"`
}
// TableName returns the table name for ModelPublicOauthClients
func (m ModelPublicOauthClients) TableName() string {
return "public.oauth_clients"
}
// TableNameOnly returns the table name without schema for ModelPublicOauthClients
func (m ModelPublicOauthClients) TableNameOnly() string {
return "oauth_clients"
}
// SchemaName returns the schema name for ModelPublicOauthClients
func (m ModelPublicOauthClients) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicOauthClients) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicOauthClients) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicOauthClients) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicOauthClients) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicOauthClients) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicOauthClients) GetPrefix() string {
return "OCA"
}
@@ -1,65 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicPersonaArc struct {
bun.BaseModel `bun:"table:public.persona_arc,alias:persona_arc"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
ArcID int64 `bun:"arc_id,type:bigint,notnull," json:"arc_id"`
CurrentStageID int64 `bun:"current_stage_id,type:bigint,notnull," json:"current_stage_id"`
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
RelArcID *ModelPublicCharacterArcs `bun:"rel:has-one,join:arc_id=id" json:"relarcid,omitempty"` // Has one ModelPublicCharacterArcs
RelCurrentStageID *ModelPublicArcStages `bun:"rel:has-one,join:current_stage_id=id" json:"relcurrentstageid,omitempty"` // Has one ModelPublicArcStages
RelPersonaID *ModelPublicAgentPersonas `bun:"rel:has-one,join:persona_id=id" json:"relpersonaid,omitempty"` // Has one ModelPublicAgentPersonas
}
// TableName returns the table name for ModelPublicPersonaArc
func (m ModelPublicPersonaArc) TableName() string {
return "public.persona_arc"
}
// TableNameOnly returns the table name without schema for ModelPublicPersonaArc
func (m ModelPublicPersonaArc) TableNameOnly() string {
return "persona_arc"
}
// SchemaName returns the schema name for ModelPublicPersonaArc
func (m ModelPublicPersonaArc) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicPersonaArc) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicPersonaArc) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicPersonaArc) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicPersonaArc) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicPersonaArc) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicPersonaArc) GetPrefix() string {
return "PAE"
}
@@ -1,63 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicPlanDependencies struct {
bun.BaseModel `bun:"table:public.plan_dependencies,alias:plan_dependencies"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
DependsOnPlanID int64 `bun:"depends_on_plan_id,type:bigint,notnull,unique:uidx_plan_dependencies_plan_id_depends_on_plan_id," json:"depends_on_plan_id"`
PlanID int64 `bun:"plan_id,type:bigint,notnull,unique:uidx_plan_dependencies_plan_id_depends_on_plan_id," json:"plan_id"`
RelDependsOnPlanID *ModelPublicPlans `bun:"rel:has-one,join:depends_on_plan_id=id" json:"reldependsonplanid,omitempty"` // Has one ModelPublicPlans
RelPlanID *ModelPublicPlans `bun:"rel:has-one,join:plan_id=id" json:"relplanid,omitempty"` // Has one ModelPublicPlans
}
// TableName returns the table name for ModelPublicPlanDependencies
func (m ModelPublicPlanDependencies) TableName() string {
return "public.plan_dependencies"
}
// TableNameOnly returns the table name without schema for ModelPublicPlanDependencies
func (m ModelPublicPlanDependencies) TableNameOnly() string {
return "plan_dependencies"
}
// SchemaName returns the schema name for ModelPublicPlanDependencies
func (m ModelPublicPlanDependencies) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicPlanDependencies) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicPlanDependencies) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicPlanDependencies) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicPlanDependencies) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicPlanDependencies) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicPlanDependencies) GetPrefix() string {
return "PDL"
}
@@ -1,63 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicPlanGuardrails struct {
bun.BaseModel `bun:"table:public.plan_guardrails,alias:plan_guardrails"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
GuardrailID int64 `bun:"guardrail_id,type:bigint,notnull,unique:uidx_plan_guardrails_plan_id_guardrail_id," json:"guardrail_id"`
PlanID int64 `bun:"plan_id,type:bigint,notnull,unique:uidx_plan_guardrails_plan_id_guardrail_id," json:"plan_id"`
RelGuardrailID *ModelPublicAgentGuardrails `bun:"rel:has-one,join:guardrail_id=id" json:"relguardrailid,omitempty"` // Has one ModelPublicAgentGuardrails
RelPlanID *ModelPublicPlans `bun:"rel:has-one,join:plan_id=id" json:"relplanid,omitempty"` // Has one ModelPublicPlans
}
// TableName returns the table name for ModelPublicPlanGuardrails
func (m ModelPublicPlanGuardrails) TableName() string {
return "public.plan_guardrails"
}
// TableNameOnly returns the table name without schema for ModelPublicPlanGuardrails
func (m ModelPublicPlanGuardrails) TableNameOnly() string {
return "plan_guardrails"
}
// SchemaName returns the schema name for ModelPublicPlanGuardrails
func (m ModelPublicPlanGuardrails) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicPlanGuardrails) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicPlanGuardrails) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicPlanGuardrails) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicPlanGuardrails) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicPlanGuardrails) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicPlanGuardrails) GetPrefix() string {
return "PGL"
}
@@ -1,63 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicPlanRelatedPlans struct {
bun.BaseModel `bun:"table:public.plan_related_plans,alias:plan_related_plans"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
PlanAID int64 `bun:"plan_a_id,type:bigint,notnull,unique:uidx_plan_related_plans_plan_a_id_plan_b_id," json:"plan_a_id"`
PlanBID int64 `bun:"plan_b_id,type:bigint,notnull,unique:uidx_plan_related_plans_plan_a_id_plan_b_id," json:"plan_b_id"`
RelPlanAID *ModelPublicPlans `bun:"rel:has-one,join:plan_a_id=id" json:"relplanaid,omitempty"` // Has one ModelPublicPlans
RelPlanBID *ModelPublicPlans `bun:"rel:has-one,join:plan_b_id=id" json:"relplanbid,omitempty"` // Has one ModelPublicPlans
}
// TableName returns the table name for ModelPublicPlanRelatedPlans
func (m ModelPublicPlanRelatedPlans) TableName() string {
return "public.plan_related_plans"
}
// TableNameOnly returns the table name without schema for ModelPublicPlanRelatedPlans
func (m ModelPublicPlanRelatedPlans) TableNameOnly() string {
return "plan_related_plans"
}
// SchemaName returns the schema name for ModelPublicPlanRelatedPlans
func (m ModelPublicPlanRelatedPlans) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicPlanRelatedPlans) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicPlanRelatedPlans) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicPlanRelatedPlans) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicPlanRelatedPlans) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicPlanRelatedPlans) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicPlanRelatedPlans) GetPrefix() string {
return "PRP"
}
@@ -1,63 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicPlanSkills struct {
bun.BaseModel `bun:"table:public.plan_skills,alias:plan_skills"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
PlanID int64 `bun:"plan_id,type:bigint,notnull,unique:uidx_plan_skills_plan_id_skill_id," json:"plan_id"`
SkillID int64 `bun:"skill_id,type:bigint,notnull,unique:uidx_plan_skills_plan_id_skill_id," json:"skill_id"`
RelPlanID *ModelPublicPlans `bun:"rel:has-one,join:plan_id=id" json:"relplanid,omitempty"` // Has one ModelPublicPlans
RelSkillID *ModelPublicAgentSkills `bun:"rel:has-one,join:skill_id=id" json:"relskillid,omitempty"` // Has one ModelPublicAgentSkills
}
// TableName returns the table name for ModelPublicPlanSkills
func (m ModelPublicPlanSkills) TableName() string {
return "public.plan_skills"
}
// TableNameOnly returns the table name without schema for ModelPublicPlanSkills
func (m ModelPublicPlanSkills) TableNameOnly() string {
return "plan_skills"
}
// SchemaName returns the schema name for ModelPublicPlanSkills
func (m ModelPublicPlanSkills) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicPlanSkills) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicPlanSkills) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicPlanSkills) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicPlanSkills) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicPlanSkills) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicPlanSkills) GetPrefix() string {
return "PSL"
}
@@ -1,83 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicPlans struct {
bun.BaseModel `bun:"table:public.plans,alias:plans"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
CompletedAt sql_types.SqlTimeStamp `bun:"completed_at,type:timestamptz,nullzero," json:"completed_at"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
Description sql_types.SqlString `bun:"description,type:text,default:'',notnull," json:"description"`
DueDate sql_types.SqlTimeStamp `bun:"due_date,type:timestamptz,nullzero," json:"due_date"`
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
LastReviewedAt sql_types.SqlTimeStamp `bun:"last_reviewed_at,type:timestamptz,nullzero," json:"last_reviewed_at"`
Owner sql_types.SqlString `bun:"owner,type:text,nullzero," json:"owner"`
Priority sql_types.SqlString `bun:"priority,type:text,default:'medium',notnull," json:"priority"` // low, medium, high, critical
ProjectID sql_types.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
ReviewedBy sql_types.SqlString `bun:"reviewed_by,type:text,nullzero," json:"reviewed_by"`
Status sql_types.SqlString `bun:"status,type:text,default:'draft',notnull," json:"status"` // draft, active, blocked, completed, cancelled, superseded
SupersedesPlanID sql_types.SqlInt64 `bun:"supersedes_plan_id,type:bigint,nullzero," json:"supersedes_plan_id"`
Tags sql_types.SqlStringArray `bun:"tags,type:text[],default:'{}',notnull," json:"tags"`
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
Title sql_types.SqlString `bun:"title,type:text,notnull," json:"title"`
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
RelSupersedesPlanID *ModelPublicPlans `bun:"rel:has-one,join:supersedes_plan_id=id" json:"relsupersedesplanid,omitempty"` // Has one ModelPublicPlans
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
RelDependsOnPlanIDPublicPlanDependencies []*ModelPublicPlanDependencies `bun:"rel:has-many,join:id=depends_on_plan_id" json:"reldependsonplanidpublicplandependencies,omitempty"` // Has many ModelPublicPlanDependencies
RelPlanIDPublicPlanDependencies []*ModelPublicPlanDependencies `bun:"rel:has-many,join:id=plan_id" json:"relplanidpublicplandependencies,omitempty"` // Has many ModelPublicPlanDependencies
RelPlanAIDPublicPlanRelatedPlans []*ModelPublicPlanRelatedPlans `bun:"rel:has-many,join:id=plan_a_id" json:"relplanaidpublicplanrelatedplans,omitempty"` // Has many ModelPublicPlanRelatedPlans
RelPlanBIDPublicPlanRelatedPlans []*ModelPublicPlanRelatedPlans `bun:"rel:has-many,join:id=plan_b_id" json:"relplanbidpublicplanrelatedplans,omitempty"` // Has many ModelPublicPlanRelatedPlans
RelPlanIDPublicPlanSkills []*ModelPublicPlanSkills `bun:"rel:has-many,join:id=plan_id" json:"relplanidpublicplanskills,omitempty"` // Has many ModelPublicPlanSkills
RelPlanIDPublicPlanGuardrails []*ModelPublicPlanGuardrails `bun:"rel:has-many,join:id=plan_id" json:"relplanidpublicplanguardrails,omitempty"` // Has many ModelPublicPlanGuardrails
}
// TableName returns the table name for ModelPublicPlans
func (m ModelPublicPlans) TableName() string {
return "public.plans"
}
// TableNameOnly returns the table name without schema for ModelPublicPlans
func (m ModelPublicPlans) TableNameOnly() string {
return "plans"
}
// SchemaName returns the schema name for ModelPublicPlans
func (m ModelPublicPlans) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicPlans) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicPlans) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicPlans) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicPlans) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicPlans) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicPlans) GetPrefix() string {
return "PLA"
}
@@ -1,63 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicProjectGuardrails struct {
bun.BaseModel `bun:"table:public.project_guardrails,alias:project_guardrails"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
GuardrailID int64 `bun:"guardrail_id,type:bigint,notnull," json:"guardrail_id"`
ProjectID int64 `bun:"project_id,type:bigint,notnull," json:"project_id"`
RelGuardrailID *ModelPublicAgentGuardrails `bun:"rel:has-one,join:guardrail_id=id" json:"relguardrailid,omitempty"` // Has one ModelPublicAgentGuardrails
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
}
// TableName returns the table name for ModelPublicProjectGuardrails
func (m ModelPublicProjectGuardrails) TableName() string {
return "public.project_guardrails"
}
// TableNameOnly returns the table name without schema for ModelPublicProjectGuardrails
func (m ModelPublicProjectGuardrails) TableNameOnly() string {
return "project_guardrails"
}
// SchemaName returns the schema name for ModelPublicProjectGuardrails
func (m ModelPublicProjectGuardrails) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicProjectGuardrails) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicProjectGuardrails) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicProjectGuardrails) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicProjectGuardrails) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicProjectGuardrails) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicProjectGuardrails) GetPrefix() string {
return "PGR"
}
@@ -1,64 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicProjectPersonas struct {
bun.BaseModel `bun:"table:public.project_personas,alias:project_personas"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
IsDefault bool `bun:"is_default,type:boolean,default:false,notnull," json:"is_default"`
PersonaID int64 `bun:"persona_id,type:bigint,notnull," json:"persona_id"`
ProjectID int64 `bun:"project_id,type:bigint,notnull," json:"project_id"`
RelPersonaID *ModelPublicAgentPersonas `bun:"rel:has-one,join:persona_id=id" json:"relpersonaid,omitempty"` // Has one ModelPublicAgentPersonas
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
}
// TableName returns the table name for ModelPublicProjectPersonas
func (m ModelPublicProjectPersonas) TableName() string {
return "public.project_personas"
}
// TableNameOnly returns the table name without schema for ModelPublicProjectPersonas
func (m ModelPublicProjectPersonas) TableNameOnly() string {
return "project_personas"
}
// SchemaName returns the schema name for ModelPublicProjectPersonas
func (m ModelPublicProjectPersonas) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicProjectPersonas) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicProjectPersonas) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicProjectPersonas) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicProjectPersonas) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicProjectPersonas) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicProjectPersonas) GetPrefix() string {
return "PPR"
}
@@ -1,64 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicProjectSkills struct {
bun.BaseModel `bun:"table:public.project_skills,alias:project_skills"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
Override bool `bun:"override,type:boolean,default:false,notnull," json:"override"`
ProjectID int64 `bun:"project_id,type:bigint,notnull," json:"project_id"`
SkillID int64 `bun:"skill_id,type:bigint,notnull," json:"skill_id"`
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
RelSkillID *ModelPublicAgentSkills `bun:"rel:has-one,join:skill_id=id" json:"relskillid,omitempty"` // Has one ModelPublicAgentSkills
}
// TableName returns the table name for ModelPublicProjectSkills
func (m ModelPublicProjectSkills) TableName() string {
return "public.project_skills"
}
// TableNameOnly returns the table name without schema for ModelPublicProjectSkills
func (m ModelPublicProjectSkills) TableNameOnly() string {
return "project_skills"
}
// SchemaName returns the schema name for ModelPublicProjectSkills
func (m ModelPublicProjectSkills) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicProjectSkills) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicProjectSkills) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicProjectSkills) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicProjectSkills) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicProjectSkills) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicProjectSkills) GetPrefix() string {
return "PSR"
}
@@ -1,73 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicProjects struct {
bun.BaseModel `bun:"table:public.projects,alias:projects"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),nullzero," json:"created_at"`
Description sql_types.SqlString `bun:"description,type:text,nullzero," json:"description"`
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
LastActiveAt sql_types.SqlTimeStamp `bun:"last_active_at,type:timestamptz,default:now(),nullzero," json:"last_active_at"`
Name sql_types.SqlString `bun:"name,type:text,notnull,unique:uidx_projects_tenant_id_name," json:"name"`
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero,unique:uidx_projects_tenant_id_name," json:"tenant_id"`
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
RelProjectIDPublicProjectPersonas []*ModelPublicProjectPersonas `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicprojectpersonas,omitempty"` // Has many ModelPublicProjectPersonas
RelProjectIDPublicThoughts []*ModelPublicThoughts `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicthoughts,omitempty"` // Has many ModelPublicThoughts
RelProjectIDPublicStoredFiles []*ModelPublicStoredFiles `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicstoredfiles,omitempty"` // Has many ModelPublicStoredFiles
RelProjectIDPublicChatHistories []*ModelPublicChatHistories `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicchathistories,omitempty"` // Has many ModelPublicChatHistories
RelProjectIDPublicLearnings []*ModelPublicLearnings `bun:"rel:has-many,join:id=project_id" json:"relprojectidpubliclearnings,omitempty"` // Has many ModelPublicLearnings
RelProjectIDPublicPlans []*ModelPublicPlans `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicplans,omitempty"` // Has many ModelPublicPlans
RelProjectIDPublicProjectSkills []*ModelPublicProjectSkills `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicprojectskills,omitempty"` // Has many ModelPublicProjectSkills
RelProjectIDPublicProjectGuardrails []*ModelPublicProjectGuardrails `bun:"rel:has-many,join:id=project_id" json:"relprojectidpublicprojectguardrails,omitempty"` // Has many ModelPublicProjectGuardrails
}
// TableName returns the table name for ModelPublicProjects
func (m ModelPublicProjects) TableName() string {
return "public.projects"
}
// TableNameOnly returns the table name without schema for ModelPublicProjects
func (m ModelPublicProjects) TableNameOnly() string {
return "projects"
}
// SchemaName returns the schema name for ModelPublicProjects
func (m ModelPublicProjects) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicProjects) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicProjects) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicProjects) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicProjects) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicProjects) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicProjects) GetPrefix() string {
return "PRO"
}
@@ -1,74 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicStoredFiles struct {
bun.BaseModel `bun:"table:public.stored_files,alias:stored_files"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
Content []byte `bun:"content,type:bytea,notnull," json:"content"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
Encoding sql_types.SqlString `bun:"encoding,type:text,default:'base64',notnull," json:"encoding"`
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
Kind sql_types.SqlString `bun:"kind,type:text,default:'file',notnull," json:"kind"`
MediaType sql_types.SqlString `bun:"media_type,type:text,notnull," json:"media_type"`
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
ProjectID sql_types.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
Sha256 sql_types.SqlString `bun:"sha256,type:text,notnull," json:"sha256"`
SizeBytes int64 `bun:"size_bytes,type:bigint,notnull," json:"size_bytes"`
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
ThoughtID sql_types.SqlInt64 `bun:"thought_id,type:bigint,nullzero," json:"thought_id"`
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
RelThoughtID *ModelPublicThoughts `bun:"rel:has-one,join:thought_id=id" json:"relthoughtid,omitempty"` // Has one ModelPublicThoughts
}
// TableName returns the table name for ModelPublicStoredFiles
func (m ModelPublicStoredFiles) TableName() string {
return "public.stored_files"
}
// TableNameOnly returns the table name without schema for ModelPublicStoredFiles
func (m ModelPublicStoredFiles) TableNameOnly() string {
return "stored_files"
}
// SchemaName returns the schema name for ModelPublicStoredFiles
func (m ModelPublicStoredFiles) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicStoredFiles) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicStoredFiles) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicStoredFiles) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicStoredFiles) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicStoredFiles) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicStoredFiles) GetPrefix() string {
return "SFT"
}
@@ -1,63 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicTenantUsers struct {
bun.BaseModel `bun:"table:public.tenant_users,alias:tenant_users"`
ID sql_types.SqlString `bun:"id,type:text,pk," json:"id"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
Email sql_types.SqlString `bun:"email,type:text,nullzero,unique:uidx_tenant_users_tenant_id_email," json:"email"`
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
TenantID sql_types.SqlString `bun:"tenant_id,type:text,notnull,unique:uidx_tenant_users_tenant_id_email," json:"tenant_id"`
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
RelUserIDPublicAPIKeyAssignments []*ModelPublicAPIKeyAssignments `bun:"rel:has-many,join:id=user_id" json:"reluseridpublicapikeyassignments,omitempty"` // Has many ModelPublicAPIKeyAssignments
}
// TableName returns the table name for ModelPublicTenantUsers
func (m ModelPublicTenantUsers) TableName() string {
return "public.tenant_users"
}
// TableNameOnly returns the table name without schema for ModelPublicTenantUsers
func (m ModelPublicTenantUsers) TableNameOnly() string {
return "tenant_users"
}
// SchemaName returns the schema name for ModelPublicTenantUsers
func (m ModelPublicTenantUsers) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicTenantUsers) GetID() string {
return m.ID.String()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicTenantUsers) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicTenantUsers) SetID(newid string) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicTenantUsers) UpdateID(newid string) {
m.ID.FromString(newid)
}
// GetIDName returns the name of the primary key column
func (m ModelPublicTenantUsers) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicTenantUsers) GetPrefix() string {
return "TUE"
}
@@ -1,73 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicTenants struct {
bun.BaseModel `bun:"table:public.tenants,alias:tenants"`
ID sql_types.SqlString `bun:"id,type:text,pk," json:"id"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
Name sql_types.SqlString `bun:"name,type:text,notnull," json:"name"`
RelTenantIDPublicAgentPersonas []*ModelPublicAgentPersonas `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicagentpersonas,omitempty"` // Has many ModelPublicAgentPersonas
RelTenantIDPublicAgentParts []*ModelPublicAgentParts `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicagentparts,omitempty"` // Has many ModelPublicAgentParts
RelTenantIDPublicAgentTraits []*ModelPublicAgentTraits `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicagenttraits,omitempty"` // Has many ModelPublicAgentTraits
RelTenantIDPublicCharacterArcs []*ModelPublicCharacterArcs `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpubliccharacterarcs,omitempty"` // Has many ModelPublicCharacterArcs
RelTenantIDPublicThoughts []*ModelPublicThoughts `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicthoughts,omitempty"` // Has many ModelPublicThoughts
RelTenantIDPublicProjects []*ModelPublicProjects `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicprojects,omitempty"` // Has many ModelPublicProjects
RelTenantIDPublicStoredFiles []*ModelPublicStoredFiles `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicstoredfiles,omitempty"` // Has many ModelPublicStoredFiles
RelTenantIDPublicTenantUsers []*ModelPublicTenantUsers `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublictenantusers,omitempty"` // Has many ModelPublicTenantUsers
RelTenantIDPublicAPIKeyAssignments []*ModelPublicAPIKeyAssignments `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicapikeyassignments,omitempty"` // Has many ModelPublicAPIKeyAssignments
RelTenantIDPublicChatHistories []*ModelPublicChatHistories `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicchathistories,omitempty"` // Has many ModelPublicChatHistories
RelTenantIDPublicLearnings []*ModelPublicLearnings `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpubliclearnings,omitempty"` // Has many ModelPublicLearnings
RelTenantIDPublicPlans []*ModelPublicPlans `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicplans,omitempty"` // Has many ModelPublicPlans
RelTenantIDPublicAgentSkills []*ModelPublicAgentSkills `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicagentskills,omitempty"` // Has many ModelPublicAgentSkills
RelTenantIDPublicAgentGuardrails []*ModelPublicAgentGuardrails `bun:"rel:has-many,join:id=tenant_id" json:"reltenantidpublicagentguardrails,omitempty"` // Has many ModelPublicAgentGuardrails
}
// TableName returns the table name for ModelPublicTenants
func (m ModelPublicTenants) TableName() string {
return "public.tenants"
}
// TableNameOnly returns the table name without schema for ModelPublicTenants
func (m ModelPublicTenants) TableNameOnly() string {
return "tenants"
}
// SchemaName returns the schema name for ModelPublicTenants
func (m ModelPublicTenants) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicTenants) GetID() string {
return m.ID.String()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicTenants) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicTenants) SetID(newid string) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicTenants) UpdateID(newid string) {
m.ID.FromString(newid)
}
// GetIDName returns the name of the primary key column
func (m ModelPublicTenants) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicTenants) GetPrefix() string {
return "TEN"
}
@@ -1,64 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicThoughtLearningLinks struct {
bun.BaseModel `bun:"table:public.thought_learning_links,alias:thought_learning_links"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
LearningID int64 `bun:"learning_id,type:bigint,notnull,unique:uidx_thought_learning_links_thought_id_learning_id," json:"learning_id"`
Relation sql_types.SqlString `bun:"relation,type:text,default:'source',notnull," json:"relation"`
ThoughtID int64 `bun:"thought_id,type:bigint,notnull,unique:uidx_thought_learning_links_thought_id_learning_id," json:"thought_id"`
RelLearningID *ModelPublicLearnings `bun:"rel:has-one,join:learning_id=id" json:"rellearningid,omitempty"` // Has one ModelPublicLearnings
RelThoughtID *ModelPublicThoughts `bun:"rel:has-one,join:thought_id=id" json:"relthoughtid,omitempty"` // Has one ModelPublicThoughts
}
// TableName returns the table name for ModelPublicThoughtLearningLinks
func (m ModelPublicThoughtLearningLinks) TableName() string {
return "public.thought_learning_links"
}
// TableNameOnly returns the table name without schema for ModelPublicThoughtLearningLinks
func (m ModelPublicThoughtLearningLinks) TableNameOnly() string {
return "thought_learning_links"
}
// SchemaName returns the schema name for ModelPublicThoughtLearningLinks
func (m ModelPublicThoughtLearningLinks) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicThoughtLearningLinks) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicThoughtLearningLinks) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicThoughtLearningLinks) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicThoughtLearningLinks) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicThoughtLearningLinks) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicThoughtLearningLinks) GetPrefix() string {
return "TLL"
}
@@ -1,64 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicThoughtLinks struct {
bun.BaseModel `bun:"table:public.thought_links,alias:thought_links"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),nullzero," json:"created_at"`
FromID int64 `bun:"from_id,type:bigint,notnull," json:"from_id"`
Relation sql_types.SqlString `bun:"relation,type:text,notnull," json:"relation"`
ToID int64 `bun:"to_id,type:bigint,notnull," json:"to_id"`
RelFromID *ModelPublicThoughts `bun:"rel:has-one,join:from_id=id" json:"relfromid,omitempty"` // Has one ModelPublicThoughts
RelToID *ModelPublicThoughts `bun:"rel:has-one,join:to_id=id" json:"reltoid,omitempty"` // Has one ModelPublicThoughts
}
// TableName returns the table name for ModelPublicThoughtLinks
func (m ModelPublicThoughtLinks) TableName() string {
return "public.thought_links"
}
// TableNameOnly returns the table name without schema for ModelPublicThoughtLinks
func (m ModelPublicThoughtLinks) TableNameOnly() string {
return "thought_links"
}
// SchemaName returns the schema name for ModelPublicThoughtLinks
func (m ModelPublicThoughtLinks) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicThoughtLinks) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicThoughtLinks) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicThoughtLinks) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicThoughtLinks) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicThoughtLinks) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicThoughtLinks) GetPrefix() string {
return "TLH"
}
@@ -1,74 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicThoughts struct {
bun.BaseModel `bun:"table:public.thoughts,alias:thoughts"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
ArchivedAt sql_types.SqlTimeStamp `bun:"archived_at,type:timestamptz,nullzero," json:"archived_at"`
Content sql_types.SqlString `bun:"content,type:text,notnull," json:"content"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),nullzero," json:"created_at"`
GUID sql_types.SqlUUID `bun:"guid,type:uuid,default:gen_random_uuid(),notnull," json:"guid"`
Metadata sql_types.SqlJSONB `bun:"metadata,type:jsonb,default:{}::jsonb,nullzero," json:"metadata"`
ProjectID sql_types.SqlInt64 `bun:"project_id,type:bigint,nullzero," json:"project_id"`
TenantID sql_types.SqlString `bun:"tenant_id,type:text,nullzero," json:"tenant_id"`
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),nullzero," json:"updated_at"`
RelProjectID *ModelPublicProjects `bun:"rel:has-one,join:project_id=id" json:"relprojectid,omitempty"` // Has one ModelPublicProjects
RelTenantID *ModelPublicTenants `bun:"rel:has-one,join:tenant_id=id" json:"reltenantid,omitempty"` // Has one ModelPublicTenants
RelFromIDPublicThoughtLinks []*ModelPublicThoughtLinks `bun:"rel:has-many,join:id=from_id" json:"relfromidpublicthoughtlinks,omitempty"` // Has many ModelPublicThoughtLinks
RelToIDPublicThoughtLinks []*ModelPublicThoughtLinks `bun:"rel:has-many,join:id=to_id" json:"reltoidpublicthoughtlinks,omitempty"` // Has many ModelPublicThoughtLinks
RelThoughtIDPublicThoughtLearningLinks []*ModelPublicThoughtLearningLinks `bun:"rel:has-many,join:id=thought_id" json:"relthoughtidpublicthoughtlearninglinks,omitempty"` // Has many ModelPublicThoughtLearningLinks
RelThoughtIDPublicEmbeddings []*ModelPublicEmbeddings `bun:"rel:has-many,join:id=thought_id" json:"relthoughtidpublicembeddings,omitempty"` // Has many ModelPublicEmbeddings
RelThoughtIDPublicStoredFiles []*ModelPublicStoredFiles `bun:"rel:has-many,join:id=thought_id" json:"relthoughtidpublicstoredfiles,omitempty"` // Has many ModelPublicStoredFiles
RelRelatedThoughtIDPublicLearnings []*ModelPublicLearnings `bun:"rel:has-many,join:id=related_thought_id" json:"relrelatedthoughtidpubliclearnings,omitempty"` // Has many ModelPublicLearnings
}
// TableName returns the table name for ModelPublicThoughts
func (m ModelPublicThoughts) TableName() string {
return "public.thoughts"
}
// TableNameOnly returns the table name without schema for ModelPublicThoughts
func (m ModelPublicThoughts) TableNameOnly() string {
return "thoughts"
}
// SchemaName returns the schema name for ModelPublicThoughts
func (m ModelPublicThoughts) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicThoughts) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicThoughts) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicThoughts) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicThoughts) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicThoughts) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicThoughts) GetPrefix() string {
return "THO"
}
@@ -1,62 +0,0 @@
// Code generated by relspecgo. DO NOT EDIT.
package generatedmodels
import (
"fmt"
sql_types "git.warky.dev/wdevs/relspecgo/pkg/sqltypes"
"github.com/uptrace/bun"
)
type ModelPublicToolAnnotations struct {
bun.BaseModel `bun:"table:public.tool_annotations,alias:tool_annotations"`
ID sql_types.SqlInt64 `bun:"id,type:bigserial,pk,autoincrement," json:"id"`
CreatedAt sql_types.SqlTimeStamp `bun:"created_at,type:timestamptz,default:now(),notnull," json:"created_at"`
Notes sql_types.SqlString `bun:"notes,type:text,default:'',notnull," json:"notes"`
ToolName sql_types.SqlString `bun:"tool_name,type:text,notnull," json:"tool_name"`
UpdatedAt sql_types.SqlTimeStamp `bun:"updated_at,type:timestamptz,default:now(),notnull," json:"updated_at"`
}
// TableName returns the table name for ModelPublicToolAnnotations
func (m ModelPublicToolAnnotations) TableName() string {
return "public.tool_annotations"
}
// TableNameOnly returns the table name without schema for ModelPublicToolAnnotations
func (m ModelPublicToolAnnotations) TableNameOnly() string {
return "tool_annotations"
}
// SchemaName returns the schema name for ModelPublicToolAnnotations
func (m ModelPublicToolAnnotations) SchemaName() string {
return "public"
}
// GetID returns the primary key value
func (m ModelPublicToolAnnotations) GetID() int64 {
return m.ID.Int64()
}
// GetIDStr returns the primary key as a string
func (m ModelPublicToolAnnotations) GetIDStr() string {
return m.ID.String()
}
// SetID sets the primary key value
func (m ModelPublicToolAnnotations) SetID(newid int64) {
m.UpdateID(newid)
}
// UpdateID updates the primary key value
func (m *ModelPublicToolAnnotations) UpdateID(newid int64) {
m.ID.FromString(fmt.Sprintf("%d", newid))
}
// GetIDName returns the name of the primary key column
func (m ModelPublicToolAnnotations) GetIDName() string {
return "id"
}
// GetPrefix returns the table prefix
func (m ModelPublicToolAnnotations) GetPrefix() string {
return "TAO"
}
+6 -3
View File
@@ -31,7 +31,7 @@ func TestSetToolSchemasAddsEmptyPropertiesForNoArgInput(t *testing.T) {
}
}
func TestSetToolSchemasUsesIntegerIDsInListOutput(t *testing.T) {
func TestSetToolSchemasUsesStringUUIDsInListOutput(t *testing.T) {
tool := &mcp.Tool{Name: "list_thoughts"}
if err := setToolSchemas[tools.ListInput, tools.ListOutput](tool); err != nil {
@@ -55,8 +55,11 @@ func TestSetToolSchemasUsesIntegerIDsInListOutput(t *testing.T) {
if idSchema == nil {
t.Fatal("missing id schema")
}
if idSchema.Type != "integer" {
t.Fatalf("id schema type = %q, want %q", idSchema.Type, "integer")
if idSchema.Type != "string" {
t.Fatalf("id schema type = %q, want %q", idSchema.Type, "string")
}
if idSchema.Format != "uuid" {
t.Fatalf("id schema format = %q, want %q", idSchema.Format, "uuid")
}
}
+57 -426
View File
@@ -1,7 +1,6 @@
package mcpserver
import (
"context"
"log/slog"
"net/http"
"strings"
@@ -19,35 +18,28 @@ const (
)
type ToolSet struct {
Version *tools.VersionTool
Capture *tools.CaptureTool
Search *tools.SearchTool
List *tools.ListTool
Stats *tools.StatsTool
Get *tools.GetTool
Update *tools.UpdateTool
Delete *tools.DeleteTool
Archive *tools.ArchiveTool
DuplicateAudit *tools.DuplicateAuditTool
Projects *tools.ProjectsTool
Context *tools.ContextTool
Recall *tools.RecallTool
Summarize *tools.SummarizeTool
Links *tools.LinksTool
Files *tools.FilesTool
Backfill *tools.BackfillTool
Reparse *tools.ReparseMetadataTool
RetryMetadata *tools.RetryEnrichmentTool
//Maintenance *tools.MaintenanceTool
Skills *tools.SkillsTool
Personas *tools.AgentPersonasTool
ChatHistory *tools.ChatHistoryTool
Describe *tools.DescribeTool
Learnings *tools.LearningsTool
Plans *tools.PlansTool
ProjectPersonas *tools.ProjectPersonasTool
WorldModel *tools.WorldModelTool
ThoughtLearningLinks *tools.ThoughtLearningLinksTool
Version *tools.VersionTool
Capture *tools.CaptureTool
Search *tools.SearchTool
List *tools.ListTool
Stats *tools.StatsTool
Get *tools.GetTool
Update *tools.UpdateTool
Delete *tools.DeleteTool
Archive *tools.ArchiveTool
Projects *tools.ProjectsTool
Context *tools.ContextTool
Recall *tools.RecallTool
Summarize *tools.SummarizeTool
Links *tools.LinksTool
Files *tools.FilesTool
Backfill *tools.BackfillTool
Reparse *tools.ReparseMetadataTool
RetryMetadata *tools.RetryEnrichmentTool
Maintenance *tools.MaintenanceTool
Skills *tools.SkillsTool
ChatHistory *tools.ChatHistoryTool
Describe *tools.DescribeTool
}
// Handlers groups the HTTP handlers produced for an MCP server instance.
@@ -91,14 +83,9 @@ func NewHandlers(cfg config.MCPConfig, logger *slog.Logger, toolSet ToolSet, onS
registerSystemTools,
registerThoughtTools,
registerProjectTools,
registerWorldModelTools,
registerLearningTools,
registerThoughtLearningLinkTools,
registerPlanTools,
registerFileTools,
registerMaintenanceTools,
registerSkillTools,
registerPersonaTools,
registerChatHistoryTools,
registerDescribeTools,
} {
@@ -130,33 +117,6 @@ func NewHandlers(cfg config.MCPConfig, logger *slog.Logger, toolSet ToolSet, onS
return h, nil
}
func registerWorldModelTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
server.AddResource(&mcp.Resource{
Name: "world_model_intro",
URI: "amcs://world-model/intro",
MIMEType: "text/markdown",
Description: "Mandatory startup instructions for discovering and loading the AMCS world model.",
}, func(_ context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
return &mcp.ReadResourceResult{Contents: []*mcp.ResourceContents{{URI: req.Params.URI, MIMEType: "text/markdown", Text: string(amcsllm.WorldModelIntro)}}}, nil
})
if err := addTool(server, logger, &mcp.Tool{Name: "bootstrap_world_model", Description: "Validate and activate a project, then return its effective skills, guardrails, personas, and bounded recent context."}, toolSet.WorldModel.Bootstrap); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "add_project_persona", Description: "Link a persona to a project and optionally make it the default."}, toolSet.ProjectPersonas.Add); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "remove_project_persona", Description: "Unlink a persona from a project."}, toolSet.ProjectPersonas.Remove); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "set_default_project_persona", Description: "Set a linked persona as the project's default persona."}, toolSet.ProjectPersonas.SetDefault); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "list_project_personas", Description: "List personas linked to a project, with the default marked."}, toolSet.ProjectPersonas.List); err != nil {
return err
}
return nil
}
// buildServerIcons returns icon definitions referencing the server's own /images/icon.png endpoint.
// Returns nil when publicURL is empty so the icons field is omitted from the MCP identity.
func buildServerIcons(publicURL string) []mcp.Icon {
@@ -228,12 +188,6 @@ func registerThoughtTools(server *mcp.Server, logger *slog.Logger, toolSet ToolS
}, toolSet.Archive.Handle); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "audit_duplicates",
Description: "Dry-run duplicate audit for projects, thoughts, and metadata normalization candidates; performs no writes.",
}, toolSet.DuplicateAudit.Handle); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "summarize_thoughts",
Description: "LLM summary of a filtered set of thoughts.",
@@ -295,150 +249,6 @@ func registerProjectTools(server *mcp.Server, logger *slog.Logger, toolSet ToolS
return nil
}
func registerLearningTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
if err := addTool(server, logger, &mcp.Tool{
Name: "add_learning",
Description: "Create a curated learning record distinct from raw thoughts.",
}, toolSet.Learnings.Add); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "get_learning",
Description: "Retrieve a structured learning by id.",
}, toolSet.Learnings.Get); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "list_learnings",
Description: "List structured learnings with optional project, status, priority, tag, and text filters.",
}, toolSet.Learnings.List); err != nil {
return err
}
return nil
}
func registerThoughtLearningLinkTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
if err := addTool(server, logger, &mcp.Tool{
Name: "link_thought_learning",
Description: "Create or update an explicit association between a raw thought and a curated learning.",
}, toolSet.ThoughtLearningLinks.Link); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "unlink_thought_learning",
Description: "Remove an explicit association between a thought and a learning.",
}, toolSet.ThoughtLearningLinks.Unlink); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "get_thought_learnings",
Description: "List curated learnings explicitly linked to a thought.",
}, toolSet.ThoughtLearningLinks.GetThoughtLearnings); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "get_learning_thoughts",
Description: "List raw thoughts explicitly linked to a learning.",
}, toolSet.ThoughtLearningLinks.GetLearningThoughts); err != nil {
return err
}
return nil
}
func registerPlanTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
if err := addTool(server, logger, &mcp.Tool{
Name: "create_plan",
Description: "Create a structured plan linked to a project.",
}, toolSet.Plans.Create); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "get_plan",
Description: "Retrieve a plan with its dependencies, related plans, skills, and guardrails.",
}, toolSet.Plans.Get); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "update_plan",
Description: "Update plan fields; only provided fields are changed.",
}, toolSet.Plans.Update); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "delete_plan",
Description: "Hard-delete a plan by id.",
}, toolSet.Plans.Delete); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "list_plans",
Description: "List plans with optional project, status, priority, owner, tag, and text filters.",
}, toolSet.Plans.List); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "add_plan_dependency",
Description: "Mark plan_id as depending on depends_on_plan_id (must complete first).",
}, toolSet.Plans.AddDependency); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "remove_plan_dependency",
Description: "Remove a dependency between two plans.",
}, toolSet.Plans.RemoveDependency); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "add_related_plan",
Description: "Link two plans as thematically related (bidirectional).",
}, toolSet.Plans.AddRelated); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "remove_related_plan",
Description: "Unlink two related plans.",
}, toolSet.Plans.RemoveRelated); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "add_plan_skill",
Description: "Link an agent skill to a plan.",
}, toolSet.Plans.AddSkill); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "remove_plan_skill",
Description: "Unlink an agent skill from a plan.",
}, toolSet.Plans.RemoveSkill); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "list_plan_skills",
Description: "List skills linked to a plan.",
}, toolSet.Plans.ListSkills); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "add_plan_guardrail",
Description: "Link an agent guardrail to a plan.",
}, toolSet.Plans.AddGuardrail); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "remove_plan_guardrail",
Description: "Unlink an agent guardrail from a plan.",
}, toolSet.Plans.RemoveGuardrail); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "list_plan_guardrails",
Description: "List guardrails linked to a plan.",
}, toolSet.Plans.ListGuardrails); err != nil {
return err
}
return nil
}
func registerFileTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
server.AddResourceTemplate(&mcp.ResourceTemplate{
Name: "stored_file",
@@ -492,30 +302,30 @@ func registerMaintenanceTools(server *mcp.Server, logger *slog.Logger, toolSet T
}, toolSet.RetryMetadata.Handle); err != nil {
return err
}
// if err := addTool(server, logger, &mcp.Tool{
// Name: "add_maintenance_task",
// Description: "Create a recurring or one-time home maintenance task.",
// }, toolSet.Maintenance.AddTask); err != nil {
// return err
// }
// if err := addTool(server, logger, &mcp.Tool{
// Name: "log_maintenance",
// Description: "Log completed maintenance; updates next due date.",
// }, toolSet.Maintenance.LogWork); err != nil {
// return err
// }
// if err := addTool(server, logger, &mcp.Tool{
// Name: "get_upcoming_maintenance",
// Description: "List maintenance tasks due within the next N days.",
// }, toolSet.Maintenance.GetUpcoming); err != nil {
// return err
// }
// if err := addTool(server, logger, &mcp.Tool{
// Name: "search_maintenance_history",
// Description: "Search the maintenance log by task name, category, or date range.",
// }, toolSet.Maintenance.SearchHistory); err != nil {
// return err
// }
if err := addTool(server, logger, &mcp.Tool{
Name: "add_maintenance_task",
Description: "Create a recurring or one-time home maintenance task.",
}, toolSet.Maintenance.AddTask); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "log_maintenance",
Description: "Log completed maintenance; updates next due date.",
}, toolSet.Maintenance.LogWork); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "get_upcoming_maintenance",
Description: "List maintenance tasks due within the next N days.",
}, toolSet.Maintenance.GetUpcoming); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "search_maintenance_history",
Description: "Search the maintenance log by task name, category, or date range.",
}, toolSet.Maintenance.SearchHistory); err != nil {
return err
}
return nil
}
@@ -534,16 +344,10 @@ func registerSkillTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet
}
if err := addTool(server, logger, &mcp.Tool{
Name: "list_skills",
Description: "List agent skills (metadata only by default). Set include_content=true to get full content, or use get_skill for a single skill.",
Description: "List all agent skills, optionally filtered by tag.",
}, toolSet.Skills.ListSkills); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "get_skill",
Description: "Fetch a single agent skill with full content by id or name.",
}, toolSet.Skills.GetSkill); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "add_guardrail",
Description: "Store an agent guardrail (constraint or safety rule).",
@@ -562,12 +366,6 @@ func registerSkillTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet
}, toolSet.Skills.ListGuardrails); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "get_guardrail",
Description: "Fetch a single agent guardrail with full content by id or name.",
}, toolSet.Skills.GetGuardrail); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{
Name: "add_project_skill",
Description: "Link a skill to a project. Pass project if client is stateless.",
@@ -635,114 +433,10 @@ func registerChatHistoryTools(server *mcp.Server, logger *slog.Logger, toolSet T
return nil
}
func registerPersonaTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
p := toolSet.Personas
if err := addTool(server, logger, &mcp.Tool{Name: "create_agent_persona", Description: "Create a named, loadable agent persona."}, p.CreatePersona); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "update_agent_persona", Description: "Update persona fields (name, description, summary, detail, tags)."}, p.UpdatePersona); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "delete_agent_persona", Description: "Delete a persona by name."}, p.DeletePersona); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "list_agent_personas", Description: "List all personas, optionally filtered by tag."}, p.ListPersonas); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "get_agent_persona", Description: "Load a persona with assembled parts. detail=true returns full content. overrides replaces parts per type at runtime."}, p.GetPersona); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "get_persona_manifest", Description: "Lightweight structure-only view: parts, traits, skills, guardrails — no content. Includes on_demand_tools hints."}, p.GetPersonaManifest); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "compile_persona", Description: "Regenerate compiled_summary and compiled_detail from current parts and arc stage. Use compiled_summary for agents with tight context budgets."}, p.CompilePersona); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "create_agent_part", Description: "Create a reusable behaviour building block. Parts compose personas and can be overridden at load time by name."}, p.CreatePart); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "update_agent_part", Description: "Update part fields (name, part_type, description, summary, content, tags)."}, p.UpdatePart); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "delete_agent_part", Description: "Delete a part by name."}, p.DeletePart); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "list_agent_parts", Description: "List parts, optionally filtered by part_type or tag."}, p.ListParts); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "get_agent_part", Description: "Fetch a single part with full content by name."}, p.GetPart); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "add_persona_part", Description: "Link a part to a persona with optional order and priority."}, p.AddPersonaPart); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "remove_persona_part", Description: "Unlink a part from a persona."}, p.RemovePersonaPart); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "add_persona_skill", Description: "Link an agent_skill to a persona."}, p.AddPersonaSkill); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "remove_persona_skill", Description: "Unlink an agent_skill from a persona."}, p.RemovePersonaSkill); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "add_persona_guardrail", Description: "Link an agent_guardrail to a persona."}, p.AddPersonaGuardrail); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "remove_persona_guardrail", Description: "Unlink an agent_guardrail from a persona."}, p.RemovePersonaGuardrail); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "create_agent_trait", Description: "Create an atomic personality trait (personality, cognitive, emotional, social, behavioral)."}, p.CreateTrait); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "update_agent_trait", Description: "Update trait fields."}, p.UpdateTrait); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "delete_agent_trait", Description: "Delete a trait by name."}, p.DeleteTrait); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "list_agent_traits", Description: "List traits, optionally filtered by trait_type or tag."}, p.ListTraits); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "get_agent_trait", Description: "Fetch a single trait with instruction by name."}, p.GetTrait); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "add_persona_trait", Description: "Link a trait to a persona."}, p.AddPersonaTrait); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "remove_persona_trait", Description: "Unlink a trait from a persona."}, p.RemovePersonaTrait); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "create_character_arc", Description: "Define a named character progression arc."}, p.CreateCharacterArc); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "list_character_arcs", Description: "List all character arcs."}, p.ListCharacterArcs); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "add_arc_stage", Description: "Add an ordered stage to a character arc."}, p.AddArcStage); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "add_stage_part", Description: "Link a part to an arc stage — active when the persona is at that stage."}, p.AddStagePart); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "remove_stage_part", Description: "Unlink a part from an arc stage."}, p.RemoveStagePart); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "assign_persona_arc", Description: "Attach an arc to a persona and set the starting stage."}, p.AssignPersonaArc); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "advance_persona_stage", Description: "Move persona to the next stage in its arc."}, p.AdvancePersonaStage); err != nil {
return err
}
if err := addTool(server, logger, &mcp.Tool{Name: "reset_persona_stage", Description: "Reset persona to the first stage of its arc."}, p.ResetPersonaStage); err != nil {
return err
}
return nil
}
func registerDescribeTools(server *mcp.Server, logger *slog.Logger, toolSet ToolSet) error {
if err := addTool(server, logger, &mcp.Tool{
Name: "describe_tools",
Description: "Call first each session. All tools with categories and usage notes. Categories: system, thoughts, projects, files, admin, maintenance, skills, personas, plans, chat, meta.",
Description: "Call first each session. All tools with categories and usage notes. Categories: system, thoughts, projects, files, admin, maintenance, skills, chat, meta.",
}, toolSet.Describe.Describe); err != nil {
return err
}
@@ -771,7 +465,6 @@ func BuildToolCatalog() []tools.ToolEntry {
{Name: "update_thought", Description: "Update thought content or merge metadata.", Category: "thoughts"},
{Name: "delete_thought", Description: "Hard-delete a thought by id.", Category: "thoughts"},
{Name: "archive_thought", Description: "Archive a thought so it is hidden from default search and listing.", Category: "thoughts"},
{Name: "audit_duplicates", Description: "Dry-run duplicate audit for exact/normalized project names, thought content, and metadata value variants. Reports candidates and recommendations; performs no writes.", Category: "admin"},
{Name: "summarize_thoughts", Description: "Produce an LLM prose summary of a filtered or searched set of thoughts.", Category: "thoughts"},
{Name: "recall_context", Description: "Recall semantically relevant and recent context for prompt injection. Combines vector similarity with recency. Falls back to full-text search when no embeddings exist.", Category: "thoughts"},
{Name: "link_thoughts", Description: "Create a typed relationship between two thoughts.", Category: "thoughts"},
@@ -782,39 +475,8 @@ func BuildToolCatalog() []tools.ToolEntry {
{Name: "list_projects", Description: "List projects and their current thought counts.", Category: "projects"},
{Name: "set_active_project", Description: "Set the active project for the current MCP session. Requires a stateful MCP client that reuses the same session across calls. If your client does not preserve sessions, pass project explicitly to each tool instead.", Category: "projects"},
{Name: "get_active_project", Description: "Return the active project for the current MCP session. If your client does not preserve MCP sessions, pass project explicitly to project-scoped tools instead of relying on this.", Category: "projects"},
{Name: "bootstrap_world_model", Description: "Validate and activate a project, then return effective skills, guardrails, personas, and bounded recent context.", Category: "projects"},
{Name: "add_project_persona", Description: "Link a persona to a project and optionally make it the default.", Category: "personas"},
{Name: "remove_project_persona", Description: "Unlink a persona from a project.", Category: "personas"},
{Name: "set_default_project_persona", Description: "Set a linked persona as the project's default persona.", Category: "personas"},
{Name: "list_project_personas", Description: "List personas linked to a project, with the default marked.", Category: "personas"},
{Name: "get_project_context", Description: "Get recent and semantic context for a project. Uses the explicit project when provided, otherwise the active MCP session project. Falls back to full-text search when no embeddings exist.", Category: "projects"},
// learnings
{Name: "add_learning", Description: "Create a curated learning record distinct from raw thoughts.", Category: "projects"},
{Name: "get_learning", Description: "Retrieve a structured learning by id.", Category: "projects"},
{Name: "list_learnings", Description: "List structured learnings with optional project, category, area, status, priority, tag, and text filters.", Category: "projects"},
{Name: "link_thought_learning", Description: "Create or update a lightweight explicit association between a raw thought and a curated learning.", Category: "projects"},
{Name: "unlink_thought_learning", Description: "Remove a lightweight explicit association between a thought and a learning.", Category: "projects"},
{Name: "get_thought_learnings", Description: "List curated learnings explicitly associated with a thought.", Category: "projects"},
{Name: "get_learning_thoughts", Description: "List raw source thoughts explicitly associated with a learning.", Category: "projects"},
// plans
{Name: "create_plan", Description: "Create a structured plan with status, priority, owner, due date, and optional project link.", Category: "plans"},
{Name: "get_plan", Description: "Retrieve a full plan including dependencies (depends_on/blocks), related plans, linked skills, and guardrails.", Category: "plans"},
{Name: "update_plan", Description: "Partially update a plan; only provided fields are changed. Use mark_reviewed to stamp last_reviewed_at.", Category: "plans"},
{Name: "delete_plan", Description: "Hard-delete a plan by id.", Category: "plans"},
{Name: "list_plans", Description: "List plans with optional filters: project, status, priority, owner, tag, and full-text query.", Category: "plans"},
{Name: "add_plan_dependency", Description: "Declare that plan_id cannot proceed until depends_on_plan_id is complete.", Category: "plans"},
{Name: "remove_plan_dependency", Description: "Remove a directional dependency between two plans.", Category: "plans"},
{Name: "add_related_plan", Description: "Link two plans as thematically related (bidirectional, order-independent).", Category: "plans"},
{Name: "remove_related_plan", Description: "Unlink two related plans.", Category: "plans"},
{Name: "add_plan_skill", Description: "Link an agent skill to a plan so it is loaded with the plan's context.", Category: "plans"},
{Name: "remove_plan_skill", Description: "Unlink an agent skill from a plan.", Category: "plans"},
{Name: "list_plan_skills", Description: "List all skills linked to a plan.", Category: "plans"},
{Name: "add_plan_guardrail", Description: "Link an agent guardrail to a plan so it applies during plan execution.", Category: "plans"},
{Name: "remove_plan_guardrail", Description: "Unlink an agent guardrail from a plan.", Category: "plans"},
{Name: "list_plan_guardrails", Description: "List all guardrails linked to a plan.", Category: "plans"},
// files
{Name: "upload_file", Description: "Stage a file and get an amcs://files/{id} resource URI. Use content_path (absolute server-side path, no size limit) for large or binary files, or content_base64 (≤10 MB) for small files. Pass thought_id/project to link immediately, or omit and pass the URI to save_file later.", Category: "files"},
{Name: "save_file", Description: "Store a file and optionally link it to a thought. Use content_base64 (≤10 MB) for small files, or content_uri (amcs://files/{id} from a prior upload_file) for previously staged files. For files larger than 10 MB, use upload_file with content_path first. If the goal is to retain the artifact, store the file directly instead of reading or summarising it first.", Category: "files"},
@@ -826,15 +488,19 @@ func BuildToolCatalog() []tools.ToolEntry {
{Name: "reparse_thought_metadata", Description: "Re-extract and normalize metadata for stored thoughts from their content.", Category: "admin"},
{Name: "retry_failed_metadata", Description: "Retry metadata extraction for thoughts still marked pending or failed.", Category: "admin"},
// maintenance
{Name: "add_maintenance_task", Description: "Create a recurring or one-time home maintenance task.", Category: "maintenance"},
{Name: "log_maintenance", Description: "Log completed maintenance work; automatically updates the task's next due date.", Category: "maintenance"},
{Name: "get_upcoming_maintenance", Description: "List maintenance tasks due within the next N days.", Category: "maintenance"},
{Name: "search_maintenance_history", Description: "Search the maintenance log by task name, category, or date range.", Category: "maintenance"},
// skills
{Name: "add_skill", Description: "Store a reusable agent skill. Supports language_tags, library_tags, framework_tags, and domain_tags for precise retrieval.", Category: "skills"},
{Name: "add_skill", Description: "Store a reusable agent skill (behavioural instruction or capability prompt).", Category: "skills"},
{Name: "remove_skill", Description: "Delete an agent skill by id.", Category: "skills"},
{Name: "list_skills", Description: "List agent skills (metadata only by default). Filter by tag (searches all tag fields). Set include_content=true for full bodies, or use get_skill to load one.", Category: "skills"},
{Name: "get_skill", Description: "Fetch a single agent skill with full content by id or name. Prefer this over list_skills when you know which skill you need.", Category: "skills"},
{Name: "list_skills", Description: "List all agent skills, optionally filtered by tag.", Category: "skills"},
{Name: "add_guardrail", Description: "Store a reusable agent guardrail (constraint or safety rule).", Category: "skills"},
{Name: "remove_guardrail", Description: "Delete an agent guardrail by id.", Category: "skills"},
{Name: "list_guardrails", Description: "List all agent guardrails, optionally filtered by tag or severity.", Category: "skills"},
{Name: "get_guardrail", Description: "Fetch a single agent guardrail with full content by id or name.", Category: "skills"},
{Name: "add_project_skill", Description: "Link an agent skill to a project. Pass project explicitly when your client does not preserve MCP sessions.", Category: "skills"},
{Name: "remove_project_skill", Description: "Unlink an agent skill from a project. Pass project explicitly when your client does not preserve MCP sessions.", Category: "skills"},
{Name: "list_project_skills", Description: "List all skills linked to a project. Call this at the start of every project session to load agent behaviour instructions before generating new ones. Only create new skills if none are returned. Pass project explicitly when your client does not preserve MCP sessions.", Category: "skills"},
@@ -842,41 +508,6 @@ func BuildToolCatalog() []tools.ToolEntry {
{Name: "remove_project_guardrail", Description: "Unlink an agent guardrail from a project. Pass project explicitly when your client does not preserve MCP sessions.", Category: "skills"},
{Name: "list_project_guardrails", Description: "List all guardrails linked to a project. Call this at the start of every project session to load agent constraints before generating new ones. Only create new guardrails if none are returned. Pass project explicitly when your client does not preserve MCP sessions.", Category: "skills"},
// personas
{Name: "create_agent_persona", Description: "Create a named, loadable agent persona that composes parts, traits, skills, and guardrails.", Category: "personas"},
{Name: "update_agent_persona", Description: "Update persona fields.", Category: "personas"},
{Name: "delete_agent_persona", Description: "Delete a persona by name.", Category: "personas"},
{Name: "list_agent_personas", Description: "List all personas, optionally filtered by tag.", Category: "personas"},
{Name: "get_agent_persona", Description: "Load a persona with all assembled parts. detail=true returns full content. overrides replaces parts per type at runtime without modifying the persona.", Category: "personas"},
{Name: "get_persona_manifest", Description: "Lightweight discovery: returns persona structure (parts, traits, skills, guardrails) with no content, plus on_demand_tools hints. Call this first for unknown personas.", Category: "personas"},
{Name: "compile_persona", Description: "Regenerate compiled_summary and compiled_detail from current parts and arc stage. Agents with tight context budgets can use compiled_summary directly.", Category: "personas"},
{Name: "create_agent_part", Description: "Create a reusable behaviour building block. Part types: system, agent, soul, identity, skill, specialization, tone, goal, context, protocol, backstory, motivation, voice, archetype, flaw, relationship.", Category: "personas"},
{Name: "update_agent_part", Description: "Update part fields.", Category: "personas"},
{Name: "delete_agent_part", Description: "Delete a part by name.", Category: "personas"},
{Name: "list_agent_parts", Description: "List parts, optionally filtered by part_type or tag.", Category: "personas"},
{Name: "get_agent_part", Description: "Fetch a single part with full content by name.", Category: "personas"},
{Name: "add_persona_part", Description: "Link a part to a persona with optional order and priority.", Category: "personas"},
{Name: "remove_persona_part", Description: "Unlink a part from a persona.", Category: "personas"},
{Name: "add_persona_skill", Description: "Link an agent_skill to a persona.", Category: "personas"},
{Name: "remove_persona_skill", Description: "Unlink an agent_skill from a persona.", Category: "personas"},
{Name: "add_persona_guardrail", Description: "Link an agent_guardrail to a persona.", Category: "personas"},
{Name: "remove_persona_guardrail", Description: "Unlink an agent_guardrail from a persona.", Category: "personas"},
{Name: "create_agent_trait", Description: "Create an atomic personality trait. Trait types: personality, cognitive, emotional, social, behavioral.", Category: "personas"},
{Name: "update_agent_trait", Description: "Update trait fields.", Category: "personas"},
{Name: "delete_agent_trait", Description: "Delete a trait by name.", Category: "personas"},
{Name: "list_agent_traits", Description: "List traits, optionally filtered by trait_type or tag.", Category: "personas"},
{Name: "get_agent_trait", Description: "Fetch a single trait with instruction by name. Use this for on-demand trait loading.", Category: "personas"},
{Name: "add_persona_trait", Description: "Link a trait to a persona.", Category: "personas"},
{Name: "remove_persona_trait", Description: "Unlink a trait from a persona.", Category: "personas"},
{Name: "create_character_arc", Description: "Define a named character progression arc with ordered stages.", Category: "personas"},
{Name: "list_character_arcs", Description: "List all character arcs.", Category: "personas"},
{Name: "add_arc_stage", Description: "Add an ordered stage to a character arc.", Category: "personas"},
{Name: "add_stage_part", Description: "Link a part to an arc stage — overrides matching persona parts when that stage is active.", Category: "personas"},
{Name: "remove_stage_part", Description: "Unlink a part from an arc stage.", Category: "personas"},
{Name: "assign_persona_arc", Description: "Attach a character arc to a persona and set the starting stage.", Category: "personas"},
{Name: "advance_persona_stage", Description: "Move persona to the next stage in its arc.", Category: "personas"},
{Name: "reset_persona_stage", Description: "Reset persona to the first stage of its arc.", Category: "personas"},
// chat
{Name: "save_chat_history", Description: "Save a chat session's message history for later retrieval. Stores messages with optional title, summary, channel, agent, and project metadata.", Category: "chat"},
{Name: "get_chat_history", Description: "Retrieve a saved chat history by its UUID or session_id. Returns the full message list.", Category: "chat"},
@@ -884,7 +515,7 @@ func BuildToolCatalog() []tools.ToolEntry {
{Name: "delete_chat_history", Description: "Permanently delete a saved chat history by id.", Category: "chat"},
// meta
{Name: "describe_tools", Description: "Call this first in every session. Returns all available MCP tools with names, descriptions, categories, and your accumulated usage notes. Filter by category to narrow results. Available categories: system, thoughts, projects, files, admin, household, maintenance, calendar, meals, crm, skills, personas, plans, chat, meta.", Category: "meta"},
{Name: "describe_tools", Description: "Call this first in every session. Returns all available MCP tools with names, descriptions, categories, and your accumulated usage notes. Filter by category to narrow results. Available categories: system, thoughts, projects, files, admin, household, maintenance, calendar, meals, crm, skills, chat, meta.", Category: "meta"},
{Name: "annotate_tool", Description: "Persist usage notes, gotchas, or workflow patterns for a specific tool. Notes survive across sessions and are returned by describe_tools. Call this whenever you discover something non-obvious about a tool's behaviour. Pass an empty string to clear notes.", Category: "meta"},
}
}
+48 -25
View File
@@ -5,7 +5,6 @@ import (
"net/http/httptest"
"reflect"
"sort"
"strings"
"testing"
"time"
@@ -28,10 +27,54 @@ func TestNewListsAllRegisteredTools(t *testing.T) {
}
sort.Strings(got)
catalog := BuildToolCatalog()
want := make([]string, 0, len(catalog))
for _, tool := range catalog {
want = append(want, tool.Name)
want := []string{
"add_guardrail",
"add_maintenance_task",
"add_project_guardrail",
"add_project_skill",
"add_skill",
"annotate_tool",
"archive_thought",
"backfill_embeddings",
"capture_thought",
"create_project",
"delete_chat_history",
"delete_thought",
"describe_tools",
"get_active_project",
"get_chat_history",
"get_project_context",
"get_thought",
"get_upcoming_maintenance",
"get_version_info",
"link_thoughts",
"list_chat_histories",
"list_files",
"list_guardrails",
"list_project_guardrails",
"list_project_skills",
"list_projects",
"list_skills",
"list_thoughts",
"load_file",
"log_maintenance",
"recall_context",
"related_thoughts",
"remove_guardrail",
"remove_project_guardrail",
"remove_project_skill",
"remove_skill",
"reparse_thought_metadata",
"retry_failed_metadata",
"save_chat_history",
"save_file",
"search_maintenance_history",
"search_thoughts",
"set_active_project",
"summarize_thoughts",
"thought_stats",
"update_thought",
"upload_file",
}
sort.Strings(want)
@@ -61,26 +104,6 @@ func TestNewListsStoredFileResourceTemplate(t *testing.T) {
}
}
func TestNewExposesWorldModelIntroResource(t *testing.T) {
cs := newStreamableTestClient(t)
listed, err := cs.ListResources(context.Background(), nil)
if err != nil {
t.Fatalf("ListResources() error = %v", err)
}
if len(listed.Resources) != 1 || listed.Resources[0].URI != "amcs://world-model/intro" {
t.Fatalf("ListResources() = %#v, want world model intro", listed.Resources)
}
read, err := cs.ReadResource(context.Background(), &mcp.ReadResourceParams{URI: "amcs://world-model/intro"})
if err != nil {
t.Fatalf("ReadResource() error = %v", err)
}
if len(read.Contents) != 1 || !strings.Contains(read.Contents[0].Text, "bootstrap_world_model") {
t.Fatalf("ReadResource() did not return bootstrap instructions: %#v", read.Contents)
}
}
func newStreamableTestClient(t *testing.T) *mcp.ClientSession {
t.Helper()
+20 -106
View File
@@ -105,115 +105,29 @@ func TestStreamableHTTPReturnsStructuredToolErrors(t *testing.T) {
t.Fatalf("build_date = %#v, want %q", got["build_date"], "2026-03-31T00:00:00Z")
}
})
t.Run("add_learning_requires_summary", func(t *testing.T) {
_, err := cs.CallTool(context.Background(), &mcp.CallToolParams{
Name: "add_learning",
Arguments: map[string]any{},
})
if err == nil {
t.Fatal("CallTool(add_learning) error = nil, want error")
}
rpcErr, data := requireWireError(t, err)
if rpcErr.Code != jsonrpc.CodeInvalidParams {
t.Fatalf("add_learning code = %d, want %d", rpcErr.Code, jsonrpc.CodeInvalidParams)
}
if data.Type != mcperrors.TypeInvalidArguments {
t.Fatalf("add_learning data.type = %q, want %q", data.Type, mcperrors.TypeInvalidArguments)
}
if data.Field != "summary" {
t.Fatalf("add_learning data.field = %q, want %q", data.Field, "summary")
}
})
t.Run("get_learning_requires_id", func(t *testing.T) {
_, err := cs.CallTool(context.Background(), &mcp.CallToolParams{
Name: "get_learning",
Arguments: map[string]any{},
})
if err == nil {
t.Fatal("CallTool(get_learning) error = nil, want error")
}
rpcErr, data := requireWireError(t, err)
if rpcErr.Code != jsonrpc.CodeInvalidParams {
t.Fatalf("get_learning code = %d, want %d", rpcErr.Code, jsonrpc.CodeInvalidParams)
}
if data.Type != mcperrors.TypeInvalidArguments {
t.Fatalf("get_learning data.type = %q, want %q", data.Type, mcperrors.TypeInvalidArguments)
}
if data.Field != "id" {
t.Fatalf("get_learning data.field = %q, want %q", data.Field, "id")
}
})
t.Run("add_learning_unconfigured_returns_structured_error", func(t *testing.T) {
_, err := cs.CallTool(context.Background(), &mcp.CallToolParams{
Name: "add_learning",
Arguments: map[string]any{
"summary": "Learning with configured check",
},
})
if err == nil {
t.Fatal("CallTool(add_learning) error = nil, want error")
}
rpcErr, data := requireWireError(t, err)
if rpcErr.Code != jsonrpc.CodeInvalidParams {
t.Fatalf("add_learning code = %d, want %d", rpcErr.Code, jsonrpc.CodeInvalidParams)
}
if data.Type != mcperrors.TypeInvalidInput {
t.Fatalf("add_learning data.type = %q, want %q", data.Type, mcperrors.TypeInvalidInput)
}
})
t.Run("list_learnings_unconfigured_returns_structured_error", func(t *testing.T) {
_, err := cs.CallTool(context.Background(), &mcp.CallToolParams{
Name: "list_learnings",
Arguments: map[string]any{},
})
if err == nil {
t.Fatal("CallTool(list_learnings) error = nil, want error")
}
rpcErr, data := requireWireError(t, err)
if rpcErr.Code != jsonrpc.CodeInvalidParams {
t.Fatalf("list_learnings code = %d, want %d", rpcErr.Code, jsonrpc.CodeInvalidParams)
}
if data.Type != mcperrors.TypeInvalidInput {
t.Fatalf("list_learnings data.type = %q, want %q", data.Type, mcperrors.TypeInvalidInput)
}
})
}
func streamableTestToolSet() ToolSet {
return ToolSet{
Version: tools.NewVersionTool("test", buildinfo.Info{Version: "0.0.1", TagName: "v0.0.1", Commit: "test", BuildDate: "2026-03-31T00:00:00Z"}),
Capture: new(tools.CaptureTool),
Search: new(tools.SearchTool),
List: new(tools.ListTool),
Stats: new(tools.StatsTool),
Get: new(tools.GetTool),
Update: new(tools.UpdateTool),
Delete: new(tools.DeleteTool),
Archive: new(tools.ArchiveTool),
Projects: new(tools.ProjectsTool),
Context: new(tools.ContextTool),
Recall: new(tools.RecallTool),
Summarize: new(tools.SummarizeTool),
Links: new(tools.LinksTool),
Files: new(tools.FilesTool),
Backfill: new(tools.BackfillTool),
Reparse: new(tools.ReparseMetadataTool),
RetryMetadata: new(tools.RetryEnrichmentTool),
Skills: new(tools.SkillsTool),
ChatHistory: new(tools.ChatHistoryTool),
Describe: new(tools.DescribeTool),
Learnings: new(tools.LearningsTool),
Plans: new(tools.PlansTool),
ProjectPersonas: new(tools.ProjectPersonasTool),
WorldModel: new(tools.WorldModelTool),
ThoughtLearningLinks: new(tools.ThoughtLearningLinksTool),
Version: tools.NewVersionTool("test", buildinfo.Info{Version: "0.0.1", TagName: "v0.0.1", Commit: "test", BuildDate: "2026-03-31T00:00:00Z"}),
Capture: new(tools.CaptureTool),
Search: new(tools.SearchTool),
List: new(tools.ListTool),
Stats: new(tools.StatsTool),
Get: new(tools.GetTool),
Update: new(tools.UpdateTool),
Delete: new(tools.DeleteTool),
Archive: new(tools.ArchiveTool),
Projects: new(tools.ProjectsTool),
Context: new(tools.ContextTool),
Recall: new(tools.RecallTool),
Summarize: new(tools.SummarizeTool),
Links: new(tools.LinksTool),
Files: new(tools.FilesTool),
Backfill: new(tools.BackfillTool),
Reparse: new(tools.ReparseMetadataTool),
RetryMetadata: new(tools.RetryEnrichmentTool),
Maintenance: new(tools.MaintenanceTool),
Skills: new(tools.SkillsTool),
}
}
-22
View File
@@ -53,7 +53,6 @@ func Normalize(in thoughttypes.ThoughtMetadata, capture config.CaptureConfig) th
Type: normalizeType(in.Type),
Source: normalizeSource(in.Source),
Attachments: normalizeAttachments(in.Attachments),
Webhook: normalizeWebhook(in.Webhook),
MetadataStatus: normalizeMetadataStatus(in.MetadataStatus),
MetadataUpdatedAt: strings.TrimSpace(in.MetadataUpdatedAt),
MetadataLastAttemptedAt: strings.TrimSpace(in.MetadataLastAttemptedAt),
@@ -202,31 +201,10 @@ func Merge(base, patch thoughttypes.ThoughtMetadata, capture config.CaptureConfi
if len(patch.Attachments) > 0 {
merged.Attachments = append(append([]thoughttypes.ThoughtAttachment{}, merged.Attachments...), patch.Attachments...)
}
if patch.Webhook != nil {
merged.Webhook = patch.Webhook
}
return Normalize(merged, capture)
}
func normalizeWebhook(value *thoughttypes.WebhookMetadata) *thoughttypes.WebhookMetadata {
if value == nil {
return nil
}
out := &thoughttypes.WebhookMetadata{
ReceivedAt: strings.TrimSpace(value.ReceivedAt),
IDempotencyKey: strings.TrimSpace(value.IDempotencyKey),
ExternalID: strings.TrimSpace(value.ExternalID),
}
if len(value.SourceMetadata) > 0 {
out.SourceMetadata = value.SourceMetadata
}
if out.ReceivedAt == "" && out.IDempotencyKey == "" && out.ExternalID == "" && len(out.SourceMetadata) == 0 {
return nil
}
return out
}
func normalizeAttachments(values []thoughttypes.ThoughtAttachment) []thoughttypes.ThoughtAttachment {
seen := make(map[string]struct{}, len(values))
result := make([]thoughttypes.ThoughtAttachment, 0, len(values))
-5
View File
@@ -106,11 +106,6 @@ func RequestIDFromContext(ctx context.Context) string {
return value
}
func MCPToolFromContext(ctx context.Context) string {
value, _ := ctx.Value(mcpToolContextKey).(string)
return strings.TrimSpace(value)
}
type statusRecorder struct {
http.ResponseWriter
status int
File diff suppressed because it is too large Load Diff
+186 -182
View File
@@ -1,206 +1,210 @@
package store
// import (
// "context"
// "fmt"
// "strings"
// "time"
import (
"context"
"fmt"
"strings"
"time"
// "github.com/google/uuid"
"github.com/google/uuid"
// "git.warky.dev/wdevs/amcs/internal/generatedmodels"
// ext "git.warky.dev/wdevs/amcs/internal/types"
// )
ext "git.warky.dev/wdevs/amcs/internal/types"
)
// func (db *DB) AddFamilyMember(ctx context.Context, m ext.FamilyMember) (ext.FamilyMember, error) {
// row := db.pool.QueryRow(ctx, `
// insert into family_members (name, relationship, birth_date, notes)
// values ($1, $2, $3, $4)
// returning id, created_at
// `, m.Name, nullStr(m.Relationship), m.BirthDate, nullStr(m.Notes))
func (db *DB) AddFamilyMember(ctx context.Context, m ext.FamilyMember) (ext.FamilyMember, error) {
row := db.pool.QueryRow(ctx, `
insert into family_members (name, relationship, birth_date, notes)
values ($1, $2, $3, $4)
returning id, created_at
`, m.Name, nullStr(m.Relationship), m.BirthDate, nullStr(m.Notes))
// created := m
// var model generatedmodels.ModelPublicFamilyMembers
// if err := row.Scan(&model.ID, &model.CreatedAt); err != nil {
// return ext.FamilyMember{}, fmt.Errorf("insert family member: %w", err)
// }
// created.ID = model.ID.UUID()
// created.CreatedAt = model.CreatedAt.Time()
// return created, nil
// }
created := m
if err := row.Scan(&created.ID, &created.CreatedAt); err != nil {
return ext.FamilyMember{}, fmt.Errorf("insert family member: %w", err)
}
return created, nil
}
// func (db *DB) ListFamilyMembers(ctx context.Context) ([]ext.FamilyMember, error) {
// rows, err := db.pool.Query(ctx, `select id, name, relationship, birth_date, notes, created_at from family_members order by name`)
// if err != nil {
// return nil, fmt.Errorf("list family members: %w", err)
// }
// defer rows.Close()
func (db *DB) ListFamilyMembers(ctx context.Context) ([]ext.FamilyMember, error) {
rows, err := db.pool.Query(ctx, `select id, name, relationship, birth_date, notes, created_at from family_members order by name`)
if err != nil {
return nil, fmt.Errorf("list family members: %w", err)
}
defer rows.Close()
// var members []ext.FamilyMember
// for rows.Next() {
// var model generatedmodels.ModelPublicFamilyMembers
// if err := rows.Scan(&model.ID, &model.Name, &model.Relationship, &model.BirthDate, &model.Notes, &model.CreatedAt); err != nil {
// return nil, fmt.Errorf("scan family member: %w", err)
// }
// members = append(members, familyMemberFromModel(model))
// }
// return members, rows.Err()
// }
var members []ext.FamilyMember
for rows.Next() {
var m ext.FamilyMember
var relationship, notes *string
if err := rows.Scan(&m.ID, &m.Name, &relationship, &m.BirthDate, &notes, &m.CreatedAt); err != nil {
return nil, fmt.Errorf("scan family member: %w", err)
}
m.Relationship = strVal(relationship)
m.Notes = strVal(notes)
members = append(members, m)
}
return members, rows.Err()
}
// func (db *DB) AddActivity(ctx context.Context, a ext.Activity) (ext.Activity, error) {
// row := db.pool.QueryRow(ctx, `
// insert into activities (family_member_id, title, activity_type, day_of_week, start_time, end_time, start_date, end_date, location, notes)
// values ($1, $2, $3, $4, $5::time, $6::time, $7, $8, $9, $10)
// returning id, created_at
// `, a.FamilyMemberID, a.Title, nullStr(a.ActivityType), nullStr(a.DayOfWeek),
// nullStr(a.StartTime), nullStr(a.EndTime), a.StartDate, a.EndDate,
// nullStr(a.Location), nullStr(a.Notes))
func (db *DB) AddActivity(ctx context.Context, a ext.Activity) (ext.Activity, error) {
row := db.pool.QueryRow(ctx, `
insert into activities (family_member_id, title, activity_type, day_of_week, start_time, end_time, start_date, end_date, location, notes)
values ($1, $2, $3, $4, $5::time, $6::time, $7, $8, $9, $10)
returning id, created_at
`, a.FamilyMemberID, a.Title, nullStr(a.ActivityType), nullStr(a.DayOfWeek),
nullStr(a.StartTime), nullStr(a.EndTime), a.StartDate, a.EndDate,
nullStr(a.Location), nullStr(a.Notes))
// created := a
// var model generatedmodels.ModelPublicActivities
// if err := row.Scan(&model.ID, &model.CreatedAt); err != nil {
// return ext.Activity{}, fmt.Errorf("insert activity: %w", err)
// }
// created.ID = model.ID.UUID()
// created.CreatedAt = model.CreatedAt.Time()
// return created, nil
// }
created := a
if err := row.Scan(&created.ID, &created.CreatedAt); err != nil {
return ext.Activity{}, fmt.Errorf("insert activity: %w", err)
}
return created, nil
}
// func (db *DB) GetWeekSchedule(ctx context.Context, weekStart time.Time) ([]ext.Activity, error) {
// weekEnd := weekStart.AddDate(0, 0, 7)
func (db *DB) GetWeekSchedule(ctx context.Context, weekStart time.Time) ([]ext.Activity, error) {
weekEnd := weekStart.AddDate(0, 0, 7)
// rows, err := db.pool.Query(ctx, `
// select a.id, a.family_member_id, fm.name, a.title, a.activity_type,
// a.day_of_week, a.start_time::text, a.end_time::text,
// a.start_date, a.end_date, a.location, a.notes, a.created_at
// from activities a
// left join family_members fm on fm.id = a.family_member_id
// where (a.start_date >= $1 and a.start_date < $2)
// or (a.day_of_week is not null and (a.end_date is null or a.end_date >= $1))
// order by a.start_date, a.start_time
// `, weekStart, weekEnd)
// if err != nil {
// return nil, fmt.Errorf("get week schedule: %w", err)
// }
// defer rows.Close()
rows, err := db.pool.Query(ctx, `
select a.id, a.family_member_id, fm.name, a.title, a.activity_type,
a.day_of_week, a.start_time::text, a.end_time::text,
a.start_date, a.end_date, a.location, a.notes, a.created_at
from activities a
left join family_members fm on fm.id = a.family_member_id
where (a.start_date >= $1 and a.start_date < $2)
or (a.day_of_week is not null and (a.end_date is null or a.end_date >= $1))
order by a.start_date, a.start_time
`, weekStart, weekEnd)
if err != nil {
return nil, fmt.Errorf("get week schedule: %w", err)
}
defer rows.Close()
// return scanActivities(rows)
// }
return scanActivities(rows)
}
// func (db *DB) SearchActivities(ctx context.Context, query, activityType string, memberID *uuid.UUID) ([]ext.Activity, error) {
// args := []any{}
// conditions := []string{}
func (db *DB) SearchActivities(ctx context.Context, query, activityType string, memberID *uuid.UUID) ([]ext.Activity, error) {
args := []any{}
conditions := []string{}
// if q := strings.TrimSpace(query); q != "" {
// args = append(args, "%"+q+"%")
// conditions = append(conditions, fmt.Sprintf("(a.title ILIKE $%d OR a.notes ILIKE $%d)", len(args), len(args)))
// }
// if t := strings.TrimSpace(activityType); t != "" {
// args = append(args, t)
// conditions = append(conditions, fmt.Sprintf("a.activity_type = $%d", len(args)))
// }
// if memberID != nil {
// args = append(args, *memberID)
// conditions = append(conditions, fmt.Sprintf("a.family_member_id = $%d", len(args)))
// }
if q := strings.TrimSpace(query); q != "" {
args = append(args, "%"+q+"%")
conditions = append(conditions, fmt.Sprintf("(a.title ILIKE $%d OR a.notes ILIKE $%d)", len(args), len(args)))
}
if t := strings.TrimSpace(activityType); t != "" {
args = append(args, t)
conditions = append(conditions, fmt.Sprintf("a.activity_type = $%d", len(args)))
}
if memberID != nil {
args = append(args, *memberID)
conditions = append(conditions, fmt.Sprintf("a.family_member_id = $%d", len(args)))
}
// q := `
// select a.id, a.family_member_id, fm.name, a.title, a.activity_type,
// a.day_of_week, a.start_time::text, a.end_time::text,
// a.start_date, a.end_date, a.location, a.notes, a.created_at
// from activities a
// left join family_members fm on fm.id = a.family_member_id
// `
// if len(conditions) > 0 {
// q += " where " + strings.Join(conditions, " and ")
// }
// q += " order by a.start_date, a.start_time"
q := `
select a.id, a.family_member_id, fm.name, a.title, a.activity_type,
a.day_of_week, a.start_time::text, a.end_time::text,
a.start_date, a.end_date, a.location, a.notes, a.created_at
from activities a
left join family_members fm on fm.id = a.family_member_id
`
if len(conditions) > 0 {
q += " where " + strings.Join(conditions, " and ")
}
q += " order by a.start_date, a.start_time"
// rows, err := db.pool.Query(ctx, q, args...)
// if err != nil {
// return nil, fmt.Errorf("search activities: %w", err)
// }
// defer rows.Close()
rows, err := db.pool.Query(ctx, q, args...)
if err != nil {
return nil, fmt.Errorf("search activities: %w", err)
}
defer rows.Close()
// return scanActivities(rows)
// }
return scanActivities(rows)
}
// func (db *DB) AddImportantDate(ctx context.Context, d ext.ImportantDate) (ext.ImportantDate, error) {
// row := db.pool.QueryRow(ctx, `
// insert into important_dates (family_member_id, title, date_value, recurring_yearly, reminder_days_before, notes)
// values ($1, $2, $3, $4, $5, $6)
// returning id, created_at
// `, d.FamilyMemberID, d.Title, d.DateValue, d.RecurringYearly, d.ReminderDaysBefore, nullStr(d.Notes))
func (db *DB) AddImportantDate(ctx context.Context, d ext.ImportantDate) (ext.ImportantDate, error) {
row := db.pool.QueryRow(ctx, `
insert into important_dates (family_member_id, title, date_value, recurring_yearly, reminder_days_before, notes)
values ($1, $2, $3, $4, $5, $6)
returning id, created_at
`, d.FamilyMemberID, d.Title, d.DateValue, d.RecurringYearly, d.ReminderDaysBefore, nullStr(d.Notes))
// created := d
// var model generatedmodels.ModelPublicImportantDates
// if err := row.Scan(&model.ID, &model.CreatedAt); err != nil {
// return ext.ImportantDate{}, fmt.Errorf("insert important date: %w", err)
// }
// created.ID = model.ID.UUID()
// created.CreatedAt = model.CreatedAt.Time()
// return created, nil
// }
created := d
if err := row.Scan(&created.ID, &created.CreatedAt); err != nil {
return ext.ImportantDate{}, fmt.Errorf("insert important date: %w", err)
}
return created, nil
}
// func (db *DB) GetUpcomingDates(ctx context.Context, daysAhead int) ([]ext.ImportantDate, error) {
// if daysAhead <= 0 {
// daysAhead = 30
// }
// now := time.Now()
// cutoff := now.AddDate(0, 0, daysAhead)
func (db *DB) GetUpcomingDates(ctx context.Context, daysAhead int) ([]ext.ImportantDate, error) {
if daysAhead <= 0 {
daysAhead = 30
}
now := time.Now()
cutoff := now.AddDate(0, 0, daysAhead)
// // For yearly recurring events, check if this year's occurrence falls in range
// rows, err := db.pool.Query(ctx, `
// select d.id, d.family_member_id, fm.name, d.title, d.date_value,
// d.recurring_yearly, d.reminder_days_before, d.notes, d.created_at
// from important_dates d
// left join family_members fm on fm.id = d.family_member_id
// where (
// (d.recurring_yearly = false and d.date_value between $1 and $2)
// or
// (d.recurring_yearly = true and
// make_date(extract(year from now())::int, extract(month from d.date_value)::int, extract(day from d.date_value)::int)
// between $1 and $2)
// )
// order by d.date_value
// `, now, cutoff)
// if err != nil {
// return nil, fmt.Errorf("get upcoming dates: %w", err)
// }
// defer rows.Close()
// For yearly recurring events, check if this year's occurrence falls in range
rows, err := db.pool.Query(ctx, `
select d.id, d.family_member_id, fm.name, d.title, d.date_value,
d.recurring_yearly, d.reminder_days_before, d.notes, d.created_at
from important_dates d
left join family_members fm on fm.id = d.family_member_id
where (
(d.recurring_yearly = false and d.date_value between $1 and $2)
or
(d.recurring_yearly = true and
make_date(extract(year from now())::int, extract(month from d.date_value)::int, extract(day from d.date_value)::int)
between $1 and $2)
)
order by d.date_value
`, now, cutoff)
if err != nil {
return nil, fmt.Errorf("get upcoming dates: %w", err)
}
defer rows.Close()
// var dates []ext.ImportantDate
// for rows.Next() {
// var model generatedmodels.ModelPublicImportantDates
// var memberName *string
// if err := rows.Scan(&model.ID, &model.FamilyMemberID, &memberName, &model.Title, &model.DateValue,
// &model.RecurringYearly, &model.ReminderDaysBefore, &model.Notes, &model.CreatedAt); err != nil {
// return nil, fmt.Errorf("scan important date: %w", err)
// }
// dates = append(dates, importantDateFromModel(model, strVal(memberName)))
// }
// return dates, rows.Err()
// }
var dates []ext.ImportantDate
for rows.Next() {
var d ext.ImportantDate
var memberID *uuid.UUID
var memberName, notes *string
if err := rows.Scan(&d.ID, &memberID, &memberName, &d.Title, &d.DateValue,
&d.RecurringYearly, &d.ReminderDaysBefore, &notes, &d.CreatedAt); err != nil {
return nil, fmt.Errorf("scan important date: %w", err)
}
d.FamilyMemberID = memberID
d.MemberName = strVal(memberName)
d.Notes = strVal(notes)
dates = append(dates, d)
}
return dates, rows.Err()
}
// func scanActivities(rows interface {
// Next() bool
// Scan(...any) error
// Err() error
// Close()
// }) ([]ext.Activity, error) {
// defer rows.Close()
// var activities []ext.Activity
// for rows.Next() {
// var model generatedmodels.ModelPublicActivities
// var memberName *string
// if err := rows.Scan(
// &model.ID, &model.FamilyMemberID, &memberName, &model.Title, &model.ActivityType,
// &model.DayOfWeek, &model.StartTime, &model.EndTime,
// &model.StartDate, &model.EndDate, &model.Location, &model.Notes, &model.CreatedAt,
// ); err != nil {
// return nil, fmt.Errorf("scan activity: %w", err)
// }
// activities = append(activities, activityFromModel(model, strVal(memberName)))
// }
// return activities, rows.Err()
// }
func scanActivities(rows interface {
Next() bool
Scan(...any) error
Err() error
Close()
}) ([]ext.Activity, error) {
defer rows.Close()
var activities []ext.Activity
for rows.Next() {
var a ext.Activity
var memberName, activityType, dayOfWeek, startTime, endTime, location, notes *string
if err := rows.Scan(
&a.ID, &a.FamilyMemberID, &memberName, &a.Title, &activityType,
&dayOfWeek, &startTime, &endTime,
&a.StartDate, &a.EndDate, &location, &notes, &a.CreatedAt,
); err != nil {
return nil, fmt.Errorf("scan activity: %w", err)
}
a.MemberName = strVal(memberName)
a.ActivityType = strVal(activityType)
a.DayOfWeek = strVal(dayOfWeek)
a.StartTime = strVal(startTime)
a.EndTime = strVal(endTime)
a.Location = strVal(location)
a.Notes = strVal(notes)
activities = append(activities, a)
}
return activities, rows.Err()
}
+18 -31
View File
@@ -7,9 +7,9 @@ import (
"strings"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"git.warky.dev/wdevs/amcs/internal/generatedmodels"
ext "git.warky.dev/wdevs/amcs/internal/types"
)
@@ -27,25 +27,20 @@ func (db *DB) SaveChatHistory(ctx context.Context, h ext.ChatHistory) (ext.ChatH
insert into chat_histories
(session_id, title, channel, agent_id, project_id, messages, summary, metadata)
values ($1, $2, $3, $4, $5, $6, $7, $8)
returning id, guid, created_at, updated_at
returning id, created_at, updated_at
`,
h.SessionID, nullStr(h.Title), nullStr(h.Channel), nullStr(h.AgentID),
h.ProjectID, messages, nullStr(h.Summary), meta,
)
created := h
var model generatedmodels.ModelPublicChatHistories
if err := row.Scan(&model.ID, &model.GUID, &model.CreatedAt, &model.UpdatedAt); err != nil {
if err := row.Scan(&created.ID, &created.CreatedAt, &created.UpdatedAt); err != nil {
return ext.ChatHistory{}, fmt.Errorf("insert chat history: %w", err)
}
created.ID = model.ID.Int64()
created.GUID = model.GUID.UUID()
created.CreatedAt = model.CreatedAt.Time()
created.UpdatedAt = model.UpdatedAt.Time()
return created, nil
}
func (db *DB) GetChatHistory(ctx context.Context, id int64) (ext.ChatHistory, bool, error) {
func (db *DB) GetChatHistory(ctx context.Context, id uuid.UUID) (ext.ChatHistory, bool, error) {
row := db.pool.QueryRow(ctx, `
select id, session_id, title, channel, agent_id, project_id,
messages, summary, metadata, created_at, updated_at
@@ -81,7 +76,7 @@ func (db *DB) GetChatHistoryBySessionID(ctx context.Context, sessionID string) (
}
type ListChatHistoriesFilter struct {
ProjectID *int64
ProjectID *uuid.UUID
Channel string
AgentID string
SessionID string
@@ -149,7 +144,7 @@ func (db *DB) ListChatHistories(ctx context.Context, f ListChatHistoriesFilter)
return result, rows.Err()
}
func (db *DB) DeleteChatHistory(ctx context.Context, id int64) (bool, error) {
func (db *DB) DeleteChatHistory(ctx context.Context, id uuid.UUID) (bool, error) {
tag, err := db.pool.Exec(ctx, `delete from chat_histories where id = $1`, id)
if err != nil {
return false, fmt.Errorf("delete chat history: %w", err)
@@ -162,34 +157,26 @@ type rowScanner interface {
}
func scanChatHistory(row rowScanner) (ext.ChatHistory, error) {
var model generatedmodels.ModelPublicChatHistories
var h ext.ChatHistory
var title, channel, agentID, summary *string
var messagesJSON, metaJSON []byte
if err := row.Scan(
&model.ID, &model.SessionID, &model.Title, &model.Channel, &model.AgentID, &model.ProjectID,
&model.Messages, &model.Summary, &model.Metadata, &model.CreatedAt, &model.UpdatedAt,
&h.ID, &h.SessionID, &title, &channel, &agentID, &h.ProjectID,
&messagesJSON, &summary, &metaJSON, &h.CreatedAt, &h.UpdatedAt,
); err != nil {
return ext.ChatHistory{}, err
}
h := ext.ChatHistory{
ID: model.ID.Int64(),
GUID: model.GUID.UUID(),
SessionID: model.SessionID.String(),
Title: model.Title.String(),
Channel: model.Channel.String(),
AgentID: model.AgentID.String(),
Summary: model.Summary.String(),
CreatedAt: model.CreatedAt.Time(),
UpdatedAt: model.UpdatedAt.Time(),
}
if model.ProjectID.Valid {
id := model.ProjectID.Int64()
h.ProjectID = &id
}
h.Title = strVal(title)
h.Channel = strVal(channel)
h.AgentID = strVal(agentID)
h.Summary = strVal(summary)
if err := json.Unmarshal(model.Messages, &h.Messages); err != nil {
if err := json.Unmarshal(messagesJSON, &h.Messages); err != nil {
return ext.ChatHistory{}, fmt.Errorf("unmarshal messages: %w", err)
}
if err := json.Unmarshal(model.Metadata, &h.Metadata); err != nil {
if err := json.Unmarshal(metaJSON, &h.Metadata); err != nil {
return ext.ChatHistory{}, fmt.Errorf("unmarshal metadata: %w", err)
}
if h.Messages == nil {
+217 -205
View File
@@ -1,235 +1,247 @@
package store
// import (
// "context"
// "fmt"
// "strings"
// "time"
import (
"context"
"fmt"
"strings"
"time"
// "github.com/google/uuid"
"github.com/google/uuid"
// "git.warky.dev/wdevs/amcs/internal/generatedmodels"
// ext "git.warky.dev/wdevs/amcs/internal/types"
// )
ext "git.warky.dev/wdevs/amcs/internal/types"
)
// func (db *DB) AddProfessionalContact(ctx context.Context, c ext.ProfessionalContact) (ext.ProfessionalContact, error) {
// if c.Tags == nil {
// c.Tags = []string{}
// }
func (db *DB) AddProfessionalContact(ctx context.Context, c ext.ProfessionalContact) (ext.ProfessionalContact, error) {
if c.Tags == nil {
c.Tags = []string{}
}
// row := db.pool.QueryRow(ctx, `
// insert into professional_contacts (name, company, title, email, phone, linkedin_url, how_we_met, tags, notes, follow_up_date)
// values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
// returning id, created_at, updated_at
// `, c.Name, nullStr(c.Company), nullStr(c.Title), nullStr(c.Email), nullStr(c.Phone),
// nullStr(c.LinkedInURL), nullStr(c.HowWeMet), c.Tags, nullStr(c.Notes), c.FollowUpDate)
row := db.pool.QueryRow(ctx, `
insert into professional_contacts (name, company, title, email, phone, linkedin_url, how_we_met, tags, notes, follow_up_date)
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
returning id, created_at, updated_at
`, c.Name, nullStr(c.Company), nullStr(c.Title), nullStr(c.Email), nullStr(c.Phone),
nullStr(c.LinkedInURL), nullStr(c.HowWeMet), c.Tags, nullStr(c.Notes), c.FollowUpDate)
// created := c
// var model generatedmodels.ModelPublicProfessionalContacts
// if err := row.Scan(&model.ID, &model.CreatedAt, &model.UpdatedAt); err != nil {
// return ext.ProfessionalContact{}, fmt.Errorf("insert contact: %w", err)
// }
// created.ID = model.ID.UUID()
// created.CreatedAt = model.CreatedAt.Time()
// created.UpdatedAt = model.UpdatedAt.Time()
// return created, nil
// }
created := c
if err := row.Scan(&created.ID, &created.CreatedAt, &created.UpdatedAt); err != nil {
return ext.ProfessionalContact{}, fmt.Errorf("insert contact: %w", err)
}
return created, nil
}
// func (db *DB) SearchContacts(ctx context.Context, query string, tags []string) ([]ext.ProfessionalContact, error) {
// args := []any{}
// conditions := []string{}
func (db *DB) SearchContacts(ctx context.Context, query string, tags []string) ([]ext.ProfessionalContact, error) {
args := []any{}
conditions := []string{}
// if q := strings.TrimSpace(query); q != "" {
// args = append(args, "%"+q+"%")
// idx := len(args)
// conditions = append(conditions, fmt.Sprintf(
// "(name ILIKE $%[1]d OR company ILIKE $%[1]d OR title ILIKE $%[1]d OR notes ILIKE $%[1]d)", idx))
// }
// if len(tags) > 0 {
// args = append(args, tags)
// conditions = append(conditions, fmt.Sprintf("tags @> $%d", len(args)))
// }
if q := strings.TrimSpace(query); q != "" {
args = append(args, "%"+q+"%")
idx := len(args)
conditions = append(conditions, fmt.Sprintf(
"(name ILIKE $%[1]d OR company ILIKE $%[1]d OR title ILIKE $%[1]d OR notes ILIKE $%[1]d)", idx))
}
if len(tags) > 0 {
args = append(args, tags)
conditions = append(conditions, fmt.Sprintf("tags @> $%d", len(args)))
}
// q := `select id, name, company, title, email, phone, linkedin_url, how_we_met, tags::text[], notes, last_contacted, follow_up_date, created_at, updated_at from professional_contacts`
// if len(conditions) > 0 {
// q += " where " + strings.Join(conditions, " and ")
// }
// q += " order by name"
q := `select id, name, company, title, email, phone, linkedin_url, how_we_met, tags, notes, last_contacted, follow_up_date, created_at, updated_at from professional_contacts`
if len(conditions) > 0 {
q += " where " + strings.Join(conditions, " and ")
}
q += " order by name"
// rows, err := db.pool.Query(ctx, q, args...)
// if err != nil {
// return nil, fmt.Errorf("search contacts: %w", err)
// }
// defer rows.Close()
rows, err := db.pool.Query(ctx, q, args...)
if err != nil {
return nil, fmt.Errorf("search contacts: %w", err)
}
defer rows.Close()
// return scanContacts(rows)
// }
return scanContacts(rows)
}
// func (db *DB) GetContact(ctx context.Context, id uuid.UUID) (ext.ProfessionalContact, error) {
// row := db.pool.QueryRow(ctx, `
// select id, name, company, title, email, phone, linkedin_url, how_we_met, tags::text[], notes, last_contacted, follow_up_date, created_at, updated_at
// from professional_contacts where id = $1
// `, id)
func (db *DB) GetContact(ctx context.Context, id uuid.UUID) (ext.ProfessionalContact, error) {
row := db.pool.QueryRow(ctx, `
select id, name, company, title, email, phone, linkedin_url, how_we_met, tags, notes, last_contacted, follow_up_date, created_at, updated_at
from professional_contacts where id = $1
`, id)
// var model generatedmodels.ModelPublicProfessionalContacts
// var tags []string
// if err := row.Scan(&model.ID, &model.Name, &model.Company, &model.Title, &model.Email, &model.Phone,
// &model.LinkedinURL, &model.HowWeMet, &tags, &model.Notes, &model.LastContacted, &model.FollowUpDate,
// &model.CreatedAt, &model.UpdatedAt); err != nil {
// return ext.ProfessionalContact{}, fmt.Errorf("get contact: %w", err)
// }
// c := professionalContactFromModel(model, tags)
// return c, nil
// }
var c ext.ProfessionalContact
var company, title, email, phone, linkedInURL, howWeMet, notes *string
if err := row.Scan(&c.ID, &c.Name, &company, &title, &email, &phone,
&linkedInURL, &howWeMet, &c.Tags, &notes, &c.LastContacted, &c.FollowUpDate,
&c.CreatedAt, &c.UpdatedAt); err != nil {
return ext.ProfessionalContact{}, fmt.Errorf("get contact: %w", err)
}
c.Company = strVal(company)
c.Title = strVal(title)
c.Email = strVal(email)
c.Phone = strVal(phone)
c.LinkedInURL = strVal(linkedInURL)
c.HowWeMet = strVal(howWeMet)
c.Notes = strVal(notes)
if c.Tags == nil {
c.Tags = []string{}
}
return c, nil
}
// func (db *DB) LogInteraction(ctx context.Context, interaction ext.ContactInteraction) (ext.ContactInteraction, error) {
// occurredAt := interaction.OccurredAt
// if occurredAt.IsZero() {
// occurredAt = time.Now()
// }
func (db *DB) LogInteraction(ctx context.Context, interaction ext.ContactInteraction) (ext.ContactInteraction, error) {
occurredAt := interaction.OccurredAt
if occurredAt.IsZero() {
occurredAt = time.Now()
}
// row := db.pool.QueryRow(ctx, `
// insert into contact_interactions (contact_id, interaction_type, occurred_at, summary, follow_up_needed, follow_up_notes)
// values ($1, $2, $3, $4, $5, $6)
// returning id, created_at
// `, interaction.ContactID, interaction.InteractionType, occurredAt, interaction.Summary,
// interaction.FollowUpNeeded, nullStr(interaction.FollowUpNotes))
row := db.pool.QueryRow(ctx, `
insert into contact_interactions (contact_id, interaction_type, occurred_at, summary, follow_up_needed, follow_up_notes)
values ($1, $2, $3, $4, $5, $6)
returning id, created_at
`, interaction.ContactID, interaction.InteractionType, occurredAt, interaction.Summary,
interaction.FollowUpNeeded, nullStr(interaction.FollowUpNotes))
// created := interaction
// created.OccurredAt = occurredAt
// var model generatedmodels.ModelPublicContactInteractions
// if err := row.Scan(&model.ID, &model.CreatedAt); err != nil {
// return ext.ContactInteraction{}, fmt.Errorf("insert interaction: %w", err)
// }
// created.ID = model.ID.UUID()
// created.CreatedAt = model.CreatedAt.Time()
// return created, nil
// }
created := interaction
created.OccurredAt = occurredAt
if err := row.Scan(&created.ID, &created.CreatedAt); err != nil {
return ext.ContactInteraction{}, fmt.Errorf("insert interaction: %w", err)
}
return created, nil
}
// func (db *DB) GetContactHistory(ctx context.Context, contactID uuid.UUID) (ext.ContactHistory, error) {
// contact, err := db.GetContact(ctx, contactID)
// if err != nil {
// return ext.ContactHistory{}, err
// }
func (db *DB) GetContactHistory(ctx context.Context, contactID uuid.UUID) (ext.ContactHistory, error) {
contact, err := db.GetContact(ctx, contactID)
if err != nil {
return ext.ContactHistory{}, err
}
// rows, err := db.pool.Query(ctx, `
// select id, contact_id, interaction_type, occurred_at, summary, follow_up_needed, follow_up_notes, created_at
// from contact_interactions where contact_id = $1 order by occurred_at desc
// `, contactID)
// if err != nil {
// return ext.ContactHistory{}, fmt.Errorf("get interactions: %w", err)
// }
// defer rows.Close()
rows, err := db.pool.Query(ctx, `
select id, contact_id, interaction_type, occurred_at, summary, follow_up_needed, follow_up_notes, created_at
from contact_interactions where contact_id = $1 order by occurred_at desc
`, contactID)
if err != nil {
return ext.ContactHistory{}, fmt.Errorf("get interactions: %w", err)
}
defer rows.Close()
// var interactions []ext.ContactInteraction
// for rows.Next() {
// var model generatedmodels.ModelPublicContactInteractions
// if err := rows.Scan(&model.ID, &model.ContactID, &model.InteractionType, &model.OccurredAt, &model.Summary,
// &model.FollowUpNeeded, &model.FollowUpNotes, &model.CreatedAt); err != nil {
// return ext.ContactHistory{}, fmt.Errorf("scan interaction: %w", err)
// }
// interactions = append(interactions, contactInteractionFromModel(model))
// }
// if err := rows.Err(); err != nil {
// return ext.ContactHistory{}, err
// }
var interactions []ext.ContactInteraction
for rows.Next() {
var i ext.ContactInteraction
var followUpNotes *string
if err := rows.Scan(&i.ID, &i.ContactID, &i.InteractionType, &i.OccurredAt, &i.Summary,
&i.FollowUpNeeded, &followUpNotes, &i.CreatedAt); err != nil {
return ext.ContactHistory{}, fmt.Errorf("scan interaction: %w", err)
}
i.FollowUpNotes = strVal(followUpNotes)
interactions = append(interactions, i)
}
if err := rows.Err(); err != nil {
return ext.ContactHistory{}, err
}
// oppRows, err := db.pool.Query(ctx, `
// select id, contact_id, title, description, stage, value, expected_close_date, notes, created_at, updated_at
// from opportunities where contact_id = $1 order by created_at desc
// `, contactID)
// if err != nil {
// return ext.ContactHistory{}, fmt.Errorf("get opportunities: %w", err)
// }
// defer oppRows.Close()
oppRows, err := db.pool.Query(ctx, `
select id, contact_id, title, description, stage, value, expected_close_date, notes, created_at, updated_at
from opportunities where contact_id = $1 order by created_at desc
`, contactID)
if err != nil {
return ext.ContactHistory{}, fmt.Errorf("get opportunities: %w", err)
}
defer oppRows.Close()
// var opportunities []ext.Opportunity
// for oppRows.Next() {
// var model generatedmodels.ModelPublicOpportunities
// if err := oppRows.Scan(&model.ID, &model.ContactID, &model.Title, &model.Description, &model.Stage, &model.Value,
// &model.ExpectedCloseDate, &model.Notes, &model.CreatedAt, &model.UpdatedAt); err != nil {
// return ext.ContactHistory{}, fmt.Errorf("scan opportunity: %w", err)
// }
// opportunities = append(opportunities, opportunityFromModel(model))
// }
// if err := oppRows.Err(); err != nil {
// return ext.ContactHistory{}, err
// }
var opportunities []ext.Opportunity
for oppRows.Next() {
var o ext.Opportunity
var description, notes *string
if err := oppRows.Scan(&o.ID, &o.ContactID, &o.Title, &description, &o.Stage, &o.Value,
&o.ExpectedCloseDate, &notes, &o.CreatedAt, &o.UpdatedAt); err != nil {
return ext.ContactHistory{}, fmt.Errorf("scan opportunity: %w", err)
}
o.Description = strVal(description)
o.Notes = strVal(notes)
opportunities = append(opportunities, o)
}
if err := oppRows.Err(); err != nil {
return ext.ContactHistory{}, err
}
// return ext.ContactHistory{
// Contact: contact,
// Interactions: interactions,
// Opportunities: opportunities,
// }, nil
// }
return ext.ContactHistory{
Contact: contact,
Interactions: interactions,
Opportunities: opportunities,
}, nil
}
// func (db *DB) CreateOpportunity(ctx context.Context, o ext.Opportunity) (ext.Opportunity, error) {
// row := db.pool.QueryRow(ctx, `
// insert into opportunities (contact_id, title, description, stage, value, expected_close_date, notes)
// values ($1, $2, $3, $4, $5, $6, $7)
// returning id, created_at, updated_at
// `, o.ContactID, o.Title, nullStr(o.Description), o.Stage, o.Value, o.ExpectedCloseDate, nullStr(o.Notes))
func (db *DB) CreateOpportunity(ctx context.Context, o ext.Opportunity) (ext.Opportunity, error) {
row := db.pool.QueryRow(ctx, `
insert into opportunities (contact_id, title, description, stage, value, expected_close_date, notes)
values ($1, $2, $3, $4, $5, $6, $7)
returning id, created_at, updated_at
`, o.ContactID, o.Title, nullStr(o.Description), o.Stage, o.Value, o.ExpectedCloseDate, nullStr(o.Notes))
// created := o
// var model generatedmodels.ModelPublicOpportunities
// if err := row.Scan(&model.ID, &model.CreatedAt, &model.UpdatedAt); err != nil {
// return ext.Opportunity{}, fmt.Errorf("insert opportunity: %w", err)
// }
// created.ID = model.ID.UUID()
// created.CreatedAt = model.CreatedAt.Time()
// created.UpdatedAt = model.UpdatedAt.Time()
// return created, nil
// }
created := o
if err := row.Scan(&created.ID, &created.CreatedAt, &created.UpdatedAt); err != nil {
return ext.Opportunity{}, fmt.Errorf("insert opportunity: %w", err)
}
return created, nil
}
// func (db *DB) GetFollowUpsDue(ctx context.Context, daysAhead int) ([]ext.ProfessionalContact, error) {
// if daysAhead <= 0 {
// daysAhead = 7
// }
// cutoff := time.Now().AddDate(0, 0, daysAhead)
func (db *DB) GetFollowUpsDue(ctx context.Context, daysAhead int) ([]ext.ProfessionalContact, error) {
if daysAhead <= 0 {
daysAhead = 7
}
cutoff := time.Now().AddDate(0, 0, daysAhead)
// rows, err := db.pool.Query(ctx, `
// select id, name, company, title, email, phone, linkedin_url, how_we_met, tags::text[], notes, last_contacted, follow_up_date, created_at, updated_at
// from professional_contacts
// where follow_up_date <= $1
// order by follow_up_date asc
// `, cutoff)
// if err != nil {
// return nil, fmt.Errorf("get follow-ups: %w", err)
// }
// defer rows.Close()
rows, err := db.pool.Query(ctx, `
select id, name, company, title, email, phone, linkedin_url, how_we_met, tags, notes, last_contacted, follow_up_date, created_at, updated_at
from professional_contacts
where follow_up_date <= $1
order by follow_up_date asc
`, cutoff)
if err != nil {
return nil, fmt.Errorf("get follow-ups: %w", err)
}
defer rows.Close()
// return scanContacts(rows)
// }
return scanContacts(rows)
}
// func (db *DB) AppendThoughtToContactNotes(ctx context.Context, contactID uuid.UUID, thoughtContent string) error {
// _, err := db.pool.Exec(ctx, `
// update professional_contacts
// set notes = coalesce(notes, '') || $2
// where id = $1
// `, contactID, thoughtContent)
// if err != nil {
// return fmt.Errorf("append thought to contact: %w", err)
// }
// return nil
// }
func (db *DB) AppendThoughtToContactNotes(ctx context.Context, contactID uuid.UUID, thoughtContent string) error {
_, err := db.pool.Exec(ctx, `
update professional_contacts
set notes = coalesce(notes, '') || $2
where id = $1
`, contactID, thoughtContent)
if err != nil {
return fmt.Errorf("append thought to contact: %w", err)
}
return nil
}
// func scanContacts(rows interface {
// Next() bool
// Scan(...any) error
// Err() error
// Close()
// }) ([]ext.ProfessionalContact, error) {
// defer rows.Close()
// var contacts []ext.ProfessionalContact
// for rows.Next() {
// var model generatedmodels.ModelPublicProfessionalContacts
// var tags []string
// if err := rows.Scan(&model.ID, &model.Name, &model.Company, &model.Title, &model.Email, &model.Phone,
// &model.LinkedinURL, &model.HowWeMet, &tags, &model.Notes, &model.LastContacted, &model.FollowUpDate,
// &model.CreatedAt, &model.UpdatedAt); err != nil {
// return nil, fmt.Errorf("scan contact: %w", err)
// }
// contacts = append(contacts, professionalContactFromModel(model, tags))
// }
// return contacts, rows.Err()
// }
func scanContacts(rows interface {
Next() bool
Scan(...any) error
Err() error
Close()
}) ([]ext.ProfessionalContact, error) {
defer rows.Close()
var contacts []ext.ProfessionalContact
for rows.Next() {
var c ext.ProfessionalContact
var company, title, email, phone, linkedInURL, howWeMet, notes *string
if err := rows.Scan(&c.ID, &c.Name, &company, &title, &email, &phone,
&linkedInURL, &howWeMet, &c.Tags, &notes, &c.LastContacted, &c.FollowUpDate,
&c.CreatedAt, &c.UpdatedAt); err != nil {
return nil, fmt.Errorf("scan contact: %w", err)
}
c.Company = strVal(company)
c.Title = strVal(title)
c.Email = strVal(email)
c.Phone = strVal(phone)
c.LinkedInURL = strVal(linkedInURL)
c.HowWeMet = strVal(howWeMet)
c.Notes = strVal(notes)
if c.Tags == nil {
c.Tags = []string{}
}
contacts = append(contacts, c)
}
return contacts, rows.Err()
}
+3 -39
View File
@@ -2,23 +2,18 @@ package store
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
pgxvec "github.com/pgvector/pgvector-go/pgx"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/uptrace/bun/driver/pgdriver"
"git.warky.dev/wdevs/amcs/internal/config"
)
type DB struct {
pool *pgxpool.Pool
bun *bun.DB
}
func New(ctx context.Context, cfg config.DatabaseConfig) (*DB, error) {
@@ -40,20 +35,8 @@ func New(ctx context.Context, cfg config.DatabaseConfig) (*DB, error) {
return nil, fmt.Errorf("create database pool: %w", err)
}
bunSQLDB := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(cfg.URL)))
bunSQLDB.SetMaxOpenConns(int(cfg.MaxConns))
bunSQLDB.SetMaxIdleConns(int(cfg.MinConns))
bunSQLDB.SetConnMaxLifetime(cfg.MaxConnLifetime)
bunSQLDB.SetConnMaxIdleTime(cfg.MaxConnIdleTime)
db := &DB{
pool: pool,
bun: bun.NewDB(bunSQLDB, pgdialect.New()),
}
db := &DB{pool: pool}
if err := db.Ping(ctx); err != nil {
if db.bun != nil {
_ = db.bun.Close()
}
pool.Close()
return nil, err
}
@@ -62,16 +45,11 @@ func New(ctx context.Context, cfg config.DatabaseConfig) (*DB, error) {
}
func (db *DB) Close() {
if db == nil {
if db == nil || db.pool == nil {
return
}
if db.bun != nil {
_ = db.bun.Close()
}
if db.pool != nil {
db.pool.Close()
}
db.pool.Close()
}
func (db *DB) Ping(ctx context.Context) error {
@@ -124,17 +102,3 @@ func (db *DB) VerifyRequirements(ctx context.Context) error {
return nil
}
func (db *DB) Bun() *bun.DB {
if db == nil {
return nil
}
return db.bun
}
func (db *DB) Pool() *pgxpool.Pool {
if db == nil {
return nil
}
return db.pool
}
+51 -56
View File
@@ -9,60 +9,57 @@ import (
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"git.warky.dev/wdevs/amcs/internal/generatedmodels"
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
)
func (db *DB) InsertStoredFile(ctx context.Context, file thoughttypes.StoredFile) (thoughttypes.StoredFile, error) {
row := db.pool.QueryRow(ctx, `
insert into stored_files (thought_id, project_id, tenant_id, name, media_type, kind, encoding, size_bytes, sha256, content)
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
returning id, guid, thought_id, project_id, name, media_type, kind, encoding, size_bytes, sha256, created_at, updated_at
`, file.ThoughtID, file.ProjectID, tenantKeyPtr(ctx), file.Name, file.MediaType, file.Kind, file.Encoding, file.SizeBytes, file.SHA256, file.Content)
insert into stored_files (thought_id, project_id, name, media_type, kind, encoding, size_bytes, sha256, content)
values ($1, $2, $3, $4, $5, $6, $7, $8, $9)
returning guid, thought_id, project_id, name, media_type, kind, encoding, size_bytes, sha256, created_at, updated_at
`, file.ThoughtID, file.ProjectID, file.Name, file.MediaType, file.Kind, file.Encoding, file.SizeBytes, file.SHA256, file.Content)
var model generatedmodels.ModelPublicStoredFiles
var created thoughttypes.StoredFile
if err := row.Scan(
&model.ID,
&model.GUID,
&model.ThoughtID,
&model.ProjectID,
&model.Name,
&model.MediaType,
&model.Kind,
&model.Encoding,
&model.SizeBytes,
&model.Sha256,
&model.CreatedAt,
&model.UpdatedAt,
&created.ID,
&created.ThoughtID,
&created.ProjectID,
&created.Name,
&created.MediaType,
&created.Kind,
&created.Encoding,
&created.SizeBytes,
&created.SHA256,
&created.CreatedAt,
&created.UpdatedAt,
); err != nil {
return thoughttypes.StoredFile{}, fmt.Errorf("insert stored file: %w", err)
}
return storedFileFromModel(model), nil
return created, nil
}
func (db *DB) GetStoredFile(ctx context.Context, id uuid.UUID) (thoughttypes.StoredFile, error) {
args := []any{id}
row := db.pool.QueryRow(ctx, `
select id, guid, thought_id, project_id, name, media_type, kind, encoding, size_bytes, sha256, content, created_at, updated_at
select guid, thought_id, project_id, name, media_type, kind, encoding, size_bytes, sha256, content, created_at, updated_at
from stored_files
where guid = $1`+tenantSQL(ctx, &args, "tenant_id"), args...)
where guid = $1
`, id)
var model generatedmodels.ModelPublicStoredFiles
var file thoughttypes.StoredFile
if err := row.Scan(
&model.ID,
&model.GUID,
&model.ThoughtID,
&model.ProjectID,
&model.Name,
&model.MediaType,
&model.Kind,
&model.Encoding,
&model.SizeBytes,
&model.Sha256,
&model.Content,
&model.CreatedAt,
&model.UpdatedAt,
&file.ID,
&file.ThoughtID,
&file.ProjectID,
&file.Name,
&file.MediaType,
&file.Kind,
&file.Encoding,
&file.SizeBytes,
&file.SHA256,
&file.Content,
&file.CreatedAt,
&file.UpdatedAt,
); err != nil {
if err == pgx.ErrNoRows {
return thoughttypes.StoredFile{}, err
@@ -70,14 +67,13 @@ func (db *DB) GetStoredFile(ctx context.Context, id uuid.UUID) (thoughttypes.Sto
return thoughttypes.StoredFile{}, fmt.Errorf("get stored file: %w", err)
}
return storedFileFromModel(model), nil
return file, nil
}
func (db *DB) ListStoredFiles(ctx context.Context, filter thoughttypes.StoredFileFilter) ([]thoughttypes.StoredFile, error) {
args := make([]any, 0, 4)
conditions := make([]string, 0, 3)
addTenantCondition(ctx, &args, &conditions, "tenant_id")
if filter.ThoughtID != nil {
args = append(args, *filter.ThoughtID)
conditions = append(conditions, fmt.Sprintf("thought_id = $%d", len(args)))
@@ -92,7 +88,7 @@ func (db *DB) ListStoredFiles(ctx context.Context, filter thoughttypes.StoredFil
}
query := `
select id, guid, thought_id, project_id, name, media_type, kind, encoding, size_bytes, sha256, created_at, updated_at
select guid, thought_id, project_id, name, media_type, kind, encoding, size_bytes, sha256, created_at, updated_at
from stored_files
`
if len(conditions) > 0 {
@@ -110,24 +106,23 @@ func (db *DB) ListStoredFiles(ctx context.Context, filter thoughttypes.StoredFil
files := make([]thoughttypes.StoredFile, 0, filter.Limit)
for rows.Next() {
var model generatedmodels.ModelPublicStoredFiles
var file thoughttypes.StoredFile
if err := rows.Scan(
&model.ID,
&model.GUID,
&model.ThoughtID,
&model.ProjectID,
&model.Name,
&model.MediaType,
&model.Kind,
&model.Encoding,
&model.SizeBytes,
&model.Sha256,
&model.CreatedAt,
&model.UpdatedAt,
&file.ID,
&file.ThoughtID,
&file.ProjectID,
&file.Name,
&file.MediaType,
&file.Kind,
&file.Encoding,
&file.SizeBytes,
&file.SHA256,
&file.CreatedAt,
&file.UpdatedAt,
); err != nil {
return nil, fmt.Errorf("scan stored file: %w", err)
}
files = append(files, storedFileFromModel(model))
files = append(files, file)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate stored files: %w", err)
@@ -136,7 +131,7 @@ func (db *DB) ListStoredFiles(ctx context.Context, filter thoughttypes.StoredFil
return files, nil
}
func (db *DB) AddThoughtAttachment(ctx context.Context, thoughtID int64, attachment thoughttypes.ThoughtAttachment) error {
func (db *DB) AddThoughtAttachment(ctx context.Context, thoughtID uuid.UUID, attachment thoughttypes.ThoughtAttachment) error {
tx, err := db.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin transaction: %w", err)
@@ -146,7 +141,7 @@ func (db *DB) AddThoughtAttachment(ctx context.Context, thoughtID int64, attachm
}()
var metadataBytes []byte
if err := tx.QueryRow(ctx, `select metadata from thoughts where id = $1 for update`, thoughtID).Scan(&metadataBytes); err != nil {
if err := tx.QueryRow(ctx, `select metadata from thoughts where guid = $1 for update`, thoughtID).Scan(&metadataBytes); err != nil {
if err == pgx.ErrNoRows {
return err
}
@@ -181,7 +176,7 @@ func (db *DB) AddThoughtAttachment(ctx context.Context, thoughtID int64, attachm
update thoughts
set metadata = $2::jsonb,
updated_at = now()
where id = $1
where guid = $1
`, thoughtID, updatedMetadata)
if err != nil {
return fmt.Errorf("update thought attachments: %w", err)
+131 -114
View File
@@ -1,133 +1,150 @@
package store
// import (
// "context"
// "encoding/json"
// "fmt"
// "strings"
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
// "github.com/google/uuid"
"github.com/google/uuid"
// "git.warky.dev/wdevs/amcs/internal/generatedmodels"
// ext "git.warky.dev/wdevs/amcs/internal/types"
// )
ext "git.warky.dev/wdevs/amcs/internal/types"
)
// func (db *DB) AddHouseholdItem(ctx context.Context, item ext.HouseholdItem) (ext.HouseholdItem, error) {
// details, err := json.Marshal(item.Details)
// if err != nil {
// return ext.HouseholdItem{}, fmt.Errorf("marshal details: %w", err)
// }
func (db *DB) AddHouseholdItem(ctx context.Context, item ext.HouseholdItem) (ext.HouseholdItem, error) {
details, err := json.Marshal(item.Details)
if err != nil {
return ext.HouseholdItem{}, fmt.Errorf("marshal details: %w", err)
}
// row := db.pool.QueryRow(ctx, `
// insert into household_items (name, category, location, details, notes)
// values ($1, $2, $3, $4::jsonb, $5)
// returning id, created_at, updated_at
// `, item.Name, nullStr(item.Category), nullStr(item.Location), details, nullStr(item.Notes))
row := db.pool.QueryRow(ctx, `
insert into household_items (name, category, location, details, notes)
values ($1, $2, $3, $4::jsonb, $5)
returning id, created_at, updated_at
`, item.Name, nullStr(item.Category), nullStr(item.Location), details, nullStr(item.Notes))
// created := item
// var model generatedmodels.ModelPublicHouseholdItems
// if err := row.Scan(&model.ID, &model.CreatedAt, &model.UpdatedAt); err != nil {
// return ext.HouseholdItem{}, fmt.Errorf("insert household item: %w", err)
// }
// created.ID = model.ID.UUID()
// created.CreatedAt = model.CreatedAt.Time()
// created.UpdatedAt = model.UpdatedAt.Time()
// return created, nil
// }
created := item
if err := row.Scan(&created.ID, &created.CreatedAt, &created.UpdatedAt); err != nil {
return ext.HouseholdItem{}, fmt.Errorf("insert household item: %w", err)
}
return created, nil
}
// func (db *DB) SearchHouseholdItems(ctx context.Context, query, category, location string) ([]ext.HouseholdItem, error) {
// args := []any{}
// conditions := []string{}
func (db *DB) SearchHouseholdItems(ctx context.Context, query, category, location string) ([]ext.HouseholdItem, error) {
args := []any{}
conditions := []string{}
// if q := strings.TrimSpace(query); q != "" {
// args = append(args, "%"+q+"%")
// conditions = append(conditions, fmt.Sprintf("(name ILIKE $%d OR notes ILIKE $%d)", len(args), len(args)))
// }
// if c := strings.TrimSpace(category); c != "" {
// args = append(args, c)
// conditions = append(conditions, fmt.Sprintf("category = $%d", len(args)))
// }
// if l := strings.TrimSpace(location); l != "" {
// args = append(args, "%"+l+"%")
// conditions = append(conditions, fmt.Sprintf("location ILIKE $%d", len(args)))
// }
if q := strings.TrimSpace(query); q != "" {
args = append(args, "%"+q+"%")
conditions = append(conditions, fmt.Sprintf("(name ILIKE $%d OR notes ILIKE $%d)", len(args), len(args)))
}
if c := strings.TrimSpace(category); c != "" {
args = append(args, c)
conditions = append(conditions, fmt.Sprintf("category = $%d", len(args)))
}
if l := strings.TrimSpace(location); l != "" {
args = append(args, "%"+l+"%")
conditions = append(conditions, fmt.Sprintf("location ILIKE $%d", len(args)))
}
// q := `select id, name, category, location, details, notes, created_at, updated_at from household_items`
// if len(conditions) > 0 {
// q += " where " + strings.Join(conditions, " and ")
// }
// q += " order by name"
q := `select id, name, category, location, details, notes, created_at, updated_at from household_items`
if len(conditions) > 0 {
q += " where " + strings.Join(conditions, " and ")
}
q += " order by name"
// rows, err := db.pool.Query(ctx, q, args...)
// if err != nil {
// return nil, fmt.Errorf("search household items: %w", err)
// }
// defer rows.Close()
rows, err := db.pool.Query(ctx, q, args...)
if err != nil {
return nil, fmt.Errorf("search household items: %w", err)
}
defer rows.Close()
// var items []ext.HouseholdItem
// for rows.Next() {
// var model generatedmodels.ModelPublicHouseholdItems
// if err := rows.Scan(&model.ID, &model.Name, &model.Category, &model.Location, &model.Details, &model.Notes, &model.CreatedAt, &model.UpdatedAt); err != nil {
// return nil, fmt.Errorf("scan household item: %w", err)
// }
// items = append(items, householdItemFromModel(model))
// }
// return items, rows.Err()
// }
var items []ext.HouseholdItem
for rows.Next() {
var item ext.HouseholdItem
var detailsBytes []byte
var category, location, notes *string
if err := rows.Scan(&item.ID, &item.Name, &category, &location, &detailsBytes, &notes, &item.CreatedAt, &item.UpdatedAt); err != nil {
return nil, fmt.Errorf("scan household item: %w", err)
}
item.Category = strVal(category)
item.Location = strVal(location)
item.Notes = strVal(notes)
if err := json.Unmarshal(detailsBytes, &item.Details); err != nil {
item.Details = map[string]any{}
}
items = append(items, item)
}
return items, rows.Err()
}
// func (db *DB) GetHouseholdItem(ctx context.Context, id uuid.UUID) (ext.HouseholdItem, error) {
// row := db.pool.QueryRow(ctx, `
// select id, name, category, location, details, notes, created_at, updated_at
// from household_items where id = $1
// `, id)
func (db *DB) GetHouseholdItem(ctx context.Context, id uuid.UUID) (ext.HouseholdItem, error) {
row := db.pool.QueryRow(ctx, `
select id, name, category, location, details, notes, created_at, updated_at
from household_items where id = $1
`, id)
// var model generatedmodels.ModelPublicHouseholdItems
// if err := row.Scan(&model.ID, &model.Name, &model.Category, &model.Location, &model.Details, &model.Notes, &model.CreatedAt, &model.UpdatedAt); err != nil {
// return ext.HouseholdItem{}, fmt.Errorf("get household item: %w", err)
// }
// return householdItemFromModel(model), nil
// }
var item ext.HouseholdItem
var detailsBytes []byte
var category, location, notes *string
if err := row.Scan(&item.ID, &item.Name, &category, &location, &detailsBytes, &notes, &item.CreatedAt, &item.UpdatedAt); err != nil {
return ext.HouseholdItem{}, fmt.Errorf("get household item: %w", err)
}
item.Category = strVal(category)
item.Location = strVal(location)
item.Notes = strVal(notes)
if err := json.Unmarshal(detailsBytes, &item.Details); err != nil {
item.Details = map[string]any{}
}
return item, nil
}
// func (db *DB) AddVendor(ctx context.Context, v ext.HouseholdVendor) (ext.HouseholdVendor, error) {
// row := db.pool.QueryRow(ctx, `
// insert into household_vendors (name, service_type, phone, email, website, notes, rating, last_used)
// values ($1, $2, $3, $4, $5, $6, $7, $8)
// returning id, created_at
// `, v.Name, nullStr(v.ServiceType), nullStr(v.Phone), nullStr(v.Email),
// nullStr(v.Website), nullStr(v.Notes), v.Rating, v.LastUsed)
func (db *DB) AddVendor(ctx context.Context, v ext.HouseholdVendor) (ext.HouseholdVendor, error) {
row := db.pool.QueryRow(ctx, `
insert into household_vendors (name, service_type, phone, email, website, notes, rating, last_used)
values ($1, $2, $3, $4, $5, $6, $7, $8)
returning id, created_at
`, v.Name, nullStr(v.ServiceType), nullStr(v.Phone), nullStr(v.Email),
nullStr(v.Website), nullStr(v.Notes), v.Rating, v.LastUsed)
// created := v
// var model generatedmodels.ModelPublicHouseholdVendors
// if err := row.Scan(&model.ID, &model.CreatedAt); err != nil {
// return ext.HouseholdVendor{}, fmt.Errorf("insert vendor: %w", err)
// }
// created.ID = model.ID.UUID()
// created.CreatedAt = model.CreatedAt.Time()
// return created, nil
// }
created := v
if err := row.Scan(&created.ID, &created.CreatedAt); err != nil {
return ext.HouseholdVendor{}, fmt.Errorf("insert vendor: %w", err)
}
return created, nil
}
// func (db *DB) ListVendors(ctx context.Context, serviceType string) ([]ext.HouseholdVendor, error) {
// args := []any{}
// q := `select id, name, service_type, phone, email, website, notes, rating, last_used, created_at from household_vendors`
// if st := strings.TrimSpace(serviceType); st != "" {
// args = append(args, st)
// q += " where service_type = $1"
// }
// q += " order by name"
func (db *DB) ListVendors(ctx context.Context, serviceType string) ([]ext.HouseholdVendor, error) {
args := []any{}
q := `select id, name, service_type, phone, email, website, notes, rating, last_used, created_at from household_vendors`
if st := strings.TrimSpace(serviceType); st != "" {
args = append(args, st)
q += " where service_type = $1"
}
q += " order by name"
// rows, err := db.pool.Query(ctx, q, args...)
// if err != nil {
// return nil, fmt.Errorf("list vendors: %w", err)
// }
// defer rows.Close()
rows, err := db.pool.Query(ctx, q, args...)
if err != nil {
return nil, fmt.Errorf("list vendors: %w", err)
}
defer rows.Close()
// var vendors []ext.HouseholdVendor
// for rows.Next() {
// var model generatedmodels.ModelPublicHouseholdVendors
// if err := rows.Scan(&model.ID, &model.Name, &model.ServiceType, &model.Phone, &model.Email, &model.Website, &model.Notes, &model.Rating, &model.LastUsed, &model.CreatedAt); err != nil {
// return nil, fmt.Errorf("scan vendor: %w", err)
// }
// vendors = append(vendors, householdVendorFromModel(model))
// }
// return vendors, rows.Err()
// }
var vendors []ext.HouseholdVendor
for rows.Next() {
var v ext.HouseholdVendor
var serviceType, phone, email, website, notes *string
var lastUsed *time.Time
if err := rows.Scan(&v.ID, &v.Name, &serviceType, &phone, &email, &website, &notes, &v.Rating, &lastUsed, &v.CreatedAt); err != nil {
return nil, fmt.Errorf("scan vendor: %w", err)
}
v.ServiceType = strVal(serviceType)
v.Phone = strVal(phone)
v.Email = strVal(email)
v.Website = strVal(website)
v.Notes = strVal(notes)
v.LastUsed = lastUsed
vendors = append(vendors, v)
}
return vendors, rows.Err()
}
+130 -171
View File
@@ -2,207 +2,166 @@ package store
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"git.warky.dev/wdevs/amcs/internal/generatedmodels"
"github.com/jackc/pgx/v5"
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
"github.com/google/uuid"
"github.com/lib/pq"
"amcs/internal/types"
)
func (db *DB) CreateLearning(ctx context.Context, learning thoughttypes.Learning) (thoughttypes.Learning, error) {
row := db.pool.QueryRow(ctx, `
insert into learnings (
summary, details, category, area, status, priority, confidence,
action_required, source_type, source_ref, project_id, related_thought_id,
related_skill_id, reviewed_by, reviewed_at, duplicate_of_learning_id,
supersedes_learning_id, tags
) values (
$1, $2, $3, $4, $5, $6, $7,
$8, $9, $10, $11, $12,
$13, $14, $15, $16,
$17, $18
)
returning id, guid, created_at, updated_at
`,
strings.TrimSpace(learning.Summary),
strings.TrimSpace(learning.Details),
strings.TrimSpace(learning.Category),
strings.TrimSpace(learning.Area),
string(learning.Status),
string(learning.Priority),
string(learning.Confidence),
learning.ActionRequired,
nullableText(learning.SourceType),
nullableText(learning.SourceRef),
learning.ProjectID,
learning.RelatedThoughtID,
learning.RelatedSkillID,
nullableTextPtr(learning.ReviewedBy),
learning.ReviewedAt,
learning.DuplicateOfLearningID,
learning.SupersedesLearningID,
learning.Tags,
)
created := learning
var model generatedmodels.ModelPublicLearnings
if err := row.Scan(&model.ID, &model.GUID, &model.CreatedAt, &model.UpdatedAt); err != nil {
return thoughttypes.Learning{}, fmt.Errorf("create learning: %w", err)
}
created.ID = model.ID.Int64()
created.GUID = model.GUID.UUID()
created.CreatedAt = model.CreatedAt.Time()
created.UpdatedAt = model.UpdatedAt.Time()
return created, nil
type LearningStore struct {
db *sql.DB
}
func (db *DB) GetLearning(ctx context.Context, id int64) (thoughttypes.Learning, error) {
row := db.pool.QueryRow(ctx, `
select id, summary, details, category, area, status, priority, confidence,
action_required, source_type, source_ref, project_id, related_thought_id,
related_skill_id, reviewed_by, reviewed_at, duplicate_of_learning_id,
supersedes_learning_id, tags::text[], created_at, updated_at
from learnings
where id = $1
`, id)
func NewLearningStore(db *sql.DB) *LearningStore {
return &LearningStore{db: db}
}
func (s *LearningStore) Create(ctx context.Context, l *types.Learning) error {
query := `
INSERT INTO learnings (
summary, details, category, area, status, priority, confidence,
action_required, source_type, source_ref, project_id, related_thought_id,
related_skill_id, reviewed_by, reviewed_at, duplicate_of, supersedes, tags
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18
) RETURNING guid, created_at, updated_at`
var id uuid.UUID
var createdAt, updatedAt time.Time
err := s.db.QueryRowContext(ctx, query,
l.Summary, l.Details, l.Category, l.Area, l.Status, l.Priority, l.Confidence,
l.ActionRequired, l.SourceType, l.SourceRef, l.ProjectID, l.RelatedThoughtID,
l.RelatedSkillID, l.ReviewedBy, l.ReviewedAt, l.DuplicateOf, l.Supersedes,
pq.Array(l.Tags),
).Scan(&id, &createdAt, &updatedAt)
learning, err := scanLearning(row)
if err != nil {
if err == pgx.ErrNoRows {
return thoughttypes.Learning{}, fmt.Errorf("learning not found: %d", id)
}
return thoughttypes.Learning{}, fmt.Errorf("get learning: %w", err)
return fmt.Errorf("failed to create learning: %w", err)
}
return learning, nil
l.ID = id
l.CreatedAt = createdAt
l.UpdatedAt = updatedAt
return nil
}
func (db *DB) ListLearnings(ctx context.Context, filter thoughttypes.LearningFilter) ([]thoughttypes.Learning, error) {
args := make([]any, 0, 8)
conditions := make([]string, 0, 8)
func (s *LearningStore) Get(ctx context.Context, id uuid.UUID) (*types.Learning, error) {
query := `SELECT * FROM learnings WHERE guid = $1`
l := &types.Learning{}
err := s.db.QueryRowContext(ctx, query, id).Scan(
&l.ID, &l.Summary, &l.Details, &l.Category, &l.Area, &l.Status, &l.Priority,
&l.Confidence, &l.ActionRequired, &l.SourceType, &l.SourceRef, &l.ProjectID,
&l.RelatedThoughtID, &l.RelatedSkillID, &l.ReviewedBy, &l.ReviewedAt,
&l.DuplicateOf, &l.Supersedes, &l.Tags, &l.CreatedAt, &l.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to get learning: %w", err)
}
return l, nil
}
func (s *LearningStore) Update(ctx context.Context, l *types.Learning) error {
query := `
UPDATE learnings SET
summary=$1, details=$2, category=$3, area=$4, status=$5, priority=$6,
confidence=$7, action_required=$8, source_type=$9, source_ref=$10,
project_id=$11, related_thought_id=$12, related_skill_id=$13,
reviewed_by=$14, reviewed_at=$15, duplicate_of=$16, supersedes=$17,
tags=$18, updated_at=now()
WHERE guid=$19`
_, err := s.db.ExecContext(ctx, query,
l.Summary, l.Details, l.Category, l.Area, l.Status, l.Priority, l.Confidence,
l.ActionRequired, l.SourceType, l.SourceRef, l.ProjectID, l.RelatedThoughtID,
l.RelatedSkillID, l.ReviewedBy, l.ReviewedAt, l.DuplicateOf, l.Supersedes,
pq.Array(l.Tags), l.ID,
)
if err != nil {
return fmt.Errorf("failed to update learning: %w", err)
}
return nil
}
func (s *LearningStore) Delete(ctx context.Context, id uuid.UUID) error {
query := `DELETE FROM learnings WHERE guid = $1`
_, err := s.db.ExecContext(ctx, query, id)
if err != nil {
return fmt.Errorf("failed to delete learning: %w", err)
}
return nil
}
func (s *LearningStore) List(ctx context.Context, filter types.LearningFilter) ([]types.Learning, error) {
query := `SELECT * FROM learnings WHERE 1=1`
args := []interface{}{}
argCount := 1
if filter.ProjectID != nil {
query += fmt.Sprintf(` AND project_id = $%d`, argCount)
args = append(args, *filter.ProjectID)
conditions = append(conditions, fmt.Sprintf("project_id = $%d", len(args)))
argCount++
}
if value := strings.TrimSpace(filter.Category); value != "" {
args = append(args, value)
conditions = append(conditions, fmt.Sprintf("category = $%d", len(args)))
if filter.Category != "" {
query += fmt.Sprintf(` AND category = $%d`, argCount)
args = append(args, filter.Category)
argCount++
}
if value := strings.TrimSpace(filter.Area); value != "" {
args = append(args, value)
conditions = append(conditions, fmt.Sprintf("area = $%d", len(args)))
if filter.Area != "" {
query += fmt.Sprintf(` AND area = $%d`, argCount)
args = append(args, filter.Area)
argCount++
}
if value := strings.TrimSpace(filter.Status); value != "" {
args = append(args, value)
conditions = append(conditions, fmt.Sprintf("status = $%d", len(args)))
if filter.Status != "" {
query += fmt.Sprintf(` AND status = $%d`, argCount)
args = append(args, filter.Status)
argCount++
}
if value := strings.TrimSpace(filter.Priority); value != "" {
args = append(args, value)
conditions = append(conditions, fmt.Sprintf("priority = $%d", len(args)))
if filter.Priority != "" {
query += fmt.Sprintf(` AND priority = $%d`, argCount)
args = append(args, filter.Priority)
argCount++
}
if value := strings.TrimSpace(filter.Tag); value != "" {
args = append(args, value)
conditions = append(conditions, fmt.Sprintf("$%d = any(tags)", len(args)))
if filter.Tag != "" {
query += fmt.Sprintf(` AND %d = ANY(tags)`, argCount) // Wait, tags is array. Correct is:
query = fmt.Sprintf("%s AND $%d = ANY(tags)", query, argCount)
args = append(args, filter.Tag)
argCount++
}
if value := strings.TrimSpace(filter.Query); value != "" {
args = append(args, value)
conditions = append(conditions, fmt.Sprintf("to_tsvector('simple', summary || ' ' || coalesce(details, '')) @@ websearch_to_tsquery('simple', $%d)", len(args)))
if filter.Query != "" {
query += fmt.Sprintf(` AND to_tsvector('simple', summary || ' ' || coalesce(details, '')) @@ websearch_to_tsquery('simple', $%d)`, argCount)
args = append(args, filter.Query)
argCount++
}
query := `
select id, summary, details, category, area, status, priority, confidence,
action_required, source_type, source_ref, project_id, related_thought_id,
related_skill_id, reviewed_by, reviewed_at, duplicate_of_learning_id,
supersedes_learning_id, tags::text[], created_at, updated_at
from learnings
`
if len(conditions) > 0 {
query += " where " + strings.Join(conditions, " and ")
}
query += " order by updated_at desc"
if filter.Limit > 0 {
args = append(args, filter.Limit)
query += fmt.Sprintf(" limit $%d", len(args))
query += fmt.Sprintf(` ORDER BY created_at DESC LIMIT %d`, filter.Limit)
if filter.Limit == 0 {
query = query[:len(query)-10] // remove LIMIT 0
}
rows, err := db.pool.Query(ctx, query, args...)
rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("list learnings: %w", err)
return nil, err
}
defer rows.Close()
items := make([]thoughttypes.Learning, 0)
var learnings []types.Learning
for rows.Next() {
item, err := scanLearning(rows)
l := types.Learning{}
err := rows.Scan(
&l.ID, &l.Summary, &l.Details, &l.Category, &l.Area, &l.Status, &l.Priority,
&l.Confidence, &l.ActionRequired, &l.SourceType, &l.SourceRef, &l.ProjectID,
&l.RelatedThoughtID, &l.RelatedSkillID, &l.ReviewedBy, &l.ReviewedAt,
&l.DuplicateOf, &l.Supersedes, &l.Tags, &l.CreatedAt, &l.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("scan learning: %w", err)
return nil, err
}
items = append(items, item)
learnings = append(learnings, l)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate learnings: %w", err)
}
return items, nil
}
type learningScanner interface {
Scan(dest ...any) error
}
func scanLearning(row learningScanner) (thoughttypes.Learning, error) {
var model generatedmodels.ModelPublicLearnings
var tags []string
err := row.Scan(
&model.ID,
&model.Summary,
&model.Details,
&model.Category,
&model.Area,
&model.Status,
&model.Priority,
&model.Confidence,
&model.ActionRequired,
&model.SourceType,
&model.SourceRef,
&model.ProjectID,
&model.RelatedThoughtID,
&model.RelatedSkillID,
&model.ReviewedBy,
&model.ReviewedAt,
&model.DuplicateOfLearningID,
&model.SupersedesLearningID,
&tags,
&model.CreatedAt,
&model.UpdatedAt,
)
if err != nil {
return thoughttypes.Learning{}, err
}
if tags == nil {
tags = []string{}
}
return learningFromModel(model, tags), nil
}
func nullableText(value string) *string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return nil
}
return &trimmed
}
func nullableTextPtr(value *string) *string {
if value == nil {
return nil
}
trimmed := strings.TrimSpace(*value)
if trimmed == "" {
return nil
}
return &trimmed
return learnings, nil
}
+13 -16
View File
@@ -2,11 +2,11 @@ package store
import (
"context"
"encoding/json"
"fmt"
"github.com/google/uuid"
"git.warky.dev/wdevs/amcs/internal/generatedmodels"
thoughttypes "git.warky.dev/wdevs/amcs/internal/types"
)
@@ -25,12 +25,12 @@ func (db *DB) InsertLink(ctx context.Context, link thoughttypes.ThoughtLink) err
func (db *DB) LinkedThoughts(ctx context.Context, thoughtID uuid.UUID) ([]thoughttypes.LinkedThought, error) {
rows, err := db.pool.Query(ctx, `
select t.id, t.guid, t.content, t.metadata, t.project_id, t.archived_at, t.created_at, t.updated_at, l.relation, 'outgoing' as direction, l.created_at
select t.guid, t.content, t.metadata, t.project_id, t.archived_at, t.created_at, t.updated_at, l.relation, 'outgoing' as direction, l.created_at
from thought_links l
join thoughts t on t.id = l.to_id
where l.from_id = (select id from thoughts where guid = $1)
union all
select t.id, t.guid, t.content, t.metadata, t.project_id, t.archived_at, t.created_at, t.updated_at, l.relation, 'incoming' as direction, l.created_at
select t.guid, t.content, t.metadata, t.project_id, t.archived_at, t.created_at, t.updated_at, l.relation, 'incoming' as direction, l.created_at
from thought_links l
join thoughts t on t.id = l.from_id
where l.to_id = (select id from thoughts where guid = $1)
@@ -44,27 +44,24 @@ func (db *DB) LinkedThoughts(ctx context.Context, thoughtID uuid.UUID) ([]though
links := make([]thoughttypes.LinkedThought, 0)
for rows.Next() {
var linked thoughttypes.LinkedThought
var model generatedmodels.ModelPublicThoughts
var metadataBytes []byte
if err := rows.Scan(
&model.ID,
&model.GUID,
&model.Content,
&model.Metadata,
&model.ProjectID,
&model.ArchivedAt,
&model.CreatedAt,
&model.UpdatedAt,
&linked.Thought.ID,
&linked.Thought.Content,
&metadataBytes,
&linked.Thought.ProjectID,
&linked.Thought.ArchivedAt,
&linked.Thought.CreatedAt,
&linked.Thought.UpdatedAt,
&linked.Relation,
&linked.Direction,
&linked.CreatedAt,
); err != nil {
return nil, fmt.Errorf("scan linked thought: %w", err)
}
thought, err := thoughtFromModel(model)
if err != nil {
return nil, fmt.Errorf("map linked thought: %w", err)
if err := json.Unmarshal(metadataBytes, &linked.Thought.Metadata); err != nil {
return nil, fmt.Errorf("decode linked thought metadata: %w", err)
}
linked.Thought = thought
links = append(links, linked)
}
if err := rows.Err(); err != nil {
+126 -121
View File
@@ -1,137 +1,142 @@
package store
// import (
// "context"
// "fmt"
// "strings"
// "time"
import (
"context"
"fmt"
"strings"
"time"
// "git.warky.dev/wdevs/amcs/internal/generatedmodels"
// ext "git.warky.dev/wdevs/amcs/internal/types"
// )
ext "git.warky.dev/wdevs/amcs/internal/types"
)
// func (db *DB) AddMaintenanceTask(ctx context.Context, t ext.MaintenanceTask) (ext.MaintenanceTask, error) {
// row := db.pool.QueryRow(ctx, `
// insert into maintenance_tasks (name, category, frequency_days, next_due, priority, notes)
// values ($1, $2, $3, $4, $5, $6)
// returning id, created_at, updated_at
// `, t.Name, nullStr(t.Category), t.FrequencyDays, t.NextDue, t.Priority, nullStr(t.Notes))
func (db *DB) AddMaintenanceTask(ctx context.Context, t ext.MaintenanceTask) (ext.MaintenanceTask, error) {
row := db.pool.QueryRow(ctx, `
insert into maintenance_tasks (name, category, frequency_days, next_due, priority, notes)
values ($1, $2, $3, $4, $5, $6)
returning id, created_at, updated_at
`, t.Name, nullStr(t.Category), t.FrequencyDays, t.NextDue, t.Priority, nullStr(t.Notes))
// created := t
// var model generatedmodels.ModelPublicMaintenanceTasks
// if err := row.Scan(&model.ID, &model.CreatedAt, &model.UpdatedAt); err != nil {
// return ext.MaintenanceTask{}, fmt.Errorf("insert maintenance task: %w", err)
// }
// created.ID = model.ID.UUID()
// created.CreatedAt = model.CreatedAt.Time()
// created.UpdatedAt = model.UpdatedAt.Time()
// return created, nil
// }
created := t
if err := row.Scan(&created.ID, &created.CreatedAt, &created.UpdatedAt); err != nil {
return ext.MaintenanceTask{}, fmt.Errorf("insert maintenance task: %w", err)
}
return created, nil
}
// func (db *DB) LogMaintenance(ctx context.Context, log ext.MaintenanceLog) (ext.MaintenanceLog, error) {
// completedAt := log.CompletedAt
// if completedAt.IsZero() {
// completedAt = time.Now()
// }
func (db *DB) LogMaintenance(ctx context.Context, log ext.MaintenanceLog) (ext.MaintenanceLog, error) {
completedAt := log.CompletedAt
if completedAt.IsZero() {
completedAt = time.Now()
}
// row := db.pool.QueryRow(ctx, `
// insert into maintenance_logs (task_id, completed_at, performed_by, cost, notes, next_action)
// values ($1, $2, $3, $4, $5, $6)
// returning id
// `, log.TaskID, completedAt, nullStr(log.PerformedBy), log.Cost, nullStr(log.Notes), nullStr(log.NextAction))
row := db.pool.QueryRow(ctx, `
insert into maintenance_logs (task_id, completed_at, performed_by, cost, notes, next_action)
values ($1, $2, $3, $4, $5, $6)
returning id
`, log.TaskID, completedAt, nullStr(log.PerformedBy), log.Cost, nullStr(log.Notes), nullStr(log.NextAction))
// created := log
// created.CompletedAt = completedAt
// var model generatedmodels.ModelPublicMaintenanceLogs
// if err := row.Scan(&model.ID); err != nil {
// return ext.MaintenanceLog{}, fmt.Errorf("insert maintenance log: %w", err)
// }
// created.ID = model.ID.UUID()
// return created, nil
// }
created := log
created.CompletedAt = completedAt
if err := row.Scan(&created.ID); err != nil {
return ext.MaintenanceLog{}, fmt.Errorf("insert maintenance log: %w", err)
}
return created, nil
}
// func (db *DB) GetUpcomingMaintenance(ctx context.Context, daysAhead int) ([]ext.MaintenanceTask, error) {
// if daysAhead <= 0 {
// daysAhead = 30
// }
// cutoff := time.Now().Add(time.Duration(daysAhead) * 24 * time.Hour)
func (db *DB) GetUpcomingMaintenance(ctx context.Context, daysAhead int) ([]ext.MaintenanceTask, error) {
if daysAhead <= 0 {
daysAhead = 30
}
cutoff := time.Now().Add(time.Duration(daysAhead) * 24 * time.Hour)
// rows, err := db.pool.Query(ctx, `
// select id, name, category, frequency_days, last_completed, next_due, priority, notes, created_at, updated_at
// from maintenance_tasks
// where next_due <= $1 or next_due is null
// order by next_due asc nulls last, priority desc
// `, cutoff)
// if err != nil {
// return nil, fmt.Errorf("get upcoming maintenance: %w", err)
// }
// defer rows.Close()
rows, err := db.pool.Query(ctx, `
select id, name, category, frequency_days, last_completed, next_due, priority, notes, created_at, updated_at
from maintenance_tasks
where next_due <= $1 or next_due is null
order by next_due asc nulls last, priority desc
`, cutoff)
if err != nil {
return nil, fmt.Errorf("get upcoming maintenance: %w", err)
}
defer rows.Close()
// tasks := make([]ext.MaintenanceTask, 0)
// for rows.Next() {
// var model generatedmodels.ModelPublicMaintenanceTasks
// if err := rows.Scan(&model.ID, &model.Name, &model.Category, &model.FrequencyDays, &model.LastCompleted, &model.NextDue, &model.Priority, &model.Notes, &model.CreatedAt, &model.UpdatedAt); err != nil {
// return nil, fmt.Errorf("scan maintenance task: %w", err)
// }
// tasks = append(tasks, maintenanceTaskFromModel(model))
// }
// return tasks, rows.Err()
// }
return scanMaintenanceTasks(rows)
}
// func (db *DB) SearchMaintenanceHistory(ctx context.Context, query, category string, start, end *time.Time) ([]ext.MaintenanceLogWithTask, error) {
// args := []any{}
// conditions := []string{}
func (db *DB) SearchMaintenanceHistory(ctx context.Context, query, category string, start, end *time.Time) ([]ext.MaintenanceLogWithTask, error) {
args := []any{}
conditions := []string{}
// if q := strings.TrimSpace(query); q != "" {
// args = append(args, "%"+q+"%")
// conditions = append(conditions, fmt.Sprintf("(mt.name ILIKE $%d OR ml.notes ILIKE $%d)", len(args), len(args)))
// }
// if c := strings.TrimSpace(category); c != "" {
// args = append(args, c)
// conditions = append(conditions, fmt.Sprintf("mt.category = $%d", len(args)))
// }
// if start != nil {
// args = append(args, *start)
// conditions = append(conditions, fmt.Sprintf("ml.completed_at >= $%d", len(args)))
// }
// if end != nil {
// args = append(args, *end)
// conditions = append(conditions, fmt.Sprintf("ml.completed_at <= $%d", len(args)))
// }
if q := strings.TrimSpace(query); q != "" {
args = append(args, "%"+q+"%")
conditions = append(conditions, fmt.Sprintf("(mt.name ILIKE $%d OR ml.notes ILIKE $%d)", len(args), len(args)))
}
if c := strings.TrimSpace(category); c != "" {
args = append(args, c)
conditions = append(conditions, fmt.Sprintf("mt.category = $%d", len(args)))
}
if start != nil {
args = append(args, *start)
conditions = append(conditions, fmt.Sprintf("ml.completed_at >= $%d", len(args)))
}
if end != nil {
args = append(args, *end)
conditions = append(conditions, fmt.Sprintf("ml.completed_at <= $%d", len(args)))
}
// q := `
// select ml.id, ml.task_id, ml.completed_at, ml.performed_by, ml.cost, ml.notes, ml.next_action,
// mt.name, mt.category
// from maintenance_logs ml
// join maintenance_tasks mt on mt.id = ml.task_id
// `
// if len(conditions) > 0 {
// q += " where " + strings.Join(conditions, " and ")
// }
// q += " order by ml.completed_at desc"
q := `
select ml.id, ml.task_id, ml.completed_at, ml.performed_by, ml.cost, ml.notes, ml.next_action,
mt.name, mt.category
from maintenance_logs ml
join maintenance_tasks mt on mt.id = ml.task_id
`
if len(conditions) > 0 {
q += " where " + strings.Join(conditions, " and ")
}
q += " order by ml.completed_at desc"
// rows, err := db.pool.Query(ctx, q, args...)
// if err != nil {
// return nil, fmt.Errorf("search maintenance history: %w", err)
// }
// defer rows.Close()
rows, err := db.pool.Query(ctx, q, args...)
if err != nil {
return nil, fmt.Errorf("search maintenance history: %w", err)
}
defer rows.Close()
// var logs []ext.MaintenanceLogWithTask
// for rows.Next() {
// var model generatedmodels.ModelPublicMaintenanceLogs
// var taskName, taskCategory string
// if err := rows.Scan(
// &model.ID, &model.TaskID, &model.CompletedAt, &model.PerformedBy, &model.Cost, &model.Notes, &model.NextAction,
// &taskName, &taskCategory,
// ); err != nil {
// return nil, fmt.Errorf("scan maintenance log: %w", err)
// }
// l := ext.MaintenanceLogWithTask{
// MaintenanceLog: maintenanceLogFromModel(model),
// TaskName: taskName,
// TaskCategory: taskCategory,
// }
// logs = append(logs, l)
// }
// return logs, rows.Err()
// }
var logs []ext.MaintenanceLogWithTask
for rows.Next() {
var l ext.MaintenanceLogWithTask
var performedBy, notes, nextAction, taskCategory *string
if err := rows.Scan(
&l.ID, &l.TaskID, &l.CompletedAt, &performedBy, &l.Cost, &notes, &nextAction,
&l.TaskName, &taskCategory,
); err != nil {
return nil, fmt.Errorf("scan maintenance log: %w", err)
}
l.PerformedBy = strVal(performedBy)
l.Notes = strVal(notes)
l.NextAction = strVal(nextAction)
l.TaskCategory = strVal(taskCategory)
logs = append(logs, l)
}
return logs, rows.Err()
}
func scanMaintenanceTasks(rows interface {
Next() bool
Scan(...any) error
Err() error
Close()
}) ([]ext.MaintenanceTask, error) {
defer rows.Close()
var tasks []ext.MaintenanceTask
for rows.Next() {
var t ext.MaintenanceTask
var category, notes *string
if err := rows.Scan(&t.ID, &t.Name, &category, &t.FrequencyDays, &t.LastCompleted, &t.NextDue, &t.Priority, &notes, &t.CreatedAt, &t.UpdatedAt); err != nil {
return nil, fmt.Errorf("scan maintenance task: %w", err)
}
t.Category = strVal(category)
t.Notes = strVal(notes)
tasks = append(tasks, t)
}
return tasks, rows.Err()
}
+260 -251
View File
@@ -1,280 +1,289 @@
package store
// import (
// "context"
// "encoding/json"
// "fmt"
// "strings"
// "time"
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
// "github.com/google/uuid"
"github.com/google/uuid"
// "git.warky.dev/wdevs/amcs/internal/generatedmodels"
// ext "git.warky.dev/wdevs/amcs/internal/types"
// )
ext "git.warky.dev/wdevs/amcs/internal/types"
)
// func (db *DB) AddRecipe(ctx context.Context, r ext.Recipe) (ext.Recipe, error) {
// ingredients, err := json.Marshal(r.Ingredients)
// if err != nil {
// return ext.Recipe{}, fmt.Errorf("marshal ingredients: %w", err)
// }
// instructions, err := json.Marshal(r.Instructions)
// if err != nil {
// return ext.Recipe{}, fmt.Errorf("marshal instructions: %w", err)
// }
// if r.Tags == nil {
// r.Tags = []string{}
// }
func (db *DB) AddRecipe(ctx context.Context, r ext.Recipe) (ext.Recipe, error) {
ingredients, err := json.Marshal(r.Ingredients)
if err != nil {
return ext.Recipe{}, fmt.Errorf("marshal ingredients: %w", err)
}
instructions, err := json.Marshal(r.Instructions)
if err != nil {
return ext.Recipe{}, fmt.Errorf("marshal instructions: %w", err)
}
if r.Tags == nil {
r.Tags = []string{}
}
// row := db.pool.QueryRow(ctx, `
// insert into recipes (name, cuisine, prep_time_minutes, cook_time_minutes, servings, ingredients, instructions, tags, rating, notes)
// values ($1, $2, $3, $4, $5, $6::jsonb, $7::jsonb, $8, $9, $10)
// returning id, created_at, updated_at
// `, r.Name, nullStr(r.Cuisine), r.PrepTimeMinutes, r.CookTimeMinutes, r.Servings,
// ingredients, instructions, r.Tags, r.Rating, nullStr(r.Notes))
row := db.pool.QueryRow(ctx, `
insert into recipes (name, cuisine, prep_time_minutes, cook_time_minutes, servings, ingredients, instructions, tags, rating, notes)
values ($1, $2, $3, $4, $5, $6::jsonb, $7::jsonb, $8, $9, $10)
returning id, created_at, updated_at
`, r.Name, nullStr(r.Cuisine), r.PrepTimeMinutes, r.CookTimeMinutes, r.Servings,
ingredients, instructions, r.Tags, r.Rating, nullStr(r.Notes))
// created := r
// var model generatedmodels.ModelPublicRecipes
// if err := row.Scan(&model.ID, &model.CreatedAt, &model.UpdatedAt); err != nil {
// return ext.Recipe{}, fmt.Errorf("insert recipe: %w", err)
// }
// created.ID = model.ID.UUID()
// created.CreatedAt = model.CreatedAt.Time()
// created.UpdatedAt = model.UpdatedAt.Time()
// return created, nil
// }
created := r
if err := row.Scan(&created.ID, &created.CreatedAt, &created.UpdatedAt); err != nil {
return ext.Recipe{}, fmt.Errorf("insert recipe: %w", err)
}
return created, nil
}
// func (db *DB) SearchRecipes(ctx context.Context, query, cuisine string, tags []string, ingredient string) ([]ext.Recipe, error) {
// args := []any{}
// conditions := []string{}
func (db *DB) SearchRecipes(ctx context.Context, query, cuisine string, tags []string, ingredient string) ([]ext.Recipe, error) {
args := []any{}
conditions := []string{}
// if q := strings.TrimSpace(query); q != "" {
// args = append(args, "%"+q+"%")
// conditions = append(conditions, fmt.Sprintf("name ILIKE $%d", len(args)))
// }
// if c := strings.TrimSpace(cuisine); c != "" {
// args = append(args, c)
// conditions = append(conditions, fmt.Sprintf("cuisine = $%d", len(args)))
// }
// if len(tags) > 0 {
// args = append(args, tags)
// conditions = append(conditions, fmt.Sprintf("tags @> $%d", len(args)))
// }
// if ing := strings.TrimSpace(ingredient); ing != "" {
// args = append(args, "%"+ing+"%")
// conditions = append(conditions, fmt.Sprintf("ingredients::text ILIKE $%d", len(args)))
// }
if q := strings.TrimSpace(query); q != "" {
args = append(args, "%"+q+"%")
conditions = append(conditions, fmt.Sprintf("name ILIKE $%d", len(args)))
}
if c := strings.TrimSpace(cuisine); c != "" {
args = append(args, c)
conditions = append(conditions, fmt.Sprintf("cuisine = $%d", len(args)))
}
if len(tags) > 0 {
args = append(args, tags)
conditions = append(conditions, fmt.Sprintf("tags @> $%d", len(args)))
}
if ing := strings.TrimSpace(ingredient); ing != "" {
args = append(args, "%"+ing+"%")
conditions = append(conditions, fmt.Sprintf("ingredients::text ILIKE $%d", len(args)))
}
// q := `select id, name, cuisine, prep_time_minutes, cook_time_minutes, servings, ingredients, instructions, tags::text[], rating, notes, created_at, updated_at from recipes`
// if len(conditions) > 0 {
// q += " where " + strings.Join(conditions, " and ")
// }
// q += " order by name"
q := `select id, name, cuisine, prep_time_minutes, cook_time_minutes, servings, ingredients, instructions, tags, rating, notes, created_at, updated_at from recipes`
if len(conditions) > 0 {
q += " where " + strings.Join(conditions, " and ")
}
q += " order by name"
// rows, err := db.pool.Query(ctx, q, args...)
// if err != nil {
// return nil, fmt.Errorf("search recipes: %w", err)
// }
// defer rows.Close()
rows, err := db.pool.Query(ctx, q, args...)
if err != nil {
return nil, fmt.Errorf("search recipes: %w", err)
}
defer rows.Close()
// var recipes []ext.Recipe
// for rows.Next() {
// r, err := scanRecipeRow(rows)
// if err != nil {
// return nil, err
// }
// recipes = append(recipes, r)
// }
// return recipes, rows.Err()
// }
var recipes []ext.Recipe
for rows.Next() {
r, err := scanRecipeRow(rows)
if err != nil {
return nil, err
}
recipes = append(recipes, r)
}
return recipes, rows.Err()
}
// func (db *DB) GetRecipe(ctx context.Context, id uuid.UUID) (ext.Recipe, error) {
// row := db.pool.QueryRow(ctx, `
// select id, name, cuisine, prep_time_minutes, cook_time_minutes, servings, ingredients, instructions, tags::text[], rating, notes, created_at, updated_at
// from recipes where id = $1
// `, id)
func (db *DB) GetRecipe(ctx context.Context, id uuid.UUID) (ext.Recipe, error) {
row := db.pool.QueryRow(ctx, `
select id, name, cuisine, prep_time_minutes, cook_time_minutes, servings, ingredients, instructions, tags, rating, notes, created_at, updated_at
from recipes where id = $1
`, id)
// var model generatedmodels.ModelPublicRecipes
// var tags []string
// if err := row.Scan(&model.ID, &model.Name, &model.Cuisine, &model.PrepTimeMinutes, &model.CookTimeMinutes, &model.Servings,
// &model.Ingredients, &model.Instructions, &tags, &model.Rating, &model.Notes, &model.CreatedAt, &model.UpdatedAt); err != nil {
// return ext.Recipe{}, fmt.Errorf("get recipe: %w", err)
// }
// if tags == nil {
// tags = []string{}
// }
// return recipeFromModel(model, tags), nil
// }
var r ext.Recipe
var cuisine, notes *string
var ingredientsBytes, instructionsBytes []byte
if err := row.Scan(&r.ID, &r.Name, &cuisine, &r.PrepTimeMinutes, &r.CookTimeMinutes, &r.Servings,
&ingredientsBytes, &instructionsBytes, &r.Tags, &r.Rating, &notes, &r.CreatedAt, &r.UpdatedAt); err != nil {
return ext.Recipe{}, fmt.Errorf("get recipe: %w", err)
}
r.Cuisine = strVal(cuisine)
r.Notes = strVal(notes)
if r.Tags == nil {
r.Tags = []string{}
}
if err := json.Unmarshal(ingredientsBytes, &r.Ingredients); err != nil {
r.Ingredients = []ext.Ingredient{}
}
if err := json.Unmarshal(instructionsBytes, &r.Instructions); err != nil {
r.Instructions = []string{}
}
return r, nil
}
// func (db *DB) UpdateRecipe(ctx context.Context, id uuid.UUID, r ext.Recipe) (ext.Recipe, error) {
// ingredients, err := json.Marshal(r.Ingredients)
// if err != nil {
// return ext.Recipe{}, fmt.Errorf("marshal ingredients: %w", err)
// }
// instructions, err := json.Marshal(r.Instructions)
// if err != nil {
// return ext.Recipe{}, fmt.Errorf("marshal instructions: %w", err)
// }
// if r.Tags == nil {
// r.Tags = []string{}
// }
func (db *DB) UpdateRecipe(ctx context.Context, id uuid.UUID, r ext.Recipe) (ext.Recipe, error) {
ingredients, err := json.Marshal(r.Ingredients)
if err != nil {
return ext.Recipe{}, fmt.Errorf("marshal ingredients: %w", err)
}
instructions, err := json.Marshal(r.Instructions)
if err != nil {
return ext.Recipe{}, fmt.Errorf("marshal instructions: %w", err)
}
if r.Tags == nil {
r.Tags = []string{}
}
// _, err = db.pool.Exec(ctx, `
// update recipes set
// name = $2, cuisine = $3, prep_time_minutes = $4, cook_time_minutes = $5,
// servings = $6, ingredients = $7::jsonb, instructions = $8::jsonb,
// tags = $9, rating = $10, notes = $11
// where id = $1
// `, id, r.Name, nullStr(r.Cuisine), r.PrepTimeMinutes, r.CookTimeMinutes, r.Servings,
// ingredients, instructions, r.Tags, r.Rating, nullStr(r.Notes))
// if err != nil {
// return ext.Recipe{}, fmt.Errorf("update recipe: %w", err)
// }
// return db.GetRecipe(ctx, id)
// }
_, err = db.pool.Exec(ctx, `
update recipes set
name = $2, cuisine = $3, prep_time_minutes = $4, cook_time_minutes = $5,
servings = $6, ingredients = $7::jsonb, instructions = $8::jsonb,
tags = $9, rating = $10, notes = $11
where id = $1
`, id, r.Name, nullStr(r.Cuisine), r.PrepTimeMinutes, r.CookTimeMinutes, r.Servings,
ingredients, instructions, r.Tags, r.Rating, nullStr(r.Notes))
if err != nil {
return ext.Recipe{}, fmt.Errorf("update recipe: %w", err)
}
return db.GetRecipe(ctx, id)
}
// func (db *DB) CreateMealPlan(ctx context.Context, weekStart time.Time, entries []ext.MealPlanInput) ([]ext.MealPlanEntry, error) {
// if _, err := db.pool.Exec(ctx, `delete from meal_plans where week_start = $1`, weekStart); err != nil {
// return nil, fmt.Errorf("clear meal plan: %w", err)
// }
func (db *DB) CreateMealPlan(ctx context.Context, weekStart time.Time, entries []ext.MealPlanInput) ([]ext.MealPlanEntry, error) {
if _, err := db.pool.Exec(ctx, `delete from meal_plans where week_start = $1`, weekStart); err != nil {
return nil, fmt.Errorf("clear meal plan: %w", err)
}
// var results []ext.MealPlanEntry
// for _, e := range entries {
// row := db.pool.QueryRow(ctx, `
// insert into meal_plans (week_start, day_of_week, meal_type, recipe_id, custom_meal, servings, notes)
// values ($1, $2, $3, $4, $5, $6, $7)
// returning id, created_at
// `, weekStart, e.DayOfWeek, e.MealType, e.RecipeID, nullStr(e.CustomMeal), e.Servings, nullStr(e.Notes))
var results []ext.MealPlanEntry
for _, e := range entries {
row := db.pool.QueryRow(ctx, `
insert into meal_plans (week_start, day_of_week, meal_type, recipe_id, custom_meal, servings, notes)
values ($1, $2, $3, $4, $5, $6, $7)
returning id, created_at
`, weekStart, e.DayOfWeek, e.MealType, e.RecipeID, nullStr(e.CustomMeal), e.Servings, nullStr(e.Notes))
// entry := ext.MealPlanEntry{
// WeekStart: weekStart,
// DayOfWeek: e.DayOfWeek,
// MealType: e.MealType,
// RecipeID: e.RecipeID,
// CustomMeal: e.CustomMeal,
// Servings: e.Servings,
// Notes: e.Notes,
// }
// var model generatedmodels.ModelPublicMealPlans
// if err := row.Scan(&model.ID, &model.CreatedAt); err != nil {
// return nil, fmt.Errorf("insert meal plan entry: %w", err)
// }
// entry.ID = model.ID.UUID()
// entry.CreatedAt = model.CreatedAt.Time()
// results = append(results, entry)
// }
// return results, nil
// }
entry := ext.MealPlanEntry{
WeekStart: weekStart,
DayOfWeek: e.DayOfWeek,
MealType: e.MealType,
RecipeID: e.RecipeID,
CustomMeal: e.CustomMeal,
Servings: e.Servings,
Notes: e.Notes,
}
if err := row.Scan(&entry.ID, &entry.CreatedAt); err != nil {
return nil, fmt.Errorf("insert meal plan entry: %w", err)
}
results = append(results, entry)
}
return results, nil
}
// func (db *DB) GetMealPlan(ctx context.Context, weekStart time.Time) ([]ext.MealPlanEntry, error) {
// rows, err := db.pool.Query(ctx, `
// select mp.id, mp.week_start, mp.day_of_week, mp.meal_type, mp.recipe_id, r.name, mp.custom_meal, mp.servings, mp.notes, mp.created_at
// from meal_plans mp
// left join recipes r on r.id = mp.recipe_id
// where mp.week_start = $1
// order by
// case mp.day_of_week
// when 'monday' then 1 when 'tuesday' then 2 when 'wednesday' then 3
// when 'thursday' then 4 when 'friday' then 5 when 'saturday' then 6
// when 'sunday' then 7 else 8
// end,
// case mp.meal_type
// when 'breakfast' then 1 when 'lunch' then 2 when 'dinner' then 3
// when 'snack' then 4 else 5
// end
// `, weekStart)
// if err != nil {
// return nil, fmt.Errorf("get meal plan: %w", err)
// }
// defer rows.Close()
func (db *DB) GetMealPlan(ctx context.Context, weekStart time.Time) ([]ext.MealPlanEntry, error) {
rows, err := db.pool.Query(ctx, `
select mp.id, mp.week_start, mp.day_of_week, mp.meal_type, mp.recipe_id, r.name, mp.custom_meal, mp.servings, mp.notes, mp.created_at
from meal_plans mp
left join recipes r on r.id = mp.recipe_id
where mp.week_start = $1
order by
case mp.day_of_week
when 'monday' then 1 when 'tuesday' then 2 when 'wednesday' then 3
when 'thursday' then 4 when 'friday' then 5 when 'saturday' then 6
when 'sunday' then 7 else 8
end,
case mp.meal_type
when 'breakfast' then 1 when 'lunch' then 2 when 'dinner' then 3
when 'snack' then 4 else 5
end
`, weekStart)
if err != nil {
return nil, fmt.Errorf("get meal plan: %w", err)
}
defer rows.Close()
// var entries []ext.MealPlanEntry
// for rows.Next() {
// var model generatedmodels.ModelPublicMealPlans
// var recipeName *string
// if err := rows.Scan(&model.ID, &model.WeekStart, &model.DayOfWeek, &model.MealType, &model.RecipeID, &recipeName, &model.CustomMeal, &model.Servings, &model.Notes, &model.CreatedAt); err != nil {
// return nil, fmt.Errorf("scan meal plan entry: %w", err)
// }
// entries = append(entries, mealPlanEntryFromModel(model, strVal(recipeName)))
// }
// return entries, rows.Err()
// }
var entries []ext.MealPlanEntry
for rows.Next() {
var e ext.MealPlanEntry
var recipeName, customMeal, notes *string
if err := rows.Scan(&e.ID, &e.WeekStart, &e.DayOfWeek, &e.MealType, &e.RecipeID, &recipeName, &customMeal, &e.Servings, &notes, &e.CreatedAt); err != nil {
return nil, fmt.Errorf("scan meal plan entry: %w", err)
}
e.RecipeName = strVal(recipeName)
e.CustomMeal = strVal(customMeal)
e.Notes = strVal(notes)
entries = append(entries, e)
}
return entries, rows.Err()
}
// func (db *DB) GenerateShoppingList(ctx context.Context, weekStart time.Time) (ext.ShoppingList, error) {
// entries, err := db.GetMealPlan(ctx, weekStart)
// if err != nil {
// return ext.ShoppingList{}, err
// }
func (db *DB) GenerateShoppingList(ctx context.Context, weekStart time.Time) (ext.ShoppingList, error) {
entries, err := db.GetMealPlan(ctx, weekStart)
if err != nil {
return ext.ShoppingList{}, err
}
// recipeIDs := map[uuid.UUID]bool{}
// for _, e := range entries {
// if e.RecipeID != nil {
// recipeIDs[*e.RecipeID] = true
// }
// }
recipeIDs := map[uuid.UUID]bool{}
for _, e := range entries {
if e.RecipeID != nil {
recipeIDs[*e.RecipeID] = true
}
}
// aggregated := map[string]*ext.ShoppingItem{}
// for id := range recipeIDs {
// recipe, err := db.GetRecipe(ctx, id)
// if err != nil {
// continue
// }
// for _, ing := range recipe.Ingredients {
// key := strings.ToLower(ing.Name)
// if existing, ok := aggregated[key]; ok {
// if ing.Quantity != "" {
// existing.Quantity += "+" + ing.Quantity
// }
// } else {
// recipeIDCopy := id
// aggregated[key] = &ext.ShoppingItem{
// Name: ing.Name,
// Quantity: ing.Quantity,
// Unit: ing.Unit,
// Purchased: false,
// RecipeID: &recipeIDCopy,
// }
// }
// }
// }
aggregated := map[string]*ext.ShoppingItem{}
for id := range recipeIDs {
recipe, err := db.GetRecipe(ctx, id)
if err != nil {
continue
}
for _, ing := range recipe.Ingredients {
key := strings.ToLower(ing.Name)
if existing, ok := aggregated[key]; ok {
if ing.Quantity != "" {
existing.Quantity += "+" + ing.Quantity
}
} else {
recipeIDCopy := id
aggregated[key] = &ext.ShoppingItem{
Name: ing.Name,
Quantity: ing.Quantity,
Unit: ing.Unit,
Purchased: false,
RecipeID: &recipeIDCopy,
}
}
}
}
// items := make([]ext.ShoppingItem, 0, len(aggregated))
// for _, item := range aggregated {
// items = append(items, *item)
// }
items := make([]ext.ShoppingItem, 0, len(aggregated))
for _, item := range aggregated {
items = append(items, *item)
}
// itemsJSON, err := json.Marshal(items)
// if err != nil {
// return ext.ShoppingList{}, fmt.Errorf("marshal shopping items: %w", err)
// }
itemsJSON, err := json.Marshal(items)
if err != nil {
return ext.ShoppingList{}, fmt.Errorf("marshal shopping items: %w", err)
}
// row := db.pool.QueryRow(ctx, `
// insert into shopping_lists (week_start, items)
// values ($1, $2::jsonb)
// on conflict (week_start) do update set items = excluded.items, updated_at = now()
// returning id, created_at, updated_at
// `, weekStart, itemsJSON)
row := db.pool.QueryRow(ctx, `
insert into shopping_lists (week_start, items)
values ($1, $2::jsonb)
on conflict (week_start) do update set items = excluded.items, updated_at = now()
returning id, created_at, updated_at
`, weekStart, itemsJSON)
// var model generatedmodels.ModelPublicShoppingLists
// list := ext.ShoppingList{WeekStart: weekStart, Items: items}
// if err := row.Scan(&model.ID, &model.CreatedAt, &model.UpdatedAt); err != nil {
// return ext.ShoppingList{}, fmt.Errorf("upsert shopping list: %w", err)
// }
// list.ID = model.ID.UUID()
// list.CreatedAt = model.CreatedAt.Time()
// list.UpdatedAt = model.UpdatedAt.Time()
// return list, nil
// }
list := ext.ShoppingList{WeekStart: weekStart, Items: items}
if err := row.Scan(&list.ID, &list.CreatedAt, &list.UpdatedAt); err != nil {
return ext.ShoppingList{}, fmt.Errorf("upsert shopping list: %w", err)
}
return list, nil
}
// func scanRecipeRow(rows interface{ Scan(...any) error }) (ext.Recipe, error) {
// var model generatedmodels.ModelPublicRecipes
// var tags []string
// if err := rows.Scan(&model.ID, &model.Name, &model.Cuisine, &model.PrepTimeMinutes, &model.CookTimeMinutes, &model.Servings,
// &model.Ingredients, &model.Instructions, &tags, &model.Rating, &model.Notes, &model.CreatedAt, &model.UpdatedAt); err != nil {
// return ext.Recipe{}, fmt.Errorf("scan recipe: %w", err)
// }
// if tags == nil {
// tags = []string{}
// }
// return recipeFromModel(model, tags), nil
// }
func scanRecipeRow(rows interface{ Scan(...any) error }) (ext.Recipe, error) {
var r ext.Recipe
var cuisine, notes *string
var ingredientsBytes, instructionsBytes []byte
if err := rows.Scan(&r.ID, &r.Name, &cuisine, &r.PrepTimeMinutes, &r.CookTimeMinutes, &r.Servings,
&ingredientsBytes, &instructionsBytes, &r.Tags, &r.Rating, &notes, &r.CreatedAt, &r.UpdatedAt); err != nil {
return ext.Recipe{}, fmt.Errorf("scan recipe: %w", err)
}
r.Cuisine = strVal(cuisine)
r.Notes = strVal(notes)
if r.Tags == nil {
r.Tags = []string{}
}
if err := json.Unmarshal(ingredientsBytes, &r.Ingredients); err != nil {
r.Ingredients = []ext.Ingredient{}
}
if err := json.Unmarshal(instructionsBytes, &r.Instructions); err != nil {
r.Instructions = []string{}
}
return r, nil
}

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