Compare commits

...

229 Commits

Author SHA1 Message Date
argoyle 06aeedc3b0 Merge branch 'next-release' into 'main'
chore(release): prepare for v0.8.0

See merge request unboundsoftware/schemas!624
2025-11-21 11:32:33 +01:00
Unbound Release fce85782f0 chore(release): prepare for v0.8.0 2025-11-21 11:32:33 +01:00
argoyle 9cd8218eb4 Merge branch 'test/cache-reduce-goroutines-stability' into 'main'
test(cache): reduce goroutines for race detector stability

See merge request unboundsoftware/schemas!633
2025-11-21 11:19:28 +01:00
argoyle 98ef62b144 Merge branch 'renovate/github.com-auth0-go-jwt-middleware-v2-2.x' into 'main'
fix(deps): update module github.com/auth0/go-jwt-middleware/v2 to v2.3.1

See merge request unboundsoftware/schemas!630
2025-11-21 11:10:23 +01:00
argoyle e0cdd2aa58 Merge branch 'renovate/golang.org-x-crypto-0.x' into 'main'
fix(deps): update module golang.org/x/crypto to v0.45.0

See merge request unboundsoftware/schemas!629
2025-11-21 11:09:17 +01:00
argoyle e22e8b339c Merge branch 'renovate/node-24.x' into 'main'
chore(deps): update node.js to v24

See merge request unboundsoftware/schemas!627
2025-11-21 11:09:03 +01:00
argoyle 6404f7a497 test(cache): reduce goroutines for race detector stability
Decrease the number of goroutines in concurrent read and write tests to 
minimize race conditions during testing. This ensures more reliable 
test results and makes it easier to identify concurrency issues.
2025-11-21 11:06:36 +01:00
argoyle 5dc5043d46 Merge branch 'feat/cache-concurrency-logging' into 'main'
feat(cache): add concurrency safety and logging improvements

See merge request unboundsoftware/schemas!631
2025-11-21 10:45:48 +01:00
argoyle bcca005256 Merge branch 'feat/add-health-check-endpoints' into 'main'
feat(health): add health checking endpoints and logic

See merge request unboundsoftware/schemas!632
2025-11-21 10:38:01 +01:00
argoyle a9dea19531 feat(health): add health checking endpoints and logic
Introduce health checking functionality with liveness and readiness 
endpoints to monitor the application's status. Implement a health 
checker that verifies database connectivity and provides a simple 
liveness check. Update service routing to use the new health 
checker functionality. Add corresponding unit tests for validation.
2025-11-21 10:24:34 +01:00
argoyle 130e92dc5f feat(cache): add concurrency safety and logging improvements
Implement read-write mutex locks for cache functions to ensure
concurrency safety. Add debug logging for cache updates to enhance
traceability of operations. Optimize user addition logic to prevent
duplicates. Introduce a new test file for comprehensive cache
functionality testing, ensuring reliable behavior.
2025-11-21 10:21:08 +01:00
Renovate c4112a005f fix(deps): update module github.com/auth0/go-jwt-middleware/v2 to v2.3.1 2025-11-21 08:08:20 +00:00
Renovate 549f6617df fix(deps): update module golang.org/x/crypto to v0.45.0 2025-11-20 22:08:30 +00:00
argoyle a1b0f49aab Merge branch 'feat/cache-hashed-api-key-storage' into 'main'
feat(cache): implement hashed API key storage and retrieval

See merge request unboundsoftware/schemas!628
2025-11-20 22:30:49 +01:00
argoyle 4468903535 feat(cache): implement hashed API key storage and retrieval
Adds a new hashed key storage mechanism for API keys in the cache. 
Replaces direct mapping to API keys with composite keys based on 
organizationId and name. Implements searching of API keys using 
hash comparisons for improved security. Updates related tests to 
ensure correct functionality and validate the hashing. Also, 
adds support for a new dependency `golang.org/x/crypto`.
2025-11-20 22:11:24 +01:00
Renovate df054ca451 chore(deps): update node.js to v24 2025-11-20 21:09:40 +00:00
argoyle 1e2236dc9e Merge branch 'add-claude-md-documentation' into 'main'
feat: add CLAUDE.md for project documentation and guidelines

See merge request unboundsoftware/schemas!625
2025-11-20 21:50:16 +01:00
argoyle 6ccd7f4f25 feat: add CLAUDE.md for project documentation and guidelines
Adds CLAUDE.md to provide comprehensive documentation for the 
GraphQL schema registry service, covering architecture, event 
sourcing, GraphQL layer, schema merging, authentication, 
Cosmo Router integration, and development workflow. Updates 
.gitignore to include the claude directory.
2025-11-20 21:36:58 +01:00
argoyle b1a46f9d4e Merge branch 'enhance-api-key-handling-logging' into 'main'
fix: enhance API key handling and logging in middleware

See merge request unboundsoftware/schemas!623
2025-11-20 21:26:21 +01:00
argoyle 47dbf827f2 fix: add command executor interface for better testing
Introduce the CommandExecutor interface to abstract command execution, 
allowing for easier mocking in tests. Implement DefaultCommandExecutor 
to use the os/exec package for executing commands. Update the 
GenerateCosmoRouterConfig function to utilize the new 
GenerateCosmoRouterConfigWithExecutor function that accepts a command 
executor parameter. Add a MockCommandExecutor for simulating command 
execution in unit tests with realistic behavior based on input YAML 
files. This enhances test coverage and simplifies error handling.
2025-11-20 21:09:00 +01:00
argoyle df44ddbb8e test: enhance assertions for version and subscription config
Update version check to validate it is a non-empty string. Improve 
assertions for the subscription configuration by ensuring the presence 
of required fields and correct types. Adapt checks for routing URLs 
and decentralize subscription validation for more robust testing. 
These changes ensure better verification of configuration 
integrity and correctness in test scenarios.
2025-11-20 18:24:49 +01:00
argoyle 9368d77bc8 feat: add latestSchema query for retrieving schema updates
Implements the `latestSchema` query to fetch the latest schema 
updates for an organization. This change enhances the GraphQL API by
allowing clients to retrieve the most recent schema version and its 
associated subgraphs. The resolver performs necessary access checks, 
logs relevant information, and generates the Cosmo router configuration 
from fetched subgraph SDLs, returning structured schema update details.
2025-11-20 18:24:36 +01:00
argoyle 4d18cf4175 feat(tests): add unit tests for WebSocket initialization logic
Adds unit tests for the WebSocket initialization function to validate
behavior with valid, invalid, and absent API keys. Introduces a mock
cache implementation to simulate organization retrieval based on
hashed API keys. Ensures proper context value setting upon
initialization, enhancing test coverage and reliability for API key
handling in WebSocket connections.
2025-11-20 14:25:02 +01:00
argoyle bb0c08be06 fix: enhance API key handling and logging in middleware
Refactor API key processing to improve clarity and reduce code 
duplication. Introduce detailed logging for schema updates and 
initializations, capturing relevant context information. Use 
background context for async operations to avoid blocking. 
Implement organization lookup logic in the WebSocket init 
function for consistent API key handling across connections.
2025-11-20 12:58:15 +01:00
argoyle a9a47c1690 Merge branch 'renovate/gitleaks-gitleaks-8.x' into 'main'
chore(deps): update pre-commit hook gitleaks/gitleaks to v8.29.1

See merge request unboundsoftware/schemas!622
2025-11-20 09:21:24 +01:00
Renovate de073ce2da chore(deps): update pre-commit hook gitleaks/gitleaks to v8.29.1 2025-11-19 21:58:54 +00:00
argoyle af045687ae Merge branch 'next-release' into 'main'
chore(release): prepare for v0.7.0

See merge request unboundsoftware/schemas!598
2025-11-19 12:01:31 +01:00
Unbound Release c90ee3c9b1 chore(release): prepare for v0.7.0 2025-11-19 12:01:31 +01:00
argoyle 83e99e7d0a Merge branch 'feat/add-cosmo-router-config-pubsub' into 'main'
feat: add Cosmo Router config generation and PubSub support

See merge request unboundsoftware/schemas!621
2025-11-19 11:42:36 +01:00
argoyle 80daed081d feat: add Cosmo Router config generation and PubSub support
Creates a new `GenerateCosmoRouterConfig` function to build and 
serialize a Cosmo Router configuration from subgraphs. Implements 
PubSub mechanism for managing schema updates, allowing 
subscription to updates. Adds Subscription resolver and updates 
existing structures to accommodate new functionalities. This 
enhances the system's capabilities for dynamic updates and 
configuration management.
2025-11-19 11:29:30 +01:00
argoyle f6e4458efa Merge branch 'renovate/golang-1.25.4' into 'main'
chore(deps): update golang:1.25.4 docker digest to efe81fa

See merge request unboundsoftware/schemas!620
2025-11-18 21:29:05 +01:00
Renovate 11b9a46802 chore(deps): update golang:1.25.4 docker digest to efe81fa 2025-11-18 11:58:47 +00:00
argoyle d2324d27df Merge branch 'renovate/github.com-wundergraph-graphql-go-tools-v2-2.x' into 'main'
fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.238

See merge request unboundsoftware/schemas!619
2025-11-17 10:25:53 +01:00
Renovate c496ed025e fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.238 2025-11-17 08:58:02 +00:00
argoyle 4773ada816 Merge branch 'renovate/golangci-golangci-lint-2.x' into 'main'
chore(deps): update pre-commit hook golangci/golangci-lint to v2.6.2

See merge request unboundsoftware/schemas!618
2025-11-14 16:22:16 +01:00
Renovate 6447a299b3 chore(deps): update pre-commit hook golangci/golangci-lint to v2.6.2 2025-11-14 13:58:47 +00:00
argoyle b8e9e0d632 Merge branch 'renovate/github.com-alecthomas-kong-1.x' into 'main'
fix(deps): update module github.com/alecthomas/kong to v1.13.0

See merge request unboundsoftware/schemas!617
2025-11-14 12:57:15 +01:00
Renovate 3179bb7ae3 fix(deps): update module github.com/alecthomas/kong to v1.13.0 2025-11-13 22:59:09 +00:00
argoyle 73f6fe31d9 Merge branch 'renovate/github.com-99designs-gqlgen-0.x' into 'main'
fix(deps): update module github.com/99designs/gqlgen to v0.17.83

See merge request unboundsoftware/schemas!616
2025-11-11 10:50:15 +01:00
Renovate 87eab3a04e fix(deps): update module github.com/99designs/gqlgen to v0.17.83 2025-11-10 23:10:38 +00:00
argoyle e751d35e38 Merge branch 'renovate/github.com-wundergraph-graphql-go-tools-v2-2.x' into 'main'
fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.237

See merge request unboundsoftware/schemas!615
2025-11-10 17:54:01 +01:00
Renovate 84e30c0771 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.237 2025-11-10 13:58:40 +00:00
argoyle bdb3b80f4a Merge branch 'renovate/github.com-99designs-gqlgen-0.x' into 'main'
fix(deps): update module github.com/99designs/gqlgen to v0.17.82

See merge request unboundsoftware/schemas!614
2025-11-07 08:36:04 +01:00
Renovate d08c9663fe fix(deps): update module github.com/99designs/gqlgen to v0.17.82 2025-11-07 01:59:53 +00:00
argoyle a17b8dd122 Merge branch 'renovate/golang-1.x' into 'main'
chore(deps): update golang docker tag to v1.25.4

See merge request unboundsoftware/schemas!613
2025-11-06 07:03:06 +01:00
Renovate ae8cf15b0a chore(deps): update golang docker tag to v1.25.4 2025-11-05 22:11:22 +00:00
argoyle 8fd2c1790b Merge branch 'renovate/gitleaks-gitleaks-8.x' into 'main'
chore(deps): update pre-commit hook gitleaks/gitleaks to v8.29.0

See merge request unboundsoftware/schemas!612
2025-11-05 06:52:56 +01:00
Renovate fe4efa7b97 chore(deps): update pre-commit hook gitleaks/gitleaks to v8.29.0 2025-11-05 01:59:15 +00:00
argoyle 78599baa5b Merge branch 'renovate/golangci-golangci-lint-2.x' into 'main'
chore(deps): update pre-commit hook golangci/golangci-lint to v2.6.1

See merge request unboundsoftware/schemas!611
2025-11-04 13:43:40 +01:00
argoyle 8ad47c2e54 Merge branch 'renovate/golang-1.25.3' into 'main'
chore(deps): update golang:1.25.3 docker digest to 9ac0edc

See merge request unboundsoftware/schemas!610
2025-11-04 13:43:05 +01:00
Renovate ecce66b579 chore(deps): update pre-commit hook golangci/golangci-lint to v2.6.1 2025-11-04 11:59:20 +00:00
Renovate a26e66649a chore(deps): update golang:1.25.3 docker digest to 9ac0edc 2025-11-04 11:59:17 +00:00
argoyle b8b5951883 Merge branch 'renovate/github.com-wundergraph-graphql-go-tools-v2-2.x' into 'main'
fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.236

See merge request unboundsoftware/schemas!609
2025-10-31 02:14:34 +01:00
Renovate f860d80a81 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.236 2025-10-31 00:57:50 +00:00
argoyle d27331cef0 Merge branch 'renovate/golangci-golangci-lint-2.x' into 'main'
chore(deps): update pre-commit hook golangci/golangci-lint to v2.6.0

See merge request unboundsoftware/schemas!607
2025-10-30 11:43:43 +01:00
Renovate e343ff7538 chore(deps): update pre-commit hook golangci/golangci-lint to v2.6.0 2025-10-30 11:19:43 +01:00
argoyle 4fe45bf125 Merge branch 'renovate/github.com-vektah-gqlparser-v2-2.x' into 'main'
fix(deps): update module github.com/vektah/gqlparser/v2 to v2.5.31

See merge request unboundsoftware/schemas!608
2025-10-30 07:56:05 +01:00
Renovate 89e35c4ee7 fix(deps): update module github.com/vektah/gqlparser/v2 to v2.5.31 2025-10-30 01:59:20 +00:00
argoyle 0f077a53bb Merge branch 'renovate/github.com-wundergraph-graphql-go-tools-v2-2.x' into 'main'
fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.235

See merge request unboundsoftware/schemas!606
2025-10-29 15:03:09 +01:00
Renovate 51fd889f6a fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.235 2025-10-29 12:58:02 +00:00
argoyle e13c9b1a28 Merge branch 'renovate/goreleaser-goreleaser-2.x' into 'main'
chore(deps): update goreleaser/goreleaser docker tag to v2.12.7

See merge request unboundsoftware/schemas!605
2025-10-25 09:55:26 +02:00
argoyle 576a530886 Merge branch 'renovate/github.com-wundergraph-graphql-go-tools-v2-2.x' into 'main'
fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.234

See merge request unboundsoftware/schemas!604
2025-10-25 09:54:52 +02:00
Renovate aa9fea580f chore(deps): update goreleaser/goreleaser docker tag to v2.12.7 2025-10-24 18:57:11 +00:00
Renovate ccb8a10f92 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.234 2025-10-24 17:57:43 +00:00
argoyle d86beb8308 Merge branch 'renovate/github.com-wundergraph-graphql-go-tools-v2-2.x' into 'main'
fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.233

See merge request unboundsoftware/schemas!603
2025-10-23 20:50:39 +02:00
Renovate 429cf6e66d fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.233 2025-10-23 15:58:14 +00:00
argoyle 2dd0ac9392 Merge branch 'renovate/goreleaser-goreleaser-2.x' into 'main'
chore(deps): update goreleaser/goreleaser docker tag to v2.12.6

See merge request unboundsoftware/schemas!601
2025-10-22 11:47:28 +02:00
argoyle 780244b165 Merge branch 'renovate/github.com-wundergraph-graphql-go-tools-v2-2.x' into 'main'
fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.232

See merge request unboundsoftware/schemas!602
2025-10-22 11:21:04 +02:00
Renovate 540e90a577 chore(deps): update goreleaser/goreleaser docker tag to v2.12.6 2025-10-22 11:19:22 +02:00
Renovate 1b527bab74 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.232 2025-10-22 08:57:48 +00:00
argoyle 5b64cd0165 Merge branch 'renovate/golang-1.25.3' into 'main'
chore(deps): update golang:1.25.3 docker digest to 69d1009

See merge request unboundsoftware/schemas!600
2025-10-21 14:19:45 +02:00
Renovate e2fb56f505 chore(deps): update golang:1.25.3 docker digest to 69d1009 2025-10-21 09:58:16 +00:00
argoyle 6bdbe36c7f Merge branch 'renovate/github.com-wundergraph-graphql-go-tools-v2-2.x' into 'main'
fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.231

See merge request unboundsoftware/schemas!599
2025-10-20 17:17:03 +02:00
Renovate 2a1415bc35 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.231 2025-10-20 14:58:41 +00:00
argoyle 8e53b1fbf4 Merge branch 'ci-add-git-cliff-changelog-config' into 'main'
ci: add git-cliff configuration for changelog generation

See merge request unboundsoftware/schemas!597
2025-10-14 11:37:20 +02:00
argoyle 2cf992c948 ci: add git-cliff configuration for changelog generation
This commit introduces a new configuration file for git-cliff to 
automate changelog generation. It defines templates for the header, 
body, and footer of the changelog, ensuring that all notable changes 
are documented effectively. The configuration enables parsing of 
conventional commit messages, allowing for better organization of 
changes by type. This enhancement streamlines the release process and 
improves project maintainability.
2025-10-14 11:28:16 +02:00
argoyle ca9e7c3aa0 Merge branch 'next-release' into 'main'
chore(release): prepare for v0.6.6

See merge request unboundsoftware/schemas!584
2025-10-14 11:17:51 +02:00
Unbound Release db762cb496 chore(release): prepare for v0.6.6 2025-10-14 11:17:50 +02:00
argoyle c33885a0ab Merge branch 'renovate/golang-1.x' into 'main'
chore(deps): update golang docker tag to v1.25.3

See merge request unboundsoftware/schemas!596
2025-10-14 08:59:55 +02:00
Renovate 1135d77c35 chore(deps): update golang docker tag to v1.25.3 2025-10-13 23:58:57 +00:00
argoyle d1d14c1097 Merge branch 'renovate/github.com-wundergraph-graphql-go-tools-v2-2.x' into 'main'
fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.230

See merge request unboundsoftware/schemas!595
2025-10-10 10:32:46 +02:00
Renovate 41075e06a3 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.230 2025-10-09 15:02:02 +00:00
argoyle a7d4c01089 Merge branch 'renovate/golang-1.x' into 'main'
chore(deps): update golang docker tag to v1.25.2

See merge request unboundsoftware/schemas!594
2025-10-08 09:19:12 +02:00
Renovate 7c30a66144 chore(deps): update golang docker tag to v1.25.2 2025-10-07 22:08:51 +00:00
argoyle 98c17196c8 Merge branch 'renovate/github.com-pressly-goose-v3-3.x' into 'main'
fix(deps): update module github.com/pressly/goose/v3 to v3.26.0

See merge request unboundsoftware/schemas!593
2025-10-05 11:04:54 +02:00
Renovate 6635019035 fix(deps): update module github.com/pressly/goose/v3 to v3.26.0 2025-10-03 13:59:47 +00:00
argoyle 1ae73e7203 Merge branch 'renovate/alessandrojcm-commitlint-pre-commit-hook-9.x' into 'main'
chore(deps): update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.23.0

See merge request unboundsoftware/schemas!592
2025-10-02 17:06:07 +02:00
Renovate 91c02eb499 chore(deps): update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.23.0 2025-10-02 10:00:25 +00:00
argoyle 32fc6b2641 Merge branch 'renovate/golang-1.25.1' into 'main'
chore(deps): update golang:1.25.1 docker digest to 12640a4

See merge request unboundsoftware/schemas!591
2025-10-01 19:19:11 +02:00
Renovate ce86af4486 chore(deps): update golang:1.25.1 docker digest to 12640a4 2025-10-01 13:58:52 +00:00
argoyle 5258a68682 Merge branch 'renovate/goreleaser-goreleaser-2.x' into 'main'
chore(deps): update goreleaser/goreleaser docker tag to v2.12.5

See merge request unboundsoftware/schemas!590
2025-10-01 15:16:26 +02:00
Renovate 1d69be641c chore(deps): update goreleaser/goreleaser docker tag to v2.12.5 2025-10-01 12:57:39 +00:00
argoyle 3c571a1dc3 Merge branch 'renovate/goreleaser-goreleaser-2.x' into 'main'
chore(deps): update goreleaser/goreleaser docker tag to v2.12.4

See merge request unboundsoftware/schemas!589
2025-10-01 07:35:32 +02:00
Renovate 4c16e293c0 chore(deps): update goreleaser/goreleaser docker tag to v2.12.4 2025-10-01 02:56:55 +00:00
argoyle 18fb8da472 Merge branch 'renovate/github.com-99designs-gqlgen-0.x' into 'main'
fix(deps): update module github.com/99designs/gqlgen to v0.17.81

See merge request unboundsoftware/schemas!588
2025-09-26 11:24:45 +02:00
Renovate c9fa0ecc2c fix(deps): update module github.com/99designs/gqlgen to v0.17.81 2025-09-25 22:58:35 +00:00
argoyle 35db2fc74e Merge branch 'renovate/github.com-wundergraph-graphql-go-tools-v2-2.x' into 'main'
fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.229

See merge request unboundsoftware/schemas!587
2025-09-25 16:16:31 +02:00
Renovate e10ab9d75f fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.229 2025-09-25 13:57:58 +00:00
argoyle d6100bcb76 Merge branch 'renovate/goreleaser-goreleaser-2.x' into 'main'
chore(deps): update goreleaser/goreleaser docker tag to v2.12.3

See merge request unboundsoftware/schemas!586
2025-09-25 07:59:03 +02:00
argoyle 47d122aa8d Merge branch 'renovate/github.com-wundergraph-graphql-go-tools-v2-2.x' into 'main'
fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.228

See merge request unboundsoftware/schemas!585
2025-09-25 07:58:22 +02:00
Renovate b089801216 chore(deps): update goreleaser/goreleaser docker tag to v2.12.3 2025-09-25 03:56:44 +00:00
Renovate 3c071cb300 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.228 2025-09-24 15:57:59 +00:00
argoyle 8fe56f6ce0 Merge branch 'renovate/github.com-wundergraph-graphql-go-tools-v2-2.x' into 'main'
fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.227

See merge request unboundsoftware/schemas!583
2025-09-23 16:11:22 +02:00
Renovate c9aa447a9f fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.227 2025-09-23 13:59:32 +00:00
argoyle 575f97e8d1 Merge branch 'renovate/golangci-golangci-lint-2.x' into 'main'
chore(deps): update pre-commit hook golangci/golangci-lint to v2.5.0

See merge request unboundsoftware/schemas!582
2025-09-22 11:22:53 +02:00
Renovate dde2a965ec chore(deps): update pre-commit hook golangci/golangci-lint to v2.5.0 2025-09-21 19:57:48 +00:00
argoyle b6ae9826b2 Merge branch 'next-release' into 'main'
chore(release): prepare for v0.6.5

See merge request unboundsoftware/schemas!575
2025-09-18 13:57:02 +02:00
Unbound Release 787f388168 chore(release): prepare for v0.6.5 2025-09-18 13:57:02 +02:00
argoyle c4b01dbe07 Merge branch 'renovate/eventsourced' into 'main'
fix(deps): update module gitlab.com/unboundsoftware/eventsourced/eventsourced to v1.19.3

See merge request unboundsoftware/schemas!581
2025-09-18 12:14:01 +02:00
Renovate 992a95ff8d fix(deps): update module gitlab.com/unboundsoftware/eventsourced/eventsourced to v1.19.3 2025-09-18 09:58:42 +00:00
argoyle 0c31b8c2b9 Merge branch 'renovate/goreleaser-goreleaser-2.x' into 'main'
chore(deps): update goreleaser/goreleaser docker tag to v2.12.2

See merge request unboundsoftware/schemas!580
2025-09-18 09:36:58 +02:00
Renovate b75b61f724 chore(deps): update goreleaser/goreleaser docker tag to v2.12.2 2025-09-18 02:57:23 +00:00
argoyle 0e76377865 Merge branch 'renovate/github.com-99designs-gqlgen-0.x' into 'main'
fix(deps): update module github.com/99designs/gqlgen to v0.17.80

See merge request unboundsoftware/schemas!579
2025-09-17 22:08:20 +02:00
Renovate 52e58f8df8 fix(deps): update module github.com/99designs/gqlgen to v0.17.80 2025-09-17 21:19:09 +02:00
argoyle 35ba7679c9 Merge branch 'renovate/goreleaser-goreleaser-2.x' into 'main'
chore(deps): update goreleaser/goreleaser docker tag to v2.12.1

See merge request unboundsoftware/schemas!578
2025-09-16 16:44:34 +02:00
Renovate 9d5dc75c2b chore(deps): update goreleaser/goreleaser docker tag to v2.12.1 2025-09-16 02:56:58 +00:00
argoyle 83cb25a2bb Merge branch 'renovate/github.com-99designs-gqlgen-0.x' into 'main'
fix(deps): update module github.com/99designs/gqlgen to v0.17.79

See merge request unboundsoftware/schemas!577
2025-09-15 07:53:07 +02:00
Renovate e7a09e8322 fix(deps): update module github.com/99designs/gqlgen to v0.17.79 2025-09-15 07:19:10 +02:00
argoyle 2eae446669 Merge branch 'renovate/lietu-go-pre-commit-1.x' into 'main'
chore(deps): update pre-commit hook lietu/go-pre-commit to v1

See merge request unboundsoftware/schemas!576
2025-09-13 17:23:59 +02:00
Renovate 2e86f80727 chore(deps): update pre-commit hook lietu/go-pre-commit to v1 2025-09-13 09:12:24 +00:00
argoyle 1311a07dcb Merge branch 'renovate/github.com-wundergraph-graphql-go-tools-v2-2.x' into 'main'
fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.226

See merge request unboundsoftware/schemas!574
2025-09-12 14:17:49 +02:00
Renovate cd080189a9 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.226 2025-09-12 11:57:31 +00:00
argoyle 2d86c77c39 Merge branch 'next-release' into 'main'
chore(release): prepare for v0.6.4

See merge request unboundsoftware/schemas!573
2025-09-11 13:23:32 +02:00
Unbound Release eea7f86bbe chore(release): prepare for v0.6.4 2025-09-11 13:23:32 +02:00
argoyle baccd12e63 Merge branch 'renovate/eventsourced' into 'main'
fix(deps): update module gitlab.com/unboundsoftware/eventsourced/amqp to v1.9.0

See merge request unboundsoftware/schemas!572
2025-09-11 12:09:50 +02:00
Renovate 7fe90ee9af fix(deps): update module gitlab.com/unboundsoftware/eventsourced/amqp to v1.9.0 2025-09-11 09:58:15 +00:00
argoyle a3b9cae8eb Merge branch 'next-release' into 'main'
chore(release): prepare for v0.6.3

See merge request unboundsoftware/schemas!561
2025-09-11 10:42:36 +02:00
Unbound Release 769cbd3f14 chore(release): prepare for v0.6.3 2025-09-11 10:42:35 +02:00
Renovate ecc4da28ff fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.225 2025-09-09 14:59:00 +00:00
Renovate 4a14f41324 chore(deps): update golang:1.25.1 docker digest to 53f7808 2025-09-08 23:23:22 +00:00
Renovate 3f8fdce292 chore(deps): update golang docker tag to v1.25.1 2025-09-03 19:58:22 +00:00
Renovate 335c419a44 chore(deps): update goreleaser/goreleaser docker tag to v2.12.0 2025-09-03 03:57:02 +00:00
Renovate 82d2aa1812 fix(deps): update module go.opentelemetry.io/contrib/bridges/otelslog to v0.13.0 2025-09-01 11:25:33 +02:00
Renovate 24bbb39c3d fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.224 2025-09-01 08:57:51 +00:00
Renovate d98a20afff fix(deps): update opentelemetry-go monorepo 2025-08-29 20:07:41 +00:00
Renovate 737b133b8b fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.223 2025-08-28 09:57:27 +00:00
Renovate 177f923fc2 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.222 2025-08-28 08:52:22 +02:00
Renovate fd0c89dce9 fix(deps): update module github.com/stretchr/testify to v1.11.1 2025-08-27 12:00:45 +00:00
Renovate 1e58f8e0d5 fix(deps): update module github.com/pressly/goose/v3 to v3.25.0 2025-08-25 08:34:02 +02:00
Renovate 53349071aa fix(deps): update module github.com/stretchr/testify to v1.11.0 2025-08-24 16:59:02 +00:00
Renovate efe9e0a5a0 chore(deps): update golang:1.25.0 docker digest to f6b9e1a 2025-08-22 18:58:38 +00:00
Unbound Release 406cb8e4d0 chore(release): prepare for v0.6.2 2025-08-22 13:47:28 +00:00
argoyle 6e7ee0110b chore: remove conflicts entry from homebrew-taps config
Remove the conflicts entry for the unbound-schemas formula in the 
homebrew-taps configuration to simplify the integration process. This 
change enhances compatibility and streamlines the build setup.
2025-08-22 15:32:55 +02:00
Unbound Release 6eac3a7796 chore(release): prepare for v0.6.1 2025-08-22 14:33:00 +02:00
Renovate 890a6fd50e fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.221 2025-08-22 09:58:39 +00:00
Renovate e1cf0d8cb3 chore(deps): update golang docker tag to v1.25.0 2025-08-14 18:57:49 +00:00
Renovate 63d70d7e35 chore(deps): update golang:1.24.6 docker digest to cd8f653 2025-08-14 13:57:53 +00:00
Renovate 35a454f8b1 fix(deps): update module github.com/sparetimecoders/goamqp to v0.3.3 2025-08-14 13:01:31 +00:00
Renovate 6651553246 chore(deps): update pre-commit hook golangci/golangci-lint to v2.4.0 2025-08-13 20:58:38 +00:00
Renovate 311ef3f530 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.220 2025-08-13 09:58:44 +00:00
Renovate d8cce2fb05 chore(deps): update golang:1.24.6 docker digest to 958bfd1 2025-08-12 22:58:04 +00:00
Renovate 309b9423a4 chore(deps): update pre-commit hook pre-commit/pre-commit-hooks to v6 2025-08-09 19:58:04 +00:00
Renovate 323145a076 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.219 2025-08-08 10:57:11 +00:00
Renovate 28b4dc5572 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.218 2025-08-07 15:57:39 +00:00
Renovate c1173c20ae chore(deps): update golang docker tag to v1.24.6 2025-08-06 20:58:16 +00:00
Renovate 2d12077016 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.217 2025-08-06 12:57:31 +00:00
Renovate c38db83cd1 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.216 2025-08-06 09:58:51 +00:00
Renovate 25bd438d05 chore(deps): update pre-commit hook golangci/golangci-lint to v2.3.1 2025-08-02 21:58:23 +00:00
Renovate f4355d620f fix(deps): update module github.com/golang-jwt/jwt/v5 to v5.3.0 2025-07-31 22:14:05 +02:00
Renovate 887ba69c5e fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.215 2025-07-31 22:12:36 +02:00
Renovate 8e6c7aade7 chore(deps): update goreleaser/goreleaser docker tag to v2.11.2 2025-07-31 14:11:53 +00:00
Renovate 0f0f50111f fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.213 2025-07-29 00:05:28 +00:00
Renovate 81a895e5a6 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.212 2025-07-28 15:57:36 +00:00
Renovate 0fab1d5098 fix(deps): update module github.com/99designs/gqlgen to v0.17.78 2025-07-28 12:21:43 +02:00
Renovate 4fbfc0f42e chore(deps): update goreleaser/goreleaser docker tag to v2.11.1 2025-07-25 02:57:47 +00:00
Renovate 0ba3706c12 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.210 2025-07-22 19:56:58 +00:00
Renovate c44ac87b5e chore(deps): update golang:1.24.5 docker digest to 0a156a4 2025-07-22 04:57:28 +00:00
Renovate deefcd7045 chore(deps): update pre-commit hook golangci/golangci-lint to v2.3.0 2025-07-21 20:04:12 +02:00
Renovate 4933857351 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.208 2025-07-21 14:57:27 +00:00
Renovate 18616e8346 fix(deps): update module github.com/alecthomas/kong to v1.12.1 2025-07-21 07:37:41 +02:00
Renovate dd075afb8d chore(deps): update pre-commit hook gitleaks/gitleaks to v8.28.0 2025-07-20 16:56:45 +00:00
Renovate 003bd3cd50 fix(deps): update module github.com/golang-jwt/jwt/v5 to v5.2.3 2025-07-15 21:09:48 +02:00
Renovate a68fb437dc fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.207 2025-07-15 17:54:08 +00:00
Renovate 8aa57e8f68 chore(deps): update pre-commit hook golangci/golangci-lint to v2.2.2 2025-07-11 12:54:20 +00:00
Renovate 84e8764054 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.204 2025-07-11 06:54:06 +00:00
Renovate c40bbad892 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.203 2025-07-10 09:54:19 +00:00
Renovate c159c6d20e chore(deps): update goreleaser/goreleaser docker tag to v2.11.0 2025-07-09 21:49:06 +02:00
Renovate ae0d796e93 chore(deps): update golang docker tag to v1.24.5 2025-07-09 18:55:13 +00:00
Renovate 1ec0f9a3a7 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.202 2025-07-09 12:54:39 +00:00
Renovate 0dad651959 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.200 2025-07-08 15:54:10 +00:00
Renovate 24fcc4e6a2 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.199 2025-07-07 15:54:28 +00:00
Renovate 4c7406d97b fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.198 2025-07-04 12:54:27 +00:00
Renovate 51affc5a55 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.197 2025-07-03 11:54:02 +00:00
Renovate 10871a9b32 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.196 2025-07-02 17:54:39 +00:00
Renovate 411b51f895 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.195 2025-07-02 02:22:25 +00:00
Renovate 6a478209ea chore(deps): update golang:1.24.4 docker digest to 9f820b6 2025-07-01 05:54:30 +00:00
Renovate 043ca65698 chore(deps): update pre-commit hook golangci/golangci-lint to v2.2.1 2025-06-29 21:53:56 +00:00
Unbound Release 8131042a1c chore(release): prepare for v0.6.0 2025-06-29 13:35:39 +02:00
Renovate 2fb2c1947a fix(deps): update module github.com/99designs/gqlgen to v0.17.76 2025-06-29 10:25:22 +02:00
Renovate 1cf4faa17f fix(deps): update module github.com/vektah/gqlparser/v2 to v2.5.30 2025-06-28 23:55:22 +00:00
Renovate 8bb6cb7279 chore(deps): update pre-commit hook golangci/golangci-lint to v2.2.0 2025-06-28 20:54:12 +00:00
Renovate 376ae41b4f fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.194 2025-06-28 09:21:08 +02:00
Renovate dc6e57e815 fix(deps): update module github.com/vektah/gqlparser/v2 to v2.5.29 2025-06-27 21:54:30 +00:00
Renovate f8c7de447a fix(deps): update module go.opentelemetry.io/contrib/bridges/otelslog to v0.12.0 2025-06-25 20:02:41 +00:00
Renovate 92050aa31f fix(deps): update opentelemetry-go monorepo 2025-06-25 07:54:48 +00:00
Renovate e20829bb2b fix(deps): update module github.com/alecthomas/kong to v1.12.0 2025-06-25 00:54:50 +00:00
Renovate 1918ec3da4 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.192 2025-06-19 09:03:08 +00:00
Renovate 735c387c58 fix(deps): update module github.com/99designs/gqlgen to v0.17.75 2025-06-16 14:55:10 +00:00
Renovate 98e2f660a6 fix(deps): update module github.com/vektah/gqlparser/v2 to v2.5.28 2025-06-15 20:54:42 +00:00
Renovate 7a0159a33f fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.190 2025-06-13 16:54:19 +00:00
argoyle b4447bb15e feat: add build version injection via CI_COMMIT argument
Inject the build version into the binary using the CI_COMMIT 
argument for better traceability of deployments. Update the 
Dockerfile to pass the commit hash to the build process, 
ensuring that each build contains version information tied 
to the specific commit.
2025-06-13 13:26:56 +02:00
argoyle 2948905005 feat(k8s): add OpenTelemetry exporter endpoint to deploy.yaml
Add the OTEL_EXPORTER_OTLP_ENDPOINT environment variable to 
the deployment configuration. This change enables the application 
to send telemetry data to the specified OpenTelemetry collector 
endpoint for better monitoring and observability.
2025-06-13 11:36:19 +02:00
Unbound Release 53141720ca chore(release): prepare for v0.5.3 2025-06-13 11:28:30 +02:00
argoyle e84df1db08 refactor: remove Sentry integration and replace with OpenTelemetry
Remove Sentry dependencies and configuration. Introduce monitoring 
setup for OpenTelemetry. Update logging to include log format 
options, and replace Sentry error handling middleware with 
monitoring handlers for GraphQL playground. Adjust environment 
variable handling to enhance configuration clarity and flexibility.
2025-06-13 11:00:52 +02:00
Renovate 9539e6bb1b fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.189 2025-06-12 19:14:49 +00:00
Renovate 291ef08ad7 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.188 2025-06-11 17:54:30 +00:00
Renovate 98b84772df chore(deps): update golang:1.24.4 docker digest to 3494bbe 2025-06-11 03:01:58 +00:00
Renovate 26278066b8 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.187 2025-06-10 10:53:51 +00:00
Unbound Release 5bdacce71a chore(release): prepare for v0.5.2 2025-06-09 06:33:38 +00:00
argoyle 0b4bbdeef0 fix(goreleaser): specify binary name in configuration
Adds the binary name "schemactl" to the Goreleaser 
configuration file to ensure proper build and release 
process for the Homebrew tap. This improves clarity 
and correctness in the configuration.
2025-06-09 08:22:34 +02:00
Unbound Release 3c3c939447 chore(release): prepare for v0.5.1 2025-06-09 08:00:57 +02:00
Renovate 93a12a2909 chore(deps): update goreleaser/goreleaser docker tag to v2.10.2 2025-06-09 07:10:55 +02:00
Renovate c36802570a chore(deps): update pre-commit hook gitleaks/gitleaks to v8.27.2 2025-06-09 00:54:24 +00:00
Renovate 5d64a3a45c chore(deps): update pre-commit hook gitleaks/gitleaks to v8.27.1 2025-06-08 02:53:51 +00:00
Renovate e55d3400e6 chore(deps): update golang docker tag to v1.24.4 2025-06-05 20:54:48 +00:00
Renovate 7da95e7566 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.186 2025-06-05 14:54:09 +00:00
Renovate b8ea2690fc fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.185 2025-06-04 10:54:07 +00:00
Renovate 3689486fa8 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.184 2025-06-03 22:54:02 +00:00
Renovate 1787815299 fix(deps): update module github.com/99designs/gqlgen to v0.17.74 2025-06-02 17:56:04 +00:00
Renovate 2886835d18 chore(deps): update pre-commit hook gitleaks/gitleaks to v8.27.0 2025-06-01 16:54:04 +00:00
Renovate 097e1274db fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.183 2025-05-28 18:55:37 +00:00
Renovate 640ede7de2 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.182 2025-05-27 10:53:51 +00:00
Renovate 122c87dab4 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.181 2025-05-23 10:53:47 +00:00
Renovate 4647d7ad1e chore(deps): update golang:1.24.3 docker digest to f255a7d 2025-05-22 11:23:04 +02:00
Renovate f2c73e8bf6 fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.180 2025-05-21 16:54:04 +00:00
Renovate ba7bbd082a fix(deps): update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.179 2025-05-19 11:54:14 +00:00
Renovate 2466d2a4ab fix(deps): update module github.com/khan/genqlient to v0.8.1 2025-05-18 19:53:47 +00:00
Renovate 86a61a1a64 fix(deps): update module github.com/getsentry/sentry-go to v0.33.0 2025-05-15 12:55:30 +00:00
Renovate 75e85c0339 fix(deps): update module github.com/alecthomas/kong to v1.11.0 2025-05-15 11:04:36 +00:00
42 changed files with 5610 additions and 2438 deletions
+2
View File
@@ -1,9 +1,11 @@
.idea
.claude
.testCoverage.txt
.testCoverage.txt.tmp
coverage.html
/exported
/release
/schemactl
/service
CHANGES.md
VERSION
+4 -4
View File
@@ -21,7 +21,7 @@ variables:
check:
stage: .pre
image: amd64/golang:1.24.3@sha256:f169469d1e8328fd0e26a2b5156f670922a2afc0ca8c984ec17e1be51ca94c30
image: amd64/golang:1.25.4@sha256:efe81fa41fdf81fb873ab7cd931b9bb29bd10aced6c252cbd91739c34e654f01
script:
- go install mvdan.cc/gofumpt@latest
- go install golang.org/x/tools/cmd/goimports@latest
@@ -40,7 +40,7 @@ build:
vulnerabilities:
stage: build
image: amd64/golang:1.24.3@sha256:f169469d1e8328fd0e26a2b5156f670922a2afc0ca8c984ec17e1be51ca94c30
image: amd64/golang:1.25.4@sha256:efe81fa41fdf81fb873ab7cd931b9bb29bd10aced6c252cbd91739c34e654f01
script:
- go install golang.org/x/vuln/cmd/govulncheck@latest
- govulncheck ./...
@@ -61,7 +61,7 @@ deploy-prod:
check_release:
stage: test
image:
name: goreleaser/goreleaser:v2.9.0@sha256:da5dbdb1fe1c8fa9a73e152070e4a9b178c3500c3db383d8cff2f206b06ef748
name: goreleaser/goreleaser:v2.12.7@sha256:a2a47c0dda85f8d40eaaa5b9765bf76c69addb6060666f8a51441410d9b008e9
entrypoint: [ '' ]
variables:
GOTOOLCHAIN: auto
@@ -74,7 +74,7 @@ release:
needs:
- unbound_release_prepare_release
image:
name: goreleaser/goreleaser:v2.9.0@sha256:da5dbdb1fe1c8fa9a73e152070e4a9b178c3500c3db383d8cff2f206b06ef748
name: goreleaser/goreleaser:v2.12.7@sha256:a2a47c0dda85f8d40eaaa5b9765bf76c69addb6060666f8a51441410d9b008e9
entrypoint: [ '' ]
variables:
# Disable shallow cloning so that goreleaser can diff between tags to
+8 -4
View File
@@ -16,18 +16,22 @@ builds:
- amd64
- arm64
brews:
homebrew_casks:
- name: unbound-schemas
repository:
owner: unboundsoftware
name: homebrew-taps
directory: Formula
install: |
bin.install "schemactl"
binaries: [schemactl]
directory: Casks
commit_author:
name: "Joakim Olsson"
email: joakim@unbound.se
homepage: "https://schemas.unbound.se/"
hooks:
post:
install: |
# replace foo with the actual binary name
system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/schemactl"]
archives:
- id: unbound-schemas
+5 -5
View File
@@ -2,7 +2,7 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@@ -18,7 +18,7 @@ repos:
- --project
- unboundsoftware/schemas
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
rev: v9.22.0
rev: v9.23.0
hooks:
- id: commitlint
stages: [ commit-msg ]
@@ -32,16 +32,16 @@ repos:
- -local
- gitlab.com/unboundsoftware/schemas
- repo: https://github.com/lietu/go-pre-commit
rev: v0.1.0
rev: v1.0.0
hooks:
- id: go-test
- id: gofumpt
- repo: https://github.com/golangci/golangci-lint
rev: v2.1.6
rev: v2.6.2
hooks:
- id: golangci-lint-full
- repo: https://github.com/gitleaks/gitleaks
rev: v8.26.0
rev: v8.29.1
hooks:
- id: gitleaks
exclude: '^ctl/generated.go|graph/generated/.*$|^graph/model/models_gen.go|^tools/.*$$'
+1
View File
@@ -0,0 +1 @@
{"version":"v0.8.0"}
+545
View File
@@ -2,6 +2,263 @@
All notable changes to this project will be documented in this file.
## [0.8.0] - 2025-11-21
### 🚀 Features
- *(tests)* Add unit tests for WebSocket initialization logic
- Add latestSchema query for retrieving schema updates
- Add CLAUDE.md for project documentation and guidelines
- *(cache)* Implement hashed API key storage and retrieval
- *(health)* Add health checking endpoints and logic
- *(cache)* Add concurrency safety and logging improvements
### 🐛 Bug Fixes
- Enhance API key handling and logging in middleware
- Add command executor interface for better testing
- *(deps)* Update module golang.org/x/crypto to v0.45.0
- *(deps)* Update module github.com/auth0/go-jwt-middleware/v2 to v2.3.1
### 🧪 Testing
- Enhance assertions for version and subscription config
- *(cache)* Reduce goroutines for race detector stability
### ⚙️ Miscellaneous Tasks
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.29.1
- *(deps)* Update node.js to v24
## [0.7.0] - 2025-11-19
### 🚀 Features
- Add Cosmo Router config generation and PubSub support
### 🐛 Bug Fixes
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.231
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.232
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.233
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.234
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.235
- *(deps)* Update module github.com/vektah/gqlparser/v2 to v2.5.31
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.236
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.82
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.237
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.83
- *(deps)* Update module github.com/alecthomas/kong to v1.13.0
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.238
### ⚙️ Miscellaneous Tasks
- Add git-cliff configuration for changelog generation
- *(deps)* Update golang:1.25.3 docker digest to 69d1009
- *(deps)* Update goreleaser/goreleaser docker tag to v2.12.6
- *(deps)* Update goreleaser/goreleaser docker tag to v2.12.7
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.6.0
- *(deps)* Update golang:1.25.3 docker digest to 9ac0edc
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.6.1
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.29.0
- *(deps)* Update golang docker tag to v1.25.4
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.6.2
- *(deps)* Update golang:1.25.4 docker digest to efe81fa
## [0.6.6] - 2025-10-14
### 🐛 Bug Fixes
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.227
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.228
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.229
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.81
- *(deps)* Update module github.com/pressly/goose/v3 to v3.26.0
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.230
### ⚙️ Miscellaneous Tasks
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.5.0
- *(deps)* Update goreleaser/goreleaser docker tag to v2.12.3
- *(deps)* Update goreleaser/goreleaser docker tag to v2.12.4
- *(deps)* Update goreleaser/goreleaser docker tag to v2.12.5
- *(deps)* Update golang:1.25.1 docker digest to 12640a4
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.23.0
- *(deps)* Update golang docker tag to v1.25.2
- *(deps)* Update golang docker tag to v1.25.3
## [0.6.5] - 2025-09-18
### 🐛 Bug Fixes
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.226
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.79
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.80
- *(deps)* Update module gitlab.com/unboundsoftware/eventsourced/eventsourced to v1.19.3
### ⚙️ Miscellaneous Tasks
- *(deps)* Update pre-commit hook lietu/go-pre-commit to v1
- *(deps)* Update goreleaser/goreleaser docker tag to v2.12.1
- *(deps)* Update goreleaser/goreleaser docker tag to v2.12.2
## [0.6.4] - 2025-09-11
### 🐛 Bug Fixes
- *(deps)* Update module gitlab.com/unboundsoftware/eventsourced/amqp to v1.9.0
## [0.6.3] - 2025-09-09
### 🐛 Bug Fixes
- *(deps)* Update module github.com/stretchr/testify to v1.11.0
- *(deps)* Update module github.com/pressly/goose/v3 to v3.25.0
- *(deps)* Update module github.com/stretchr/testify to v1.11.1
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.222
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.223
- *(deps)* Update opentelemetry-go monorepo
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.224
- *(deps)* Update module go.opentelemetry.io/contrib/bridges/otelslog to v0.13.0
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.225
### ⚙️ Miscellaneous Tasks
- *(deps)* Update golang:1.25.0 docker digest to f6b9e1a
- *(deps)* Update goreleaser/goreleaser docker tag to v2.12.0
- *(deps)* Update golang docker tag to v1.25.1
- *(deps)* Update golang:1.25.1 docker digest to 53f7808
## [0.6.2] - 2025-08-22
### ⚙️ Miscellaneous Tasks
- Remove conflicts entry from homebrew-taps config
## [0.6.1] - 2025-08-22
### 🐛 Bug Fixes
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.195
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.196
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.197
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.198
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.199
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.200
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.202
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.203
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.204
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.207
- *(deps)* Update module github.com/golang-jwt/jwt/v5 to v5.2.3
- *(deps)* Update module github.com/alecthomas/kong to v1.12.1
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.208
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.210
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.78
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.212
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.213
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.215
- *(deps)* Update module github.com/golang-jwt/jwt/v5 to v5.3.0
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.216
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.217
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.218
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.219
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.220
- *(deps)* Update module github.com/sparetimecoders/goamqp to v0.3.3
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.221
### ⚙️ Miscellaneous Tasks
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.2.1
- *(deps)* Update golang:1.24.4 docker digest to 9f820b6
- *(deps)* Update golang docker tag to v1.24.5
- *(deps)* Update goreleaser/goreleaser docker tag to v2.11.0
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.2.2
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.28.0
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.3.0
- *(deps)* Update golang:1.24.5 docker digest to 0a156a4
- *(deps)* Update goreleaser/goreleaser docker tag to v2.11.1
- *(deps)* Update goreleaser/goreleaser docker tag to v2.11.2
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.3.1
- *(deps)* Update golang docker tag to v1.24.6
- *(deps)* Update pre-commit hook pre-commit/pre-commit-hooks to v6
- *(deps)* Update golang:1.24.6 docker digest to 958bfd1
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.4.0
- *(deps)* Update golang:1.24.6 docker digest to cd8f653
- *(deps)* Update golang docker tag to v1.25.0
## [0.6.0] - 2025-06-29
### 🚀 Features
- *(k8s)* Add OpenTelemetry exporter endpoint to deploy.yaml
- Add build version injection via CI_COMMIT argument
### 🐛 Bug Fixes
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.190
- *(deps)* Update module github.com/vektah/gqlparser/v2 to v2.5.28
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.75
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.192
- *(deps)* Update module github.com/alecthomas/kong to v1.12.0
- *(deps)* Update opentelemetry-go monorepo
- *(deps)* Update module go.opentelemetry.io/contrib/bridges/otelslog to v0.12.0
- *(deps)* Update module github.com/vektah/gqlparser/v2 to v2.5.29
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.194
- *(deps)* Update module github.com/vektah/gqlparser/v2 to v2.5.30
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.76
### ⚙️ Miscellaneous Tasks
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.2.0
## [0.5.3] - 2025-06-13
### 🐛 Bug Fixes
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.187
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.188
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.189
### 🚜 Refactor
- Remove Sentry integration and replace with OpenTelemetry
### ⚙️ Miscellaneous Tasks
- *(deps)* Update golang:1.24.4 docker digest to 3494bbe
## [0.5.2] - 2025-06-09
### 🐛 Bug Fixes
- *(goreleaser)* Specify binary name in configuration
## [0.5.1] - 2025-06-09
### 🐛 Bug Fixes
- *(deps)* Update module github.com/alecthomas/kong to v1.11.0
- *(deps)* Update module github.com/getsentry/sentry-go to v0.33.0
- *(deps)* Update module github.com/khan/genqlient to v0.8.1
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.179
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.180
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.181
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.182
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.183
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.74
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.184
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.185
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.186
### ⚙️ Miscellaneous Tasks
- *(deps)* Update golang:1.24.3 docker digest to f255a7d
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.27.0
- *(deps)* Update golang docker tag to v1.24.4
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.27.1
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.27.2
- *(deps)* Update goreleaser/goreleaser docker tag to v2.10.2
## [0.5.0] - 2025-05-15
### 🚀 Features
@@ -24,7 +281,16 @@ All notable changes to this project will be documented in this file.
### ⚙️ Miscellaneous Tasks
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.1.4
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.1.5
- *(deps)* Update golang:1.24.2 docker digest to bf7899c
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.25.0
- *(deps)* Update goreleaser/goreleaser docker tag to v2.9.0
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.25.1
- *(ci)* Update GitLab CI configuration for templates
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.1.6
- *(deps)* Update golang docker tag to v1.24.3
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.26.0
## [0.4.1] - 2025-04-24
@@ -37,6 +303,11 @@ All notable changes to this project will be documented in this file.
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.171
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.172
### ⚙️ Miscellaneous Tasks
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.1.1
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.1.2
## [0.4.0] - 2025-04-12
### 🚀 Features
@@ -56,6 +327,10 @@ All notable changes to this project will be documented in this file.
- *(deploy)* Remove cpu and memory limits for schemas
### ⚙️ Miscellaneous Tasks
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.24.3
## [0.3.0] - 2025-04-08
### 🚀 Features
@@ -87,6 +362,24 @@ All notable changes to this project will be documented in this file.
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.168
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.169
### ⚙️ Miscellaneous Tasks
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.64.6
- *(deps)* Update golang docker tag to v1.24.1
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.22.0
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.64.7
- *(deps)* Update goreleaser/goreleaser docker tag to v2.8.0
- *(deps)* Update goreleaser/goreleaser docker tag to v2.8.1
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.64.8
- *(deps)* Update golang:1.24.1 docker digest to 5ecf333
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.24.2
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.0.1
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.0.2
- *(deps)* Update goreleaser/goreleaser docker tag to v2.8.2
- *(deps)* Update golang docker tag to v1.24.2
- *(deps)* Update golang:1.24.2 docker digest to aebb7df
## [0.2.0] - 2025-02-28
### 🚀 Features
@@ -111,7 +404,24 @@ All notable changes to this project will be documented in this file.
### ⚙️ Miscellaneous Tasks
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.23.3
- *(deps)* Update golang:1.23.5 docker digest to e213430
- *(deps)* Update dependency go to v1.23.6
- *(deps)* Update golang docker tag to v1.23.6
- *(deps)* Update golang:1.23.6 docker digest to 958bd2e
- *(deps)* Update golang:1.23.6 docker digest to 9271129
- *(deps)* Update goreleaser/goreleaser docker tag to v2.7.0
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.64.2
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.64.3
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.64.4
- *(go)* Update go version to 1.23.6 and remove toolchain
- *(deps)* Update golang docker tag to v1.24.0
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.64.5
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.21.0
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.24.0
- *(deps)* Update golang:1.24.0 docker digest
- *(deps)* Update golang:1.24.0 docker digest to a14c5a6
- *(deps)* Update golang:1.24.0 docker digest to 58cf31c
- *(docker)* Update base image architecture to amd64
## [0.1.1] - 2025-01-24
@@ -125,6 +435,27 @@ All notable changes to this project will be documented in this file.
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.63
- *(k8s)* Standardize app label to app.kubernetes.io/name
### ⚙️ Miscellaneous Tasks
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.63.1
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.63.2
- *(deps)* Update goreleaser/goreleaser docker tag to v2.5.1
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.63.3
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.63.4
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.23.0
- *(deps)* Update golang:1.23.4 docker digest to 3b1a7de
- *(deps)* Update golang:1.23.4 docker digest to 08e1417
- *(deps)* Update golang:1.23.4 docker digest to 585103a
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.23.1
- *(deps)* Update golang:1.23.4 docker digest to 5305905
- *(deps)* Update golang:1.23.4 docker digest to 9820aca
- *(deps)* Update dependency go to v1.23.5
- *(deps)* Update golang docker tag to v1.23.5
- *(deps)* Update goreleaser/goreleaser docker tag to v2.6.0
- *(deps)* Update golang:1.23.5 docker digest to 8c10f21
- *(deps)* Update goreleaser/goreleaser docker tag to v2.6.1
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.23.2
## [0.1.0] - 2025-01-01
### 🚀 Features
@@ -165,8 +496,47 @@ All notable changes to this project will be documented in this file.
### ⚙️ Miscellaneous Tasks
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.21.2
- *(deps)* Update goreleaser/goreleaser docker tag to v2.4.1
- *(deps)* Pin dependencies
- *(deps)* Pin dependencies
- *(deps)* Update goreleaser/goreleaser docker tag to v2.4.2
- *(deps)* Update goreleaser/goreleaser docker tag to v2.4.4
- *(deps)* Update dependency go to v1.23.3
- *(deps)* Update golang docker tag to v1.23.3
- *(deps)* Update unbound/pre-commit docker digest to 596abf5
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.62.0
- *(deps)* Update golang:1.23.3 docker digest to 8956c08
- *(deps)* Update unbound/pre-commit docker digest to e78425c
- *(deps)* Update golang:1.23.3 docker digest to 3694e36
- *(deps)* Update golang:1.23.3 docker digest to b2ca381
- *(deps)* Update golang:1.23.3 docker digest to 2660218
- *(deps)* Update golang:1.23.3 docker digest to c2d828f
- *(deps)* Update goreleaser/goreleaser docker tag to v2.4.5
- *(deps)* Update golang:1.23.3 docker digest to 73f06be
- *(deps)* Update goreleaser/goreleaser docker tag to v2.4.6
- *(deps)* Update goreleaser/goreleaser docker tag to v2.4.7
- *(deps)* Update goreleaser/goreleaser docker tag to v2.4.8
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.62.2
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.19.0
- *(deps)* Update golang:1.23.3 docker digest to ee5f0ad
- *(deps)* Update golang:1.23.3 docker digest to b4aabba
- *(deps)* Update golang:1.23.3 docker digest to 2b01164
- *(deps)* Update golang:1.23.3 docker digest to 017ec6b
- *(deps)* Update dependency go to v1.23.4
- *(deps)* Update golang docker tag to v1.23.4
- *(deps)* Update golang:1.23.4 docker digest to 574185e
- Remove unnecessary Docker variables from configuration
- *(ci)* Remove unused docker service from buildtools
- *(deps)* Update golang:1.23.4 docker digest to 7003184
- *(deps)* Update goreleaser/goreleaser docker tag to v2.5.0
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.20.0
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.21.3
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.21.4
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.22.0
- *(deps)* Update golang:1.23.4 docker digest to 7ea4c9d
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.22.1
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.63.0
## [0.0.7] - 2024-10-22
@@ -211,9 +581,49 @@ All notable changes to this project will be documented in this file.
### ⚙️ Miscellaneous Tasks
- *(deps)* Update pre-commit hook pre-commit/pre-commit-hooks to v4.6.0
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.15.0
- *(deps)* Update dependency go to v1.22.2
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.16.0
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.58.0
- *(deps)* Update dependency go to v1.22.3
- *(deps)* Update golang docker tag to v1.22.3
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.58.1
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.58.2
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.59.0
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.18.3
- *(deps)* Update dependency go to v1.22.4
- *(deps)* Update golang docker tag to v1.22.4
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.59.1
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.18.4
- *(deps)* Update dependency go to v1.22.5
- *(deps)* Update golang docker tag to v1.22.5
- *(deps)* Update dependency go to v1.22.6
- *(deps)* Update golang docker tag to v1.22.6
- *(deps)* Update dependency go to v1.23.0
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.60.1
- *(deps)* Update golang docker tag to v1.23.0
- Update golangci-lint hook identifier in pre-commit config
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.60.2
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.60.3
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.17.0
- *(deps)* Update dependency go to v1.23.1
- *(deps)* Update golang docker tag to v1.23.1
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.61.0
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.18.0
- Update goreleaser image to v2.3.1
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.19.1
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.19.2
- Add generate check
- *(deps)* Update goreleaser/goreleaser docker tag to v2.3.2
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.19.3
- *(deps)* Update dependency go to v1.23.2
- *(deps)* Update golang docker tag to v1.23.2
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.20.0
- *(deps)* Update pre-commit hook pre-commit/pre-commit-hooks to v5
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.20.1
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.21.0
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.21.1
- Add release notes for goreleaser command in .gitlab-ci.yml
## [0.0.6] - 2024-04-04
@@ -225,6 +635,7 @@ All notable changes to this project will be documented in this file.
### ⚙️ Miscellaneous Tasks
- Add step for checking release
- *(deps)* Update golang docker tag to v1.22.2
## [0.0.5] - 2024-04-03
@@ -249,15 +660,106 @@ All notable changes to this project will be documented in this file.
### ⚙️ Miscellaneous Tasks
- *(deps)* Bump github.com/stretchr/testify from 1.8.3 to 1.8.4
- Update golangci-lint
- *(deps)* Bump golang from 1.20.4 to 1.20.5
- Update Go version for vulnerabilities
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.31 to 0.17.32
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
- *(deps)* Bump github.com/vektah/gqlparser/v2
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.32 to 0.17.33
- *(deps)* Bump github.com/getsentry/sentry-go from 0.21.0 to 0.22.0
- *(deps)* Bump github.com/alecthomas/kong from 0.7.1 to 0.8.0
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.33 to 0.17.34
- *(deps)* Bump github.com/vektah/gqlparser/v2 from 2.5.3 to 2.5.5
- *(deps)* Bump github.com/vektah/gqlparser/v2 from 2.5.5 to 2.5.6
- *(deps)* Bump github.com/pressly/goose/v3 from 3.11.2 to 3.13.0
- *(deps)* Bump github.com/sparetimecoders/goamqp from 0.1.4 to 0.1.5
- *(deps)* Bump github.com/pressly/goose/v3 from 3.13.0 to 3.13.1
- *(deps)* Bump github.com/pressly/goose/v3 from 3.13.1 to 3.13.4
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
- *(deps)* Bump golang from 1.20.5 to 1.20.6
- *(deps)* Bump github.com/vektah/gqlparser/v2 from 2.5.6 to 2.5.7
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.34 to 0.17.35
- *(deps)* Bump github.com/vektah/gqlparser/v2 from 2.5.7 to 2.5.8
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
- *(deps)* Bump github.com/pressly/goose/v3 from 3.13.4 to 3.14.0
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.35 to 0.17.36
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
- *(deps)* Bump github.com/getsentry/sentry-go from 0.22.0 to 0.23.0
- *(deps)* Bump golang from 1.20.6 to 1.20.7
- Update to Go 1.20.7
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
- *(deps)* Bump golang from 1.20.7 to 1.21.0
- *(deps)* Bump github.com/pressly/goose/v3 from 3.14.0 to 3.15.0
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
- Update to Golang 1.21.0 for vulnerabilities
- Update pre-commit versions
- *(deps)* Bump github.com/getsentry/sentry-go from 0.23.0 to 0.24.0
- *(deps)* Bump github.com/rs/cors from 1.9.0 to 1.10.0
- *(deps)* Bump golang from 1.21.0 to 1.21.1
- Update to Go 1.21.1 for vulnerabilities
- *(deps)* Bump github.com/vektah/gqlparser/v2 from 2.5.8 to 2.5.9
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.36 to 0.17.37
- *(deps)* Bump github.com/getsentry/sentry-go from 0.24.0 to 0.24.1
- *(deps)* Bump github.com/vektah/gqlparser/v2 from 2.5.9 to 2.5.10
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.37 to 0.17.38
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
- *(deps)* Bump github.com/rs/cors from 1.10.0 to 1.10.1
- *(deps)* Bump github.com/getsentry/sentry-go from 0.24.1 to 0.25.0
- *(deps)* Bump github.com/sparetimecoders/goamqp from 0.1.5 to 0.2.0
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.38 to 0.17.39
- *(deps)* Bump golang from 1.21.1 to 1.21.2
- *(deps)* Bump github.com/pressly/goose/v3 from 3.15.0 to 3.15.1
- *(deps)* Bump golang from 1.21.2 to 1.21.3
- *(deps)* Bump github.com/alecthomas/kong from 0.8.0 to 0.8.1
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.39 to 0.17.40
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/eventsourced
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
- *(deps)* Bump golang from 1.21.3 to 1.21.4
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
- *(deps)* Bump github.com/pressly/goose/v3 from 3.15.1 to 3.16.0
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.40 to 0.17.41
- *(deps)* Bump github.com/auth0/go-jwt-middleware/v2
- *(deps)* Bump golang from 1.21.4 to 1.21.5
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
- *(deps)* Bump github.com/pressly/goose/v3 from 3.16.0 to 3.17.0
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
- *(deps)* Bump github.com/sparetimecoders/goamqp from 0.2.0 to 0.2.1
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.41 to 0.17.42
- *(deps)* Bump golang from 1.21.5 to 1.21.6
- *(deps)* Bump github.com/getsentry/sentry-go from 0.25.0 to 0.26.0
- *(deps)* Bump github.com/sparetimecoders/goamqp from 0.2.1 to 0.3.0
- *(deps)* Bump github.com/vektah/gqlparser/v2 from 2.5.10 to 2.5.11
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.42 to 0.17.43
- *(deps)* Bump github.com/auth0/go-jwt-middleware/v2
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
- *(deps)* Bump github.com/pressly/goose/v3 from 3.17.0 to 3.18.0
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/eventsourced
- *(deps)* Bump golang from 1.21.6 to 1.22.0
- *(deps)* Bump github.com/getsentry/sentry-go from 0.26.0 to 0.27.0
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.43 to 0.17.44
- *(deps)* Update pre-commit hook devopshq/gitlab-ci-linter to v1.0.6
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.11.0
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.56.2
- *(deps)* Update pre-commit hook lietu/go-pre-commit to v0.1.0
- *(deps)* Update pre-commit hook pre-commit/pre-commit-hooks to v4.5.0
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.12.0
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.13.0
- Use OrbStack for local dev
- *(deps)* Update golang docker tag to v1.22.1
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.57.0
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.57.1
- Add gitleaks to pre-commit setup
- Update resources
- *(deps)* Update pre-commit hook golangci/golangci-lint to v1.57.2
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.14.0
- Run release on medium instance
- Back to small and upgrade goreleaser
- Remove deprecated replacements
@@ -270,8 +772,18 @@ All notable changes to this project will be documented in this file.
### ⚙️ Miscellaneous Tasks
- *(deps)* Bump golang from 1.20.3 to 1.20.4
- *(deps)* Bump github.com/pressly/goose/v3 from 3.10.0 to 3.11.0
- Update Go version for vulnerabilities
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.30 to 0.17.31
- *(deps)* Bump github.com/Khan/genqlient from 0.5.0 to 0.6.0
- *(deps)* Bump github.com/getsentry/sentry-go from 0.20.0 to 0.21.0
- *(deps)* Bump github.com/pressly/goose/v3 from 3.11.0 to 3.11.2
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
- *(deps)* Bump github.com/sparetimecoders/goamqp from 0.1.3 to 0.1.4
- *(deps)* Bump github.com/wundergraph/graphql-go-tools
- Update pre-commit and fix golangci-lint
- *(deps)* Bump github.com/stretchr/testify from 1.8.2 to 1.8.3
- Actually validate API key privileges and refs
## [0.0.3] - 2023-04-27
@@ -290,19 +802,52 @@ All notable changes to this project will be documented in this file.
- Update schema if URLs have changed
- Add pre-commit and remove those checks from Dockerfile
- *(deps)* Bump github.com/alecthomas/kong from 0.6.1 to 0.7.1
- *(deps)* Bump github.com/stretchr/testify from 1.8.0 to 1.8.1
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.20 to 0.17.22
- *(deps)* Bump github.com/getsentry/sentry-go from 0.14.0 to 0.16.0
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/eventsourced
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/amqp
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
- Add context and error handling
- *(deps)* Bump github.com/rs/cors from 1.8.2 to 1.8.3
- Move to default ingress group
- Decrease trace sample rate
- Improve docker caching
- *(deps)* Bump golang from 1.19.4 to 1.19.5
- *(deps)* Bump github.com/getsentry/sentry-go from 0.16.0 to 0.17.0
- Add local module to pre-commit config
- Only ignore generated files with do not edit
- Default ingress group
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/eventsourced
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/eventsourced
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.22 to 0.17.24
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/eventsourced
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/eventsourced
- *(deps)* Bump golang from 1.19.5 to 1.20.0
- Use Docker DinD version from variable
- *(deps)* Bump github.com/getsentry/sentry-go from 0.17.0 to 0.18.0
- Switch to manual rebases for Dependabot
- *(deps)* Bump golang from 1.20.0 to 1.20.1
- Update to golang 1.20.1
- *(deps)* Bump github.com/stretchr/testify from 1.8.1 to 1.8.2
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.24 to 0.17.25
- *(deps)* Bump github.com/getsentry/sentry-go from 0.18.0 to 0.19.0
- *(deps)* Bump golang from 1.20.1 to 1.20.2
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.25 to 0.17.26
- Update Go verion for vulnerabilities scan
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.26 to 0.17.27
- Reduce sample rate
- *(deps)* Bump github.com/getsentry/sentry-go from 0.19.0 to 0.20.0
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.27 to 0.17.28
- *(deps)* Bump golang from 1.20.2 to 1.20.3
- Update to Go 1.20.3
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.28 to 0.17.29
- *(deps)* Bump github.com/rs/cors from 1.8.3 to 1.9.0
- *(deps)* Bump gitlab.com/unboundsoftware/eventsourced/pg
- *(deps)* Bump github.com/99designs/gqlgen from 0.17.29 to 0.17.30
- Fix Gitlab CI lint
## [0.0.2] - 2022-10-14
+136
View File
@@ -0,0 +1,136 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a GraphQL schema registry service that manages federated GraphQL schemas for microservices. It allows services to publish their subgraph schemas and provides merged supergraphs with Cosmo Router configuration for federated GraphQL gateways.
## Architecture
### Event Sourcing
The system uses event sourcing via `gitlab.com/unboundsoftware/eventsourced`. Key domain aggregates are:
- **Organization** (domain/aggregates.go): Manages organizations, users, and API keys
- **SubGraph** (domain/aggregates.go): Tracks subgraph schemas with versioning
All state changes flow through events (domain/events.go) and commands (domain/commands.go). The EventStore persists events to PostgreSQL, and events are published to RabbitMQ for downstream consumers.
### GraphQL Layer
- **Schema**: graph/schema.graphqls defines the API
- **Resolvers**: graph/schema.resolvers.go implements mutations/queries
- **Generated Code**: graph/generated/ and graph/model/ (auto-generated by gqlgen)
The resolver (graph/resolver.go) coordinates between the EventStore, Publisher (RabbitMQ), Cache, and PubSub for subscriptions.
### Schema Merging
The sdlmerge/ package handles GraphQL schema federation:
- Merges multiple subgraph SDL schemas into a unified supergraph
- Uses wundergraph/graphql-go-tools for AST manipulation
- Removes duplicates, extends types, and applies federation directives
### Authentication & Authorization
- **Auth0 JWT** (middleware/auth0.go): Validates user tokens from Auth0
- **API Keys** (middleware/apikey.go): Validates service API keys
- **Auth Middleware** (middleware/auth.go): Routes auth based on context
The @auth directive controls field-level access (user vs organization API key).
### Cosmo Router Integration
The service generates Cosmo Router configuration (graph/cosmo.go) using the wgc CLI tool installed in the Docker container. This config enables federated query execution across subgraphs.
### PubSub for Real-time Updates
graph/pubsub.go implements subscription support for schemaUpdates, allowing clients to receive real-time notifications when schemas change.
## Commands
### Code Generation
```bash
# Generate GraphQL server code (gqlgen), format, and organize imports
go generate ./...
```
Always run this after modifying graph/schema.graphqls. The go:generate directives are in:
- graph/resolver.go: runs gqlgen, gofumpt, and goimports
- ctl/ctl.go: generates genqlient client code
### Testing
```bash
# Run all tests
go test ./... -v
# Run tests with race detection and coverage (as used in CI)
CGO_ENABLED=1 go test -race -coverprofile=coverage.txt -covermode=atomic ./...
# Run specific package tests
go test ./middleware -v
go test ./graph -v -run TestGenerateCosmoRouterConfig
# Run single test
go test ./cmd/service -v -run TestWebSocket
```
### Building
```bash
# Build the service binary
go build -o service ./cmd/service/service.go
# Build the CLI tool
go build -o schemactl ./cmd/schemactl/schemactl.go
# Docker build (multi-stage)
docker build -t schemas .
```
The Dockerfile runs tests with coverage before building the production binary.
### Running the Service
```bash
# Start the service (requires PostgreSQL and RabbitMQ)
go run ./cmd/service/service.go \
--postgres-url="postgres://user:pass@localhost:5432/schemas?sslmode=disable" \
--amqp-url="amqp://user:pass@localhost:5672/" \
--issuer="your-auth0-domain.auth0.com"
# The service listens on port 8080 by default
# GraphQL Playground available at http://localhost:8080/
```
### Using the schemactl CLI
```bash
# Publish a subgraph schema
schemactl publish \
--api-key="your-api-key" \
--schema-ref="production" \
--service="users" \
--url="http://users-service:8080/query" \
--sdl=schema.graphql
# List subgraphs for a ref
schemactl list \
--api-key="your-api-key" \
--schema-ref="production"
```
## Development Workflow
1. **Schema Changes**: Edit graph/schema.graphqls → run `go generate ./...`
2. **Resolver Implementation**: Implement in graph/schema.resolvers.go
3. **Testing**: Write tests, run `go test ./...`
4. **Pre-commit**: Hooks run go-mod-tidy, goimports, gofumpt, golangci-lint, and tests
## Key Dependencies
- **gqlgen**: GraphQL server generation
- **genqlient**: GraphQL client generation (for ctl package)
- **eventsourced**: Event sourcing framework
- **wundergraph/graphql-go-tools**: Schema federation and composition
- **wgc CLI**: Cosmo Router config generation (Node.js tool)
- **Auth0**: JWT authentication
- **OpenTelemetry**: Observability (traces, metrics, logs)
## Important Files
- gqlgen.yml: gqlgen configuration
- graph/tools.go: Declares build-time tool dependencies
- .pre-commit-config.yaml: Pre-commit hooks configuration
- cliff.toml: Changelog generation config
+12 -3
View File
@@ -1,9 +1,10 @@
FROM amd64/golang:1.24.3@sha256:f169469d1e8328fd0e26a2b5156f670922a2afc0ca8c984ec17e1be51ca94c30 as modules
FROM amd64/golang:1.25.4@sha256:efe81fa41fdf81fb873ab7cd931b9bb29bd10aced6c252cbd91739c34e654f01 as modules
WORKDIR /build
ADD go.* /build
RUN go mod download
FROM modules as build
ARG CI_COMMIT
WORKDIR /build
ENV CGO_ENABLED=0
ADD . /build
@@ -17,15 +18,23 @@ RUN GOOS=linux GOARCH=amd64 go build \
-a -installsuffix cgo \
-mod=readonly \
-o /release/service \
-ldflags '-w -s' \
-ldflags "-w -s -X main.buildVersion=${CI_COMMIT}" \
./cmd/service/service.go
FROM scratch as export
COPY --from=build /build/coverage.txt /
FROM scratch
FROM node:24-alpine@sha256:2867d550cf9d8bb50059a0fff528741f11a84d985c732e60e19e8e75c7239c43
ENV TZ Europe/Stockholm
# Install wgc CLI globally for Cosmo Router composition
RUN npm install -g wgc@latest
# Copy timezone data and certificates
COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy the service binary
COPY --from=build /release/service /
CMD ["/service"]
+67 -20
View File
@@ -3,18 +3,21 @@ package cache
import (
"fmt"
"log/slog"
"sync"
"time"
"github.com/sparetimecoders/goamqp"
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
"gitlab.com/unboundsoftware/schemas/domain"
"gitlab.com/unboundsoftware/schemas/hash"
)
type Cache struct {
mu sync.RWMutex
organizations map[string]domain.Organization
users map[string][]string
apiKeys map[string]domain.APIKey
apiKeys map[string]domain.APIKey // keyed by organizationId-name
services map[string]map[string]map[string]struct{}
subGraphs map[string]string
lastUpdate map[string]string
@@ -22,18 +25,26 @@ type Cache struct {
}
func (c *Cache) OrganizationByAPIKey(apiKey string) *domain.Organization {
key, exists := c.apiKeys[apiKey]
if !exists {
return nil
c.mu.RLock()
defer c.mu.RUnlock()
// Find the API key by comparing hashes
for _, key := range c.apiKeys {
if hash.CompareAPIKey(key.Key, apiKey) {
org, exists := c.organizations[key.OrganizationId]
if !exists {
return nil
}
return &org
}
}
org, exists := c.organizations[key.OrganizationId]
if !exists {
return nil
}
return &org
return nil
}
func (c *Cache) OrganizationsByUser(sub string) []domain.Organization {
c.mu.RLock()
defer c.mu.RUnlock()
orgIds := c.users[sub]
orgs := make([]domain.Organization, len(orgIds))
for i, id := range orgIds {
@@ -43,14 +54,22 @@ func (c *Cache) OrganizationsByUser(sub string) []domain.Organization {
}
func (c *Cache) ApiKeyByKey(key string) *domain.APIKey {
k, exists := c.apiKeys[hash.String(key)]
if !exists {
return nil
c.mu.RLock()
defer c.mu.RUnlock()
// Find the API key by comparing hashes
for _, apiKey := range c.apiKeys {
if hash.CompareAPIKey(apiKey.Key, key) {
return &apiKey
}
}
return &k
return nil
}
func (c *Cache) Services(orgId, ref, lastUpdate string) ([]string, string) {
c.mu.RLock()
defer c.mu.RUnlock()
key := refKey(orgId, ref)
var services []string
if lastUpdate == "" || c.lastUpdate[key] > lastUpdate {
@@ -62,41 +81,56 @@ func (c *Cache) Services(orgId, ref, lastUpdate string) ([]string, string) {
}
func (c *Cache) SubGraphId(orgId, ref, service string) string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.subGraphs[subGraphKey(orgId, ref, service)]
}
func (c *Cache) Update(msg any, _ goamqp.Headers) (any, error) {
c.mu.Lock()
defer c.mu.Unlock()
switch m := msg.(type) {
case *domain.OrganizationAdded:
o := domain.Organization{}
o := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(m.ID.String()),
}
m.UpdateOrganization(&o)
c.organizations[m.ID.String()] = o
c.addUser(m.Initiator, o)
c.logger.With("org_id", m.ID.String(), "event", "OrganizationAdded").Debug("cache updated")
case *domain.APIKeyAdded:
key := domain.APIKey{
Name: m.Name,
OrganizationId: m.OrganizationId,
Key: m.Key,
Key: m.Key, // This is now the hashed key
Refs: m.Refs,
Read: m.Read,
Publish: m.Publish,
CreatedBy: m.Initiator,
CreatedAt: m.When(),
}
c.apiKeys[m.Key] = key
// Use composite key: organizationId-name
c.apiKeys[apiKeyId(m.OrganizationId, m.Name)] = key
org := c.organizations[m.OrganizationId]
org.APIKeys = append(org.APIKeys, key)
c.organizations[m.OrganizationId] = org
c.logger.With("org_id", m.OrganizationId, "key_name", m.Name, "event", "APIKeyAdded").Debug("cache updated")
case *domain.SubGraphUpdated:
c.updateSubGraph(m.OrganizationId, m.Ref, m.ID.String(), m.Service, m.Time)
c.logger.With("org_id", m.OrganizationId, "ref", m.Ref, "service", m.Service, "event", "SubGraphUpdated").Debug("cache updated")
case *domain.Organization:
c.organizations[m.ID.String()] = *m
c.addUser(m.CreatedBy, *m)
for _, k := range m.APIKeys {
c.apiKeys[k.Key] = k
// Use composite key: organizationId-name
c.apiKeys[apiKeyId(k.OrganizationId, k.Name)] = k
}
c.logger.With("org_id", m.ID.String(), "event", "Organization aggregate loaded").Debug("cache updated")
case *domain.SubGraph:
c.updateSubGraph(m.OrganizationId, m.Ref, m.ID.String(), m.Service, m.ChangedAt)
c.logger.With("org_id", m.OrganizationId, "ref", m.Ref, "service", m.Service, "event", "SubGraph aggregate loaded").Debug("cache updated")
default:
c.logger.With("msg", msg).Warn("unexpected message received")
}
@@ -117,11 +151,20 @@ func (c *Cache) updateSubGraph(orgId string, ref string, subGraphId string, serv
func (c *Cache) addUser(sub string, organization domain.Organization) {
user, exists := c.users[sub]
orgId := organization.ID.String()
if !exists {
c.users[sub] = []string{organization.ID.String()}
} else {
c.users[sub] = append(user, organization.ID.String())
c.users[sub] = []string{orgId}
return
}
// Check if organization already exists for this user
for _, id := range user {
if id == orgId {
return // Already exists, no need to add
}
}
c.users[sub] = append(user, orgId)
}
func New(logger *slog.Logger) *Cache {
@@ -143,3 +186,7 @@ func refKey(orgId string, ref string) string {
func subGraphKey(orgId string, ref string, service string) string {
return fmt.Sprintf("%s<->%s<->%s", orgId, ref, service)
}
func apiKeyId(orgId string, name string) string {
return fmt.Sprintf("%s<->%s", orgId, name)
}
+447
View File
@@ -0,0 +1,447 @@
package cache
import (
"log/slog"
"os"
"sync"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
"gitlab.com/unboundsoftware/schemas/domain"
"gitlab.com/unboundsoftware/schemas/hash"
)
func TestCache_OrganizationByAPIKey(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
orgID := uuid.New().String()
apiKey := "test-api-key-123" // gitleaks:allow
hashedKey, err := hash.APIKey(apiKey)
require.NoError(t, err)
// Add organization to cache
org := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
Name: "Test Org",
}
c.organizations[orgID] = org
// Add API key to cache
c.apiKeys[apiKeyId(orgID, "test-key")] = domain.APIKey{
Name: "test-key",
OrganizationId: orgID,
Key: hashedKey,
Refs: []string{"main"},
Read: true,
Publish: true,
}
// Test finding organization by plaintext API key
foundOrg := c.OrganizationByAPIKey(apiKey)
require.NotNil(t, foundOrg)
assert.Equal(t, org.Name, foundOrg.Name)
assert.Equal(t, orgID, foundOrg.ID.String())
// Test with wrong API key
notFoundOrg := c.OrganizationByAPIKey("wrong-key")
assert.Nil(t, notFoundOrg)
}
func TestCache_OrganizationByAPIKey_Legacy(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
orgID := uuid.New().String()
apiKey := "legacy-api-key-456" // gitleaks:allow
legacyHash := hash.String(apiKey)
// Add organization to cache
org := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
Name: "Legacy Org",
}
c.organizations[orgID] = org
// Add API key with legacy SHA256 hash
c.apiKeys[apiKeyId(orgID, "legacy-key")] = domain.APIKey{
Name: "legacy-key",
OrganizationId: orgID,
Key: legacyHash,
Refs: []string{"main"},
Read: true,
Publish: false,
}
// Test finding organization with legacy hash
foundOrg := c.OrganizationByAPIKey(apiKey)
require.NotNil(t, foundOrg)
assert.Equal(t, org.Name, foundOrg.Name)
}
func TestCache_OrganizationsByUser(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
userSub := "user-123"
org1ID := uuid.New().String()
org2ID := uuid.New().String()
org1 := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(org1ID),
Name: "Org 1",
}
org2 := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(org2ID),
Name: "Org 2",
}
c.organizations[org1ID] = org1
c.organizations[org2ID] = org2
c.users[userSub] = []string{org1ID, org2ID}
orgs := c.OrganizationsByUser(userSub)
assert.Len(t, orgs, 2)
assert.Contains(t, []string{orgs[0].Name, orgs[1].Name}, "Org 1")
assert.Contains(t, []string{orgs[0].Name, orgs[1].Name}, "Org 2")
}
func TestCache_ApiKeyByKey(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
orgID := uuid.New().String()
apiKey := "test-api-key-789" // gitleaks:allow
hashedKey, err := hash.APIKey(apiKey)
require.NoError(t, err)
expectedKey := domain.APIKey{
Name: "test-key",
OrganizationId: orgID,
Key: hashedKey,
Refs: []string{"main", "dev"},
Read: true,
Publish: true,
}
c.apiKeys[apiKeyId(orgID, "test-key")] = expectedKey
foundKey := c.ApiKeyByKey(apiKey)
require.NotNil(t, foundKey)
assert.Equal(t, expectedKey.Name, foundKey.Name)
assert.Equal(t, expectedKey.OrganizationId, foundKey.OrganizationId)
assert.Equal(t, expectedKey.Refs, foundKey.Refs)
// Test with wrong key
notFoundKey := c.ApiKeyByKey("wrong-key")
assert.Nil(t, notFoundKey)
}
func TestCache_Services(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
orgID := uuid.New().String()
ref := "main"
service1 := "service-1"
service2 := "service-2"
lastUpdate := "2024-01-01T12:00:00Z"
c.services[orgID] = map[string]map[string]struct{}{
ref: {
service1: {},
service2: {},
},
}
c.lastUpdate[refKey(orgID, ref)] = lastUpdate
// Test getting services with empty lastUpdate
services, returnedLastUpdate := c.Services(orgID, ref, "")
assert.Len(t, services, 2)
assert.Contains(t, services, service1)
assert.Contains(t, services, service2)
assert.Equal(t, lastUpdate, returnedLastUpdate)
// Test with older lastUpdate (should return services)
services, returnedLastUpdate = c.Services(orgID, ref, "2023-12-31T12:00:00Z")
assert.Len(t, services, 2)
assert.Equal(t, lastUpdate, returnedLastUpdate)
// Test with newer lastUpdate (should return empty)
services, returnedLastUpdate = c.Services(orgID, ref, "2024-01-02T12:00:00Z")
assert.Len(t, services, 0)
assert.Equal(t, lastUpdate, returnedLastUpdate)
}
func TestCache_SubGraphId(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
orgID := uuid.New().String()
ref := "main"
service := "test-service"
subGraphID := uuid.New().String()
c.subGraphs[subGraphKey(orgID, ref, service)] = subGraphID
foundID := c.SubGraphId(orgID, ref, service)
assert.Equal(t, subGraphID, foundID)
// Test with non-existent key
notFoundID := c.SubGraphId("wrong-org", ref, service)
assert.Empty(t, notFoundID)
}
func TestCache_Update_OrganizationAdded(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
orgID := uuid.New().String()
event := &domain.OrganizationAdded{
Name: "New Org",
Initiator: "user-123",
}
event.ID = *eventsourced.IdFromString(orgID)
_, err := c.Update(event, nil)
require.NoError(t, err)
// Verify organization was added
org, exists := c.organizations[orgID]
assert.True(t, exists)
assert.Equal(t, "New Org", org.Name)
// Verify user was added
assert.Contains(t, c.users["user-123"], orgID)
}
func TestCache_Update_APIKeyAdded(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
orgID := uuid.New().String()
keyName := "test-key"
hashedKey := "hashed-key-value"
// Add organization first
org := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
Name: "Test Org",
APIKeys: []domain.APIKey{},
}
c.organizations[orgID] = org
event := &domain.APIKeyAdded{
OrganizationId: orgID,
Name: keyName,
Key: hashedKey,
Refs: []string{"main"},
Read: true,
Publish: false,
Initiator: "user-123",
}
event.ID = *eventsourced.IdFromString(uuid.New().String())
_, err := c.Update(event, nil)
require.NoError(t, err)
// Verify API key was added to cache
key, exists := c.apiKeys[apiKeyId(orgID, keyName)]
assert.True(t, exists)
assert.Equal(t, keyName, key.Name)
assert.Equal(t, hashedKey, key.Key)
assert.Equal(t, []string{"main"}, key.Refs)
// Verify API key was added to organization
updatedOrg := c.organizations[orgID]
assert.Len(t, updatedOrg.APIKeys, 1)
assert.Equal(t, keyName, updatedOrg.APIKeys[0].Name)
}
func TestCache_Update_SubGraphUpdated(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
orgID := uuid.New().String()
ref := "main"
service := "test-service"
subGraphID := uuid.New().String()
event := &domain.SubGraphUpdated{
OrganizationId: orgID,
Ref: ref,
Service: service,
Initiator: "user-123",
}
event.ID = *eventsourced.IdFromString(subGraphID)
event.SetWhen(time.Now())
_, err := c.Update(event, nil)
require.NoError(t, err)
// Verify subgraph was added to services
assert.Contains(t, c.services[orgID][ref], subGraphID)
// Verify subgraph ID was stored
assert.Equal(t, subGraphID, c.subGraphs[subGraphKey(orgID, ref, service)])
// Verify lastUpdate was set
assert.NotEmpty(t, c.lastUpdate[refKey(orgID, ref)])
}
func TestCache_AddUser_NoDuplicates(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
userSub := "user-123"
orgID := uuid.New().String()
org := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
Name: "Test Org",
}
// Add user first time
c.addUser(userSub, org)
assert.Len(t, c.users[userSub], 1)
assert.Equal(t, orgID, c.users[userSub][0])
// Add same user/org again - should not create duplicate
c.addUser(userSub, org)
assert.Len(t, c.users[userSub], 1, "Should not add duplicate organization")
assert.Equal(t, orgID, c.users[userSub][0])
}
func TestCache_ConcurrentReads(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
// Setup test data
orgID := uuid.New().String()
apiKey := "test-concurrent-key" // gitleaks:allow
hashedKey, err := hash.APIKey(apiKey)
require.NoError(t, err)
org := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
Name: "Concurrent Test Org",
}
c.organizations[orgID] = org
c.apiKeys[apiKeyId(orgID, "test-key")] = domain.APIKey{
Name: "test-key",
OrganizationId: orgID,
Key: hashedKey,
}
// Run concurrent reads (reduced for race detector)
var wg sync.WaitGroup
numGoroutines := 20
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
org := c.OrganizationByAPIKey(apiKey)
assert.NotNil(t, org)
assert.Equal(t, "Concurrent Test Org", org.Name)
}()
}
wg.Wait()
}
func TestCache_ConcurrentWrites(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
var wg sync.WaitGroup
numGoroutines := 10 // Reduced for race detector
// Concurrent organization additions
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
orgID := uuid.New().String()
event := &domain.OrganizationAdded{
Name: "Org " + string(rune(index)),
Initiator: "user-" + string(rune(index)),
}
event.ID = *eventsourced.IdFromString(orgID)
_, err := c.Update(event, nil)
assert.NoError(t, err)
}(i)
}
wg.Wait()
// Verify all organizations were added
assert.Equal(t, numGoroutines, len(c.organizations))
}
func TestCache_ConcurrentReadsAndWrites(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
c := New(logger)
// Setup initial data
orgID := uuid.New().String()
apiKey := "test-rw-key" // gitleaks:allow
hashedKey, err := hash.APIKey(apiKey)
require.NoError(t, err)
org := domain.Organization{
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
Name: "RW Test Org",
}
c.organizations[orgID] = org
c.apiKeys[apiKeyId(orgID, "test-key")] = domain.APIKey{
Name: "test-key",
OrganizationId: orgID,
Key: hashedKey,
}
c.users["user-initial"] = []string{orgID}
var wg sync.WaitGroup
numReaders := 10 // Reduced for race detector
numWriters := 5 // Reduced for race detector
iterations := 3 // Reduced for race detector
// Concurrent readers
for i := 0; i < numReaders; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < iterations; j++ {
org := c.OrganizationByAPIKey(apiKey)
assert.NotNil(t, org)
orgs := c.OrganizationsByUser("user-initial")
assert.NotEmpty(t, orgs)
}
}()
}
// Concurrent writers
for i := 0; i < numWriters; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
newOrgID := uuid.New().String()
event := &domain.OrganizationAdded{
Name: "New Org " + string(rune(index)),
Initiator: "user-new-" + string(rune(index)),
}
event.ID = *eventsourced.IdFromString(newOrgID)
_, err := c.Update(event, nil)
assert.NoError(t, err)
}(i)
}
wg.Wait()
// Verify cache is in consistent state
assert.GreaterOrEqual(t, len(c.organizations), numWriters)
}
+80
View File
@@ -0,0 +1,80 @@
# git-cliff ~ default configuration file
# https://git-cliff.org/docs/configuration
#
# Lines starting with "#" are comments.
# Configuration options are organized into tables and keys.
# See documentation for more information on available options.
[changelog]
# template for the changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits %}
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
{% if commit.breaking %}[**breaking**] {% endif %}\
{{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}\n
"""
# template for the changelog footer
footer = """
<!-- generated by git-cliff -->
"""
# remove the leading and trailing s
trim = true
# postprocessors
postprocessors = [
# { pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL
]
# render body even when there are no releases to process
# render_always = true
# output file path
# output = "test.md"
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
# Replace issue numbers
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
# Check spelling of the commit with https://github.com/crate-ci/typos
# If the spelling is incorrect, it will be automatically fixed.
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
]
# filter out the commits that are not matched by commit parsers
filter_commits = false
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
+39 -54
View File
@@ -19,8 +19,6 @@ import (
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/alecthomas/kong"
"github.com/getsentry/sentry-go"
sentryhttp "github.com/getsentry/sentry-go/http"
"github.com/rs/cors"
"github.com/sparetimecoders/goamqp"
"github.com/vektah/gqlparser/v2/ast"
@@ -32,8 +30,10 @@ import (
"gitlab.com/unboundsoftware/schemas/domain"
"gitlab.com/unboundsoftware/schemas/graph"
"gitlab.com/unboundsoftware/schemas/graph/generated"
"gitlab.com/unboundsoftware/schemas/health"
"gitlab.com/unboundsoftware/schemas/logging"
"gitlab.com/unboundsoftware/schemas/middleware"
"gitlab.com/unboundsoftware/schemas/monitoring"
"gitlab.com/unboundsoftware/schemas/store"
)
@@ -41,16 +41,12 @@ type CLI struct {
AmqpURL string `name:"amqp-url" env:"AMQP_URL" help:"URL to use to connect to RabbitMQ" default:"amqp://user:password@unbound-control-plane.orb.local:5672/"`
Port int `name:"port" env:"PORT" help:"Listen-port for GraphQL API" default:"8080"`
LogLevel string `name:"log-level" env:"LOG_LEVEL" help:"The level of logging to use (debug, info, warn, error, fatal)" default:"info"`
LogFormat string `name:"log-format" env:"LOG_FORMAT" help:"The format of logs" default:"text" enum:"otel,json,text"`
DatabaseURL string `name:"postgres-url" env:"POSTGRES_URL" help:"URL to use to connect to Postgres" default:"postgres://postgres:postgres@unbound-control-plane.orb.local:5432/schemas?sslmode=disable"`
DatabaseDriverName string `name:"db-driver" env:"DB_DRIVER" help:"Driver to use to connect to db" default:"postgres"`
Issuer string `name:"issuer" env:"ISSUER" help:"The JWT token issuer to use" default:"unbound.eu.auth0.com"`
StrictSSL bool `name:"strict-ssl" env:"STRICT_SSL" help:"Should strict SSL handling be enabled" default:"true"`
SentryConfig
}
type SentryConfig struct {
DSN string `name:"sentry-dsn" env:"SENTRY_DSN" help:"Sentry dsn" default:""`
Environment string `name:"sentry-environment" env:"SENTRY_ENVIRONMENT" help:"Sentry environment" default:"development"`
Environment string `name:"environment" env:"ENVIRONMENT" help:"The environment we are running in" default:"development" enum:"development,staging,production"`
}
var buildVersion = "none"
@@ -60,7 +56,7 @@ const serviceName = "schemas"
func main() {
var cli CLI
_ = kong.Parse(&cli)
logger := logging.SetupLogger(cli.LogLevel, serviceName, buildVersion)
logger := logging.SetupLogger(cli.LogLevel, cli.LogFormat, serviceName, buildVersion)
closeEvents := make(chan error)
if err := start(
@@ -74,14 +70,17 @@ func main() {
}
func start(closeEvents chan error, logger *slog.Logger, connectToAmqpFunc func(url string) (Connection, error), cli CLI) error {
if err := setupSentry(logger, cli.SentryConfig); err != nil {
return err
}
defer sentry.Flush(2 * time.Second)
rootCtx, rootCancel := context.WithCancel(context.Background())
defer rootCancel()
shutdownFn, err := monitoring.SetupOTelSDK(rootCtx, cli.LogFormat == "otel", serviceName, buildVersion, cli.Environment)
if err != nil {
return err
}
defer func() {
_ = errors.Join(shutdownFn(context.Background()))
}()
db, err := store.SetupDB(cli.DatabaseDriverName, cli.DatabaseURL)
if err != nil {
return fmt.Errorf("failed to setup DB: %v", err)
@@ -197,6 +196,7 @@ func start(closeEvents chan error, logger *slog.Logger, connectToAmqpFunc func(u
Publisher: eventPublisher,
Logger: logger,
Cache: serviceCache,
PubSub: graph.NewPubSub(),
}
config := generated.Config{
@@ -211,6 +211,24 @@ func start(closeEvents chan error, logger *slog.Logger, connectToAmqpFunc func(u
srv.AddTransport(transport.Websocket{
KeepAlivePingInterval: 10 * time.Second,
InitFunc: func(ctx context.Context, initPayload transport.InitPayload) (context.Context, *transport.InitPayload, error) {
// Extract API key from WebSocket connection_init payload
if apiKey, ok := initPayload["X-Api-Key"].(string); ok && apiKey != "" {
logger.Info("WebSocket connection with API key", "has_key", true)
ctx = context.WithValue(ctx, middleware.ApiKey, apiKey)
// Look up organization by API key (cache handles hash comparison)
if organization := serviceCache.OrganizationByAPIKey(apiKey); organization != nil {
logger.Info("WebSocket: Organization found for API key", "org_id", organization.ID.String())
ctx = context.WithValue(ctx, middleware.OrganizationKey, *organization)
} else {
logger.Warn("WebSocket: No organization found for API key")
}
} else {
logger.Info("WebSocket connection without API key")
}
return ctx, &initPayload, nil
},
})
srv.AddTransport(transport.Options{})
srv.AddTransport(transport.GET{})
@@ -224,11 +242,14 @@ func start(closeEvents chan error, logger *slog.Logger, connectToAmqpFunc func(u
Cache: lru.New[string](100),
})
sentryHandler := sentryhttp.New(sentryhttp.Options{Repanic: true})
mux.Handle("/", sentryHandler.HandleFunc(playground.Handler("GraphQL playground", "/query")))
mux.Handle("/health", http.HandlerFunc(healthFunc))
healthChecker := health.New(db.DB, logger)
mux.Handle("/", monitoring.Handler(playground.Handler("GraphQL playground", "/query")))
mux.Handle("/health", http.HandlerFunc(healthChecker.LivenessHandler))
mux.Handle("/health/live", http.HandlerFunc(healthChecker.LivenessHandler))
mux.Handle("/health/ready", http.HandlerFunc(healthChecker.ReadinessHandler))
mux.Handle("/query", cors.AllowAll().Handler(
sentryHandler.Handle(
monitoring.Handler(
mw.Middleware().CheckJWT(
apiKeyMiddleware.Handler(
authMiddleware.Handler(srv),
@@ -285,42 +306,6 @@ func loadSubGraphs(ctx context.Context, eventStore eventsourced.EventStore, serv
return nil
}
func healthFunc(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("OK"))
}
func setupSentry(logger *slog.Logger, args SentryConfig) error {
if args.Environment == "" {
return fmt.Errorf("no Sentry environment supplied, exiting")
}
cfg := sentry.ClientOptions{
Dsn: args.DSN,
Environment: args.Environment,
Release: fmt.Sprintf("%s-%s", serviceName, buildVersion),
}
switch args.Environment {
case "development":
cfg.Debug = true
cfg.EnableTracing = false
cfg.TracesSampleRate = 0.0
case "production":
if args.DSN == "" {
return fmt.Errorf("no DSN supplied for non-dev environment, exiting")
}
cfg.Debug = false
cfg.EnableTracing = true
cfg.TracesSampleRate = 0.01
default:
return fmt.Errorf("illegal environment %s", args.Environment)
}
if err := sentry.Init(cfg); err != nil {
return fmt.Errorf("sentry setup: %w", err)
}
logger.With("environment", args.Environment).Info("configured Sentry")
return nil
}
func ConnectAMQP(url string) (Connection, error) {
return goamqp.NewFromURL(serviceName, url)
}
+362
View File
@@ -0,0 +1,362 @@
package main
import (
"context"
"testing"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
"gitlab.com/unboundsoftware/schemas/domain"
"gitlab.com/unboundsoftware/schemas/hash"
"gitlab.com/unboundsoftware/schemas/middleware"
)
// MockCache is a mock implementation for testing
type MockCache struct {
organizations map[string]*domain.Organization // keyed by orgId-name composite
apiKeys map[string]string // maps orgId-name to hashed key
}
func (m *MockCache) OrganizationByAPIKey(plainKey string) *domain.Organization {
// Find organization by comparing plaintext key with stored hash
for compositeKey, hashedKey := range m.apiKeys {
if hash.CompareAPIKey(hashedKey, plainKey) {
return m.organizations[compositeKey]
}
}
return nil
}
func TestWebSocketInitFunc_WithValidAPIKey(t *testing.T) {
// Setup
orgID := uuid.New()
org := &domain.Organization{
BaseAggregate: eventsourced.BaseAggregate{
ID: eventsourced.IdFromString(orgID.String()),
},
Name: "Test Organization",
}
apiKey := "test-api-key-123"
hashedKey, err := hash.APIKey(apiKey)
require.NoError(t, err)
compositeKey := orgID.String() + "-test-key"
mockCache := &MockCache{
organizations: map[string]*domain.Organization{
compositeKey: org,
},
apiKeys: map[string]string{
compositeKey: hashedKey,
},
}
// Create InitFunc (simulating the WebSocket InitFunc logic)
initFunc := func(ctx context.Context, initPayload transport.InitPayload) (context.Context, *transport.InitPayload, error) {
// Extract API key from WebSocket connection_init payload
if apiKey, ok := initPayload["X-Api-Key"].(string); ok && apiKey != "" {
ctx = context.WithValue(ctx, middleware.ApiKey, apiKey)
// Look up organization by API key (cache handles hash comparison)
if organization := mockCache.OrganizationByAPIKey(apiKey); organization != nil {
ctx = context.WithValue(ctx, middleware.OrganizationKey, *organization)
}
}
return ctx, &initPayload, nil
}
// Test
ctx := context.Background()
initPayload := transport.InitPayload{
"X-Api-Key": apiKey,
}
resultCtx, resultPayload, err := initFunc(ctx, initPayload)
// Assert
require.NoError(t, err)
require.NotNil(t, resultPayload)
// Check API key is in context
if value := resultCtx.Value(middleware.ApiKey); value != nil {
assert.Equal(t, apiKey, value.(string))
} else {
t.Fatal("API key not found in context")
}
// Check organization is in context
if value := resultCtx.Value(middleware.OrganizationKey); value != nil {
capturedOrg, ok := value.(domain.Organization)
require.True(t, ok, "Organization should be of correct type")
assert.Equal(t, org.Name, capturedOrg.Name)
assert.Equal(t, org.ID.String(), capturedOrg.ID.String())
} else {
t.Fatal("Organization not found in context")
}
}
func TestWebSocketInitFunc_WithInvalidAPIKey(t *testing.T) {
// Setup
mockCache := &MockCache{
organizations: map[string]*domain.Organization{},
apiKeys: map[string]string{},
}
apiKey := "invalid-api-key"
// Create InitFunc
initFunc := func(ctx context.Context, initPayload transport.InitPayload) (context.Context, *transport.InitPayload, error) {
// Extract API key from WebSocket connection_init payload
if apiKey, ok := initPayload["X-Api-Key"].(string); ok && apiKey != "" {
ctx = context.WithValue(ctx, middleware.ApiKey, apiKey)
// Look up organization by API key (cache handles hash comparison)
if organization := mockCache.OrganizationByAPIKey(apiKey); organization != nil {
ctx = context.WithValue(ctx, middleware.OrganizationKey, *organization)
}
}
return ctx, &initPayload, nil
}
// Test
ctx := context.Background()
initPayload := transport.InitPayload{
"X-Api-Key": apiKey,
}
resultCtx, resultPayload, err := initFunc(ctx, initPayload)
// Assert
require.NoError(t, err)
require.NotNil(t, resultPayload)
// Check API key is in context
if value := resultCtx.Value(middleware.ApiKey); value != nil {
assert.Equal(t, apiKey, value.(string))
} else {
t.Fatal("API key not found in context")
}
// Check organization is NOT in context (since API key is invalid)
value := resultCtx.Value(middleware.OrganizationKey)
assert.Nil(t, value, "Organization should not be set for invalid API key")
}
func TestWebSocketInitFunc_WithoutAPIKey(t *testing.T) {
// Setup
mockCache := &MockCache{
organizations: map[string]*domain.Organization{},
apiKeys: map[string]string{},
}
// Create InitFunc
initFunc := func(ctx context.Context, initPayload transport.InitPayload) (context.Context, *transport.InitPayload, error) {
// Extract API key from WebSocket connection_init payload
if apiKey, ok := initPayload["X-Api-Key"].(string); ok && apiKey != "" {
ctx = context.WithValue(ctx, middleware.ApiKey, apiKey)
// Look up organization by API key (cache handles hash comparison)
if organization := mockCache.OrganizationByAPIKey(apiKey); organization != nil {
ctx = context.WithValue(ctx, middleware.OrganizationKey, *organization)
}
}
return ctx, &initPayload, nil
}
// Test
ctx := context.Background()
initPayload := transport.InitPayload{}
resultCtx, resultPayload, err := initFunc(ctx, initPayload)
// Assert
require.NoError(t, err)
require.NotNil(t, resultPayload)
// Check API key is NOT in context
value := resultCtx.Value(middleware.ApiKey)
assert.Nil(t, value, "API key should not be set when not provided")
// Check organization is NOT in context
value = resultCtx.Value(middleware.OrganizationKey)
assert.Nil(t, value, "Organization should not be set when API key is not provided")
}
func TestWebSocketInitFunc_WithEmptyAPIKey(t *testing.T) {
// Setup
mockCache := &MockCache{
organizations: map[string]*domain.Organization{},
apiKeys: map[string]string{},
}
// Create InitFunc
initFunc := func(ctx context.Context, initPayload transport.InitPayload) (context.Context, *transport.InitPayload, error) {
// Extract API key from WebSocket connection_init payload
if apiKey, ok := initPayload["X-Api-Key"].(string); ok && apiKey != "" {
ctx = context.WithValue(ctx, middleware.ApiKey, apiKey)
// Look up organization by API key (cache handles hash comparison)
if organization := mockCache.OrganizationByAPIKey(apiKey); organization != nil {
ctx = context.WithValue(ctx, middleware.OrganizationKey, *organization)
}
}
return ctx, &initPayload, nil
}
// Test
ctx := context.Background()
initPayload := transport.InitPayload{
"X-Api-Key": "", // Empty string
}
resultCtx, resultPayload, err := initFunc(ctx, initPayload)
// Assert
require.NoError(t, err)
require.NotNil(t, resultPayload)
// Check API key is NOT in context (because empty string fails the condition)
value := resultCtx.Value(middleware.ApiKey)
assert.Nil(t, value, "API key should not be set when empty")
// Check organization is NOT in context
value = resultCtx.Value(middleware.OrganizationKey)
assert.Nil(t, value, "Organization should not be set when API key is empty")
}
func TestWebSocketInitFunc_WithWrongTypeAPIKey(t *testing.T) {
// Setup
mockCache := &MockCache{
organizations: map[string]*domain.Organization{},
apiKeys: map[string]string{},
}
// Create InitFunc
initFunc := func(ctx context.Context, initPayload transport.InitPayload) (context.Context, *transport.InitPayload, error) {
// Extract API key from WebSocket connection_init payload
if apiKey, ok := initPayload["X-Api-Key"].(string); ok && apiKey != "" {
ctx = context.WithValue(ctx, middleware.ApiKey, apiKey)
// Look up organization by API key (cache handles hash comparison)
if organization := mockCache.OrganizationByAPIKey(apiKey); organization != nil {
ctx = context.WithValue(ctx, middleware.OrganizationKey, *organization)
}
}
return ctx, &initPayload, nil
}
// Test
ctx := context.Background()
initPayload := transport.InitPayload{
"X-Api-Key": 12345, // Wrong type (int instead of string)
}
resultCtx, resultPayload, err := initFunc(ctx, initPayload)
// Assert
require.NoError(t, err)
require.NotNil(t, resultPayload)
// Check API key is NOT in context (type assertion fails)
value := resultCtx.Value(middleware.ApiKey)
assert.Nil(t, value, "API key should not be set when wrong type")
// Check organization is NOT in context
value = resultCtx.Value(middleware.OrganizationKey)
assert.Nil(t, value, "Organization should not be set when API key has wrong type")
}
func TestWebSocketInitFunc_WithMultipleOrganizations(t *testing.T) {
// Setup - create multiple organizations
org1ID := uuid.New()
org1 := &domain.Organization{
BaseAggregate: eventsourced.BaseAggregate{
ID: eventsourced.IdFromString(org1ID.String()),
},
Name: "Organization 1",
}
org2ID := uuid.New()
org2 := &domain.Organization{
BaseAggregate: eventsourced.BaseAggregate{
ID: eventsourced.IdFromString(org2ID.String()),
},
Name: "Organization 2",
}
apiKey1 := "api-key-org-1"
apiKey2 := "api-key-org-2"
hashedKey1, err := hash.APIKey(apiKey1)
require.NoError(t, err)
hashedKey2, err := hash.APIKey(apiKey2)
require.NoError(t, err)
compositeKey1 := org1ID.String() + "-key1"
compositeKey2 := org2ID.String() + "-key2"
mockCache := &MockCache{
organizations: map[string]*domain.Organization{
compositeKey1: org1,
compositeKey2: org2,
},
apiKeys: map[string]string{
compositeKey1: hashedKey1,
compositeKey2: hashedKey2,
},
}
// Create InitFunc
initFunc := func(ctx context.Context, initPayload transport.InitPayload) (context.Context, *transport.InitPayload, error) {
// Extract API key from WebSocket connection_init payload
if apiKey, ok := initPayload["X-Api-Key"].(string); ok && apiKey != "" {
ctx = context.WithValue(ctx, middleware.ApiKey, apiKey)
// Look up organization by API key (cache handles hash comparison)
if organization := mockCache.OrganizationByAPIKey(apiKey); organization != nil {
ctx = context.WithValue(ctx, middleware.OrganizationKey, *organization)
}
}
return ctx, &initPayload, nil
}
// Test with first API key
ctx1 := context.Background()
initPayload1 := transport.InitPayload{
"X-Api-Key": apiKey1,
}
resultCtx1, _, err := initFunc(ctx1, initPayload1)
require.NoError(t, err)
if value := resultCtx1.Value(middleware.OrganizationKey); value != nil {
capturedOrg, ok := value.(domain.Organization)
require.True(t, ok)
assert.Equal(t, org1.Name, capturedOrg.Name)
assert.Equal(t, org1.ID.String(), capturedOrg.ID.String())
} else {
t.Fatal("Organization 1 not found in context")
}
// Test with second API key
ctx2 := context.Background()
initPayload2 := transport.InitPayload{
"X-Api-Key": apiKey2,
}
resultCtx2, _, err := initFunc(ctx2, initPayload2)
require.NoError(t, err)
if value := resultCtx2.Value(middleware.OrganizationKey); value != nil {
capturedOrg, ok := value.(domain.Organization)
require.True(t, ok)
assert.Equal(t, org2.Name, capturedOrg.Name)
assert.Equal(t, org2.ID.String(), capturedOrg.ID.String())
} else {
t.Fatal("Organization 2 not found in context")
}
}
+12 -1
View File
@@ -56,9 +56,20 @@ func (a AddAPIKey) Validate(_ context.Context, aggregate eventsourced.Aggregate)
}
func (a AddAPIKey) Event(context.Context) eventsourced.Event {
// Hash the API key using bcrypt for secure storage
// Note: We can't return an error here, but bcrypt errors are extremely rare
// (only if system runs out of memory or bcrypt cost is invalid)
// We use a fixed cost of 12 which is always valid
hashedKey, err := hash.APIKey(a.Key)
if err != nil {
// This should never happen with bcrypt cost 12, but if it does,
// we'll store an empty hash which will fail validation later
hashedKey = ""
}
return &APIKeyAdded{
Name: a.Name,
Key: hash.String(a.Key),
Key: hashedKey,
Refs: a.Refs,
Read: a.Read,
Publish: a.Publish,
+24 -11
View File
@@ -2,10 +2,13 @@ package domain
import (
"context"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
"github.com/stretchr/testify/require"
"gitlab.com/unboundsoftware/schemas/hash"
)
func TestAddAPIKey_Event(t *testing.T) {
@@ -24,7 +27,6 @@ func TestAddAPIKey_Event(t *testing.T) {
name string
fields fields
args args
want eventsourced.Event
}{
{
name: "event",
@@ -37,14 +39,6 @@ func TestAddAPIKey_Event(t *testing.T) {
Initiator: "jim@example.org",
},
args: args{},
want: &APIKeyAdded{
Name: "test",
Key: "dXNfYWtfMTIzNDU2Nzg5MDEyMzQ1NuOwxEKY/BwUmvv0yJlvuSQnrkHkZJuTTKSVmRt4UrhV",
Refs: []string{"Example@dev"},
Read: true,
Publish: true,
Initiator: "jim@example.org",
},
},
}
for _, tt := range tests {
@@ -57,7 +51,26 @@ func TestAddAPIKey_Event(t *testing.T) {
Publish: tt.fields.Publish,
Initiator: tt.fields.Initiator,
}
assert.Equalf(t, tt.want, a.Event(tt.args.in0), "Event(%v)", tt.args.in0)
event := a.Event(tt.args.in0)
require.NotNil(t, event)
// Cast to APIKeyAdded to verify fields
apiKeyEvent, ok := event.(*APIKeyAdded)
require.True(t, ok, "Event should be *APIKeyAdded")
// Verify non-key fields match exactly
assert.Equal(t, tt.fields.Name, apiKeyEvent.Name)
assert.Equal(t, tt.fields.Refs, apiKeyEvent.Refs)
assert.Equal(t, tt.fields.Read, apiKeyEvent.Read)
assert.Equal(t, tt.fields.Publish, apiKeyEvent.Publish)
assert.Equal(t, tt.fields.Initiator, apiKeyEvent.Initiator)
// Verify the key is hashed correctly (bcrypt format)
assert.True(t, strings.HasPrefix(apiKeyEvent.Key, "$2"), "Key should be bcrypt hashed")
assert.NotEqual(t, tt.fields.Key, apiKeyEvent.Key, "Key should be hashed, not plaintext")
// Verify the hash matches the original key
assert.True(t, hash.CompareAPIKey(apiKeyEvent.Key, tt.fields.Key), "Hashed key should match original")
})
}
}
+49 -28
View File
@@ -1,58 +1,79 @@
module gitlab.com/unboundsoftware/schemas
go 1.23.8
go 1.25
require (
github.com/99designs/gqlgen v0.17.73
github.com/Khan/genqlient v0.8.0
github.com/alecthomas/kong v1.10.0
github.com/99designs/gqlgen v0.17.83
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/Khan/genqlient v0.8.1
github.com/alecthomas/kong v1.13.0
github.com/apex/log v1.9.0
github.com/auth0/go-jwt-middleware/v2 v2.3.0
github.com/getsentry/sentry-go v0.32.0
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/auth0/go-jwt-middleware/v2 v2.3.1
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0
github.com/jmoiron/sqlx v1.4.0
github.com/pkg/errors v0.9.1
github.com/pressly/goose/v3 v3.24.3
github.com/pressly/goose/v3 v3.26.0
github.com/rs/cors v1.11.1
github.com/sparetimecoders/goamqp v0.3.2
github.com/stretchr/testify v1.10.0
github.com/vektah/gqlparser/v2 v2.5.27
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.178
gitlab.com/unboundsoftware/eventsourced/amqp v1.8.1
gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.2
github.com/sparetimecoders/goamqp v0.3.3
github.com/stretchr/testify v1.11.1
github.com/vektah/gqlparser/v2 v2.5.31
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.238
gitlab.com/unboundsoftware/eventsourced/amqp v1.9.0
gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.3
gitlab.com/unboundsoftware/eventsourced/pg v1.17.0
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0
go.opentelemetry.io/otel/log v0.14.0
go.opentelemetry.io/otel/sdk v1.38.0
go.opentelemetry.io/otel/sdk/log v0.14.0
go.opentelemetry.io/otel/sdk/metric v1.38.0
go.opentelemetry.io/otel/trace v1.38.0
golang.org/x/crypto v0.45.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rabbitmq/amqp091-go v1.10.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/sosodev/duration v1.3.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/tidwall/gjson v1.17.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/urfave/cli/v2 v2.27.6 // indirect
github.com/urfave/cli/v3 v3.6.0 // indirect
github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/tools v0.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.38.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/grpc v1.75.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)
+114 -71
View File
@@ -1,21 +1,21 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/99designs/gqlgen v0.17.73 h1:A3Ki+rHWqKbAOlg5fxiZBnz6OjW3nwupDHEG15gEsrg=
github.com/99designs/gqlgen v0.17.73/go.mod h1:2RyGWjy2k7W9jxrs8MOQthXGkD3L3oGr0jXW3Pu8lGg=
github.com/99designs/gqlgen v0.17.83 h1:LZOd4Of2snK5V22/ZWfBAPa3WoAZkBO70dKXM0ODHQk=
github.com/99designs/gqlgen v0.17.83/go.mod h1:q6Lb64wknFqNFSbSUGzKRKupklvY/xgNr62g0GGWPB8=
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/Khan/genqlient v0.8.0 h1:Hd1a+E1CQHYbMEKakIkvBH3zW0PWEeiX6Hp1i2kP2WE=
github.com/Khan/genqlient v0.8.0/go.mod h1:hn70SpYjWteRGvxTwo0kfaqg4wxvndECGkfa1fdDdYI=
github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs=
github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU=
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/kong v1.10.0 h1:8K4rGDpT7Iu+jEXCIJUeKqvpwZHbsFRoebLbnzlmrpw=
github.com/alecthomas/kong v1.10.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alecthomas/kong v1.13.0 h1:5e/7XC3ugvhP1DQBmTS+WuHtCbcv44hsohMgcvVxSrA=
github.com/alecthomas/kong v1.13.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
@@ -27,16 +27,16 @@ github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy
github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/auth0/go-jwt-middleware/v2 v2.3.0 h1:4QREj6cS3d8dS05bEm443jhnqQF97FX9sMBeWqnNRzE=
github.com/auth0/go-jwt-middleware/v2 v2.3.0/go.mod h1:dL4ObBs1/dj4/W4cYxd8rqAdDGXYyd5rqbpMIxcbVrU=
github.com/auth0/go-jwt-middleware/v2 v2.3.1 h1:lbDyWE9aLydb3zrank+Gufb9qGJN9u//7EbJK07pRrw=
github.com/auth0/go-jwt-middleware/v2 v2.3.1/go.mod h1:mqVr0gdB5zuaFyQFWMJH/c/2hehNjbYUD4i8Dpyf+Hc=
github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
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/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=
@@ -46,20 +46,23 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/getsentry/sentry-go v0.32.0 h1:YKs+//QmwE3DcYtfKRH8/KyOOF/I6Qnx7qYGNHCGmCY=
github.com/getsentry/sentry-go v0.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY=
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-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
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-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
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/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
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/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -67,6 +70,8 @@ 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/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
@@ -78,13 +83,15 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.2.0/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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
@@ -102,15 +109,13 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
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/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/pressly/goose/v3 v3.24.3 h1:DSWWNwwggVUsYZ0X2VitiAa9sKuqtBfe+Jr9zFGwWlM=
github.com/pressly/goose/v3 v3.24.3/go.mod h1:v9zYL4xdViLHCUUJh/mhjnm6JrK7Eul8AS93IxiZM4E=
github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM=
github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -120,12 +125,10 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=
github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y=
github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=
github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
@@ -136,13 +139,15 @@ github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h
github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/sparetimecoders/goamqp v0.3.2 h1:XdlyUBAJS5RcURw+SnnPjPJJuofddZwQsjAf05VPXvI=
github.com/sparetimecoders/goamqp v0.3.2/go.mod h1:W9NRCpWLE+Vruv2dcRSbszNil2O826d2Nv6kAkETW5o=
github.com/sparetimecoders/goamqp v0.3.3 h1:z/nfTPmrjeU/rIVuNOgsVLCimp3WFoNFvS3ZzXRJ6HE=
github.com/sparetimecoders/goamqp v0.3.3/go.mod h1:W9NRCpWLE+Vruv2dcRSbszNil2O826d2Nv6kAkETW5o=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.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/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -160,55 +165,93 @@ github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
github.com/urfave/cli/v3 v3.6.0 h1:oIdArVjkdIXHWg3iqxgmqwQGC8NM0JtdgwQAj2sRwFo=
github.com/urfave/cli/v3 v3.6.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k=
github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTBjW+SZK4mhxTTBVpxcqeBgWF1Rfmltbfk=
github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE=
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.178 h1:NeZwriuQKkowZbqWo2NKuZ19epBc34JgFS5cOfSzQEg=
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.178/go.mod h1:B7eV0Qh8Lop9QzIOQcsvKp3S0ejfC6mgyWoJnI917yQ=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
gitlab.com/unboundsoftware/eventsourced/amqp v1.8.1 h1:MGHH2Uxp68J9i4V3/3vApB6gjBUjn6RjiPHhbc8Wsno=
gitlab.com/unboundsoftware/eventsourced/amqp v1.8.1/go.mod h1:clGBkdpFWb5/27aLOhJ6+DB15enJf+T4J5lR6X0lqAs=
gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.2 h1:8sCnThNHEPB3BQomcJ7u6fmc2t043fAZSMmVPDDbQOs=
gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.2/go.mod h1:KeLn3U67hxbdFLfeXd0c0LI/r1C5rijbWrfNdARWe98=
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.238 h1:ll0BtYVMziRa8v0T/f+DQOJ/1x3Dq5puifJNnxF0R+M=
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.238/go.mod h1:ErOQH1ki2+SZB8JjpTyGVnoBpg5picIyjvuWQJP4abg=
gitlab.com/unboundsoftware/eventsourced/amqp v1.9.0 h1:TdBJnrnrxJrPhC4i6KTFUElZa3k/fFXiGwg0sds5aAo=
gitlab.com/unboundsoftware/eventsourced/amqp v1.9.0/go.mod h1:VauAph7uCvEakYNdHkkSAoOOGKvEuUA/uhsR376ThbI=
gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.3 h1:0HbDHF4sHfoyDrbPLMFWvsQLbTl2ITrpI9PjDIZsV1Y=
gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.3/go.mod h1:LrA7I7etRmhIC1PjO8c26BHm+gWsy2rC3eSMe5+XUWE=
gitlab.com/unboundsoftware/eventsourced/pg v1.17.0 h1:pUJzMpNPX0GVsffRZXlpKR1d7Ws96KTxJwbLFPpASSc=
gitlab.com/unboundsoftware/eventsourced/pg v1.17.0/go.mod h1:WgPrZhyCbsZ3TG2tPUbh2MUjOEaANJjsWi/0hlIwRVU=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0 h1:bwnLpizECbPr1RrQ27waeY2SPIPeccCx/xLuoYADZ9s=
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0/go.mod h1:3nWlOiiqA9UtUnrcNk82mYasNxD8ehOspL0gOfEo6Y4=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 h1:B/g+qde6Mkzxbry5ZZag0l7QrQBCtVm7lVjaLgmpje8=
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0/go.mod h1:mOJK8eMmgW6ocDJn6Bn11CcZ05gi3P8GylBXEkZtbgA=
go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM=
go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg=
go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
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=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -223,11 +266,11 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/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=
modernc.org/libc v1.65.0 h1:e183gLDnAp9VJh6gWKdTy0CThL9Pt7MfcR/0bgb7Y1Y=
modernc.org/libc v1.65.0/go.mod h1:7m9VzGq7APssBTydds2zBcxGREwvIGpuUBaKTXdm2Qs=
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
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.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4=
modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
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.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
+1 -1
View File
@@ -38,7 +38,7 @@ func ToGqlAPIKeys(keys []domain.APIKey) []*model.APIKey {
result[i] = &model.APIKey{
ID: apiKeyId(k.OrganizationId, k.Name),
Name: k.Name,
Key: &k.Key,
Key: nil, // Never return the hashed key - only return plaintext on creation
Organization: nil,
Refs: k.Refs,
Read: k.Read,
+125
View File
@@ -0,0 +1,125 @@
package graph
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"gopkg.in/yaml.v3"
"gitlab.com/unboundsoftware/schemas/graph/model"
)
// CommandExecutor is an interface for executing external commands
// This allows for mocking in tests
type CommandExecutor interface {
Execute(name string, args ...string) ([]byte, error)
}
// DefaultCommandExecutor implements CommandExecutor using os/exec
type DefaultCommandExecutor struct{}
// Execute runs a command and returns its combined output
func (e *DefaultCommandExecutor) Execute(name string, args ...string) ([]byte, error) {
cmd := exec.Command(name, args...)
return cmd.CombinedOutput()
}
// GenerateCosmoRouterConfig generates a Cosmo Router execution config from subgraphs
// using the official wgc CLI tool via npx
func GenerateCosmoRouterConfig(subGraphs []*model.SubGraph) (string, error) {
return GenerateCosmoRouterConfigWithExecutor(subGraphs, &DefaultCommandExecutor{})
}
// GenerateCosmoRouterConfigWithExecutor generates a Cosmo Router execution config from subgraphs
// using the provided command executor (useful for testing)
func GenerateCosmoRouterConfigWithExecutor(subGraphs []*model.SubGraph, executor CommandExecutor) (string, error) {
if len(subGraphs) == 0 {
return "", fmt.Errorf("no subgraphs provided")
}
// Create a temporary directory for composition
tmpDir, err := os.MkdirTemp("", "cosmo-compose-*")
if err != nil {
return "", fmt.Errorf("create temp dir: %w", err)
}
defer os.RemoveAll(tmpDir)
// Write each subgraph SDL to a file
type SubgraphConfig struct {
Name string `yaml:"name"`
RoutingURL string `yaml:"routing_url,omitempty"`
Schema map[string]string `yaml:"schema"`
Subscription map[string]interface{} `yaml:"subscription,omitempty"`
}
type InputConfig struct {
Version int `yaml:"version"`
Subgraphs []SubgraphConfig `yaml:"subgraphs"`
}
inputConfig := InputConfig{
Version: 1,
Subgraphs: make([]SubgraphConfig, 0, len(subGraphs)),
}
for _, sg := range subGraphs {
// Write SDL to a temp file
schemaFile := filepath.Join(tmpDir, fmt.Sprintf("%s.graphql", sg.Service))
if err := os.WriteFile(schemaFile, []byte(sg.Sdl), 0o644); err != nil {
return "", fmt.Errorf("write schema file for %s: %w", sg.Service, err)
}
subgraphCfg := SubgraphConfig{
Name: sg.Service,
Schema: map[string]string{
"file": schemaFile,
},
}
if sg.URL != nil {
subgraphCfg.RoutingURL = *sg.URL
}
if sg.WsURL != nil {
subgraphCfg.Subscription = map[string]interface{}{
"url": *sg.WsURL,
"protocol": "ws",
"websocket_subprotocol": "graphql-ws",
}
}
inputConfig.Subgraphs = append(inputConfig.Subgraphs, subgraphCfg)
}
// Write input config YAML
inputFile := filepath.Join(tmpDir, "input.yaml")
inputYAML, err := yaml.Marshal(inputConfig)
if err != nil {
return "", fmt.Errorf("marshal input config: %w", err)
}
if err := os.WriteFile(inputFile, inputYAML, 0o644); err != nil {
return "", fmt.Errorf("write input config: %w", err)
}
// Execute wgc router compose
// wgc is installed globally in the Docker image
outputFile := filepath.Join(tmpDir, "config.json")
output, err := executor.Execute("wgc", "router", "compose",
"--input", inputFile,
"--out", outputFile,
"--suppress-warnings",
)
if err != nil {
return "", fmt.Errorf("wgc router compose failed: %w\nOutput: %s", err, string(output))
}
// Read the generated config
configJSON, err := os.ReadFile(outputFile)
if err != nil {
return "", fmt.Errorf("read output config: %w", err)
}
return string(configJSON), nil
}
+465
View File
@@ -0,0 +1,465 @@
package graph
import (
"encoding/json"
"fmt"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"gitlab.com/unboundsoftware/schemas/graph/model"
)
// MockCommandExecutor implements CommandExecutor for testing
type MockCommandExecutor struct {
// CallCount tracks how many times Execute was called
CallCount int
// LastArgs stores the arguments from the last call
LastArgs []string
// Error can be set to simulate command failures
Error error
}
// Execute mocks the wgc command by generating a realistic config.json file
func (m *MockCommandExecutor) Execute(name string, args ...string) ([]byte, error) {
m.CallCount++
m.LastArgs = append([]string{name}, args...)
if m.Error != nil {
return nil, m.Error
}
// Parse the input file to understand what subgraphs we're composing
var inputFile, outputFile string
for i, arg := range args {
if arg == "--input" && i+1 < len(args) {
inputFile = args[i+1]
}
if arg == "--out" && i+1 < len(args) {
outputFile = args[i+1]
}
}
if inputFile == "" || outputFile == "" {
return nil, fmt.Errorf("missing required arguments")
}
// Read the input YAML to get subgraph information
inputData, err := os.ReadFile(inputFile)
if err != nil {
return nil, fmt.Errorf("failed to read input file: %w", err)
}
var input struct {
Version int `yaml:"version"`
Subgraphs []struct {
Name string `yaml:"name"`
RoutingURL string `yaml:"routing_url,omitempty"`
Schema map[string]string `yaml:"schema"`
Subscription map[string]interface{} `yaml:"subscription,omitempty"`
} `yaml:"subgraphs"`
}
if err := yaml.Unmarshal(inputData, &input); err != nil {
return nil, fmt.Errorf("failed to parse input YAML: %w", err)
}
// Generate a realistic Cosmo Router config based on the input
config := map[string]interface{}{
"version": "mock-version-uuid",
"subgraphs": func() []map[string]interface{} {
subgraphs := make([]map[string]interface{}, len(input.Subgraphs))
for i, sg := range input.Subgraphs {
subgraph := map[string]interface{}{
"id": fmt.Sprintf("mock-id-%d", i),
"name": sg.Name,
}
if sg.RoutingURL != "" {
subgraph["routingUrl"] = sg.RoutingURL
}
subgraphs[i] = subgraph
}
return subgraphs
}(),
"engineConfig": map[string]interface{}{
"graphqlSchema": generateMockSchema(input.Subgraphs),
"datasourceConfigurations": func() []map[string]interface{} {
dsConfigs := make([]map[string]interface{}, len(input.Subgraphs))
for i, sg := range input.Subgraphs {
// Read SDL from file
sdl := ""
if schemaFile, ok := sg.Schema["file"]; ok {
if sdlData, err := os.ReadFile(schemaFile); err == nil {
sdl = string(sdlData)
}
}
dsConfig := map[string]interface{}{
"id": fmt.Sprintf("datasource-%d", i),
"kind": "GRAPHQL",
"customGraphql": map[string]interface{}{
"federation": map[string]interface{}{
"enabled": true,
"serviceSdl": sdl,
},
"subscription": func() map[string]interface{} {
if len(sg.Subscription) > 0 {
return map[string]interface{}{
"enabled": true,
"url": map[string]interface{}{
"staticVariableContent": sg.Subscription["url"],
},
"protocol": sg.Subscription["protocol"],
"websocketSubprotocol": sg.Subscription["websocket_subprotocol"],
}
}
return map[string]interface{}{
"enabled": false,
}
}(),
},
}
dsConfigs[i] = dsConfig
}
return dsConfigs
}(),
},
}
// Write the config to the output file
configJSON, err := json.Marshal(config)
if err != nil {
return nil, fmt.Errorf("failed to marshal config: %w", err)
}
if err := os.WriteFile(outputFile, configJSON, 0o644); err != nil {
return nil, fmt.Errorf("failed to write output file: %w", err)
}
return []byte("Success"), nil
}
// generateMockSchema creates a simple merged schema from subgraphs
func generateMockSchema(subgraphs []struct {
Name string `yaml:"name"`
RoutingURL string `yaml:"routing_url,omitempty"`
Schema map[string]string `yaml:"schema"`
Subscription map[string]interface{} `yaml:"subscription,omitempty"`
},
) string {
schema := strings.Builder{}
schema.WriteString("schema {\n query: Query\n")
// Check if any subgraph has subscriptions
hasSubscriptions := false
for _, sg := range subgraphs {
if len(sg.Subscription) > 0 {
hasSubscriptions = true
break
}
}
if hasSubscriptions {
schema.WriteString(" subscription: Subscription\n")
}
schema.WriteString("}\n\n")
// Add types by reading SDL files
for _, sg := range subgraphs {
if schemaFile, ok := sg.Schema["file"]; ok {
if sdlData, err := os.ReadFile(schemaFile); err == nil {
schema.WriteString(string(sdlData))
schema.WriteString("\n")
}
}
}
return schema.String()
}
func TestGenerateCosmoRouterConfig(t *testing.T) {
tests := []struct {
name string
subGraphs []*model.SubGraph
wantErr bool
validate func(t *testing.T, config string)
}{
{
name: "single subgraph with all fields",
subGraphs: []*model.SubGraph{
{
Service: "test-service",
URL: stringPtr("http://localhost:4001/query"),
WsURL: stringPtr("ws://localhost:4001/query"),
Sdl: "type Query { test: String }",
},
},
wantErr: false,
validate: func(t *testing.T, config string) {
var result map[string]interface{}
err := json.Unmarshal([]byte(config), &result)
require.NoError(t, err, "Config should be valid JSON")
// Version is a UUID string from wgc
version, ok := result["version"].(string)
require.True(t, ok, "Version should be a string")
assert.NotEmpty(t, version, "Version should not be empty")
subgraphs, ok := result["subgraphs"].([]interface{})
require.True(t, ok, "subgraphs should be an array")
require.Len(t, subgraphs, 1, "Should have 1 subgraph")
sg := subgraphs[0].(map[string]interface{})
assert.Equal(t, "test-service", sg["name"])
assert.Equal(t, "http://localhost:4001/query", sg["routingUrl"])
// Check that datasource configurations include subscription settings
engineConfig, ok := result["engineConfig"].(map[string]interface{})
require.True(t, ok, "Should have engineConfig")
dsConfigs, ok := engineConfig["datasourceConfigurations"].([]interface{})
require.True(t, ok && len(dsConfigs) > 0, "Should have datasource configurations")
ds := dsConfigs[0].(map[string]interface{})
customGraphql, ok := ds["customGraphql"].(map[string]interface{})
require.True(t, ok, "Should have customGraphql config")
subscription, ok := customGraphql["subscription"].(map[string]interface{})
require.True(t, ok, "Should have subscription config")
assert.True(t, subscription["enabled"].(bool), "Subscription should be enabled")
subUrl, ok := subscription["url"].(map[string]interface{})
require.True(t, ok, "Should have subscription URL")
assert.Equal(t, "ws://localhost:4001/query", subUrl["staticVariableContent"])
},
},
{
name: "multiple subgraphs",
subGraphs: []*model.SubGraph{
{
Service: "service-1",
URL: stringPtr("http://localhost:4001/query"),
Sdl: "type Query { field1: String }",
},
{
Service: "service-2",
URL: stringPtr("http://localhost:4002/query"),
Sdl: "type Query { field2: String }",
},
{
Service: "service-3",
URL: stringPtr("http://localhost:4003/query"),
WsURL: stringPtr("ws://localhost:4003/query"),
Sdl: "type Subscription { updates: String }",
},
},
wantErr: false,
validate: func(t *testing.T, config string) {
var result map[string]interface{}
err := json.Unmarshal([]byte(config), &result)
require.NoError(t, err)
subgraphs := result["subgraphs"].([]interface{})
assert.Len(t, subgraphs, 3, "Should have 3 subgraphs")
// Check service names
sg1 := subgraphs[0].(map[string]interface{})
assert.Equal(t, "service-1", sg1["name"])
sg3 := subgraphs[2].(map[string]interface{})
assert.Equal(t, "service-3", sg3["name"])
// Check that datasource configurations include subscription for service-3
engineConfig, ok := result["engineConfig"].(map[string]interface{})
require.True(t, ok, "Should have engineConfig")
dsConfigs, ok := engineConfig["datasourceConfigurations"].([]interface{})
require.True(t, ok && len(dsConfigs) == 3, "Should have 3 datasource configurations")
// Find service-3's datasource config (should have subscription enabled)
ds3 := dsConfigs[2].(map[string]interface{})
customGraphql, ok := ds3["customGraphql"].(map[string]interface{})
require.True(t, ok, "Service-3 should have customGraphql config")
subscription, ok := customGraphql["subscription"].(map[string]interface{})
require.True(t, ok, "Service-3 should have subscription config")
assert.True(t, subscription["enabled"].(bool), "Service-3 subscription should be enabled")
},
},
{
name: "subgraph with no URL",
subGraphs: []*model.SubGraph{
{
Service: "test-service",
URL: nil,
WsURL: nil,
Sdl: "type Query { test: String }",
},
},
wantErr: false,
validate: func(t *testing.T, config string) {
var result map[string]interface{}
err := json.Unmarshal([]byte(config), &result)
require.NoError(t, err)
subgraphs := result["subgraphs"].([]interface{})
sg := subgraphs[0].(map[string]interface{})
// Should not have routing URL when URL is nil
_, hasRoutingURL := sg["routingUrl"]
assert.False(t, hasRoutingURL, "Should not have routingUrl when URL is nil")
// Check datasource configurations don't have subscription enabled
engineConfig, ok := result["engineConfig"].(map[string]interface{})
require.True(t, ok, "Should have engineConfig")
dsConfigs, ok := engineConfig["datasourceConfigurations"].([]interface{})
require.True(t, ok && len(dsConfigs) > 0, "Should have datasource configurations")
ds := dsConfigs[0].(map[string]interface{})
customGraphql, ok := ds["customGraphql"].(map[string]interface{})
require.True(t, ok, "Should have customGraphql config")
subscription, ok := customGraphql["subscription"].(map[string]interface{})
if ok {
// wgc always enables subscription but URL should be empty when WsURL is nil
subUrl, hasUrl := subscription["url"].(map[string]interface{})
if hasUrl {
_, hasStaticContent := subUrl["staticVariableContent"]
assert.False(t, hasStaticContent, "Subscription URL should be empty when WsURL is nil")
}
}
},
},
{
name: "empty subgraphs",
subGraphs: []*model.SubGraph{},
wantErr: true,
validate: nil,
},
{
name: "nil subgraphs",
subGraphs: nil,
wantErr: true,
validate: nil,
},
{
name: "complex SDL with multiple types",
subGraphs: []*model.SubGraph{
{
Service: "complex-service",
URL: stringPtr("http://localhost:4001/query"),
Sdl: `
type Query {
user(id: ID!): User
users: [User!]!
}
type User {
id: ID!
name: String!
email: String!
}
`,
},
},
wantErr: false,
validate: func(t *testing.T, config string) {
var result map[string]interface{}
err := json.Unmarshal([]byte(config), &result)
require.NoError(t, err)
// Check the composed graphqlSchema contains the types
engineConfig, ok := result["engineConfig"].(map[string]interface{})
require.True(t, ok, "Should have engineConfig")
graphqlSchema, ok := engineConfig["graphqlSchema"].(string)
require.True(t, ok, "Should have graphqlSchema")
assert.Contains(t, graphqlSchema, "Query", "Schema should contain Query type")
assert.Contains(t, graphqlSchema, "User", "Schema should contain User type")
// Check datasource has the original SDL
dsConfigs, ok := engineConfig["datasourceConfigurations"].([]interface{})
require.True(t, ok && len(dsConfigs) > 0, "Should have datasource configurations")
ds := dsConfigs[0].(map[string]interface{})
customGraphql, ok := ds["customGraphql"].(map[string]interface{})
require.True(t, ok, "Should have customGraphql config")
federation, ok := customGraphql["federation"].(map[string]interface{})
require.True(t, ok, "Should have federation config")
serviceSdl, ok := federation["serviceSdl"].(string)
require.True(t, ok, "Should have serviceSdl")
assert.Contains(t, serviceSdl, "email: String!", "SDL should contain email field")
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Use mock executor for all tests
mockExecutor := &MockCommandExecutor{}
config, err := GenerateCosmoRouterConfigWithExecutor(tt.subGraphs, mockExecutor)
if tt.wantErr {
assert.Error(t, err)
// Verify executor was not called for error cases
if len(tt.subGraphs) == 0 {
assert.Equal(t, 0, mockExecutor.CallCount, "Should not call executor for empty subgraphs")
}
return
}
require.NoError(t, err)
assert.NotEmpty(t, config, "Config should not be empty")
// Verify executor was called correctly
assert.Equal(t, 1, mockExecutor.CallCount, "Should call executor once")
assert.Equal(t, "wgc", mockExecutor.LastArgs[0], "Should call wgc command")
assert.Contains(t, mockExecutor.LastArgs, "router", "Should include 'router' arg")
assert.Contains(t, mockExecutor.LastArgs, "compose", "Should include 'compose' arg")
if tt.validate != nil {
tt.validate(t, config)
}
})
}
}
// TestGenerateCosmoRouterConfig_MockError tests error handling with mock executor
func TestGenerateCosmoRouterConfig_MockError(t *testing.T) {
subGraphs := []*model.SubGraph{
{
Service: "test-service",
URL: stringPtr("http://localhost:4001/query"),
Sdl: "type Query { test: String }",
},
}
// Create a mock executor that returns an error
mockExecutor := &MockCommandExecutor{
Error: fmt.Errorf("simulated wgc failure"),
}
config, err := GenerateCosmoRouterConfigWithExecutor(subGraphs, mockExecutor)
// Verify error is propagated
assert.Error(t, err)
assert.Contains(t, err.Error(), "wgc router compose failed")
assert.Contains(t, err.Error(), "simulated wgc failure")
assert.Empty(t, config)
// Verify executor was called
assert.Equal(t, 1, mockExecutor.CallCount, "Should have attempted to call executor")
}
// Helper function for tests
func stringPtr(s string) *string {
return &s
}
+1480 -2225
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -49,6 +49,13 @@ type Organization struct {
type Query struct {
}
type SchemaUpdate struct {
Ref string `json:"ref"`
ID string `json:"id"`
SubGraphs []*SubGraph `json:"subGraphs"`
CosmoRouterConfig *string `json:"cosmoRouterConfig,omitempty"`
}
type SubGraph struct {
ID string `json:"id"`
Service string `json:"service"`
@@ -68,6 +75,9 @@ type SubGraphs struct {
func (SubGraphs) IsSupergraph() {}
type Subscription struct {
}
type Unchanged struct {
ID string `json:"id"`
MinDelaySeconds int `json:"minDelaySeconds"`
+66
View File
@@ -0,0 +1,66 @@
package graph
import (
"sync"
"gitlab.com/unboundsoftware/schemas/graph/model"
)
// PubSub handles publishing schema updates to subscribers
type PubSub struct {
mu sync.RWMutex
subscribers map[string][]chan *model.SchemaUpdate
}
func NewPubSub() *PubSub {
return &PubSub{
subscribers: make(map[string][]chan *model.SchemaUpdate),
}
}
// Subscribe creates a new subscription channel for a given schema ref
func (ps *PubSub) Subscribe(ref string) chan *model.SchemaUpdate {
ps.mu.Lock()
defer ps.mu.Unlock()
ch := make(chan *model.SchemaUpdate, 10)
ps.subscribers[ref] = append(ps.subscribers[ref], ch)
return ch
}
// Unsubscribe removes a subscription channel
func (ps *PubSub) Unsubscribe(ref string, ch chan *model.SchemaUpdate) {
ps.mu.Lock()
defer ps.mu.Unlock()
subs := ps.subscribers[ref]
for i, sub := range subs {
if sub == ch {
// Remove this subscriber
ps.subscribers[ref] = append(subs[:i], subs[i+1:]...)
close(sub)
break
}
}
// Clean up empty subscriber lists
if len(ps.subscribers[ref]) == 0 {
delete(ps.subscribers, ref)
}
}
// Publish sends a schema update to all subscribers of a given ref
func (ps *PubSub) Publish(ref string, update *model.SchemaUpdate) {
ps.mu.RLock()
defer ps.mu.RUnlock()
for _, ch := range ps.subscribers[ref] {
// Non-blocking send - if subscriber is slow, skip
select {
case ch <- update:
default:
// Channel full, subscriber is too slow - skip this update
}
}
}
+256
View File
@@ -0,0 +1,256 @@
package graph
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gitlab.com/unboundsoftware/schemas/graph/model"
)
func TestPubSub_SubscribeAndPublish(t *testing.T) {
ps := NewPubSub()
ref := "Test@dev"
// Subscribe
ch := ps.Subscribe(ref)
require.NotNil(t, ch, "Subscribe should return a channel")
// Publish
update := &model.SchemaUpdate{
Ref: ref,
ID: "test-id-1",
SubGraphs: []*model.SubGraph{
{
ID: "sg1",
Service: "test-service",
Sdl: "type Query { test: String }",
},
},
}
go ps.Publish(ref, update)
// Receive
select {
case received := <-ch:
assert.Equal(t, update.Ref, received.Ref, "Ref should match")
assert.Equal(t, update.ID, received.ID, "ID should match")
assert.Equal(t, len(update.SubGraphs), len(received.SubGraphs), "SubGraphs count should match")
case <-time.After(1 * time.Second):
t.Fatal("Timeout waiting for published update")
}
}
func TestPubSub_MultipleSubscribers(t *testing.T) {
ps := NewPubSub()
ref := "Test@dev"
// Create multiple subscribers
ch1 := ps.Subscribe(ref)
ch2 := ps.Subscribe(ref)
ch3 := ps.Subscribe(ref)
update := &model.SchemaUpdate{
Ref: ref,
ID: "test-id-2",
}
// Publish once
ps.Publish(ref, update)
// All subscribers should receive the update
var wg sync.WaitGroup
wg.Add(3)
checkReceived := func(ch <-chan *model.SchemaUpdate, name string) {
defer wg.Done()
select {
case received := <-ch:
assert.Equal(t, update.ID, received.ID, "%s should receive correct update", name)
case <-time.After(1 * time.Second):
t.Errorf("%s: Timeout waiting for update", name)
}
}
go checkReceived(ch1, "Subscriber 1")
go checkReceived(ch2, "Subscriber 2")
go checkReceived(ch3, "Subscriber 3")
wg.Wait()
}
func TestPubSub_DifferentRefs(t *testing.T) {
ps := NewPubSub()
ref1 := "Test1@dev"
ref2 := "Test2@dev"
ch1 := ps.Subscribe(ref1)
ch2 := ps.Subscribe(ref2)
update1 := &model.SchemaUpdate{Ref: ref1, ID: "id1"}
update2 := &model.SchemaUpdate{Ref: ref2, ID: "id2"}
// Publish to ref1
ps.Publish(ref1, update1)
// Only ch1 should receive
select {
case received := <-ch1:
assert.Equal(t, "id1", received.ID)
case <-time.After(100 * time.Millisecond):
t.Fatal("ch1 should have received update")
}
// ch2 should not receive ref1's update
select {
case <-ch2:
t.Fatal("ch2 should not receive ref1's update")
case <-time.After(100 * time.Millisecond):
// Expected - no update
}
// Publish to ref2
ps.Publish(ref2, update2)
// Now ch2 should receive
select {
case received := <-ch2:
assert.Equal(t, "id2", received.ID)
case <-time.After(100 * time.Millisecond):
t.Fatal("ch2 should have received update")
}
}
func TestPubSub_Unsubscribe(t *testing.T) {
ps := NewPubSub()
ref := "Test@dev"
ch := ps.Subscribe(ref)
// Unsubscribe
ps.Unsubscribe(ref, ch)
// Channel should be closed
_, ok := <-ch
assert.False(t, ok, "Channel should be closed after unsubscribe")
// Publishing after unsubscribe should not panic
assert.NotPanics(t, func() {
ps.Publish(ref, &model.SchemaUpdate{Ref: ref})
})
}
func TestPubSub_BufferedChannel(t *testing.T) {
ps := NewPubSub()
ref := "Test@dev"
ch := ps.Subscribe(ref)
// Publish multiple updates quickly (up to buffer size of 10)
for i := 0; i < 10; i++ {
update := &model.SchemaUpdate{
Ref: ref,
ID: string(rune('a' + i)),
}
ps.Publish(ref, update)
}
// All 10 should be buffered and receivable
received := 0
timeout := time.After(1 * time.Second)
for received < 10 {
select {
case <-ch:
received++
case <-timeout:
t.Fatalf("Only received %d out of 10 updates", received)
}
}
assert.Equal(t, 10, received, "Should receive all buffered updates")
}
func TestPubSub_SlowSubscriber(t *testing.T) {
ps := NewPubSub()
ref := "Test@dev"
ch := ps.Subscribe(ref)
// Fill the buffer (10 items)
for i := 0; i < 10; i++ {
ps.Publish(ref, &model.SchemaUpdate{Ref: ref})
}
// Publish one more - this should be dropped (channel full, non-blocking send)
ps.Publish(ref, &model.SchemaUpdate{Ref: ref, ID: "should-be-dropped"})
// Drain the channel
count := 0
timeout := time.After(500 * time.Millisecond)
drainLoop:
for {
select {
case update := <-ch:
count++
// Should not receive the dropped update
assert.NotEqual(t, "should-be-dropped", update.ID, "Should not receive dropped update")
case <-timeout:
break drainLoop
}
}
// Should have received exactly 10 (the buffer size), not 11
assert.Equal(t, 10, count, "Should only receive buffered updates, not the dropped one")
}
func TestPubSub_ConcurrentPublish(t *testing.T) {
ps := NewPubSub()
ref := "Test@dev"
ch := ps.Subscribe(ref)
numPublishers := 10
updatesPerPublisher := 10
var wg sync.WaitGroup
wg.Add(numPublishers)
// Multiple goroutines publishing concurrently
for i := 0; i < numPublishers; i++ {
go func(publisherID int) {
defer wg.Done()
for j := 0; j < updatesPerPublisher; j++ {
ps.Publish(ref, &model.SchemaUpdate{
Ref: ref,
ID: string(rune('a' + publisherID)),
})
}
}(i)
}
wg.Wait()
// Should not panic and subscriber should receive updates
// (exact count may vary due to buffer and timing)
timeout := time.After(1 * time.Second)
received := 0
receiveLoop:
for {
select {
case <-ch:
received++
case <-timeout:
break receiveLoop
}
}
assert.Greater(t, received, 0, "Should have received some updates")
}
+1
View File
@@ -28,6 +28,7 @@ type Resolver struct {
Publisher Publisher
Logger *slog.Logger
Cache *cache.Cache
PubSub *PubSub
}
func (r *Resolver) apiKeyCanAccessRef(ctx context.Context, ref string, publish bool) (string, error) {
+12
View File
@@ -1,6 +1,7 @@
type Query {
organizations: [Organization!]! @auth(user: true)
supergraph(ref: String!, isAfter: String): Supergraph! @auth(organization: true)
latestSchema(ref: String!): SchemaUpdate! @auth(organization: true)
}
type Mutation {
@@ -9,6 +10,10 @@ type Mutation {
updateSubGraph(input: InputSubGraph!): SubGraph! @auth(organization: true)
}
type Subscription {
schemaUpdates(ref: String!): SchemaUpdate! @auth(organization: true)
}
type Organization {
id: ID!
name: String!
@@ -54,6 +59,13 @@ type SubGraph {
changedAt: Time!
}
type SchemaUpdate {
ref: String!
id: ID!
subGraphs: [SubGraph!]!
cosmoRouterConfig: String
}
input InputAPIKey {
name: String!
organizationId: ID!
+209 -2
View File
@@ -119,6 +119,60 @@ func (r *mutationResolver) UpdateSubGraph(ctx context.Context, input model.Input
if err != nil {
return nil, err
}
// Publish schema update to subscribers
go func() {
services, lastUpdate := r.Cache.Services(orgId, input.Ref, "")
r.Logger.Info("Publishing schema update after subgraph change",
"ref", input.Ref,
"orgId", orgId,
"lastUpdate", lastUpdate,
"servicesCount", len(services),
)
subGraphs := make([]*model.SubGraph, len(services))
for i, id := range services {
sg, err := r.fetchSubGraph(context.Background(), id)
if err != nil {
r.Logger.Error("fetch subgraph for update notification", "error", err)
continue
}
subGraphs[i] = &model.SubGraph{
ID: sg.ID.String(),
Service: sg.Service,
URL: sg.Url,
WsURL: sg.WSUrl,
Sdl: sg.Sdl,
ChangedBy: sg.ChangedBy,
ChangedAt: sg.ChangedAt,
}
}
// Generate Cosmo router config
cosmoConfig, err := GenerateCosmoRouterConfig(subGraphs)
if err != nil {
r.Logger.Error("generate cosmo config for update", "error", err)
cosmoConfig = "" // Send empty if generation fails
}
// Publish to all subscribers of this ref
update := &model.SchemaUpdate{
Ref: input.Ref,
ID: lastUpdate,
SubGraphs: subGraphs,
CosmoRouterConfig: &cosmoConfig,
}
r.Logger.Info("Publishing schema update to subscribers",
"ref", update.Ref,
"id", update.ID,
"subGraphsCount", len(update.SubGraphs),
"cosmoConfigLength", len(cosmoConfig),
)
r.PubSub.Publish(input.Ref, update)
}()
return r.toGqlSubGraph(subGraph), nil
}
@@ -184,13 +238,166 @@ func (r *queryResolver) Supergraph(ctx context.Context, ref string, isAfter *str
}, nil
}
// LatestSchema is the resolver for the latestSchema field.
func (r *queryResolver) LatestSchema(ctx context.Context, ref string) (*model.SchemaUpdate, error) {
orgId := middleware.OrganizationFromContext(ctx)
r.Logger.Info("LatestSchema query",
"ref", ref,
"orgId", orgId,
)
_, err := r.apiKeyCanAccessRef(ctx, ref, false)
if err != nil {
r.Logger.Error("API key cannot access ref", "error", err, "ref", ref)
return nil, err
}
// Get current services and schema
services, lastUpdate := r.Cache.Services(orgId, ref, "")
r.Logger.Info("Fetching latest schema",
"ref", ref,
"orgId", orgId,
"lastUpdate", lastUpdate,
"servicesCount", len(services),
)
subGraphs := make([]*model.SubGraph, len(services))
for i, id := range services {
sg, err := r.fetchSubGraph(ctx, id)
if err != nil {
r.Logger.Error("fetch subgraph", "error", err, "id", id)
return nil, err
}
subGraphs[i] = &model.SubGraph{
ID: sg.ID.String(),
Service: sg.Service,
URL: sg.Url,
WsURL: sg.WSUrl,
Sdl: sg.Sdl,
ChangedBy: sg.ChangedBy,
ChangedAt: sg.ChangedAt,
}
}
// Generate Cosmo router config
cosmoConfig, err := GenerateCosmoRouterConfig(subGraphs)
if err != nil {
r.Logger.Error("generate cosmo config", "error", err)
cosmoConfig = "" // Return empty if generation fails
}
update := &model.SchemaUpdate{
Ref: ref,
ID: lastUpdate,
SubGraphs: subGraphs,
CosmoRouterConfig: &cosmoConfig,
}
r.Logger.Info("Latest schema fetched",
"ref", update.Ref,
"id", update.ID,
"subGraphsCount", len(update.SubGraphs),
"cosmoConfigLength", len(cosmoConfig),
)
return update, nil
}
// SchemaUpdates is the resolver for the schemaUpdates field.
func (r *subscriptionResolver) SchemaUpdates(ctx context.Context, ref string) (<-chan *model.SchemaUpdate, error) {
orgId := middleware.OrganizationFromContext(ctx)
r.Logger.Info("SchemaUpdates subscription started",
"ref", ref,
"orgId", orgId,
)
_, err := r.apiKeyCanAccessRef(ctx, ref, false)
if err != nil {
r.Logger.Error("API key cannot access ref", "error", err, "ref", ref)
return nil, err
}
// Subscribe to updates for this ref
ch := r.PubSub.Subscribe(ref)
// Send initial state immediately
go func() {
// Use background context for async operation
bgCtx := context.Background()
services, lastUpdate := r.Cache.Services(orgId, ref, "")
r.Logger.Info("Preparing initial schema update",
"ref", ref,
"orgId", orgId,
"lastUpdate", lastUpdate,
"servicesCount", len(services),
)
subGraphs := make([]*model.SubGraph, len(services))
for i, id := range services {
sg, err := r.fetchSubGraph(bgCtx, id)
if err != nil {
r.Logger.Error("fetch subgraph for initial update", "error", err, "id", id)
continue
}
subGraphs[i] = &model.SubGraph{
ID: sg.ID.String(),
Service: sg.Service,
URL: sg.Url,
WsURL: sg.WSUrl,
Sdl: sg.Sdl,
ChangedBy: sg.ChangedBy,
ChangedAt: sg.ChangedAt,
}
}
// Generate Cosmo router config
cosmoConfig, err := GenerateCosmoRouterConfig(subGraphs)
if err != nil {
r.Logger.Error("generate cosmo config", "error", err)
cosmoConfig = "" // Send empty if generation fails
}
// Send initial update
update := &model.SchemaUpdate{
Ref: ref,
ID: lastUpdate,
SubGraphs: subGraphs,
CosmoRouterConfig: &cosmoConfig,
}
r.Logger.Info("Sending initial schema update",
"ref", update.Ref,
"id", update.ID,
"subGraphsCount", len(update.SubGraphs),
"cosmoConfigLength", len(cosmoConfig),
)
ch <- update
}()
// Clean up subscription when context is done
go func() {
<-ctx.Done()
r.PubSub.Unsubscribe(ref, ch)
}()
return ch, nil
}
// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
// Subscription returns generated.SubscriptionResolver implementation.
func (r *Resolver) Subscription() generated.SubscriptionResolver { return &subscriptionResolver{r} }
type (
mutationResolver struct{ *Resolver }
queryResolver struct{ *Resolver }
mutationResolver struct{ *Resolver }
queryResolver struct{ *Resolver }
subscriptionResolver struct{ *Resolver }
)
-1
View File
@@ -1,5 +1,4 @@
//go:build tools
// +build tools
package graph
+63
View File
@@ -3,9 +3,72 @@ package hash
import (
"crypto/sha256"
"encoding/base64"
"golang.org/x/crypto/bcrypt"
)
// String creates a SHA256 hash of a string (legacy, for non-sensitive data)
func String(s string) string {
encoded := sha256.New().Sum([]byte(s))
return base64.StdEncoding.EncodeToString(encoded)
}
// APIKey hashes an API key using bcrypt for secure storage
// Cost of 12 provides a good balance between security and performance
func APIKey(key string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(key), 12)
if err != nil {
return "", err
}
return string(hash), nil
}
// CompareAPIKey compares a plaintext API key with a hash
// Supports both bcrypt (new) and SHA256 (legacy) hashes for backwards compatibility
// Returns true if they match, false otherwise
//
// Migration Strategy:
// Old API keys stored with SHA256 will continue to work. To upgrade them to bcrypt:
// 1. Keys are automatically upgraded when users re-authenticate (if implemented)
// 2. Or, run a one-time migration using MigrateAPIKeyHash when convenient
func CompareAPIKey(hashedKey, plainKey string) bool {
// Bcrypt hashes start with $2a$, $2b$, or $2y$
// If the hash starts with $2, it's a bcrypt hash
if len(hashedKey) > 2 && hashedKey[0] == '$' && hashedKey[1] == '2' {
// New bcrypt hash
err := bcrypt.CompareHashAndPassword([]byte(hashedKey), []byte(plainKey))
return err == nil
}
// Legacy SHA256 hash - compare using the old method
legacyHash := String(plainKey)
return hashedKey == legacyHash
}
// IsLegacyHash returns true if the hash is a legacy SHA256 hash (not bcrypt)
func IsLegacyHash(hashedKey string) bool {
return len(hashedKey) <= 2 || hashedKey[0] != '$' || hashedKey[1] != '2'
}
// MigrateAPIKeyHash can be used to upgrade a legacy SHA256 hash to bcrypt
// This is useful for one-time migrations of existing keys
// Returns the new bcrypt hash if the key is legacy, otherwise returns the original
func MigrateAPIKeyHash(currentHash, plainKey string) (string, bool, error) {
// If already bcrypt, no migration needed
if !IsLegacyHash(currentHash) {
return currentHash, false, nil
}
// Verify the legacy hash is correct before migrating
if !CompareAPIKey(currentHash, plainKey) {
return "", false, nil // Invalid key, don't migrate
}
// Generate new bcrypt hash
newHash, err := APIKey(plainKey)
if err != nil {
return "", false, err
}
return newHash, true, nil
}
+169
View File
@@ -0,0 +1,169 @@
package hash
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAPIKey(t *testing.T) {
key := "test_api_key_12345" // gitleaks:allow
hash1, err := APIKey(key)
require.NoError(t, err)
assert.NotEmpty(t, hash1)
assert.NotEqual(t, key, hash1, "Hash should not equal plaintext")
// Bcrypt hashes should start with $2
assert.True(t, strings.HasPrefix(hash1, "$2"), "Should be a bcrypt hash")
// Same key should produce different hashes (due to salt)
hash2, err := APIKey(key)
require.NoError(t, err)
assert.NotEqual(t, hash1, hash2, "Bcrypt should produce different hashes with different salts")
}
func TestCompareAPIKey_Bcrypt(t *testing.T) {
key := "test_api_key_12345" // gitleaks:allow
hash, err := APIKey(key)
require.NoError(t, err)
// Correct key should match
assert.True(t, CompareAPIKey(hash, key))
// Wrong key should not match
assert.False(t, CompareAPIKey(hash, "wrong_key"))
}
func TestCompareAPIKey_Legacy(t *testing.T) {
key := "test_api_key_12345" // gitleaks:allow
// Create a legacy SHA256 hash
legacyHash := String(key)
// Should still work with legacy hashes
assert.True(t, CompareAPIKey(legacyHash, key))
// Wrong key should not match
assert.False(t, CompareAPIKey(legacyHash, "wrong_key"))
}
func TestCompareAPIKey_BackwardCompatibility(t *testing.T) {
tests := []struct {
name string
hashFunc func(string) string
expectOK bool
}{
{
name: "bcrypt hash",
hashFunc: func(k string) string {
h, _ := APIKey(k)
return h
},
expectOK: true,
},
{
name: "legacy SHA256 hash",
hashFunc: func(k string) string {
return String(k)
},
expectOK: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
key := "test_key_123"
hash := tt.hashFunc(key)
result := CompareAPIKey(hash, key)
assert.Equal(t, tt.expectOK, result)
})
}
}
func TestString(t *testing.T) {
// Test that String function still works (for non-sensitive data)
input := "test_string"
hash1 := String(input)
hash2 := String(input)
// SHA256 should be deterministic
assert.Equal(t, hash1, hash2)
assert.NotEmpty(t, hash1)
assert.NotEqual(t, input, hash1)
}
func TestIsLegacyHash(t *testing.T) {
tests := []struct {
name string
hash string
isLegacy bool
}{
{
name: "bcrypt hash",
hash: "$2a$12$abcdefghijklmnopqrstuv",
isLegacy: false,
},
{
name: "SHA256 hash",
hash: "dXNfYWtfMTIzNDU2Nzg5MDEyMzQ1NuOwxEKY",
isLegacy: true,
},
{
name: "empty string",
hash: "",
isLegacy: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.isLegacy, IsLegacyHash(tt.hash))
})
}
}
func TestMigrateAPIKeyHash(t *testing.T) {
plainKey := "test_api_key_123"
t.Run("migrate legacy hash", func(t *testing.T) {
// Create a legacy SHA256 hash
legacyHash := String(plainKey)
// Migrate it
newHash, migrated, err := MigrateAPIKeyHash(legacyHash, plainKey)
require.NoError(t, err)
assert.True(t, migrated, "Should indicate migration occurred")
assert.NotEqual(t, legacyHash, newHash, "New hash should differ from legacy")
assert.True(t, strings.HasPrefix(newHash, "$2"), "New hash should be bcrypt")
// Verify new hash works
assert.True(t, CompareAPIKey(newHash, plainKey))
})
t.Run("no migration needed for bcrypt", func(t *testing.T) {
// Create a bcrypt hash
bcryptHash, err := APIKey(plainKey)
require.NoError(t, err)
// Try to migrate it
newHash, migrated, err := MigrateAPIKeyHash(bcryptHash, plainKey)
require.NoError(t, err)
assert.False(t, migrated, "Should not migrate bcrypt hash")
assert.Equal(t, bcryptHash, newHash, "Hash should remain unchanged")
})
t.Run("invalid key does not migrate", func(t *testing.T) {
legacyHash := String("correct_key")
// Try to migrate with wrong plaintext
newHash, migrated, err := MigrateAPIKeyHash(legacyHash, "wrong_key")
require.NoError(t, err)
assert.False(t, migrated, "Should not migrate invalid key")
assert.Empty(t, newHash, "Should return empty for invalid key")
})
}
+73
View File
@@ -0,0 +1,73 @@
package health
import (
"context"
"database/sql"
"encoding/json"
"log/slog"
"net/http"
"time"
)
type Checker struct {
db *sql.DB
logger *slog.Logger
}
func New(db *sql.DB, logger *slog.Logger) *Checker {
return &Checker{
db: db,
logger: logger,
}
}
type HealthStatus struct {
Status string `json:"status"`
Checks map[string]string `json:"checks,omitempty"`
}
// LivenessHandler checks if the application is running
// This is a simple check that always returns OK if the handler is reached
func (h *Checker) LivenessHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(HealthStatus{
Status: "UP",
})
}
// ReadinessHandler checks if the application is ready to accept traffic
// This checks database connectivity and other critical dependencies
func (h *Checker) ReadinessHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
checks := make(map[string]string)
allHealthy := true
// Check database connectivity
if err := h.db.PingContext(ctx); err != nil {
h.logger.With("error", err).Warn("database health check failed")
checks["database"] = "DOWN"
allHealthy = false
} else {
checks["database"] = "UP"
}
status := HealthStatus{
Status: "UP",
Checks: checks,
}
if !allHealthy {
status.Status = "DOWN"
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
_ = json.NewEncoder(w).Encode(status)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(status)
}
+75
View File
@@ -0,0 +1,75 @@
package health
import (
"database/sql"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLivenessHandler(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
db, _, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
checker := New(db, logger)
req := httptest.NewRequest(http.MethodGet, "/health/live", nil)
rec := httptest.NewRecorder()
checker.LivenessHandler(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), `"status":"UP"`)
}
func TestReadinessHandler_Healthy(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
db, mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
require.NoError(t, err)
defer db.Close()
// Expect a ping and return success
mock.ExpectPing().WillReturnError(nil)
checker := New(db, logger)
req := httptest.NewRequest(http.MethodGet, "/health/ready", nil)
rec := httptest.NewRecorder()
checker.ReadinessHandler(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), `"status":"UP"`)
assert.Contains(t, rec.Body.String(), `"database":"UP"`)
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestReadinessHandler_DatabaseDown(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
db, mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
require.NoError(t, err)
defer db.Close()
// Expect a ping and return error
mock.ExpectPing().WillReturnError(sql.ErrConnDone)
checker := New(db, logger)
req := httptest.NewRequest(http.MethodGet, "/health/ready", nil)
rec := httptest.NewRecorder()
checker.ReadinessHandler(rec, req)
assert.Equal(t, http.StatusServiceUnavailable, rec.Code)
assert.Contains(t, rec.Body.String(), `"status":"DOWN"`)
assert.Contains(t, rec.Body.String(), `"database":"DOWN"`)
assert.NoError(t, mock.ExpectationsWereMet())
}
+7
View File
@@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: schemas
data:
LOG_FORMAT: "otel"
ENVIRONMENT: "production"
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: schemas
data:
ENVIRONMENT: "development"
+15 -1
View File
@@ -44,19 +44,33 @@ spec:
requests:
cpu: "20m"
memory: "20Mi"
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 5
failureThreshold: 3
imagePullPolicy: IfNotPresent
image: registry.gitlab.com/unboundsoftware/schemas:${COMMIT}
ports:
- name: api
containerPort: 8080
env:
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://k8s-monitoring-alloy-receiver.monitoring.svc.cluster.local:4318
envFrom:
- configMapRef:
name: schemas
- secretRef:
name: schemas
restartPolicy: Always
-2
View File
@@ -15,8 +15,6 @@ spec:
data:
POSTGRES_URL: "postgres://{{ .DB_USERNAME }}:{{ .DB_PASSWORD }}@{{ .DB_HOST }}:{{ .DB_PORT }}/schemas?sslmode=disable"
API_KEY: "{{ .API_KEY }}"
SENTRY_DSN: "{{ .SENTRY_DSN }}"
SENTRY_ENVIRONMENT: "{{ .SENTRY_ENVIRONMENT }}"
dataFrom:
- extract:
key: services/schemas
+16 -3
View File
@@ -4,6 +4,8 @@ import (
"context"
"log/slog"
"os"
"go.opentelemetry.io/contrib/bridges/otelslog"
)
type Logger interface {
@@ -18,17 +20,28 @@ type contextKey string
const loggerKey = contextKey("logger")
func SetupLogger(logLevel, serviceName, buildVersion string) *slog.Logger {
func SetupLogger(logLevel, logFormat, serviceName, buildVersion string) *slog.Logger {
var leveler slog.LevelVar
err := leveler.UnmarshalText([]byte(logLevel))
defaultLogger = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
handlerOpts := &slog.HandlerOptions{
AddSource: false,
Level: leveler.Level(),
ReplaceAttr: nil,
})).With("service", serviceName).With("version", buildVersion)
}
var handler slog.Handler
switch logFormat {
case "json":
handler = slog.NewJSONHandler(os.Stdout, handlerOpts)
case "text":
handler = slog.NewTextHandler(os.Stdout, handlerOpts)
case "otel":
handler = otelslog.NewHandler(serviceName,
otelslog.WithVersion(buildVersion))
}
defaultLogger = slog.New(handler).With("service", serviceName).With("version", buildVersion)
if err != nil {
defaultLogger.With("err", err).Error("Failed to parse log level")
os.Exit(1)
+3 -2
View File
@@ -9,7 +9,6 @@ import (
"github.com/golang-jwt/jwt/v5"
"gitlab.com/unboundsoftware/schemas/domain"
"gitlab.com/unboundsoftware/schemas/hash"
)
const (
@@ -49,7 +48,9 @@ func (m *AuthMiddleware) Handler(next http.Handler) http.Handler {
_, _ = w.Write([]byte("Invalid API Key format"))
return
}
if organization := m.cache.OrganizationByAPIKey(hash.String(apiKey)); organization != nil {
// Cache handles hash comparison internally
organization := m.cache.OrganizationByAPIKey(apiKey)
if organization != nil {
ctx = context.WithValue(ctx, OrganizationKey, *organization)
}
+464
View File
@@ -0,0 +1,464 @@
package middleware
import (
"context"
"net/http"
"net/http/httptest"
"testing"
mw "github.com/auth0/go-jwt-middleware/v2"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
"gitlab.com/unboundsoftware/schemas/domain"
)
// MockCache is a mock implementation of the Cache interface
type MockCache struct {
mock.Mock
}
func (m *MockCache) OrganizationByAPIKey(apiKey string) *domain.Organization {
args := m.Called(apiKey)
if args.Get(0) == nil {
return nil
}
return args.Get(0).(*domain.Organization)
}
func TestAuthMiddleware_Handler_WithValidAPIKey(t *testing.T) {
// Setup
mockCache := new(MockCache)
authMiddleware := NewAuth(mockCache)
orgID := uuid.New()
expectedOrg := &domain.Organization{
BaseAggregate: eventsourced.BaseAggregate{
ID: eventsourced.IdFromString(orgID.String()),
},
Name: "Test Organization",
}
apiKey := "test-api-key-123"
// Mock expects plaintext key (cache handles hashing internally)
mockCache.On("OrganizationByAPIKey", apiKey).Return(expectedOrg)
// Create a test handler that checks the context
var capturedOrg *domain.Organization
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if org := r.Context().Value(OrganizationKey); org != nil {
if o, ok := org.(domain.Organization); ok {
capturedOrg = &o
}
}
w.WriteHeader(http.StatusOK)
})
// Create request with API key in context
req := httptest.NewRequest(http.MethodGet, "/test", nil)
ctx := context.WithValue(req.Context(), ApiKey, apiKey)
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
// Execute
authMiddleware.Handler(testHandler).ServeHTTP(rec, req)
// Assert
assert.Equal(t, http.StatusOK, rec.Code)
require.NotNil(t, capturedOrg)
assert.Equal(t, expectedOrg.Name, capturedOrg.Name)
assert.Equal(t, expectedOrg.ID.String(), capturedOrg.ID.String())
mockCache.AssertExpectations(t)
}
func TestAuthMiddleware_Handler_WithInvalidAPIKey(t *testing.T) {
// Setup
mockCache := new(MockCache)
authMiddleware := NewAuth(mockCache)
apiKey := "invalid-api-key"
// Mock expects plaintext key (cache handles hashing internally)
mockCache.On("OrganizationByAPIKey", apiKey).Return(nil)
// Create a test handler that checks the context
var capturedOrg *domain.Organization
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if org := r.Context().Value(OrganizationKey); org != nil {
if o, ok := org.(domain.Organization); ok {
capturedOrg = &o
}
}
w.WriteHeader(http.StatusOK)
})
// Create request with API key in context
req := httptest.NewRequest(http.MethodGet, "/test", nil)
ctx := context.WithValue(req.Context(), ApiKey, apiKey)
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
// Execute
authMiddleware.Handler(testHandler).ServeHTTP(rec, req)
// Assert
assert.Equal(t, http.StatusOK, rec.Code)
assert.Nil(t, capturedOrg, "Organization should not be set for invalid API key")
mockCache.AssertExpectations(t)
}
func TestAuthMiddleware_Handler_WithoutAPIKey(t *testing.T) {
// Setup
mockCache := new(MockCache)
authMiddleware := NewAuth(mockCache)
// The middleware passes the plaintext API key (cache handles hashing)
mockCache.On("OrganizationByAPIKey", "").Return(nil)
// Create a test handler that checks the context
var capturedOrg *domain.Organization
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if org := r.Context().Value(OrganizationKey); org != nil {
if o, ok := org.(domain.Organization); ok {
capturedOrg = &o
}
}
w.WriteHeader(http.StatusOK)
})
// Create request without API key
req := httptest.NewRequest(http.MethodGet, "/test", nil)
rec := httptest.NewRecorder()
// Execute
authMiddleware.Handler(testHandler).ServeHTTP(rec, req)
// Assert
assert.Equal(t, http.StatusOK, rec.Code)
assert.Nil(t, capturedOrg, "Organization should not be set without API key")
mockCache.AssertExpectations(t)
}
func TestAuthMiddleware_Handler_WithValidJWT(t *testing.T) {
// Setup
mockCache := new(MockCache)
authMiddleware := NewAuth(mockCache)
// The middleware passes the plaintext API key (cache handles hashing)
mockCache.On("OrganizationByAPIKey", "").Return(nil)
userID := "user-123"
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": userID,
})
// Create a test handler that checks the context
var capturedUser string
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if user := r.Context().Value(UserKey); user != nil {
if u, ok := user.(string); ok {
capturedUser = u
}
}
w.WriteHeader(http.StatusOK)
})
// Create request with JWT token in context
req := httptest.NewRequest(http.MethodGet, "/test", nil)
ctx := context.WithValue(req.Context(), mw.ContextKey{}, token)
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
// Execute
authMiddleware.Handler(testHandler).ServeHTTP(rec, req)
// Assert
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, userID, capturedUser)
}
func TestAuthMiddleware_Handler_APIKeyErrorHandling(t *testing.T) {
// Setup
mockCache := new(MockCache)
authMiddleware := NewAuth(mockCache)
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
// Create request with invalid API key type in context
req := httptest.NewRequest(http.MethodGet, "/test", nil)
ctx := context.WithValue(req.Context(), ApiKey, 12345) // Invalid type
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
// Execute
authMiddleware.Handler(testHandler).ServeHTTP(rec, req)
// Assert
assert.Equal(t, http.StatusInternalServerError, rec.Code)
assert.Contains(t, rec.Body.String(), "Invalid API Key format")
}
func TestAuthMiddleware_Handler_JWTErrorHandling(t *testing.T) {
// Setup
mockCache := new(MockCache)
authMiddleware := NewAuth(mockCache)
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
// Create request with invalid JWT token type in context
req := httptest.NewRequest(http.MethodGet, "/test", nil)
ctx := context.WithValue(req.Context(), mw.ContextKey{}, "not-a-token") // Invalid type
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
// Execute
authMiddleware.Handler(testHandler).ServeHTTP(rec, req)
// Assert
assert.Equal(t, http.StatusInternalServerError, rec.Code)
assert.Contains(t, rec.Body.String(), "Invalid JWT token format")
}
func TestAuthMiddleware_Handler_BothJWTAndAPIKey(t *testing.T) {
// Setup
mockCache := new(MockCache)
authMiddleware := NewAuth(mockCache)
orgID := uuid.New()
expectedOrg := &domain.Organization{
BaseAggregate: eventsourced.BaseAggregate{
ID: eventsourced.IdFromString(orgID.String()),
},
Name: "Test Organization",
}
userID := "user-123"
apiKey := "test-api-key-123"
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": userID,
})
// Mock expects plaintext key (cache handles hashing internally)
mockCache.On("OrganizationByAPIKey", apiKey).Return(expectedOrg)
// Create a test handler that checks both user and organization in context
var capturedUser string
var capturedOrg *domain.Organization
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if user := r.Context().Value(UserKey); user != nil {
if u, ok := user.(string); ok {
capturedUser = u
}
}
if org := r.Context().Value(OrganizationKey); org != nil {
if o, ok := org.(domain.Organization); ok {
capturedOrg = &o
}
}
w.WriteHeader(http.StatusOK)
})
// Create request with both JWT and API key in context
req := httptest.NewRequest(http.MethodGet, "/test", nil)
ctx := context.WithValue(req.Context(), mw.ContextKey{}, token)
ctx = context.WithValue(ctx, ApiKey, apiKey)
req = req.WithContext(ctx)
rec := httptest.NewRecorder()
// Execute
authMiddleware.Handler(testHandler).ServeHTTP(rec, req)
// Assert
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, userID, capturedUser)
require.NotNil(t, capturedOrg)
assert.Equal(t, expectedOrg.Name, capturedOrg.Name)
mockCache.AssertExpectations(t)
}
func TestUserFromContext(t *testing.T) {
tests := []struct {
name string
ctx context.Context
expected string
}{
{
name: "with valid user",
ctx: context.WithValue(context.Background(), UserKey, "user-123"),
expected: "user-123",
},
{
name: "without user",
ctx: context.Background(),
expected: "",
},
{
name: "with invalid type",
ctx: context.WithValue(context.Background(), UserKey, 123),
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := UserFromContext(tt.ctx)
assert.Equal(t, tt.expected, result)
})
}
}
func TestOrganizationFromContext(t *testing.T) {
orgID := uuid.New()
org := domain.Organization{
BaseAggregate: eventsourced.BaseAggregate{
ID: eventsourced.IdFromString(orgID.String()),
},
Name: "Test Org",
}
tests := []struct {
name string
ctx context.Context
expected string
}{
{
name: "with valid organization",
ctx: context.WithValue(context.Background(), OrganizationKey, org),
expected: orgID.String(),
},
{
name: "without organization",
ctx: context.Background(),
expected: "",
},
{
name: "with invalid type",
ctx: context.WithValue(context.Background(), OrganizationKey, "not-an-org"),
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := OrganizationFromContext(tt.ctx)
assert.Equal(t, tt.expected, result)
})
}
}
func TestAuthMiddleware_Directive_RequiresUser(t *testing.T) {
mockCache := new(MockCache)
authMiddleware := NewAuth(mockCache)
requireUser := true
// Test with user present
ctx := context.WithValue(context.Background(), UserKey, "user-123")
_, err := authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
return "success", nil
}, &requireUser, nil)
assert.NoError(t, err)
// Test without user
ctx = context.Background()
_, err = authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
return "success", nil
}, &requireUser, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "no user available in request")
}
func TestAuthMiddleware_Directive_RequiresOrganization(t *testing.T) {
mockCache := new(MockCache)
authMiddleware := NewAuth(mockCache)
requireOrg := true
orgID := uuid.New()
org := domain.Organization{
BaseAggregate: eventsourced.BaseAggregate{
ID: eventsourced.IdFromString(orgID.String()),
},
Name: "Test Org",
}
// Test with organization present
ctx := context.WithValue(context.Background(), OrganizationKey, org)
_, err := authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
return "success", nil
}, nil, &requireOrg)
assert.NoError(t, err)
// Test without organization
ctx = context.Background()
_, err = authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
return "success", nil
}, nil, &requireOrg)
assert.Error(t, err)
assert.Contains(t, err.Error(), "no organization available in request")
}
func TestAuthMiddleware_Directive_RequiresBoth(t *testing.T) {
mockCache := new(MockCache)
authMiddleware := NewAuth(mockCache)
requireUser := true
requireOrg := true
orgID := uuid.New()
org := domain.Organization{
BaseAggregate: eventsourced.BaseAggregate{
ID: eventsourced.IdFromString(orgID.String()),
},
Name: "Test Org",
}
// Test with both present
ctx := context.WithValue(context.Background(), UserKey, "user-123")
ctx = context.WithValue(ctx, OrganizationKey, org)
_, err := authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
return "success", nil
}, &requireUser, &requireOrg)
assert.NoError(t, err)
// Test with only user
ctx = context.WithValue(context.Background(), UserKey, "user-123")
_, err = authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
return "success", nil
}, &requireUser, &requireOrg)
assert.Error(t, err)
// Test with only organization
ctx = context.WithValue(context.Background(), OrganizationKey, org)
_, err = authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
return "success", nil
}, &requireUser, &requireOrg)
assert.Error(t, err)
}
func TestAuthMiddleware_Directive_NoRequirements(t *testing.T) {
mockCache := new(MockCache)
authMiddleware := NewAuth(mockCache)
// Test with no requirements
ctx := context.Background()
result, err := authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
return "success", nil
}, nil, nil)
assert.NoError(t, err)
assert.Equal(t, "success", result)
}
+41
View File
@@ -0,0 +1,41 @@
package monitoring
import (
"context"
"fmt"
"github.com/99designs/gqlgen/graphql"
"go.opentelemetry.io/otel/trace"
)
func AroundOperations(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
op := graphql.GetOperationContext(ctx)
spanName := fmt.Sprintf("graphql:operation:%s", op.OperationName)
// Span always injected in the http handler above
sp := trace.SpanFromContext(ctx)
if sp != nil {
sp.SetName(spanName)
}
return next(ctx)
}
func AroundRootFields(ctx context.Context, next graphql.RootResolver) graphql.Marshaler {
oc := graphql.GetRootFieldContext(ctx)
spanCtx, span := StartSpan(ctx, fmt.Sprintf("graphql:rootfield:%s", oc.Field.Name))
defer span.Finish()
return next(spanCtx)
}
func AroundFields(ctx context.Context, next graphql.Resolver) (res any, err error) {
oc := graphql.GetFieldContext(ctx)
var span Span
if oc.IsResolver {
ctx, span = StartSpan(ctx, fmt.Sprintf("graphql:field:%s", oc.Field.Name))
}
defer func() {
if span != nil {
span.Finish()
}
}()
return next(ctx)
}
+100
View File
@@ -0,0 +1,100 @@
package monitoring
import (
"context"
"errors"
"fmt"
"net/http"
"os"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/exporters/stdout/stdoutlog"
"go.opentelemetry.io/otel/log/global"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/log"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/trace"
)
// SetupOTelSDK bootstraps the OpenTelemetry pipeline.
func SetupOTelSDK(ctx context.Context, enabled bool, serviceName, buildVersion, environment string) (func(context.Context) error, error) {
if os.Getenv("OTEL_RESOURCE_ATTRIBUTES") == "" {
if err := os.Setenv("OTEL_RESOURCE_ATTRIBUTES", fmt.Sprintf("service.name=%s,service.version=%s,service.environment=%s", serviceName, buildVersion, environment)); err != nil {
return func(context.Context) error {
return nil
}, err
}
}
var shutdownFuncs []func(context.Context) error
if !enabled {
return func(context.Context) error {
return nil
}, nil
}
shutdown := func(ctx context.Context) error {
var err error
for _, fn := range shutdownFuncs {
err = errors.Join(err, fn(ctx))
}
shutdownFuncs = nil
return err
}
// handleErr calls shutdown for cleanup and makes sure that all errors are returned.
handleErr := func(inErr error) (func(context.Context) error, error) {
return nil, errors.Join(inErr, shutdown(ctx))
}
// Set up the propagator.
prop := propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
)
otel.SetTextMapPropagator(prop)
traceExporter, err := otlptracehttp.New(ctx)
if err != nil {
return handleErr(err)
}
shutdownFuncs = append(shutdownFuncs, traceExporter.Shutdown)
tracerProvider := trace.NewTracerProvider(
trace.WithBatcher(traceExporter,
trace.WithBatchTimeout(5*time.Second)),
)
shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
otel.SetTracerProvider(tracerProvider)
logExporter, err := stdoutlog.New()
if err != nil {
return handleErr(err)
}
processor := log.NewSimpleProcessor(logExporter)
logProvider := log.NewLoggerProvider(log.WithProcessor(processor))
global.SetLoggerProvider(logProvider)
shutdownFuncs = append(shutdownFuncs, logProvider.Shutdown)
exp, err := otlpmetrichttp.New(ctx)
if err != nil {
return handleErr(err)
}
meterProvider := metric.NewMeterProvider(metric.WithReader(metric.NewPeriodicReader(exp)))
shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown)
otel.SetMeterProvider(meterProvider)
return shutdown, err
}
func Handler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header))
spanCtx, s := StartSpan(ctx, "http")
defer s.Finish()
h.ServeHTTP(w, r.WithContext(spanCtx))
})
}
+46
View File
@@ -0,0 +1,46 @@
package monitoring
import (
"context"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)
type Span interface {
Context() context.Context
Finish()
}
type span struct {
otelSpan trace.Span
ctx context.Context
}
func (s *span) Finish() {
s.otelSpan.End()
}
func (s *span) Context() context.Context {
return s.ctx
}
func StartSpan(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, Span) {
ctx, otelSpan := otel.Tracer("").Start(ctx, name, opts...)
return ctx, &span{
otelSpan: otelSpan,
ctx: ctx,
}
}
type TraceHandlerFunc func(ctx context.Context, name string) (context.Context, func())
func (t TraceHandlerFunc) Trace(tx context.Context, name string) (context.Context, func()) {
return t(tx, name)
}
func Trace(ctx context.Context, name string) (context.Context, func()) {
ctx, s := StartSpan(ctx, name)
return ctx, s.Finish
}