Compare commits
241 Commits
v0.8.0
...
next-release
| Author | SHA1 | Date | |
|---|---|---|---|
| 160e1f7dac | |||
| 7e385c7a26 | |||
| 501bf39827 | |||
| e3fbc3e8cc | |||
| 37ec1f0cc4 | |||
| 36c10eb51b | |||
| 6e46a2cbe7 | |||
| 9a32cdb1b3 | |||
| 1150e82c22 | |||
| eaae92ec0b | |||
| e4fd2b2b63 | |||
| 81af7a2416 | |||
| 62e9cef341 | |||
| 972968f865 | |||
| 1ede47df82 | |||
| 882b87e882 | |||
| 85dc71fe24 | |||
| a7804ac970 | |||
| a94f9cb0c0 | |||
| 68870bb37e | |||
| d8e34ccae5 | |||
| fdfdce46c0 | |||
| 357be92d1e | |||
| 4edddee47b | |||
| f6df1a51da | |||
| 7c109f3c69 | |||
| f9d0b470c9 | |||
| 924588bef9 | |||
| 086c2175d9 | |||
| d657883648 | |||
| bd7497639d | |||
| 9db7b89d9f | |||
| a01211c089 | |||
| 20ca68ad4c | |||
| 87d1e631f9 | |||
|
c98df1b4e7
|
|||
| 7bd605d90f | |||
| 60bec67023 | |||
|
cc33c651cd
|
|||
| 38b4ab8ee2 | |||
| 1fa91ad573 | |||
| 7182ae2a11 | |||
| 19b7f0edb6 | |||
| 0b68cadd24 | |||
| 50087adeac | |||
| 03c903cf0e | |||
| 59d0d40d4d | |||
| 3bb0511277 | |||
| f0fadb4ab7 | |||
| 59c128fe0c | |||
| 9518403394 | |||
|
3b4e513653
|
|||
| fe1de50ded | |||
| 2d243469ed | |||
| a23aa5e5e9 | |||
| 2286e092d9 | |||
|
28aa32ad8c
|
|||
| a9885f8b65 | |||
| 2fbe44fea1 | |||
| 80d08c1ba7 | |||
| d6866b9f72 | |||
| 3c1fc68016 | |||
| 817449afbc | |||
| 856423a221 | |||
|
3c43f1bd80
|
|||
| 4fd36c1330 | |||
| 9411a2dd7d | |||
| f1dc71163b | |||
|
08e84ac1dc
|
|||
| 3ea1ef520c | |||
| e30ab93a47 | |||
| 0eb59235c1 | |||
| 31022037ff | |||
| 085bf35d92 | |||
| 9d655e1a23 | |||
| eee1d45d8c | |||
| 782185981f | |||
| e3f156fe9e | |||
| 230876b523 | |||
| e64adc96ae | |||
| f534752e2c | |||
| 42f4c8014d | |||
| e098562e0c | |||
| 948ab167c1 | |||
| 404c4106bc | |||
| 593874ba52 | |||
| bcbdc4458e | |||
| 4b6637b027 | |||
| bc7729b702 | |||
| 942508e467 | |||
| 59e8a3c43d | |||
| d7358d8053 | |||
| 2350099e7f | |||
| ab5a8a9f6a | |||
| 369a0b050a | |||
| 3bf43210a1 | |||
| 148f8befe7 | |||
| 90b9e91d20 | |||
| 985bca2f58 | |||
| 3fce241596 | |||
| a0baa44320 | |||
| 60939c3159 | |||
| 6530408f15 | |||
| fd6694614e | |||
| 6143a96c75 | |||
|
817927cb7d
|
|||
|
e2c1803683
|
|||
| d79a10e910 | |||
| 48f0ec0bbe | |||
| b89b124a0e | |||
| d008303068 | |||
|
e328ef5a64
|
|||
| b2506188f3 | |||
| 16ce04ea86 | |||
| 1d0f82a851 | |||
| 7be533dc6c | |||
|
73eae98929
|
|||
| aa41f48b0e | |||
| bb0c020812 | |||
| f0c7415d88 | |||
| fbe180020c | |||
| 3eba214a72 | |||
| 134b571baa | |||
| a1a23d69cf | |||
| 6f1dda7be5 | |||
| d50827018a | |||
| 8a6163a921 | |||
| 054cfa1e52 | |||
| 7d25af472a | |||
| ec0d5dff74 | |||
| e3d19384a0 | |||
| 42adcb1df5 | |||
| e072ad685b | |||
| 4222eb1268 | |||
| ab3eafa331 | |||
| 86612d9e25 | |||
| abc5668d33 | |||
| 892fc29331 | |||
| bdb6e37b22 | |||
| 8d512e0290 | |||
| 3ab0a8e701 | |||
| 3bd3daccaf | |||
| 3d6016d7e2 | |||
| 31cc2e10b6 | |||
| 136623c04b | |||
| 12ff2fa1ba | |||
| 1aef6a7a31 | |||
| c02034d6b0 | |||
| dc4555a168 | |||
| 0571f4d986 | |||
| 3c194dc127 | |||
| f7ba0e3dc7 | |||
| 33667d4de1 | |||
| bbf82399e1 | |||
| f927d1c750 | |||
| ef273520b5 | |||
| c8e6287761 | |||
| 7052f0bfb1 | |||
| c6f35f324e | |||
| 08f0ee3374 | |||
| d3c7941de8 | |||
| b1a9021fee | |||
| ab32fddd2b | |||
| ded73a3065 | |||
| 4c571197b8 | |||
| 9f276d7420 | |||
| f56dfeafa7 | |||
| f6f3810ffb | |||
| 9d32174ae4 | |||
| 26073e744d | |||
| 6e38f3d5b5 | |||
| 9199be9da5 | |||
| e3dcb013b6 | |||
| 5982f3ac41 | |||
| eb23f2f62b | |||
| 1effc3937a | |||
|
b69de655e4
|
|||
| 73fdf6c896 | |||
| bf0953b520 | |||
| 95aaea7ecf | |||
| 81c3a7ff13 | |||
| e322f7f7ad | |||
| 4997c53968 | |||
| cfcd023a83 | |||
| 34e7a0b718 | |||
| 7d38c31370 | |||
| 968553b532 | |||
| 0067b42f92 | |||
| 48776b6afc | |||
| 03cf6be52c | |||
| 6b5e66b1be | |||
| 25c3bf2356 | |||
| 7e5bbf3baa | |||
| 8adae23068 | |||
|
0e8d0965b7
|
|||
| cfc5de1bb9 | |||
| 63567eaa8b | |||
| e38e7a2936 | |||
| 718585ebe8 | |||
| 6b3515ed14 | |||
| fee47b271a | |||
| 1da8439372 | |||
| 2de1324458 | |||
| 4673ecdd85 | |||
| d92f24b0a1 | |||
| f80ee9d391 | |||
| 3da293252d | |||
| 6ae6a4d6cf | |||
|
129cd8aad1
|
|||
| 8dd2e57c70 | |||
| 1914211b85 | |||
| ea8eb0a68c | |||
| e9d0e855af | |||
| 8790fd0d82 | |||
| a77a7f3a32 | |||
| fbe962a7b7 | |||
| b5bdcc9dbc | |||
|
fd1685867e
|
|||
|
114cbf89c5
|
|||
| 000ad8b4ad | |||
| 0820fb542f | |||
| f0d4285bee | |||
| e1c10f0537 | |||
| f8c593de3e | |||
| 870f29e59f | |||
| a246c236db | |||
| cebeba4461 | |||
| b77165f6c8 | |||
| 9e85ee1473 | |||
| c113eb920b | |||
| 90cc64ece9 | |||
| ea4df08beb | |||
|
ca7e063888
|
|||
| 7b9dc1456b | |||
|
49af5f0cb1
|
|||
| e347d74a39 | |||
|
ffcf41b85a
|
|||
| 335a9f3b54 | |||
| c0e790b684 | |||
| 3b47365f10 | |||
|
862060875b
|
+1
-2
@@ -1,6 +1,5 @@
|
|||||||
.gitignore
|
.gitignore
|
||||||
/.gitlab
|
/.gitea
|
||||||
.gitlab-ci.yml
|
|
||||||
.graphqlconfig
|
.graphqlconfig
|
||||||
/exported
|
/exported
|
||||||
/k8s
|
/k8s
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
name: schemas
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
deploy_prod:
|
||||||
|
description: 'Deploy to production'
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: 'stable'
|
||||||
|
- name: Generate and format check
|
||||||
|
run: |
|
||||||
|
go install mvdan.cc/gofumpt@latest
|
||||||
|
go install golang.org/x/tools/cmd/goimports@latest
|
||||||
|
go generate ./...
|
||||||
|
git diff --stat --exit-code
|
||||||
|
- name: Run tests
|
||||||
|
run: go test -race -coverprofile=coverage.txt ./...
|
||||||
|
|
||||||
|
vulnerabilities:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: 'stable'
|
||||||
|
- name: Check vulnerabilities
|
||||||
|
run: |
|
||||||
|
go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||||
|
govulncheck ./...
|
||||||
|
|
||||||
|
check-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: '24'
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: 'stable'
|
||||||
|
- name: Check goreleaser config
|
||||||
|
uses: goreleaser/goreleaser-action@v7
|
||||||
|
with:
|
||||||
|
version: '~> v2'
|
||||||
|
args: check
|
||||||
|
- name: Test release build
|
||||||
|
uses: goreleaser/goreleaser-action@v7
|
||||||
|
with:
|
||||||
|
version: '~> v2'
|
||||||
|
args: release --snapshot --clean
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: [check, vulnerabilities, check-release]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
BUILDTOOLS_CONTENT: ${{ secrets.BUILDTOOLS_CONTENT }}
|
||||||
|
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: buildtool/setup-buildtools-action@v1
|
||||||
|
- name: Build and push
|
||||||
|
run: unset GITEA_TOKEN && build && push
|
||||||
|
|
||||||
|
deploy-prod:
|
||||||
|
needs: build
|
||||||
|
if: gitea.ref == 'refs/heads/main'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
BUILDTOOLS_CONTENT: ${{ secrets.BUILDTOOLS_CONTENT }}
|
||||||
|
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||||
|
environment: prod
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: buildtool/setup-buildtools-action@v1
|
||||||
|
- name: Deploy to production
|
||||||
|
run: deploy prod
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name: Goreleaser
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
env:
|
||||||
|
RELEASE_TOKEN_FILE: /runner-secrets/release-token
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: '24'
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: 'stable'
|
||||||
|
- name: Install goreleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v7
|
||||||
|
with:
|
||||||
|
version: 'v2.13.3'
|
||||||
|
install-only: true
|
||||||
|
- name: Release
|
||||||
|
run: |
|
||||||
|
GITEA_TOKEN=$(cat "${RELEASE_TOKEN_FILE}")
|
||||||
|
export GITEA_TOKEN
|
||||||
|
goreleaser release --clean
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
name: pre-commit
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre-commit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
SKIP: no-commit-to-branch
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: 'stable'
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: '3.14'
|
||||||
|
- name: Install goimports
|
||||||
|
run: go install golang.org/x/tools/cmd/goimports@latest
|
||||||
|
- uses: pre-commit/action@v3.0.1
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
uses: unboundsoftware/shared-workflows/.gitea/workflows/Release.yml@main
|
||||||
|
with:
|
||||||
|
tag_only: true
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
.testCoverage.txt
|
.testCoverage.txt
|
||||||
.testCoverage.txt.tmp
|
.testCoverage.txt.tmp
|
||||||
coverage.html
|
coverage.html
|
||||||
|
coverage.out
|
||||||
/exported
|
/exported
|
||||||
/release
|
/release
|
||||||
/schemactl
|
/schemactl
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
include:
|
|
||||||
- template: 'Workflows/MergeRequest-Pipelines.gitlab-ci.yml'
|
|
||||||
- project: unboundsoftware/ci-templates
|
|
||||||
file: Defaults.gitlab-ci.yml
|
|
||||||
- project: unboundsoftware/ci-templates
|
|
||||||
file: Release.gitlab-ci.yml
|
|
||||||
- project: unboundsoftware/ci-templates
|
|
||||||
file: Pre-Commit-Go.gitlab-ci.yml
|
|
||||||
|
|
||||||
stages:
|
|
||||||
- build
|
|
||||||
- test
|
|
||||||
- deploy-prod
|
|
||||||
- release
|
|
||||||
|
|
||||||
variables:
|
|
||||||
UNBOUND_RELEASE_TAG_ONLY: true
|
|
||||||
|
|
||||||
.buildtools:
|
|
||||||
image: buildtool/build-tools:${BUILDTOOLS_VERSION}
|
|
||||||
|
|
||||||
check:
|
|
||||||
stage: .pre
|
|
||||||
image: amd64/golang:1.25.4@sha256:efe81fa41fdf81fb873ab7cd931b9bb29bd10aced6c252cbd91739c34e654f01
|
|
||||||
script:
|
|
||||||
- go install mvdan.cc/gofumpt@latest
|
|
||||||
- go install golang.org/x/tools/cmd/goimports@latest
|
|
||||||
- go generate ./...
|
|
||||||
- git diff --stat --exit-code
|
|
||||||
|
|
||||||
build:
|
|
||||||
extends: .buildtools
|
|
||||||
stage: build
|
|
||||||
script:
|
|
||||||
- build
|
|
||||||
- curl -Os https://uploader.codecov.io/latest/linux/codecov
|
|
||||||
- chmod +x codecov
|
|
||||||
- ./codecov -t ${CODECOV_TOKEN} -R $CI_PROJECT_DIR -C $CI_COMMIT_SHA -r $CI_PROJECT_PATH
|
|
||||||
- push
|
|
||||||
|
|
||||||
vulnerabilities:
|
|
||||||
stage: build
|
|
||||||
image: amd64/golang:1.25.4@sha256:efe81fa41fdf81fb873ab7cd931b9bb29bd10aced6c252cbd91739c34e654f01
|
|
||||||
script:
|
|
||||||
- go install golang.org/x/vuln/cmd/govulncheck@latest
|
|
||||||
- govulncheck ./...
|
|
||||||
|
|
||||||
deploy-prod:
|
|
||||||
extends: .buildtools
|
|
||||||
stage: deploy-prod
|
|
||||||
before_script:
|
|
||||||
- echo Deploy to prod
|
|
||||||
script:
|
|
||||||
- deploy prod
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_BRANCH == "main"
|
|
||||||
environment:
|
|
||||||
name: prod
|
|
||||||
resource_group: prod
|
|
||||||
|
|
||||||
check_release:
|
|
||||||
stage: test
|
|
||||||
image:
|
|
||||||
name: goreleaser/goreleaser:v2.12.7@sha256:a2a47c0dda85f8d40eaaa5b9765bf76c69addb6060666f8a51441410d9b008e9
|
|
||||||
entrypoint: [ '' ]
|
|
||||||
variables:
|
|
||||||
GOTOOLCHAIN: auto
|
|
||||||
script: |
|
|
||||||
goreleaser check
|
|
||||||
goreleaser release --snapshot --clean
|
|
||||||
|
|
||||||
release:
|
|
||||||
stage: release
|
|
||||||
needs:
|
|
||||||
- unbound_release_prepare_release
|
|
||||||
image:
|
|
||||||
name: goreleaser/goreleaser:v2.12.7@sha256:a2a47c0dda85f8d40eaaa5b9765bf76c69addb6060666f8a51441410d9b008e9
|
|
||||||
entrypoint: [ '' ]
|
|
||||||
variables:
|
|
||||||
# Disable shallow cloning so that goreleaser can diff between tags to
|
|
||||||
# generate a changelog.
|
|
||||||
GIT_DEPTH: 0
|
|
||||||
GITLAB_TOKEN: $GITLAB_CI_TOKEN
|
|
||||||
GOTOOLCHAIN: auto
|
|
||||||
# Only run this release job for tags, not every commit (for example).
|
|
||||||
rules:
|
|
||||||
- if: $CI_COMMIT_TAG
|
|
||||||
script: |
|
|
||||||
goreleaser release --clean --release-notes=CHANGES.md
|
|
||||||
+4
-5
@@ -1,6 +1,10 @@
|
|||||||
project_name: unbound-schemas
|
project_name: unbound-schemas
|
||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
|
gitea_urls:
|
||||||
|
api: http://gitea-http.gitea.svc.cluster.local:3000/api/v1
|
||||||
|
download: https://gitea.unbound.se
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
|
|
||||||
@@ -27,11 +31,6 @@ homebrew_casks:
|
|||||||
name: "Joakim Olsson"
|
name: "Joakim Olsson"
|
||||||
email: joakim@unbound.se
|
email: joakim@unbound.se
|
||||||
homepage: "https://schemas.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:
|
archives:
|
||||||
- id: unbound-schemas
|
- id: unbound-schemas
|
||||||
|
|||||||
+4
-11
@@ -10,15 +10,8 @@ repos:
|
|||||||
args:
|
args:
|
||||||
- --allow-multiple-documents
|
- --allow-multiple-documents
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
- repo: https://gitlab.com/devopshq/gitlab-ci-linter
|
|
||||||
rev: v1.0.6
|
|
||||||
hooks:
|
|
||||||
- id: gitlab-ci-linter
|
|
||||||
args:
|
|
||||||
- --project
|
|
||||||
- unboundsoftware/schemas
|
|
||||||
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
|
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
|
||||||
rev: v9.23.0
|
rev: v9.24.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: commitlint
|
- id: commitlint
|
||||||
stages: [ commit-msg ]
|
stages: [ commit-msg ]
|
||||||
@@ -30,18 +23,18 @@ repos:
|
|||||||
- id: go-imports
|
- id: go-imports
|
||||||
args:
|
args:
|
||||||
- -local
|
- -local
|
||||||
- gitlab.com/unboundsoftware/schemas
|
- git.unbound.se/unboundsoftware/schemas
|
||||||
- repo: https://github.com/lietu/go-pre-commit
|
- repo: https://github.com/lietu/go-pre-commit
|
||||||
rev: v1.0.0
|
rev: v1.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: go-test
|
- id: go-test
|
||||||
- id: gofumpt
|
- id: gofumpt
|
||||||
- repo: https://github.com/golangci/golangci-lint
|
- repo: https://github.com/golangci/golangci-lint
|
||||||
rev: v2.6.2
|
rev: v2.11.4
|
||||||
hooks:
|
hooks:
|
||||||
- id: golangci-lint-full
|
- id: golangci-lint-full
|
||||||
- repo: https://github.com/gitleaks/gitleaks
|
- repo: https://github.com/gitleaks/gitleaks
|
||||||
rev: v8.29.1
|
rev: v8.30.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: gitleaks
|
- id: gitleaks
|
||||||
exclude: '^ctl/generated.go|graph/generated/.*$|^graph/model/models_gen.go|^tools/.*$$'
|
exclude: '^ctl/generated.go|graph/generated/.*$|^graph/model/models_gen.go|^tools/.*$$'
|
||||||
|
|||||||
+181
@@ -2,6 +2,187 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.9.7] - 2026-04-07
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update module gitlab.com/unboundsoftware/eventsourced/pg to v1.18.6 (#790)
|
||||||
|
- *(deps)* Update module github.com/alecthomas/kong to v1.15.0 (#791)
|
||||||
|
- *(deps)* Update opentelemetry-go monorepo (#793)
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update golang:1.26.1 docker digest to 5ba1126 (#795)
|
||||||
|
|
||||||
|
## [0.9.6] - 2026-03-29
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(graph)* Stabilize debouncer tests with synctest fake clock
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.257 (#743)
|
||||||
|
- *(deps)* Update golang.org/x/net to v0.51.0
|
||||||
|
- *(deps)* Update module go.opentelemetry.io/contrib/bridges/otelslog to v0.16.0 (#745)
|
||||||
|
- *(deps)* Update opentelemetry-go monorepo (#744)
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.260 (#750)
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.261 (#751)
|
||||||
|
- *(deps)* Update opentelemetry-go monorepo (#755)
|
||||||
|
- *(deps)* Update module go.opentelemetry.io/contrib/bridges/otelslog to v0.17.0 (#756)
|
||||||
|
- *(deps)* Update module golang.org/x/sync to v0.20.0 (#760)
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.88 (#762)
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.262 (#765)
|
||||||
|
- *(deps)* Update module golang.org/x/crypto to v0.49.0 (#767)
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.263 (#771)
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.264 (#773)
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.265 (#776)
|
||||||
|
- *(deps)* Update module gitlab.com/unboundsoftware/eventsourced/pg to v1.18.5 (#778)
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.89 (#782)
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.266 (#784)
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.267 (#786)
|
||||||
|
- Cap Node.js heap to prevent OOM during wgc composition (#788)
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update golang docker tag to v1.26.1 (#752)
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.11.1
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.11.2 (#758)
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.11.3 (#763)
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.30.1 (#769)
|
||||||
|
- *(deps)* Update golang:1.26.1 docker digest to 984bf90 (#774)
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.11.4 (#780)
|
||||||
|
- *(deps)* Update node.js to v24.14.1 (#783)
|
||||||
|
|
||||||
|
## [0.9.5] - 2026-02-25
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.256
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update golang:1.26.0 docker digest to 9835fb4 (#736)
|
||||||
|
- *(deps)* Update node.js to v24.14.0 (#735)
|
||||||
|
- *(deps)* Update node.js to 7fddd9d (#739)
|
||||||
|
|
||||||
|
## [0.9.4] - 2026-02-23
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(ci)* Pin goreleaser to v2.13.3 for Gitea SDK compatibility
|
||||||
|
|
||||||
|
## [0.9.3] - 2026-02-23
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.249 (#702)
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.250 (#704)
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.251
|
||||||
|
- *(deps)* Update module gitlab.com/unboundsoftware/eventsourced/pg to v1.18.3
|
||||||
|
- *(deps)* Update module github.com/vektah/gqlparser/v2 to v2.5.32
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.252 (#714)
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.253 (#716)
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.87
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.254 (#718)
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.255
|
||||||
|
- *(deps)* Update module github.com/pressly/goose/v3 to v3.27.0
|
||||||
|
- *(deps)* Update module gitlab.com/unboundsoftware/eventsourced/pg to v1.18.4 (#727)
|
||||||
|
- Prevent OOM on rapid schema publishing
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.10.1 (#706)
|
||||||
|
- *(deps)* Update goreleaser/goreleaser-action action to v7
|
||||||
|
- *(deps)* Update actions/setup-node action to v6
|
||||||
|
|
||||||
|
## [0.9.2] - 2026-02-13
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update module github.com/auth0/go-jwt-middleware/v2 to v3
|
||||||
|
- Migrate to go-jwt-middleware v3 API
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.243 (#680)
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.244 (#681)
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.245 (#682)
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.246 (#683)
|
||||||
|
- *(deps)* Update module gitlab.com/unboundsoftware/eventsourced/pg to v1.18.0 (#685)
|
||||||
|
- *(deps)* Update module gitlab.com/unboundsoftware/eventsourced/pg to v1.18.1 (#686)
|
||||||
|
- *(deps)* Update opentelemetry-go monorepo (#687)
|
||||||
|
- *(deps)* Update module go.opentelemetry.io/contrib/bridges/otelslog to v0.15.0 (#688)
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.247 (#691)
|
||||||
|
- *(deps)* Update module github.com/alecthomas/kong to v1.14.0 (#692)
|
||||||
|
- *(deps)* Update eventsourced (#693)
|
||||||
|
- *(deps)* Update module golang.org/x/crypto to v0.48.0 (#694)
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.248 (#698)
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update node.js to cd6fb7e (#684)
|
||||||
|
- *(deps)* Update golang:1.25.6 docker digest to ceda080 (#689)
|
||||||
|
- *(deps)* Update golang docker tag to v1.25.7
|
||||||
|
- *(deps)* Update golang:1.25.7 docker digest to d2819ff (#695)
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.9.0 (#697)
|
||||||
|
- *(deps)* Update golang docker tag to v1.26.0 (#696)
|
||||||
|
- *(deps)* Update node.js to v24.13.1 (#699)
|
||||||
|
|
||||||
|
## [0.9.1] - 2026-01-18
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(ci)* Run build job on tags for Docker images
|
||||||
|
|
||||||
|
## [0.9.0] - 2026-01-18
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- Add commands for managing organizations and users
|
||||||
|
- Migrate from GitLab CI to Gitea Actions
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.239
|
||||||
|
- *(k8s)* Update ingress class configuration for schema
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.84
|
||||||
|
- *(docker)* Update Node.js version to 24.11.1-alpine
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.240
|
||||||
|
- *(deps)* Update opentelemetry-go monorepo
|
||||||
|
- *(deps)* Update module golang.org/x/crypto to v0.46.0
|
||||||
|
- *(deps)* Update module go.opentelemetry.io/contrib/bridges/otelslog to v0.14.0
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.241
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.85
|
||||||
|
- *(deps)* Update module github.com/wundergraph/graphql-go-tools/v2 to v2.0.0-rc.242
|
||||||
|
- *(deps)* Update module golang.org/x/crypto to v0.47.0
|
||||||
|
- *(deps)* Update module github.com/99designs/gqlgen to v0.17.86
|
||||||
|
|
||||||
|
### 🚜 Refactor
|
||||||
|
|
||||||
|
- *(cache)* Optimize test setup and reduce iterations
|
||||||
|
|
||||||
|
### 🧪 Testing
|
||||||
|
|
||||||
|
- Add validation and event tests for organization and API key
|
||||||
|
- *(cache)* Update tests to use legacy hash for speed
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- *(deps)* Update pre-commit hook gitleaks/gitleaks to v8.30.0
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.13.0
|
||||||
|
- *(deps)* Update golang docker tag to v1.25.5
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.7.0
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.7.1
|
||||||
|
- *(deps)* Update node.js to 682368d
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.7.2
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.13.1
|
||||||
|
- *(deps)* Update golang:1.25.5 docker digest to 0c27bcf
|
||||||
|
- *(deps)* Update node.js to v24.12.0
|
||||||
|
- *(deps)* Update node.js to c921b97
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.13.2
|
||||||
|
- *(deps)* Update golang:1.25.5 docker digest to ad03ba9
|
||||||
|
- *(deps)* Update pre-commit hook golangci/golangci-lint to v2.8.0
|
||||||
|
- *(deps)* Update goreleaser/goreleaser docker tag to v2.13.3
|
||||||
|
- *(deps)* Update golang:1.25.5 docker digest to 3a01526
|
||||||
|
- *(deps)* Update pre-commit hook alessandrojcm/commitlint-pre-commit-hook to v9.24.0
|
||||||
|
- *(deps)* Update node.js to v24.13.0
|
||||||
|
- *(deps)* Update golang docker tag to v1.25.6
|
||||||
|
|
||||||
## [0.8.0] - 2025-11-21
|
## [0.8.0] - 2025-11-21
|
||||||
|
|
||||||
### 🚀 Features
|
### 🚀 Features
|
||||||
|
|||||||
+5
-2
@@ -1,4 +1,4 @@
|
|||||||
FROM amd64/golang:1.25.4@sha256:efe81fa41fdf81fb873ab7cd931b9bb29bd10aced6c252cbd91739c34e654f01 as modules
|
FROM amd64/golang:1.26.1@sha256:5ba1126a6dbb65aa517ff5eaecab6abc29480671e5c0ca176192de8da9dba4b8 as modules
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
ADD go.* /build
|
ADD go.* /build
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
@@ -24,12 +24,15 @@ RUN GOOS=linux GOARCH=amd64 go build \
|
|||||||
FROM scratch as export
|
FROM scratch as export
|
||||||
COPY --from=build /build/coverage.txt /
|
COPY --from=build /build/coverage.txt /
|
||||||
|
|
||||||
FROM node:24-alpine@sha256:2867d550cf9d8bb50059a0fff528741f11a84d985c732e60e19e8e75c7239c43
|
FROM node:24.14.1-alpine@sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b
|
||||||
ENV TZ Europe/Stockholm
|
ENV TZ Europe/Stockholm
|
||||||
|
|
||||||
# Install wgc CLI globally for Cosmo Router composition
|
# Install wgc CLI globally for Cosmo Router composition
|
||||||
RUN npm install -g wgc@latest
|
RUN npm install -g wgc@latest
|
||||||
|
|
||||||
|
# Cap Node.js heap for runtime wgc invocations to prevent OOM
|
||||||
|
ENV NODE_OPTIONS="--max-old-space-size=64"
|
||||||
|
|
||||||
# Copy timezone data and certificates
|
# Copy timezone data and certificates
|
||||||
COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo
|
COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo
|
||||||
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
|||||||
Vendored
+80
-2
@@ -9,8 +9,8 @@ import (
|
|||||||
"github.com/sparetimecoders/goamqp"
|
"github.com/sparetimecoders/goamqp"
|
||||||
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/domain"
|
"gitea.unbound.se/unboundsoftware/schemas/domain"
|
||||||
"gitlab.com/unboundsoftware/schemas/hash"
|
"gitea.unbound.se/unboundsoftware/schemas/hash"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cache struct {
|
type Cache struct {
|
||||||
@@ -53,6 +53,17 @@ func (c *Cache) OrganizationsByUser(sub string) []domain.Organization {
|
|||||||
return orgs
|
return orgs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Cache) AllOrganizations() []domain.Organization {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
|
orgs := make([]domain.Organization, 0, len(c.organizations))
|
||||||
|
for _, org := range c.organizations {
|
||||||
|
orgs = append(orgs, org)
|
||||||
|
}
|
||||||
|
return orgs
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Cache) ApiKeyByKey(key string) *domain.APIKey {
|
func (c *Cache) ApiKeyByKey(key string) *domain.APIKey {
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
@@ -100,6 +111,16 @@ func (c *Cache) Update(msg any, _ goamqp.Headers) (any, error) {
|
|||||||
c.organizations[m.ID.String()] = o
|
c.organizations[m.ID.String()] = o
|
||||||
c.addUser(m.Initiator, o)
|
c.addUser(m.Initiator, o)
|
||||||
c.logger.With("org_id", m.ID.String(), "event", "OrganizationAdded").Debug("cache updated")
|
c.logger.With("org_id", m.ID.String(), "event", "OrganizationAdded").Debug("cache updated")
|
||||||
|
case *domain.UserAddedToOrganization:
|
||||||
|
org, exists := c.organizations[m.ID.String()]
|
||||||
|
if exists {
|
||||||
|
m.UpdateOrganization(&org)
|
||||||
|
c.organizations[m.ID.String()] = org
|
||||||
|
c.addUser(m.UserId, org)
|
||||||
|
c.logger.With("org_id", m.ID.String(), "user_id", m.UserId, "event", "UserAddedToOrganization").Debug("cache updated")
|
||||||
|
} else {
|
||||||
|
c.logger.With("org_id", m.ID.String(), "event", "UserAddedToOrganization").Warn("organization not found in cache")
|
||||||
|
}
|
||||||
case *domain.APIKeyAdded:
|
case *domain.APIKeyAdded:
|
||||||
key := domain.APIKey{
|
key := domain.APIKey{
|
||||||
Name: m.Name,
|
Name: m.Name,
|
||||||
@@ -117,6 +138,63 @@ func (c *Cache) Update(msg any, _ goamqp.Headers) (any, error) {
|
|||||||
org.APIKeys = append(org.APIKeys, key)
|
org.APIKeys = append(org.APIKeys, key)
|
||||||
c.organizations[m.OrganizationId] = org
|
c.organizations[m.OrganizationId] = org
|
||||||
c.logger.With("org_id", m.OrganizationId, "key_name", m.Name, "event", "APIKeyAdded").Debug("cache updated")
|
c.logger.With("org_id", m.OrganizationId, "key_name", m.Name, "event", "APIKeyAdded").Debug("cache updated")
|
||||||
|
case *domain.APIKeyRemoved:
|
||||||
|
orgId := m.ID.String()
|
||||||
|
org, exists := c.organizations[orgId]
|
||||||
|
if exists {
|
||||||
|
// Remove from organization's API keys list
|
||||||
|
for i, key := range org.APIKeys {
|
||||||
|
if key.Name == m.KeyName {
|
||||||
|
org.APIKeys = append(org.APIKeys[:i], org.APIKeys[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.organizations[orgId] = org
|
||||||
|
// Remove from apiKeys map
|
||||||
|
delete(c.apiKeys, apiKeyId(orgId, m.KeyName))
|
||||||
|
c.logger.With("org_id", orgId, "key_name", m.KeyName, "event", "APIKeyRemoved").Debug("cache updated")
|
||||||
|
} else {
|
||||||
|
c.logger.With("org_id", orgId, "event", "APIKeyRemoved").Warn("organization not found in cache")
|
||||||
|
}
|
||||||
|
case *domain.OrganizationRemoved:
|
||||||
|
orgId := m.ID.String()
|
||||||
|
org, exists := c.organizations[orgId]
|
||||||
|
if exists {
|
||||||
|
// Remove all API keys for this organization
|
||||||
|
for _, key := range org.APIKeys {
|
||||||
|
delete(c.apiKeys, apiKeyId(orgId, key.Name))
|
||||||
|
}
|
||||||
|
// Remove organization from all users
|
||||||
|
for userId, userOrgs := range c.users {
|
||||||
|
for i, userOrgId := range userOrgs {
|
||||||
|
if userOrgId == orgId {
|
||||||
|
c.users[userId] = append(userOrgs[:i], userOrgs[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If user has no more organizations, remove from map
|
||||||
|
if len(c.users[userId]) == 0 {
|
||||||
|
delete(c.users, userId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove services for this organization
|
||||||
|
if refs, exists := c.services[orgId]; exists {
|
||||||
|
for ref := range refs {
|
||||||
|
// Remove all subgraphs for this org/ref combination
|
||||||
|
for service := range refs[ref] {
|
||||||
|
delete(c.subGraphs, subGraphKey(orgId, ref, service))
|
||||||
|
}
|
||||||
|
// Remove lastUpdate for this org/ref
|
||||||
|
delete(c.lastUpdate, refKey(orgId, ref))
|
||||||
|
}
|
||||||
|
delete(c.services, orgId)
|
||||||
|
}
|
||||||
|
// Remove organization
|
||||||
|
delete(c.organizations, orgId)
|
||||||
|
c.logger.With("org_id", orgId, "event", "OrganizationRemoved").Debug("cache updated")
|
||||||
|
} else {
|
||||||
|
c.logger.With("org_id", orgId, "event", "OrganizationRemoved").Warn("organization not found in cache")
|
||||||
|
}
|
||||||
case *domain.SubGraphUpdated:
|
case *domain.SubGraphUpdated:
|
||||||
c.updateSubGraph(m.OrganizationId, m.Ref, m.ID.String(), m.Service, m.Time)
|
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")
|
c.logger.With("org_id", m.OrganizationId, "ref", m.Ref, "service", m.Service, "event", "SubGraphUpdated").Debug("cache updated")
|
||||||
|
|||||||
Vendored
+228
-30
@@ -12,8 +12,8 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/domain"
|
"gitea.unbound.se/unboundsoftware/schemas/domain"
|
||||||
"gitlab.com/unboundsoftware/schemas/hash"
|
"gitea.unbound.se/unboundsoftware/schemas/hash"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCache_OrganizationByAPIKey(t *testing.T) {
|
func TestCache_OrganizationByAPIKey(t *testing.T) {
|
||||||
@@ -320,24 +320,18 @@ func TestCache_ConcurrentReads(t *testing.T) {
|
|||||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
c := New(logger)
|
c := New(logger)
|
||||||
|
|
||||||
// Setup test data
|
// Setup test data - use legacy hash to avoid slow bcrypt
|
||||||
orgID := uuid.New().String()
|
orgID := uuid.New().String()
|
||||||
apiKey := "test-concurrent-key" // gitleaks:allow
|
userSub := "test-user"
|
||||||
hashedKey, err := hash.APIKey(apiKey)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
org := domain.Organization{
|
org := domain.Organization{
|
||||||
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
||||||
Name: "Concurrent Test Org",
|
Name: "Concurrent Test Org",
|
||||||
}
|
}
|
||||||
c.organizations[orgID] = org
|
c.organizations[orgID] = org
|
||||||
c.apiKeys[apiKeyId(orgID, "test-key")] = domain.APIKey{
|
c.users[userSub] = []string{orgID}
|
||||||
Name: "test-key",
|
|
||||||
OrganizationId: orgID,
|
|
||||||
Key: hashedKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run concurrent reads (reduced for race detector)
|
// Run concurrent reads using fast OrganizationsByUser
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
numGoroutines := 20
|
numGoroutines := 20
|
||||||
|
|
||||||
@@ -345,9 +339,9 @@ func TestCache_ConcurrentReads(t *testing.T) {
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
org := c.OrganizationByAPIKey(apiKey)
|
orgs := c.OrganizationsByUser(userSub)
|
||||||
assert.NotNil(t, org)
|
assert.NotEmpty(t, orgs)
|
||||||
assert.Equal(t, "Concurrent Test Org", org.Name)
|
assert.Equal(t, "Concurrent Test Org", orgs[0].Name)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,11 +381,10 @@ func TestCache_ConcurrentReadsAndWrites(t *testing.T) {
|
|||||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
c := New(logger)
|
c := New(logger)
|
||||||
|
|
||||||
// Setup initial data
|
// Setup initial data - use legacy hash to avoid slow bcrypt in concurrent test
|
||||||
orgID := uuid.New().String()
|
orgID := uuid.New().String()
|
||||||
apiKey := "test-rw-key" // gitleaks:allow
|
legacyKey := "test-rw-key" // gitleaks:allow
|
||||||
hashedKey, err := hash.APIKey(apiKey)
|
legacyHash := hash.String(legacyKey)
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
org := domain.Organization{
|
org := domain.Organization{
|
||||||
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
||||||
@@ -401,26 +394,21 @@ func TestCache_ConcurrentReadsAndWrites(t *testing.T) {
|
|||||||
c.apiKeys[apiKeyId(orgID, "test-key")] = domain.APIKey{
|
c.apiKeys[apiKeyId(orgID, "test-key")] = domain.APIKey{
|
||||||
Name: "test-key",
|
Name: "test-key",
|
||||||
OrganizationId: orgID,
|
OrganizationId: orgID,
|
||||||
Key: hashedKey,
|
Key: legacyHash,
|
||||||
}
|
}
|
||||||
c.users["user-initial"] = []string{orgID}
|
c.users["user-initial"] = []string{orgID}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
numReaders := 10 // Reduced for race detector
|
numReaders := 5
|
||||||
numWriters := 5 // Reduced for race detector
|
numWriters := 3
|
||||||
iterations := 3 // Reduced for race detector
|
|
||||||
|
|
||||||
// Concurrent readers
|
// Concurrent readers - use OrganizationsByUser which is fast
|
||||||
for i := 0; i < numReaders; i++ {
|
for i := 0; i < numReaders; i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for j := 0; j < iterations; j++ {
|
orgs := c.OrganizationsByUser("user-initial")
|
||||||
org := c.OrganizationByAPIKey(apiKey)
|
assert.NotEmpty(t, orgs)
|
||||||
assert.NotNil(t, org)
|
|
||||||
orgs := c.OrganizationsByUser("user-initial")
|
|
||||||
assert.NotEmpty(t, orgs)
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,3 +433,213 @@ func TestCache_ConcurrentReadsAndWrites(t *testing.T) {
|
|||||||
// Verify cache is in consistent state
|
// Verify cache is in consistent state
|
||||||
assert.GreaterOrEqual(t, len(c.organizations), numWriters)
|
assert.GreaterOrEqual(t, len(c.organizations), numWriters)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCache_Update_APIKeyRemoved(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 with API key
|
||||||
|
org := domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
||||||
|
Name: "Test Org",
|
||||||
|
APIKeys: []domain.APIKey{
|
||||||
|
{
|
||||||
|
Name: keyName,
|
||||||
|
OrganizationId: orgID,
|
||||||
|
Key: hashedKey,
|
||||||
|
Refs: []string{"main"},
|
||||||
|
Read: true,
|
||||||
|
Publish: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c.organizations[orgID] = org
|
||||||
|
c.apiKeys[apiKeyId(orgID, keyName)] = org.APIKeys[0]
|
||||||
|
|
||||||
|
// Verify key exists before removal
|
||||||
|
_, exists := c.apiKeys[apiKeyId(orgID, keyName)]
|
||||||
|
assert.True(t, exists)
|
||||||
|
|
||||||
|
// Remove the API key
|
||||||
|
event := &domain.APIKeyRemoved{
|
||||||
|
KeyName: keyName,
|
||||||
|
Initiator: "user-123",
|
||||||
|
}
|
||||||
|
event.ID = *eventsourced.IdFromString(orgID)
|
||||||
|
|
||||||
|
_, err := c.Update(event, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify API key was removed from cache
|
||||||
|
_, exists = c.apiKeys[apiKeyId(orgID, keyName)]
|
||||||
|
assert.False(t, exists, "API key should be removed from cache")
|
||||||
|
|
||||||
|
// Verify API key was removed from organization
|
||||||
|
updatedOrg := c.organizations[orgID]
|
||||||
|
assert.Len(t, updatedOrg.APIKeys, 0, "API key should be removed from organization")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache_Update_APIKeyRemoved_MultipleKeys(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
c := New(logger)
|
||||||
|
|
||||||
|
orgID := uuid.New().String()
|
||||||
|
|
||||||
|
// Add organization with multiple API keys
|
||||||
|
org := domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
||||||
|
Name: "Test Org",
|
||||||
|
APIKeys: []domain.APIKey{
|
||||||
|
{
|
||||||
|
Name: "key1",
|
||||||
|
OrganizationId: orgID,
|
||||||
|
Key: "hash1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "key2",
|
||||||
|
OrganizationId: orgID,
|
||||||
|
Key: "hash2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "key3",
|
||||||
|
OrganizationId: orgID,
|
||||||
|
Key: "hash3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c.organizations[orgID] = org
|
||||||
|
c.apiKeys[apiKeyId(orgID, "key1")] = org.APIKeys[0]
|
||||||
|
c.apiKeys[apiKeyId(orgID, "key2")] = org.APIKeys[1]
|
||||||
|
c.apiKeys[apiKeyId(orgID, "key3")] = org.APIKeys[2]
|
||||||
|
|
||||||
|
// Remove the middle key
|
||||||
|
event := &domain.APIKeyRemoved{
|
||||||
|
KeyName: "key2",
|
||||||
|
Initiator: "user-123",
|
||||||
|
}
|
||||||
|
event.ID = *eventsourced.IdFromString(orgID)
|
||||||
|
|
||||||
|
_, err := c.Update(event, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify only key2 was removed
|
||||||
|
_, exists := c.apiKeys[apiKeyId(orgID, "key1")]
|
||||||
|
assert.True(t, exists, "key1 should still exist")
|
||||||
|
|
||||||
|
_, exists = c.apiKeys[apiKeyId(orgID, "key2")]
|
||||||
|
assert.False(t, exists, "key2 should be removed")
|
||||||
|
|
||||||
|
_, exists = c.apiKeys[apiKeyId(orgID, "key3")]
|
||||||
|
assert.True(t, exists, "key3 should still exist")
|
||||||
|
|
||||||
|
// Verify organization has 2 keys remaining
|
||||||
|
updatedOrg := c.organizations[orgID]
|
||||||
|
assert.Len(t, updatedOrg.APIKeys, 2)
|
||||||
|
assert.Equal(t, "key1", updatedOrg.APIKeys[0].Name)
|
||||||
|
assert.Equal(t, "key3", updatedOrg.APIKeys[1].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache_Update_OrganizationRemoved(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
c := New(logger)
|
||||||
|
|
||||||
|
orgID := uuid.New().String()
|
||||||
|
userSub := "user-123"
|
||||||
|
|
||||||
|
// Add organization with API keys, users, and subgraphs
|
||||||
|
org := domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
||||||
|
Name: "Test Org",
|
||||||
|
APIKeys: []domain.APIKey{
|
||||||
|
{
|
||||||
|
Name: "key1",
|
||||||
|
OrganizationId: orgID,
|
||||||
|
Key: "hash1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c.organizations[orgID] = org
|
||||||
|
c.apiKeys[apiKeyId(orgID, "key1")] = org.APIKeys[0]
|
||||||
|
c.users[userSub] = []string{orgID}
|
||||||
|
c.services[orgID] = map[string]map[string]struct{}{
|
||||||
|
"main": {
|
||||||
|
"service1": {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c.subGraphs[subGraphKey(orgID, "main", "service1")] = "subgraph-id"
|
||||||
|
c.lastUpdate[refKey(orgID, "main")] = "2024-01-01T12:00:00Z"
|
||||||
|
|
||||||
|
// Remove the organization
|
||||||
|
event := &domain.OrganizationRemoved{
|
||||||
|
Initiator: userSub,
|
||||||
|
}
|
||||||
|
event.ID = *eventsourced.IdFromString(orgID)
|
||||||
|
|
||||||
|
_, err := c.Update(event, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify organization was removed
|
||||||
|
_, exists := c.organizations[orgID]
|
||||||
|
assert.False(t, exists, "Organization should be removed from cache")
|
||||||
|
|
||||||
|
// Verify API keys were removed
|
||||||
|
_, exists = c.apiKeys[apiKeyId(orgID, "key1")]
|
||||||
|
assert.False(t, exists, "API keys should be removed from cache")
|
||||||
|
|
||||||
|
// Verify user association was removed
|
||||||
|
userOrgs := c.users[userSub]
|
||||||
|
assert.NotContains(t, userOrgs, orgID, "User should not be associated with removed organization")
|
||||||
|
|
||||||
|
// Verify services were removed
|
||||||
|
_, exists = c.services[orgID]
|
||||||
|
assert.False(t, exists, "Services should be removed from cache")
|
||||||
|
|
||||||
|
// Verify subgraphs were removed
|
||||||
|
_, exists = c.subGraphs[subGraphKey(orgID, "main", "service1")]
|
||||||
|
assert.False(t, exists, "Subgraphs should be removed from cache")
|
||||||
|
|
||||||
|
// Verify lastUpdate was removed
|
||||||
|
_, exists = c.lastUpdate[refKey(orgID, "main")]
|
||||||
|
assert.False(t, exists, "LastUpdate should be removed from cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCache_Update_OrganizationRemoved_MultipleUsers(t *testing.T) {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
c := New(logger)
|
||||||
|
|
||||||
|
orgID := uuid.New().String()
|
||||||
|
user1 := "user-1"
|
||||||
|
user2 := "user-2"
|
||||||
|
otherOrgID := uuid.New().String()
|
||||||
|
|
||||||
|
// Add organization
|
||||||
|
org := domain.Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString(orgID),
|
||||||
|
Name: "Test Org",
|
||||||
|
}
|
||||||
|
c.organizations[orgID] = org
|
||||||
|
|
||||||
|
// Add users with multiple org associations
|
||||||
|
c.users[user1] = []string{orgID, otherOrgID}
|
||||||
|
c.users[user2] = []string{orgID}
|
||||||
|
|
||||||
|
// Remove the organization
|
||||||
|
event := &domain.OrganizationRemoved{
|
||||||
|
Initiator: user1,
|
||||||
|
}
|
||||||
|
event.ID = *eventsourced.IdFromString(orgID)
|
||||||
|
|
||||||
|
_, err := c.Update(event, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify user1 still has otherOrgID but not removed orgID
|
||||||
|
assert.Len(t, c.users[user1], 1)
|
||||||
|
assert.Equal(t, otherOrgID, c.users[user1][0])
|
||||||
|
|
||||||
|
// Verify user2 has no organizations
|
||||||
|
assert.Len(t, c.users[user2], 0)
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/alecthomas/kong"
|
"github.com/alecthomas/kong"
|
||||||
"github.com/apex/log"
|
"github.com/apex/log"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/ctl"
|
"gitea.unbound.se/unboundsoftware/schemas/ctl"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Context struct {
|
type Context struct {
|
||||||
|
|||||||
+25
-14
@@ -26,15 +26,15 @@ import (
|
|||||||
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
"gitlab.com/unboundsoftware/eventsourced/pg"
|
"gitlab.com/unboundsoftware/eventsourced/pg"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/cache"
|
"gitea.unbound.se/unboundsoftware/schemas/cache"
|
||||||
"gitlab.com/unboundsoftware/schemas/domain"
|
"gitea.unbound.se/unboundsoftware/schemas/domain"
|
||||||
"gitlab.com/unboundsoftware/schemas/graph"
|
"gitea.unbound.se/unboundsoftware/schemas/graph"
|
||||||
"gitlab.com/unboundsoftware/schemas/graph/generated"
|
"gitea.unbound.se/unboundsoftware/schemas/graph/generated"
|
||||||
"gitlab.com/unboundsoftware/schemas/health"
|
"gitea.unbound.se/unboundsoftware/schemas/health"
|
||||||
"gitlab.com/unboundsoftware/schemas/logging"
|
"gitea.unbound.se/unboundsoftware/schemas/logging"
|
||||||
"gitlab.com/unboundsoftware/schemas/middleware"
|
"gitea.unbound.se/unboundsoftware/schemas/middleware"
|
||||||
"gitlab.com/unboundsoftware/schemas/monitoring"
|
"gitea.unbound.se/unboundsoftware/schemas/monitoring"
|
||||||
"gitlab.com/unboundsoftware/schemas/store"
|
"gitea.unbound.se/unboundsoftware/schemas/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CLI struct {
|
type CLI struct {
|
||||||
@@ -92,7 +92,10 @@ func start(closeEvents chan error, logger *slog.Logger, connectToAmqpFunc func(u
|
|||||||
pg.WithEventTypes(
|
pg.WithEventTypes(
|
||||||
&domain.SubGraphUpdated{},
|
&domain.SubGraphUpdated{},
|
||||||
&domain.OrganizationAdded{},
|
&domain.OrganizationAdded{},
|
||||||
|
&domain.UserAddedToOrganization{},
|
||||||
&domain.APIKeyAdded{},
|
&domain.APIKeyAdded{},
|
||||||
|
&domain.APIKeyRemoved{},
|
||||||
|
&domain.OrganizationRemoved{},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -127,10 +130,16 @@ func start(closeEvents chan error, logger *slog.Logger, connectToAmqpFunc func(u
|
|||||||
goamqp.EventStreamPublisher(publisher),
|
goamqp.EventStreamPublisher(publisher),
|
||||||
goamqp.TransientEventStreamConsumer("SubGraph.Updated", serviceCache.Update, domain.SubGraphUpdated{}),
|
goamqp.TransientEventStreamConsumer("SubGraph.Updated", serviceCache.Update, domain.SubGraphUpdated{}),
|
||||||
goamqp.TransientEventStreamConsumer("Organization.Added", serviceCache.Update, domain.OrganizationAdded{}),
|
goamqp.TransientEventStreamConsumer("Organization.Added", serviceCache.Update, domain.OrganizationAdded{}),
|
||||||
|
goamqp.TransientEventStreamConsumer("Organization.UserAdded", serviceCache.Update, domain.UserAddedToOrganization{}),
|
||||||
goamqp.TransientEventStreamConsumer("Organization.APIKeyAdded", serviceCache.Update, domain.APIKeyAdded{}),
|
goamqp.TransientEventStreamConsumer("Organization.APIKeyAdded", serviceCache.Update, domain.APIKeyAdded{}),
|
||||||
|
goamqp.TransientEventStreamConsumer("Organization.APIKeyRemoved", serviceCache.Update, domain.APIKeyRemoved{}),
|
||||||
|
goamqp.TransientEventStreamConsumer("Organization.Removed", serviceCache.Update, domain.OrganizationRemoved{}),
|
||||||
goamqp.WithTypeMapping("SubGraph.Updated", domain.SubGraphUpdated{}),
|
goamqp.WithTypeMapping("SubGraph.Updated", domain.SubGraphUpdated{}),
|
||||||
goamqp.WithTypeMapping("Organization.Added", domain.OrganizationAdded{}),
|
goamqp.WithTypeMapping("Organization.Added", domain.OrganizationAdded{}),
|
||||||
|
goamqp.WithTypeMapping("Organization.UserAdded", domain.UserAddedToOrganization{}),
|
||||||
goamqp.WithTypeMapping("Organization.APIKeyAdded", domain.APIKeyAdded{}),
|
goamqp.WithTypeMapping("Organization.APIKeyAdded", domain.APIKeyAdded{}),
|
||||||
|
goamqp.WithTypeMapping("Organization.APIKeyRemoved", domain.APIKeyRemoved{}),
|
||||||
|
goamqp.WithTypeMapping("Organization.Removed", domain.OrganizationRemoved{}),
|
||||||
}
|
}
|
||||||
if err := conn.Start(rootCtx, setups...); err != nil {
|
if err := conn.Start(rootCtx, setups...); err != nil {
|
||||||
return fmt.Errorf("failed to setup AMQP: %v", err)
|
return fmt.Errorf("failed to setup AMQP: %v", err)
|
||||||
@@ -192,11 +201,13 @@ func start(closeEvents chan error, logger *slog.Logger, connectToAmqpFunc func(u
|
|||||||
defer rootCancel()
|
defer rootCancel()
|
||||||
|
|
||||||
resolver := &graph.Resolver{
|
resolver := &graph.Resolver{
|
||||||
EventStore: eventStore,
|
EventStore: eventStore,
|
||||||
Publisher: eventPublisher,
|
Publisher: eventPublisher,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
Cache: serviceCache,
|
Cache: serviceCache,
|
||||||
PubSub: graph.NewPubSub(),
|
PubSub: graph.NewPubSub(),
|
||||||
|
CosmoGenerator: graph.NewCosmoGenerator(&graph.DefaultCommandExecutor{}, 60*time.Second),
|
||||||
|
Debouncer: graph.NewDebouncer(500 * time.Millisecond),
|
||||||
}
|
}
|
||||||
|
|
||||||
config := generated.Config{
|
config := generated.Config{
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/domain"
|
"gitea.unbound.se/unboundsoftware/schemas/domain"
|
||||||
"gitlab.com/unboundsoftware/schemas/hash"
|
"gitea.unbound.se/unboundsoftware/schemas/hash"
|
||||||
"gitlab.com/unboundsoftware/schemas/middleware"
|
"gitea.unbound.se/unboundsoftware/schemas/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockCache is a mock implementation for testing
|
// MockCache is a mock implementation for testing
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ func (o *Organization) Apply(event eventsourced.Event) error {
|
|||||||
switch e := event.(type) {
|
switch e := event.(type) {
|
||||||
case *OrganizationAdded:
|
case *OrganizationAdded:
|
||||||
e.UpdateOrganization(o)
|
e.UpdateOrganization(o)
|
||||||
|
case *UserAddedToOrganization:
|
||||||
|
e.UpdateOrganization(o)
|
||||||
case *APIKeyAdded:
|
case *APIKeyAdded:
|
||||||
o.APIKeys = append(o.APIKeys, APIKey{
|
o.APIKeys = append(o.APIKeys, APIKey{
|
||||||
Name: e.Name,
|
Name: e.Name,
|
||||||
@@ -36,6 +38,10 @@ func (o *Organization) Apply(event eventsourced.Event) error {
|
|||||||
})
|
})
|
||||||
o.ChangedBy = e.Initiator
|
o.ChangedBy = e.Initiator
|
||||||
o.ChangedAt = e.When()
|
o.ChangedAt = e.When()
|
||||||
|
case *APIKeyRemoved:
|
||||||
|
e.UpdateOrganization(o)
|
||||||
|
case *OrganizationRemoved:
|
||||||
|
e.UpdateOrganization(o)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unexpected event type: %+v", event)
|
return fmt.Errorf("unexpected event type: %+v", event)
|
||||||
}
|
}
|
||||||
|
|||||||
+83
-1
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/hash"
|
"gitea.unbound.se/unboundsoftware/schemas/hash"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AddOrganization struct {
|
type AddOrganization struct {
|
||||||
@@ -34,6 +34,37 @@ func (a AddOrganization) Event(context.Context) eventsourced.Event {
|
|||||||
|
|
||||||
var _ eventsourced.Command = AddOrganization{}
|
var _ eventsourced.Command = AddOrganization{}
|
||||||
|
|
||||||
|
type AddUserToOrganization struct {
|
||||||
|
UserId string
|
||||||
|
Initiator string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AddUserToOrganization) Validate(_ context.Context, aggregate eventsourced.Aggregate) error {
|
||||||
|
if aggregate.Identity() == nil {
|
||||||
|
return fmt.Errorf("organization does not exist")
|
||||||
|
}
|
||||||
|
if len(a.UserId) == 0 {
|
||||||
|
return fmt.Errorf("userId is required")
|
||||||
|
}
|
||||||
|
// Check if user is already in the organization
|
||||||
|
org := aggregate.(*Organization)
|
||||||
|
for _, user := range org.Users {
|
||||||
|
if user == a.UserId {
|
||||||
|
return fmt.Errorf("user is already a member of this organization")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AddUserToOrganization) Event(context.Context) eventsourced.Event {
|
||||||
|
return &UserAddedToOrganization{
|
||||||
|
UserId: a.UserId,
|
||||||
|
Initiator: a.Initiator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ eventsourced.Command = AddUserToOrganization{}
|
||||||
|
|
||||||
type AddAPIKey struct {
|
type AddAPIKey struct {
|
||||||
Name string
|
Name string
|
||||||
Key string
|
Key string
|
||||||
@@ -79,6 +110,57 @@ func (a AddAPIKey) Event(context.Context) eventsourced.Event {
|
|||||||
|
|
||||||
var _ eventsourced.Command = AddAPIKey{}
|
var _ eventsourced.Command = AddAPIKey{}
|
||||||
|
|
||||||
|
type RemoveAPIKey struct {
|
||||||
|
KeyName string
|
||||||
|
Initiator string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RemoveAPIKey) Validate(_ context.Context, aggregate eventsourced.Aggregate) error {
|
||||||
|
if aggregate.Identity() == nil {
|
||||||
|
return fmt.Errorf("organization does not exist")
|
||||||
|
}
|
||||||
|
org := aggregate.(*Organization)
|
||||||
|
found := false
|
||||||
|
for _, k := range org.APIKeys {
|
||||||
|
if k.Name == r.KeyName {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("API key '%s' not found", r.KeyName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RemoveAPIKey) Event(context.Context) eventsourced.Event {
|
||||||
|
return &APIKeyRemoved{
|
||||||
|
KeyName: r.KeyName,
|
||||||
|
Initiator: r.Initiator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ eventsourced.Command = RemoveAPIKey{}
|
||||||
|
|
||||||
|
type RemoveOrganization struct {
|
||||||
|
Initiator string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RemoveOrganization) Validate(_ context.Context, aggregate eventsourced.Aggregate) error {
|
||||||
|
if aggregate.Identity() == nil {
|
||||||
|
return fmt.Errorf("organization does not exist")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r RemoveOrganization) Event(context.Context) eventsourced.Event {
|
||||||
|
return &OrganizationRemoved{
|
||||||
|
Initiator: r.Initiator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ eventsourced.Command = RemoveOrganization{}
|
||||||
|
|
||||||
type UpdateSubGraph struct {
|
type UpdateSubGraph struct {
|
||||||
OrganizationId string
|
OrganizationId string
|
||||||
Ref string
|
Ref string
|
||||||
|
|||||||
+502
-1
@@ -7,10 +7,68 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/hash"
|
"gitea.unbound.se/unboundsoftware/schemas/hash"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AddOrganization tests
|
||||||
|
|
||||||
|
func TestAddOrganization_Validate_Success(t *testing.T) {
|
||||||
|
cmd := AddOrganization{
|
||||||
|
Name: "Test Org",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{} // New organization with no identity
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddOrganization_Validate_AlreadyExists(t *testing.T) {
|
||||||
|
cmd := AddOrganization{
|
||||||
|
Name: "Test Org",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("existing-org-id"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddOrganization_Validate_EmptyName(t *testing.T) {
|
||||||
|
cmd := AddOrganization{
|
||||||
|
Name: "",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{}
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "name is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddOrganization_Event(t *testing.T) {
|
||||||
|
cmd := AddOrganization{
|
||||||
|
Name: "Test Org",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
event := cmd.Event(context.Background())
|
||||||
|
require.NotNil(t, event)
|
||||||
|
|
||||||
|
orgEvent, ok := event.(*OrganizationAdded)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, "Test Org", orgEvent.Name)
|
||||||
|
assert.Equal(t, "user@example.com", orgEvent.Initiator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddAPIKey tests
|
||||||
|
|
||||||
func TestAddAPIKey_Event(t *testing.T) {
|
func TestAddAPIKey_Event(t *testing.T) {
|
||||||
type fields struct {
|
type fields struct {
|
||||||
Name string
|
Name string
|
||||||
@@ -74,3 +132,446 @@ func TestAddAPIKey_Event(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAddAPIKey_Validate_Success(t *testing.T) {
|
||||||
|
cmd := AddAPIKey{
|
||||||
|
Name: "production-key",
|
||||||
|
Key: "us_ak_1234567890123456",
|
||||||
|
Refs: []string{"main"},
|
||||||
|
Read: true,
|
||||||
|
Publish: false,
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
APIKeys: []APIKey{},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddAPIKey_Validate_OrganizationNotExists(t *testing.T) {
|
||||||
|
cmd := AddAPIKey{
|
||||||
|
Name: "production-key",
|
||||||
|
Key: "us_ak_1234567890123456",
|
||||||
|
Refs: []string{"main"},
|
||||||
|
Read: true,
|
||||||
|
Publish: false,
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{} // No identity means it doesn't exist
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddAPIKey_Validate_DuplicateKeyName(t *testing.T) {
|
||||||
|
cmd := AddAPIKey{
|
||||||
|
Name: "existing-key",
|
||||||
|
Key: "us_ak_1234567890123456",
|
||||||
|
Refs: []string{"main"},
|
||||||
|
Read: true,
|
||||||
|
Publish: false,
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
APIKeys: []APIKey{
|
||||||
|
{
|
||||||
|
Name: "existing-key",
|
||||||
|
Key: "hashed-key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "already exist")
|
||||||
|
assert.Contains(t, err.Error(), "existing-key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSubGraph tests
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_Success(t *testing.T) {
|
||||||
|
url := "http://example.com/graphql"
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "users",
|
||||||
|
Url: &url,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_MissingRef(t *testing.T) {
|
||||||
|
url := "http://example.com/graphql"
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "",
|
||||||
|
Service: "users",
|
||||||
|
Url: &url,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "ref is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_RefWhitespaceOnly(t *testing.T) {
|
||||||
|
url := "http://example.com/graphql"
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: " ",
|
||||||
|
Service: "users",
|
||||||
|
Url: &url,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "ref is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_MissingService(t *testing.T) {
|
||||||
|
url := "http://example.com/graphql"
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "",
|
||||||
|
Url: &url,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "service is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_ServiceWhitespaceOnly(t *testing.T) {
|
||||||
|
url := "http://example.com/graphql"
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: " ",
|
||||||
|
Url: &url,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "service is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_MissingSDL(t *testing.T) {
|
||||||
|
url := "http://example.com/graphql"
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "users",
|
||||||
|
Url: &url,
|
||||||
|
Sdl: "",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "SDL is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_SDLWhitespaceOnly(t *testing.T) {
|
||||||
|
url := "http://example.com/graphql"
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "users",
|
||||||
|
Url: &url,
|
||||||
|
Sdl: " \n\t ",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "SDL is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_MissingURL_NoExistingURL(t *testing.T) {
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "users",
|
||||||
|
Url: nil,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
Url: nil, // No existing URL
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "url is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_MissingURL_HasExistingURL(t *testing.T) {
|
||||||
|
existingURL := "http://example.com/graphql"
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "users",
|
||||||
|
Url: nil,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
Url: &existingURL, // Has existing URL, so nil is OK
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_EmptyURL_NoExistingURL(t *testing.T) {
|
||||||
|
emptyURL := ""
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "users",
|
||||||
|
Url: &emptyURL,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
Url: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "url is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_URLWhitespaceOnly_NoExistingURL(t *testing.T) {
|
||||||
|
whitespaceURL := " "
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "users",
|
||||||
|
Url: &whitespaceURL,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
subGraph := &SubGraph{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("subgraph-123"),
|
||||||
|
Url: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), subGraph)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "url is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Validate_WrongAggregateType(t *testing.T) {
|
||||||
|
url := "http://example.com/graphql"
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "users",
|
||||||
|
Url: &url,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass wrong aggregate type
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "not a SubGraph")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSubGraph_Event(t *testing.T) {
|
||||||
|
url := "http://example.com/graphql"
|
||||||
|
wsURL := "ws://example.com/graphql"
|
||||||
|
cmd := UpdateSubGraph{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "users",
|
||||||
|
Url: &url,
|
||||||
|
WSUrl: &wsURL,
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
event := cmd.Event(context.Background())
|
||||||
|
require.NotNil(t, event)
|
||||||
|
|
||||||
|
subGraphEvent, ok := event.(*SubGraphUpdated)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, "org-123", subGraphEvent.OrganizationId)
|
||||||
|
assert.Equal(t, "main", subGraphEvent.Ref)
|
||||||
|
assert.Equal(t, "users", subGraphEvent.Service)
|
||||||
|
assert.Equal(t, url, *subGraphEvent.Url)
|
||||||
|
assert.Equal(t, wsURL, *subGraphEvent.WSUrl)
|
||||||
|
assert.Equal(t, "type Query { hello: String }", subGraphEvent.Sdl)
|
||||||
|
assert.Equal(t, "user@example.com", subGraphEvent.Initiator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAPIKey tests
|
||||||
|
|
||||||
|
func TestRemoveAPIKey_Validate_Success(t *testing.T) {
|
||||||
|
cmd := RemoveAPIKey{
|
||||||
|
KeyName: "production-key",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
APIKeys: []APIKey{
|
||||||
|
{
|
||||||
|
Name: "production-key",
|
||||||
|
Key: "hashed-key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveAPIKey_Validate_OrganizationNotExists(t *testing.T) {
|
||||||
|
cmd := RemoveAPIKey{
|
||||||
|
KeyName: "production-key",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{} // No identity means it doesn't exist
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveAPIKey_Validate_KeyNotFound(t *testing.T) {
|
||||||
|
cmd := RemoveAPIKey{
|
||||||
|
KeyName: "non-existent-key",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
APIKeys: []APIKey{
|
||||||
|
{
|
||||||
|
Name: "production-key",
|
||||||
|
Key: "hashed-key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "not found")
|
||||||
|
assert.Contains(t, err.Error(), "non-existent-key")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveAPIKey_Event(t *testing.T) {
|
||||||
|
cmd := RemoveAPIKey{
|
||||||
|
KeyName: "production-key",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
event := cmd.Event(context.Background())
|
||||||
|
require.NotNil(t, event)
|
||||||
|
|
||||||
|
keyEvent, ok := event.(*APIKeyRemoved)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, "production-key", keyEvent.KeyName)
|
||||||
|
assert.Equal(t, "user@example.com", keyEvent.Initiator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveOrganization tests
|
||||||
|
|
||||||
|
func TestRemoveOrganization_Validate_Success(t *testing.T) {
|
||||||
|
cmd := RemoveOrganization{
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
Name: "Test Org",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveOrganization_Validate_OrganizationNotExists(t *testing.T) {
|
||||||
|
cmd := RemoveOrganization{
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{} // No identity means it doesn't exist
|
||||||
|
err := cmd.Validate(context.Background(), org)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveOrganization_Event(t *testing.T) {
|
||||||
|
cmd := RemoveOrganization{
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
event := cmd.Event(context.Background())
|
||||||
|
require.NotNil(t, event)
|
||||||
|
|
||||||
|
orgEvent, ok := event.(*OrganizationRemoved)
|
||||||
|
require.True(t, ok)
|
||||||
|
assert.Equal(t, "user@example.com", orgEvent.Initiator)
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,24 @@ func (a *OrganizationAdded) UpdateOrganization(o *Organization) {
|
|||||||
o.ChangedAt = a.When()
|
o.ChangedAt = a.When()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserAddedToOrganization struct {
|
||||||
|
eventsourced.BaseEvent
|
||||||
|
UserId string `json:"userId"`
|
||||||
|
Initiator string `json:"initiator"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *UserAddedToOrganization) UpdateOrganization(o *Organization) {
|
||||||
|
// Check if user is already in the organization
|
||||||
|
for _, user := range o.Users {
|
||||||
|
if user == a.UserId {
|
||||||
|
return // User already exists, no need to add
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o.Users = append(o.Users, a.UserId)
|
||||||
|
o.ChangedBy = a.Initiator
|
||||||
|
o.ChangedAt = a.When()
|
||||||
|
}
|
||||||
|
|
||||||
type APIKeyAdded struct {
|
type APIKeyAdded struct {
|
||||||
eventsourced.BaseEvent
|
eventsourced.BaseEvent
|
||||||
OrganizationId string `json:"organizationId"`
|
OrganizationId string `json:"organizationId"`
|
||||||
@@ -34,6 +52,36 @@ func (a *APIKeyAdded) EnrichFromAggregate(aggregate eventsourced.Aggregate) {
|
|||||||
|
|
||||||
var _ eventsourced.EnrichableEvent = &APIKeyAdded{}
|
var _ eventsourced.EnrichableEvent = &APIKeyAdded{}
|
||||||
|
|
||||||
|
type APIKeyRemoved struct {
|
||||||
|
eventsourced.BaseEvent
|
||||||
|
KeyName string `json:"keyName"`
|
||||||
|
Initiator string `json:"initiator"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIKeyRemoved) UpdateOrganization(o *Organization) {
|
||||||
|
// Remove the API key from the organization
|
||||||
|
for i, key := range o.APIKeys {
|
||||||
|
if key.Name == a.KeyName {
|
||||||
|
o.APIKeys = append(o.APIKeys[:i], o.APIKeys[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o.ChangedBy = a.Initiator
|
||||||
|
o.ChangedAt = a.When()
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrganizationRemoved struct {
|
||||||
|
eventsourced.BaseEvent
|
||||||
|
Initiator string `json:"initiator"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *OrganizationRemoved) UpdateOrganization(o *Organization) {
|
||||||
|
// Mark organization as removed by clearing critical fields
|
||||||
|
// The aggregate will still exist in the event store, but it's logically deleted
|
||||||
|
o.ChangedBy = a.Initiator
|
||||||
|
o.ChangedAt = a.When()
|
||||||
|
}
|
||||||
|
|
||||||
type SubGraphUpdated struct {
|
type SubGraphUpdated struct {
|
||||||
eventsourced.BaseEvent
|
eventsourced.BaseEvent
|
||||||
OrganizationId string `json:"organizationId"`
|
OrganizationId string `json:"organizationId"`
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOrganizationAdded_UpdateOrganization(t *testing.T) {
|
||||||
|
event := &OrganizationAdded{
|
||||||
|
BaseEvent: eventsourced.BaseEvent{
|
||||||
|
EventTime: eventsourced.EventTime{
|
||||||
|
Time: time.Now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Name: "Test Organization",
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
}
|
||||||
|
|
||||||
|
event.UpdateOrganization(org)
|
||||||
|
|
||||||
|
assert.Equal(t, "Test Organization", org.Name)
|
||||||
|
assert.Equal(t, []string{"user@example.com"}, org.Users)
|
||||||
|
assert.Equal(t, "user@example.com", org.CreatedBy)
|
||||||
|
assert.Equal(t, "user@example.com", org.ChangedBy)
|
||||||
|
assert.Equal(t, event.When(), org.CreatedAt)
|
||||||
|
assert.Equal(t, event.When(), org.ChangedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserAddedToOrganization_UpdateOrganization(t *testing.T) {
|
||||||
|
event := &UserAddedToOrganization{
|
||||||
|
BaseEvent: eventsourced.BaseEvent{
|
||||||
|
EventTime: eventsourced.EventTime{
|
||||||
|
Time: time.Now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
UserId: "new-user@example.com",
|
||||||
|
Initiator: "admin@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
Users: []string{"existing-user@example.com"},
|
||||||
|
}
|
||||||
|
|
||||||
|
event.UpdateOrganization(org)
|
||||||
|
|
||||||
|
assert.Len(t, org.Users, 2)
|
||||||
|
assert.Contains(t, org.Users, "existing-user@example.com")
|
||||||
|
assert.Contains(t, org.Users, "new-user@example.com")
|
||||||
|
assert.Equal(t, "admin@example.com", org.ChangedBy)
|
||||||
|
assert.Equal(t, event.When(), org.ChangedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserAddedToOrganization_UpdateOrganization_DuplicateUser(t *testing.T) {
|
||||||
|
event := &UserAddedToOrganization{
|
||||||
|
BaseEvent: eventsourced.BaseEvent{
|
||||||
|
EventTime: eventsourced.EventTime{
|
||||||
|
Time: time.Now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
UserId: "existing-user@example.com",
|
||||||
|
Initiator: "admin@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
Users: []string{"existing-user@example.com"},
|
||||||
|
ChangedBy: "previous-admin@example.com",
|
||||||
|
}
|
||||||
|
originalChangedBy := org.ChangedBy
|
||||||
|
originalChangedAt := org.ChangedAt
|
||||||
|
|
||||||
|
event.UpdateOrganization(org)
|
||||||
|
|
||||||
|
// User should not be added twice
|
||||||
|
assert.Len(t, org.Users, 1)
|
||||||
|
assert.Equal(t, "existing-user@example.com", org.Users[0])
|
||||||
|
|
||||||
|
// ChangedBy and ChangedAt should NOT be updated when user already exists (idempotent)
|
||||||
|
assert.Equal(t, originalChangedBy, org.ChangedBy)
|
||||||
|
assert.Equal(t, originalChangedAt, org.ChangedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIKeyRemoved_UpdateOrganization(t *testing.T) {
|
||||||
|
event := &APIKeyRemoved{
|
||||||
|
BaseEvent: eventsourced.BaseEvent{
|
||||||
|
EventTime: eventsourced.EventTime{
|
||||||
|
Time: time.Now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
KeyName: "production-key",
|
||||||
|
Initiator: "admin@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
APIKeys: []APIKey{
|
||||||
|
{Name: "dev-key", Key: "hashed-key-1"},
|
||||||
|
{Name: "production-key", Key: "hashed-key-2"},
|
||||||
|
{Name: "staging-key", Key: "hashed-key-3"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
event.UpdateOrganization(org)
|
||||||
|
|
||||||
|
assert.Len(t, org.APIKeys, 2)
|
||||||
|
assert.Equal(t, "dev-key", org.APIKeys[0].Name)
|
||||||
|
assert.Equal(t, "staging-key", org.APIKeys[1].Name)
|
||||||
|
assert.Equal(t, "admin@example.com", org.ChangedBy)
|
||||||
|
assert.Equal(t, event.When(), org.ChangedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIKeyRemoved_UpdateOrganization_KeyNotFound(t *testing.T) {
|
||||||
|
event := &APIKeyRemoved{
|
||||||
|
BaseEvent: eventsourced.BaseEvent{
|
||||||
|
EventTime: eventsourced.EventTime{
|
||||||
|
Time: time.Now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
KeyName: "non-existent-key",
|
||||||
|
Initiator: "admin@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
APIKeys: []APIKey{
|
||||||
|
{Name: "dev-key", Key: "hashed-key-1"},
|
||||||
|
{Name: "production-key", Key: "hashed-key-2"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
event.UpdateOrganization(org)
|
||||||
|
|
||||||
|
// No keys should be removed
|
||||||
|
assert.Len(t, org.APIKeys, 2)
|
||||||
|
assert.Equal(t, "dev-key", org.APIKeys[0].Name)
|
||||||
|
assert.Equal(t, "production-key", org.APIKeys[1].Name)
|
||||||
|
|
||||||
|
// But metadata should still be updated
|
||||||
|
assert.Equal(t, "admin@example.com", org.ChangedBy)
|
||||||
|
assert.Equal(t, event.When(), org.ChangedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIKeyRemoved_UpdateOrganization_OnlyKey(t *testing.T) {
|
||||||
|
event := &APIKeyRemoved{
|
||||||
|
BaseEvent: eventsourced.BaseEvent{
|
||||||
|
EventTime: eventsourced.EventTime{
|
||||||
|
Time: time.Now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
KeyName: "only-key",
|
||||||
|
Initiator: "admin@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
APIKeys: []APIKey{
|
||||||
|
{Name: "only-key", Key: "hashed-key"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
event.UpdateOrganization(org)
|
||||||
|
|
||||||
|
// All keys should be removed
|
||||||
|
assert.Len(t, org.APIKeys, 0)
|
||||||
|
assert.Equal(t, "admin@example.com", org.ChangedBy)
|
||||||
|
assert.Equal(t, event.When(), org.ChangedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOrganizationRemoved_UpdateOrganization(t *testing.T) {
|
||||||
|
event := &OrganizationRemoved{
|
||||||
|
BaseEvent: eventsourced.BaseEvent{
|
||||||
|
EventTime: eventsourced.EventTime{
|
||||||
|
Time: time.Now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Initiator: "admin@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString("org-123"),
|
||||||
|
Name: "Test Organization",
|
||||||
|
Users: []string{"user1@example.com", "user2@example.com"},
|
||||||
|
APIKeys: []APIKey{
|
||||||
|
{Name: "key1", Key: "hashed-key-1"},
|
||||||
|
},
|
||||||
|
CreatedBy: "creator@example.com",
|
||||||
|
CreatedAt: time.Now().Add(-24 * time.Hour),
|
||||||
|
}
|
||||||
|
|
||||||
|
event.UpdateOrganization(org)
|
||||||
|
|
||||||
|
// Organization data remains (soft delete), but metadata is updated
|
||||||
|
assert.Equal(t, "Test Organization", org.Name)
|
||||||
|
assert.Len(t, org.Users, 2)
|
||||||
|
assert.Len(t, org.APIKeys, 1)
|
||||||
|
|
||||||
|
// Metadata should be updated to reflect removal
|
||||||
|
assert.Equal(t, "admin@example.com", org.ChangedBy)
|
||||||
|
assert.Equal(t, event.When(), org.ChangedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIKeyAdded_EnrichFromAggregate(t *testing.T) {
|
||||||
|
orgId := "org-123"
|
||||||
|
aggregate := &Organization{
|
||||||
|
BaseAggregate: eventsourced.BaseAggregateFromString(orgId),
|
||||||
|
}
|
||||||
|
|
||||||
|
event := &APIKeyAdded{
|
||||||
|
Name: "test-key",
|
||||||
|
Key: "hashed-key",
|
||||||
|
Refs: []string{"main"},
|
||||||
|
Read: true,
|
||||||
|
Publish: false,
|
||||||
|
Initiator: "user@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
event.EnrichFromAggregate(aggregate)
|
||||||
|
|
||||||
|
assert.Equal(t, orgId, event.OrganizationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSubGraphUpdated_Event(t *testing.T) {
|
||||||
|
// Verify SubGraphUpdated event structure
|
||||||
|
url := "http://service.example.com"
|
||||||
|
wsUrl := "ws://service.example.com"
|
||||||
|
|
||||||
|
event := &SubGraphUpdated{
|
||||||
|
OrganizationId: "org-123",
|
||||||
|
Ref: "main",
|
||||||
|
Service: "users-service",
|
||||||
|
Url: &url,
|
||||||
|
WSUrl: &wsUrl,
|
||||||
|
Sdl: "type Query { user: User }",
|
||||||
|
Initiator: "system",
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NotNil(t, event)
|
||||||
|
assert.Equal(t, "org-123", event.OrganizationId)
|
||||||
|
assert.Equal(t, "main", event.Ref)
|
||||||
|
assert.Equal(t, "users-service", event.Service)
|
||||||
|
assert.Equal(t, url, *event.Url)
|
||||||
|
assert.Equal(t, wsUrl, *event.WSUrl)
|
||||||
|
assert.Equal(t, "type Query { user: User }", event.Sdl)
|
||||||
|
assert.Equal(t, "system", event.Initiator)
|
||||||
|
}
|
||||||
@@ -1,38 +1,37 @@
|
|||||||
module gitlab.com/unboundsoftware/schemas
|
module gitea.unbound.se/unboundsoftware/schemas
|
||||||
|
|
||||||
go 1.25
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/99designs/gqlgen v0.17.83
|
github.com/99designs/gqlgen v0.17.89
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||||
github.com/Khan/genqlient v0.8.1
|
github.com/Khan/genqlient v0.8.1
|
||||||
github.com/alecthomas/kong v1.13.0
|
github.com/alecthomas/kong v1.15.0
|
||||||
github.com/apex/log v1.9.0
|
github.com/apex/log v1.9.0
|
||||||
github.com/auth0/go-jwt-middleware/v2 v2.3.1
|
github.com/auth0/go-jwt-middleware/v3 v3.0.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jmoiron/sqlx v1.4.0
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pressly/goose/v3 v3.27.0
|
||||||
github.com/pressly/goose/v3 v3.26.0
|
|
||||||
github.com/rs/cors v1.11.1
|
github.com/rs/cors v1.11.1
|
||||||
github.com/sparetimecoders/goamqp v0.3.3
|
github.com/sparetimecoders/goamqp v0.3.3
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/vektah/gqlparser/v2 v2.5.31
|
github.com/vektah/gqlparser/v2 v2.5.32
|
||||||
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.238
|
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.267
|
||||||
gitlab.com/unboundsoftware/eventsourced/amqp v1.9.0
|
gitlab.com/unboundsoftware/eventsourced/amqp v1.9.1
|
||||||
gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.3
|
gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.4
|
||||||
gitlab.com/unboundsoftware/eventsourced/pg v1.17.0
|
gitlab.com/unboundsoftware/eventsourced/pg v1.18.6
|
||||||
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0
|
go.opentelemetry.io/contrib/bridges/otelslog v0.17.0
|
||||||
go.opentelemetry.io/otel v1.38.0
|
go.opentelemetry.io/otel v1.43.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0
|
||||||
go.opentelemetry.io/otel/log v0.14.0
|
go.opentelemetry.io/otel/log v0.19.0
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0
|
go.opentelemetry.io/otel/sdk v1.43.0
|
||||||
go.opentelemetry.io/otel/sdk/log v0.14.0
|
go.opentelemetry.io/otel/sdk/log v0.19.0
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0
|
go.opentelemetry.io/otel/sdk/metric v1.43.0
|
||||||
go.opentelemetry.io/otel/trace v1.38.0
|
go.opentelemetry.io/otel/trace v1.43.0
|
||||||
golang.org/x/crypto v0.45.0
|
golang.org/x/crypto v0.49.0
|
||||||
|
golang.org/x/sync v0.20.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,38 +41,51 @@ require (
|
|||||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
github.com/gorilla/websocket v1.5.1 // indirect
|
github.com/gorilla/websocket v1.5.1 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
github.com/lib/pq v1.10.9 // indirect
|
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
||||||
|
github.com/lestrrat-go/dsig v1.0.0 // indirect
|
||||||
|
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
|
||||||
|
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||||
|
github.com/lestrrat-go/httprc/v3 v3.0.3 // indirect
|
||||||
|
github.com/lestrrat-go/jwx/v3 v3.0.12 // indirect
|
||||||
|
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
|
||||||
|
github.com/lib/pq v1.12.1 // indirect
|
||||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rabbitmq/amqp091-go v1.10.0 // indirect
|
github.com/rabbitmq/amqp091-go v1.10.0 // indirect
|
||||||
|
github.com/segmentio/asm v1.2.1 // indirect
|
||||||
github.com/sethvargo/go-retry v0.3.0 // indirect
|
github.com/sethvargo/go-retry v0.3.0 // indirect
|
||||||
github.com/sosodev/duration v1.3.1 // indirect
|
github.com/sosodev/duration v1.4.0 // indirect
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
github.com/tidwall/gjson v1.17.0 // indirect
|
github.com/tidwall/gjson v1.17.0 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/tidwall/sjson v1.2.5 // indirect
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
github.com/urfave/cli/v3 v3.6.0 // indirect
|
github.com/urfave/cli/v3 v3.7.0 // indirect
|
||||||
github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 // indirect
|
github.com/valyala/fastjson v1.6.7 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
github.com/wundergraph/astjson v1.1.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
github.com/wundergraph/go-arena v1.1.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/mod v0.29.0 // indirect
|
golang.org/x/mod v0.33.0 // indirect
|
||||||
golang.org/x/net v0.47.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
golang.org/x/sync v0.18.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
golang.org/x/text v0.31.0 // indirect
|
golang.org/x/tools v0.42.0 // indirect
|
||||||
golang.org/x/tools v0.38.0 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
|
google.golang.org/grpc v1.80.0 // indirect
|
||||||
google.golang.org/grpc v1.75.0 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/99designs/gqlgen v0.17.83 h1:LZOd4Of2snK5V22/ZWfBAPa3WoAZkBO70dKXM0ODHQk=
|
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||||
github.com/99designs/gqlgen v0.17.83/go.mod h1:q6Lb64wknFqNFSbSUGzKRKupklvY/xgNr62g0GGWPB8=
|
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||||
|
github.com/99designs/gqlgen v0.17.89 h1:KzEcxPiMgQoMw3m/E85atUEHyZyt0PbAflMia5Kw8z8=
|
||||||
|
github.com/99designs/gqlgen v0.17.89/go.mod h1:GFqruTVGB7ZTdrf1uzOagpXbY7DrEt1pIxnTdhIbWvQ=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
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/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs=
|
github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs=
|
||||||
github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU=
|
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.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
|
||||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
|
||||||
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
|
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/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 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
github.com/alecthomas/kong v1.13.0 h1:5e/7XC3ugvhP1DQBmTS+WuHtCbcv44hsohMgcvVxSrA=
|
github.com/alecthomas/kong v1.15.0 h1:BVJstKbpO73zKpmIu+m/aLRrNmWwxXPIGTNin9VmLVI=
|
||||||
github.com/alecthomas/kong v1.13.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
|
github.com/alecthomas/kong v1.15.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
|
||||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
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/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 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||||
@@ -27,8 +28,8 @@ 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/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 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||||
github.com/auth0/go-jwt-middleware/v2 v2.3.1 h1:lbDyWE9aLydb3zrank+Gufb9qGJN9u//7EbJK07pRrw=
|
github.com/auth0/go-jwt-middleware/v3 v3.0.0 h1:+rvUPCT+VbAuK4tpS13fWfZrMyqTwLopt3VoY0Y7kvA=
|
||||||
github.com/auth0/go-jwt-middleware/v2 v2.3.1/go.mod h1:mqVr0gdB5zuaFyQFWMJH/c/2hehNjbYUD4i8Dpyf+Hc=
|
github.com/auth0/go-jwt-middleware/v3 v3.0.0/go.mod h1:iU42jqjRyeKbf9YYSnRnolr836gk6Ty/jnUNuVq2b0o=
|
||||||
github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
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/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 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||||
@@ -40,6 +41,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||||
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
|
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
|
||||||
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
@@ -55,10 +58,12 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
|
|||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
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-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.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
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.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 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
@@ -70,8 +75,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/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 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
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.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
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/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
@@ -92,8 +97,23 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
|
||||||
|
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
|
||||||
|
github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
|
||||||
|
github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
|
||||||
|
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
|
||||||
|
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
|
||||||
|
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||||
|
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||||
|
github.com/lestrrat-go/httprc/v3 v3.0.3 h1:WjLHWkDkgWXeIUrKi/7lS/sGq2DjkSAwdTbH5RHXAKs=
|
||||||
|
github.com/lestrrat-go/httprc/v3 v3.0.3/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
|
||||||
|
github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg=
|
||||||
|
github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8=
|
||||||
|
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
|
||||||
|
github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/lib/pq v1.12.1 h1:x1nbl/338GLqeDJ/FAiILallhAsqubLzEZu/pXtHUow=
|
||||||
|
github.com/lib/pq v1.12.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
@@ -105,8 +125,8 @@ github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxU
|
|||||||
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
||||||
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
||||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
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/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
@@ -114,8 +134,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM=
|
github.com/pressly/goose/v3 v3.27.0 h1:/D30gVTuQhu0WsNZYbJi4DMOsx1lNq+6SkLe+Wp59BM=
|
||||||
github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
|
github.com/pressly/goose/v3 v3.27.0/go.mod h1:3ZBeCXqzkgIRvrEMDkYh1guvtoJTU5oMMuDdkutoM78=
|
||||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
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/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
@@ -129,6 +149,8 @@ github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rY
|
|||||||
github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
|
github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
|
||||||
github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=
|
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/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||||
|
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||||
|
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
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 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||||
@@ -137,8 +159,8 @@ github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLy
|
|||||||
github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
|
github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
|
||||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
|
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
|
||||||
github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=
|
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.4.0 h1:35ed0KiVFriGHHzZZJaZLgmTEEICIyt8Sx0RQfj9IjE=
|
||||||
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
github.com/sosodev/duration v1.4.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
|
||||||
github.com/sparetimecoders/goamqp v0.3.3 h1:z/nfTPmrjeU/rIVuNOgsVLCimp3WFoNFvS3ZzXRJ6HE=
|
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/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.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
@@ -146,6 +168,7 @@ 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/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.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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
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/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.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
@@ -165,100 +188,102 @@ 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-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-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
|
||||||
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
|
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
|
||||||
github.com/urfave/cli/v3 v3.6.0 h1:oIdArVjkdIXHWg3iqxgmqwQGC8NM0JtdgwQAj2sRwFo=
|
github.com/urfave/cli/v3 v3.7.0 h1:AGSnbUyjtLiM+WJUb4dzXKldl/gL+F8OwmRDtVr6g2U=
|
||||||
github.com/urfave/cli/v3 v3.6.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
github.com/urfave/cli/v3 v3.7.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||||
github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k=
|
github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM=
|
||||||
github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
|
github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||||
github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTBjW+SZK4mhxTTBVpxcqeBgWF1Rfmltbfk=
|
github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc=
|
||||||
github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE=
|
github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
|
||||||
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.238 h1:ll0BtYVMziRa8v0T/f+DQOJ/1x3Dq5puifJNnxF0R+M=
|
github.com/wundergraph/astjson v1.1.0 h1:xORDosrZ87zQFJwNGe/HIHXqzpdHOFmqWgykCLVL040=
|
||||||
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.238/go.mod h1:ErOQH1ki2+SZB8JjpTyGVnoBpg5picIyjvuWQJP4abg=
|
github.com/wundergraph/astjson v1.1.0/go.mod h1:h12D/dxxnedtLzsKyBLK7/Oe4TAoGpRVC9nDpDrZSWw=
|
||||||
gitlab.com/unboundsoftware/eventsourced/amqp v1.9.0 h1:TdBJnrnrxJrPhC4i6KTFUElZa3k/fFXiGwg0sds5aAo=
|
github.com/wundergraph/go-arena v1.1.0 h1:9+wSRkJAkA2vbYHp6s8tEGhPViRGQNGXqPHT0QzhdIc=
|
||||||
gitlab.com/unboundsoftware/eventsourced/amqp v1.9.0/go.mod h1:VauAph7uCvEakYNdHkkSAoOOGKvEuUA/uhsR376ThbI=
|
github.com/wundergraph/go-arena v1.1.0/go.mod h1:ROOysEHWJjLQ8FSfNxZCziagb7Qw2nXY3/vgKRh7eWw=
|
||||||
gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.3 h1:0HbDHF4sHfoyDrbPLMFWvsQLbTl2ITrpI9PjDIZsV1Y=
|
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.267 h1:qMkYR0oq0Cw61aDZs9VsCCVwNVSxRxT13ytz6WqCwJg=
|
||||||
gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.3/go.mod h1:LrA7I7etRmhIC1PjO8c26BHm+gWsy2rC3eSMe5+XUWE=
|
github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.267/go.mod h1:HjTAO/cuICpu31IfHY9qmSPygx6Gza7Wt9hTSReTI+A=
|
||||||
gitlab.com/unboundsoftware/eventsourced/pg v1.17.0 h1:pUJzMpNPX0GVsffRZXlpKR1d7Ws96KTxJwbLFPpASSc=
|
gitlab.com/unboundsoftware/eventsourced/amqp v1.9.1 h1:X6269JoAzHIKCVmtgMHZH3m7xOpACSp37ca3eODe9iU=
|
||||||
gitlab.com/unboundsoftware/eventsourced/pg v1.17.0/go.mod h1:WgPrZhyCbsZ3TG2tPUbh2MUjOEaANJjsWi/0hlIwRVU=
|
gitlab.com/unboundsoftware/eventsourced/amqp v1.9.1/go.mod h1:EAs0d6Eh0aDiQkUJlSWErHqgHFQdxx0e8I7aG/2FarY=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.4 h1:+yZkhi9/sTyBEN5vJTfvycyXgGrm07QKGSh3jiWiQdM=
|
||||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
gitlab.com/unboundsoftware/eventsourced/eventsourced v1.19.4/go.mod h1:LrA7I7etRmhIC1PjO8c26BHm+gWsy2rC3eSMe5+XUWE=
|
||||||
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0 h1:bwnLpizECbPr1RrQ27waeY2SPIPeccCx/xLuoYADZ9s=
|
gitlab.com/unboundsoftware/eventsourced/pg v1.18.6 h1:UMnuwC2lUxABE6k28Zl3WTTiVhWjsNQHnWFEZ0Krzw0=
|
||||||
go.opentelemetry.io/contrib/bridges/otelslog v0.13.0/go.mod h1:3nWlOiiqA9UtUnrcNk82mYasNxD8ehOspL0gOfEo6Y4=
|
gitlab.com/unboundsoftware/eventsourced/pg v1.18.6/go.mod h1:Lzb0Q3xV6MW63rukaa1R40VFgTAAFYmXIm2tPrYXEQY=
|
||||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk=
|
go.opentelemetry.io/contrib/bridges/otelslog v0.17.0 h1:NFIS6x7wyObQ7cR84x7bt1sr8nYBx89s3x3GwRjw40k=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg=
|
go.opentelemetry.io/contrib/bridges/otelslog v0.17.0/go.mod h1:39SaByOyDMRMe872AE7uelMuQZidIw7LLFAnQi0FWTE=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0 h1:B/g+qde6Mkzxbry5ZZag0l7QrQBCtVm7lVjaLgmpje8=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.14.0/go.mod h1:mOJK8eMmgW6ocDJn6Bn11CcZ05gi3P8GylBXEkZtbgA=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||||
go.opentelemetry.io/otel/log v0.14.0 h1:2rzJ+pOAZ8qmZ3DDHg73NEKzSZkhkGIua9gXtxNGgrM=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
|
||||||
go.opentelemetry.io/otel/log v0.14.0/go.mod h1:5jRG92fEAgx0SU/vFPxmJvhIuDU9E1SUnEQrMlJpOno=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
|
||||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 h1:GJkybS+crDMdExT/BUNCEgfrmfboztcS6PhvSo88HKM=
|
||||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0/go.mod h1:NuAyxRYIG2lKX3YQkB+83StTxM7s52PUUkRRiC0wnYI=
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4=
|
||||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk=
|
||||||
go.opentelemetry.io/otel/sdk/log v0.14.0 h1:JU/U3O7N6fsAXj0+CXz21Czg532dW2V4gG1HE/e8Zrg=
|
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||||
go.opentelemetry.io/otel/sdk/log v0.14.0/go.mod h1:imQvII+0ZylXfKU7/wtOND8Hn4OpT3YUoIgqJVksUkM=
|
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||||
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0 h1:Ijbtz+JKXl8T2MngiwqBlPaHqc4YCaP/i13Qrow6gAM=
|
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||||
go.opentelemetry.io/otel/sdk/log/logtest v0.14.0/go.mod h1:dCU8aEL6q+L9cYTqcVOk8rM9Tp8WdnHOPLiBgp0SGOA=
|
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk=
|
||||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk=
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
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/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 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
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-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.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
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-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.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
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-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-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.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
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.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs=
|
|
||||||
gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI=
|
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
@@ -266,11 +291,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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
|
modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ=
|
||||||
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
|
modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
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.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
||||||
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
|
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
package graph
|
package graph
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gitlab.com/unboundsoftware/schemas/domain"
|
"gitea.unbound.se/unboundsoftware/schemas/domain"
|
||||||
"gitlab.com/unboundsoftware/schemas/graph/model"
|
"gitea.unbound.se/unboundsoftware/schemas/graph/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ToGqlOrganizations(orgs []domain.Organization) []*model.Organization {
|
func ToGqlOrganizations(orgs []domain.Organization) []*model.Organization {
|
||||||
|
|||||||
+37
-1
@@ -1,14 +1,17 @@
|
|||||||
package graph
|
package graph
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/sync/semaphore"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/graph/model"
|
"gitea.unbound.se/unboundsoftware/schemas/graph/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CommandExecutor is an interface for executing external commands
|
// CommandExecutor is an interface for executing external commands
|
||||||
@@ -123,3 +126,36 @@ func GenerateCosmoRouterConfigWithExecutor(subGraphs []*model.SubGraph, executor
|
|||||||
|
|
||||||
return string(configJSON), nil
|
return string(configJSON), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CosmoGenerator wraps config generation with a concurrency limit and timeout
|
||||||
|
// to prevent unbounded wgc process spawning under rapid schema updates.
|
||||||
|
type CosmoGenerator struct {
|
||||||
|
sem *semaphore.Weighted
|
||||||
|
executor CommandExecutor
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCosmoGenerator creates a CosmoGenerator that allows at most one concurrent
|
||||||
|
// wgc process and applies the given timeout to each generation attempt.
|
||||||
|
func NewCosmoGenerator(executor CommandExecutor, timeout time.Duration) *CosmoGenerator {
|
||||||
|
return &CosmoGenerator{
|
||||||
|
sem: semaphore.NewWeighted(1),
|
||||||
|
executor: executor,
|
||||||
|
timeout: timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate produces a Cosmo Router config, blocking if another generation is
|
||||||
|
// already in progress. The provided context (plus the configured timeout)
|
||||||
|
// controls cancellation.
|
||||||
|
func (g *CosmoGenerator) Generate(ctx context.Context, subGraphs []*model.SubGraph) (string, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, g.timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := g.sem.Acquire(ctx, 1); err != nil {
|
||||||
|
return "", fmt.Errorf("acquire cosmo generator: %w", err)
|
||||||
|
}
|
||||||
|
defer g.sem.Release(1)
|
||||||
|
|
||||||
|
return GenerateCosmoRouterConfigWithExecutor(subGraphs, g.executor)
|
||||||
|
}
|
||||||
|
|||||||
+113
-1
@@ -1,17 +1,21 @@
|
|||||||
package graph
|
package graph
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/graph/model"
|
"gitea.unbound.se/unboundsoftware/schemas/graph/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockCommandExecutor implements CommandExecutor for testing
|
// MockCommandExecutor implements CommandExecutor for testing
|
||||||
@@ -459,6 +463,114 @@ func TestGenerateCosmoRouterConfig_MockError(t *testing.T) {
|
|||||||
assert.Equal(t, 1, mockExecutor.CallCount, "Should have attempted to call executor")
|
assert.Equal(t, 1, mockExecutor.CallCount, "Should have attempted to call executor")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SlowMockExecutor simulates a slow wgc command for concurrency testing.
|
||||||
|
type SlowMockExecutor struct {
|
||||||
|
MockCommandExecutor
|
||||||
|
delay time.Duration
|
||||||
|
mu sync.Mutex
|
||||||
|
concurrent atomic.Int32
|
||||||
|
maxSeen atomic.Int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SlowMockExecutor) Execute(name string, args ...string) ([]byte, error) {
|
||||||
|
cur := m.concurrent.Add(1)
|
||||||
|
// Track the maximum concurrent executions observed.
|
||||||
|
for {
|
||||||
|
old := m.maxSeen.Load()
|
||||||
|
if cur <= old || m.maxSeen.CompareAndSwap(old, cur) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer m.concurrent.Add(-1)
|
||||||
|
|
||||||
|
time.Sleep(m.delay)
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
return m.MockCommandExecutor.Execute(name, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCosmoGenerator_ConcurrencyLimit(t *testing.T) {
|
||||||
|
executor := &SlowMockExecutor{delay: 100 * time.Millisecond}
|
||||||
|
gen := NewCosmoGenerator(executor, 5*time.Second)
|
||||||
|
|
||||||
|
subGraphs := []*model.SubGraph{
|
||||||
|
{
|
||||||
|
Service: "svc",
|
||||||
|
URL: stringPtr("http://localhost:4001/query"),
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for range 5 {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
_, _ = gen.Generate(context.Background(), subGraphs)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
assert.Equal(t, int32(1), executor.maxSeen.Load(),
|
||||||
|
"at most 1 wgc process should run concurrently")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCosmoGenerator_Timeout(t *testing.T) {
|
||||||
|
// Executor that takes longer than the timeout.
|
||||||
|
executor := &SlowMockExecutor{delay: 500 * time.Millisecond}
|
||||||
|
gen := NewCosmoGenerator(executor, 50*time.Millisecond)
|
||||||
|
|
||||||
|
subGraphs := []*model.SubGraph{
|
||||||
|
{
|
||||||
|
Service: "svc",
|
||||||
|
URL: stringPtr("http://localhost:4001/query"),
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// First call: occupies the semaphore for 500ms.
|
||||||
|
go func() {
|
||||||
|
_, _ = gen.Generate(context.Background(), subGraphs)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Give the first goroutine time to acquire the semaphore.
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
|
||||||
|
// Second call: should timeout waiting for the semaphore.
|
||||||
|
_, err := gen.Generate(context.Background(), subGraphs)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "acquire cosmo generator")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCosmoGenerator_ContextCancellation(t *testing.T) {
|
||||||
|
executor := &SlowMockExecutor{delay: 500 * time.Millisecond}
|
||||||
|
gen := NewCosmoGenerator(executor, 5*time.Second)
|
||||||
|
|
||||||
|
subGraphs := []*model.SubGraph{
|
||||||
|
{
|
||||||
|
Service: "svc",
|
||||||
|
URL: stringPtr("http://localhost:4001/query"),
|
||||||
|
Sdl: "type Query { hello: String }",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// First call: occupies the semaphore.
|
||||||
|
go func() {
|
||||||
|
_, _ = gen.Generate(context.Background(), subGraphs)
|
||||||
|
}()
|
||||||
|
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
|
||||||
|
// Second call with an already-cancelled context.
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
_, err := gen.Generate(ctx, subGraphs)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "acquire cosmo generator")
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function for tests
|
// Helper function for tests
|
||||||
func stringPtr(s string) *string {
|
func stringPtr(s string) *string {
|
||||||
return &s
|
return &s
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package graph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Debouncer coalesces rapid calls with the same key, executing only the last
|
||||||
|
// one after a configurable delay. This prevents redundant work when multiple
|
||||||
|
// updates arrive in quick succession (e.g., rapid schema publishing).
|
||||||
|
type Debouncer struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
delay time.Duration
|
||||||
|
timers map[string]*time.Timer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDebouncer creates a Debouncer with the given delay window.
|
||||||
|
func NewDebouncer(delay time.Duration) *Debouncer {
|
||||||
|
return &Debouncer{
|
||||||
|
delay: delay,
|
||||||
|
timers: make(map[string]*time.Timer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce resets the timer for key. When the timer fires (after delay with no
|
||||||
|
// new calls for the same key), fn is executed in a new goroutine.
|
||||||
|
func (d *Debouncer) Debounce(key string, fn func()) {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
if t, ok := d.timers[key]; ok {
|
||||||
|
t.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
d.timers[key] = time.AfterFunc(d.delay, func() {
|
||||||
|
d.mu.Lock()
|
||||||
|
delete(d.timers, key)
|
||||||
|
d.mu.Unlock()
|
||||||
|
|
||||||
|
fn()
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package graph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"testing/synctest"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDebouncer_Coalesces(t *testing.T) {
|
||||||
|
synctest.Test(t, func(t *testing.T) {
|
||||||
|
d := NewDebouncer(50 * time.Millisecond)
|
||||||
|
var calls atomic.Int32
|
||||||
|
|
||||||
|
// Fire 10 rapid calls for the same key — only the last should execute.
|
||||||
|
for range 10 {
|
||||||
|
d.Debounce("key1", func() {
|
||||||
|
calls.Add(1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance fake clock past the debounce delay and let goroutines settle.
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
synctest.Wait()
|
||||||
|
|
||||||
|
assert.Equal(t, int32(1), calls.Load(), "rapid calls should coalesce into a single execution")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDebouncer_DifferentKeys(t *testing.T) {
|
||||||
|
synctest.Test(t, func(t *testing.T) {
|
||||||
|
d := NewDebouncer(50 * time.Millisecond)
|
||||||
|
var calls atomic.Int32
|
||||||
|
|
||||||
|
d.Debounce("key-a", func() { calls.Add(1) })
|
||||||
|
d.Debounce("key-b", func() { calls.Add(1) })
|
||||||
|
d.Debounce("key-c", func() { calls.Add(1) })
|
||||||
|
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
synctest.Wait()
|
||||||
|
|
||||||
|
assert.Equal(t, int32(3), calls.Load(), "different keys should fire independently")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDebouncer_TimerReset(t *testing.T) {
|
||||||
|
synctest.Test(t, func(t *testing.T) {
|
||||||
|
d := NewDebouncer(100 * time.Millisecond)
|
||||||
|
var value atomic.Int32
|
||||||
|
|
||||||
|
// First call sets value to 1.
|
||||||
|
d.Debounce("key", func() { value.Store(1) })
|
||||||
|
|
||||||
|
// Advance 60ms (less than the 100ms delay) — first timer hasn't fired.
|
||||||
|
time.Sleep(60 * time.Millisecond)
|
||||||
|
|
||||||
|
// Replace with value 2 — resets the timer to fire at 60+100 = 160ms.
|
||||||
|
d.Debounce("key", func() { value.Store(2) })
|
||||||
|
|
||||||
|
// Advance another 100ms (total 160ms) to fire the reset timer.
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
synctest.Wait()
|
||||||
|
|
||||||
|
require.Equal(t, int32(2), value.Load(), "later call should replace the earlier one")
|
||||||
|
})
|
||||||
|
}
|
||||||
+679
-641
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -3,7 +3,7 @@ package graph
|
|||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/graph/model"
|
"gitea.unbound.se/unboundsoftware/schemas/graph/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PubSub handles publishing schema updates to subscribers
|
// PubSub handles publishing schema updates to subscribers
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/graph/model"
|
"gitea.unbound.se/unboundsoftware/schemas/graph/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPubSub_SubscribeAndPublish(t *testing.T) {
|
func TestPubSub_SubscribeAndPublish(t *testing.T) {
|
||||||
|
|||||||
+10
-8
@@ -7,13 +7,13 @@ import (
|
|||||||
|
|
||||||
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/cache"
|
"gitea.unbound.se/unboundsoftware/schemas/cache"
|
||||||
"gitlab.com/unboundsoftware/schemas/middleware"
|
"gitea.unbound.se/unboundsoftware/schemas/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate go run github.com/99designs/gqlgen
|
//go:generate go run github.com/99designs/gqlgen
|
||||||
//go:generate gofumpt -w .
|
//go:generate gofumpt -w .
|
||||||
//go:generate goimports -w -local gitlab.com/unboundsoftware/schemas .
|
//go:generate goimports -w -local gitea.unbound.se/unboundsoftware/schemas .
|
||||||
|
|
||||||
// This file will not be regenerated automatically.
|
// This file will not be regenerated automatically.
|
||||||
//
|
//
|
||||||
@@ -24,11 +24,13 @@ type Publisher interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Resolver struct {
|
type Resolver struct {
|
||||||
EventStore eventsourced.EventStore
|
EventStore eventsourced.EventStore
|
||||||
Publisher Publisher
|
Publisher Publisher
|
||||||
Logger *slog.Logger
|
Logger *slog.Logger
|
||||||
Cache *cache.Cache
|
Cache *cache.Cache
|
||||||
PubSub *PubSub
|
PubSub *PubSub
|
||||||
|
CosmoGenerator *CosmoGenerator
|
||||||
|
Debouncer *Debouncer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resolver) apiKeyCanAccessRef(ctx context.Context, ref string, publish bool) (string, error) {
|
func (r *Resolver) apiKeyCanAccessRef(ctx context.Context, ref string, publish bool) (string, error) {
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
type Query {
|
type Query {
|
||||||
organizations: [Organization!]! @auth(user: true)
|
organizations: [Organization!]! @auth(user: true)
|
||||||
supergraph(ref: String!, isAfter: String): Supergraph! @auth(organization: true)
|
allOrganizations: [Organization!]! @auth(user: true)
|
||||||
latestSchema(ref: String!): SchemaUpdate! @auth(organization: true)
|
supergraph(ref: String!, isAfter: String): Supergraph! @auth(user: true, organization: true)
|
||||||
|
latestSchema(ref: String!): SchemaUpdate! @auth(user: true, organization: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
addOrganization(name: String!): Organization! @auth(user: true)
|
addOrganization(name: String!): Organization! @auth(user: true)
|
||||||
|
addUserToOrganization(organizationId: ID!, userId: String!): Organization! @auth(user: true)
|
||||||
addAPIKey(input: InputAPIKey): APIKey! @auth(user: true)
|
addAPIKey(input: InputAPIKey): APIKey! @auth(user: true)
|
||||||
|
removeAPIKey(organizationId: ID!, keyName: String!): Organization! @auth(user: true)
|
||||||
|
removeOrganization(organizationId: ID!): Boolean! @auth(user: true)
|
||||||
updateSubGraph(input: InputSubGraph!): SubGraph! @auth(organization: true)
|
updateSubGraph(input: InputSubGraph!): SubGraph! @auth(organization: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
|
|
||||||
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/domain"
|
"gitea.unbound.se/unboundsoftware/schemas/domain"
|
||||||
"gitlab.com/unboundsoftware/schemas/graph/model"
|
"gitea.unbound.se/unboundsoftware/schemas/graph/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *Resolver) fetchSubGraph(ctx context.Context, subGraphId string) (*domain.SubGraph, error) {
|
func (r *Resolver) fetchSubGraph(ctx context.Context, subGraphId string) (*domain.SubGraph, error) {
|
||||||
|
|||||||
+143
-63
@@ -1,6 +1,7 @@
|
|||||||
package graph
|
package graph
|
||||||
|
|
||||||
// This file will be automatically regenerated based on the schema, any resolver implementations
|
// This file will be automatically regenerated based on the schema, any resolver
|
||||||
|
// implementations
|
||||||
// will be copied through when generating and any unknown code will be moved to the end.
|
// will be copied through when generating and any unknown code will be moved to the end.
|
||||||
// Code generated by github.com/99designs/gqlgen
|
// Code generated by github.com/99designs/gqlgen
|
||||||
|
|
||||||
@@ -11,12 +12,12 @@ import (
|
|||||||
|
|
||||||
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/domain"
|
"gitea.unbound.se/unboundsoftware/schemas/domain"
|
||||||
"gitlab.com/unboundsoftware/schemas/graph/generated"
|
"gitea.unbound.se/unboundsoftware/schemas/graph/generated"
|
||||||
"gitlab.com/unboundsoftware/schemas/graph/model"
|
"gitea.unbound.se/unboundsoftware/schemas/graph/model"
|
||||||
"gitlab.com/unboundsoftware/schemas/middleware"
|
"gitea.unbound.se/unboundsoftware/schemas/middleware"
|
||||||
"gitlab.com/unboundsoftware/schemas/rand"
|
"gitea.unbound.se/unboundsoftware/schemas/rand"
|
||||||
"gitlab.com/unboundsoftware/schemas/sdlmerge"
|
"gitea.unbound.se/unboundsoftware/schemas/sdlmerge"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddOrganization is the resolver for the addOrganization field.
|
// AddOrganization is the resolver for the addOrganization field.
|
||||||
@@ -37,6 +38,24 @@ func (r *mutationResolver) AddOrganization(ctx context.Context, name string) (*m
|
|||||||
return ToGqlOrganization(*org), nil
|
return ToGqlOrganization(*org), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddUserToOrganization is the resolver for the addUserToOrganization field.
|
||||||
|
func (r *mutationResolver) AddUserToOrganization(ctx context.Context, organizationID string, userID string) (*model.Organization, error) {
|
||||||
|
sub := middleware.UserFromContext(ctx)
|
||||||
|
org := &domain.Organization{BaseAggregate: eventsourced.BaseAggregateFromString(organizationID)}
|
||||||
|
h, err := r.handler(ctx, org)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = h.Handle(ctx, &domain.AddUserToOrganization{
|
||||||
|
UserId: userID,
|
||||||
|
Initiator: sub,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ToGqlOrganization(*org), nil
|
||||||
|
}
|
||||||
|
|
||||||
// AddAPIKey is the resolver for the addAPIKey field.
|
// AddAPIKey is the resolver for the addAPIKey field.
|
||||||
func (r *mutationResolver) AddAPIKey(ctx context.Context, input *model.InputAPIKey) (*model.APIKey, error) {
|
func (r *mutationResolver) AddAPIKey(ctx context.Context, input *model.InputAPIKey) (*model.APIKey, error) {
|
||||||
sub := middleware.UserFromContext(ctx)
|
sub := middleware.UserFromContext(ctx)
|
||||||
@@ -71,6 +90,41 @@ func (r *mutationResolver) AddAPIKey(ctx context.Context, input *model.InputAPIK
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveAPIKey is the resolver for the removeAPIKey field.
|
||||||
|
func (r *mutationResolver) RemoveAPIKey(ctx context.Context, organizationID string, keyName string) (*model.Organization, error) {
|
||||||
|
sub := middleware.UserFromContext(ctx)
|
||||||
|
org := &domain.Organization{BaseAggregate: eventsourced.BaseAggregateFromString(organizationID)}
|
||||||
|
h, err := r.handler(ctx, org)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = h.Handle(ctx, &domain.RemoveAPIKey{
|
||||||
|
KeyName: keyName,
|
||||||
|
Initiator: sub,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ToGqlOrganization(*org), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveOrganization is the resolver for the removeOrganization field.
|
||||||
|
func (r *mutationResolver) RemoveOrganization(ctx context.Context, organizationID string) (bool, error) {
|
||||||
|
sub := middleware.UserFromContext(ctx)
|
||||||
|
org := &domain.Organization{BaseAggregate: eventsourced.BaseAggregateFromString(organizationID)}
|
||||||
|
h, err := r.handler(ctx, org)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
_, err = h.Handle(ctx, &domain.RemoveOrganization{
|
||||||
|
Initiator: sub,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateSubGraph is the resolver for the updateSubGraph field.
|
// UpdateSubGraph is the resolver for the updateSubGraph field.
|
||||||
func (r *mutationResolver) UpdateSubGraph(ctx context.Context, input model.InputSubGraph) (*model.SubGraph, error) {
|
func (r *mutationResolver) UpdateSubGraph(ctx context.Context, input model.InputSubGraph) (*model.SubGraph, error) {
|
||||||
orgId := middleware.OrganizationFromContext(ctx)
|
orgId := middleware.OrganizationFromContext(ctx)
|
||||||
@@ -120,8 +174,9 @@ func (r *mutationResolver) UpdateSubGraph(ctx context.Context, input model.Input
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish schema update to subscribers
|
// Debounce schema update publishing so rapid successive updates for the
|
||||||
go func() {
|
// same org+ref only trigger one config generation.
|
||||||
|
r.Debouncer.Debounce(orgId+":"+input.Ref, func() {
|
||||||
services, lastUpdate := r.Cache.Services(orgId, input.Ref, "")
|
services, lastUpdate := r.Cache.Services(orgId, input.Ref, "")
|
||||||
r.Logger.Info("Publishing schema update after subgraph change",
|
r.Logger.Info("Publishing schema update after subgraph change",
|
||||||
"ref", input.Ref,
|
"ref", input.Ref,
|
||||||
@@ -137,19 +192,11 @@ func (r *mutationResolver) UpdateSubGraph(ctx context.Context, input model.Input
|
|||||||
r.Logger.Error("fetch subgraph for update notification", "error", err)
|
r.Logger.Error("fetch subgraph for update notification", "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
subGraphs[i] = &model.SubGraph{
|
subGraphs[i] = r.toGqlSubGraph(sg)
|
||||||
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
|
// Generate Cosmo router config (concurrency-limited)
|
||||||
cosmoConfig, err := GenerateCosmoRouterConfig(subGraphs)
|
cosmoConfig, err := r.CosmoGenerator.Generate(context.Background(), subGraphs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Logger.Error("generate cosmo config for update", "error", err)
|
r.Logger.Error("generate cosmo config for update", "error", err)
|
||||||
cosmoConfig = "" // Send empty if generation fails
|
cosmoConfig = "" // Send empty if generation fails
|
||||||
@@ -171,7 +218,7 @@ func (r *mutationResolver) UpdateSubGraph(ctx context.Context, input model.Input
|
|||||||
)
|
)
|
||||||
|
|
||||||
r.PubSub.Publish(input.Ref, update)
|
r.PubSub.Publish(input.Ref, update)
|
||||||
}()
|
})
|
||||||
|
|
||||||
return r.toGqlSubGraph(subGraph), nil
|
return r.toGqlSubGraph(subGraph), nil
|
||||||
}
|
}
|
||||||
@@ -183,13 +230,49 @@ func (r *queryResolver) Organizations(ctx context.Context) ([]*model.Organizatio
|
|||||||
return ToGqlOrganizations(orgs), nil
|
return ToGqlOrganizations(orgs), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AllOrganizations is the resolver for the allOrganizations field.
|
||||||
|
func (r *queryResolver) AllOrganizations(ctx context.Context) ([]*model.Organization, error) {
|
||||||
|
// Check if user has admin role
|
||||||
|
if !middleware.UserHasRole(ctx, "admin") {
|
||||||
|
return nil, fmt.Errorf("unauthorized: admin role required")
|
||||||
|
}
|
||||||
|
|
||||||
|
orgs := r.Cache.AllOrganizations()
|
||||||
|
return ToGqlOrganizations(orgs), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Supergraph is the resolver for the supergraph field.
|
// Supergraph is the resolver for the supergraph field.
|
||||||
func (r *queryResolver) Supergraph(ctx context.Context, ref string, isAfter *string) (model.Supergraph, error) {
|
func (r *queryResolver) Supergraph(ctx context.Context, ref string, isAfter *string) (model.Supergraph, error) {
|
||||||
orgId := middleware.OrganizationFromContext(ctx)
|
orgId := middleware.OrganizationFromContext(ctx)
|
||||||
_, err := r.apiKeyCanAccessRef(ctx, ref, false)
|
userId := middleware.UserFromContext(ctx)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
r.Logger.Info("Supergraph query",
|
||||||
|
"ref", ref,
|
||||||
|
"orgId", orgId,
|
||||||
|
"userId", userId,
|
||||||
|
)
|
||||||
|
|
||||||
|
// If authenticated with API key (organization), check access
|
||||||
|
if 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
|
||||||
|
}
|
||||||
|
} else if userId != "" {
|
||||||
|
// For user authentication, check if user has access to ref through their organizations
|
||||||
|
userOrgs := r.Cache.OrganizationsByUser(userId)
|
||||||
|
if len(userOrgs) == 0 {
|
||||||
|
r.Logger.Error("User has no organizations", "userId", userId)
|
||||||
|
return nil, fmt.Errorf("user has no access to any organizations")
|
||||||
|
}
|
||||||
|
// Use the first organization's ID for querying
|
||||||
|
orgId = userOrgs[0].ID.String()
|
||||||
|
r.Logger.Info("Using organization from user context", "orgId", orgId)
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("no authentication provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
after := ""
|
after := ""
|
||||||
if isAfter != nil {
|
if isAfter != nil {
|
||||||
after = *isAfter
|
after = *isAfter
|
||||||
@@ -202,30 +285,16 @@ func (r *queryResolver) Supergraph(ctx context.Context, ref string, isAfter *str
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
subGraphs := make([]*model.SubGraph, len(services))
|
subGraphs := make([]*model.SubGraph, len(services))
|
||||||
|
serviceSDLs := make([]string, len(services))
|
||||||
for i, id := range services {
|
for i, id := range services {
|
||||||
sg, err := r.fetchSubGraph(ctx, id)
|
sg, err := r.fetchSubGraph(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
subGraphs[i] = &model.SubGraph{
|
subGraphs[i] = r.toGqlSubGraph(sg)
|
||||||
ID: sg.ID.String(),
|
serviceSDLs[i] = sg.Sdl
|
||||||
Service: sg.Service,
|
|
||||||
URL: sg.Url,
|
|
||||||
WsURL: sg.WSUrl,
|
|
||||||
Sdl: sg.Sdl,
|
|
||||||
ChangedBy: sg.ChangedBy,
|
|
||||||
ChangedAt: sg.ChangedAt,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var serviceSDLs []string
|
|
||||||
for _, id := range services {
|
|
||||||
sg, err := r.fetchSubGraph(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
serviceSDLs = append(serviceSDLs, sg.Sdl)
|
|
||||||
}
|
|
||||||
sdl, err := sdlmerge.MergeSDLs(serviceSDLs...)
|
sdl, err := sdlmerge.MergeSDLs(serviceSDLs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -241,16 +310,34 @@ func (r *queryResolver) Supergraph(ctx context.Context, ref string, isAfter *str
|
|||||||
// LatestSchema is the resolver for the latestSchema field.
|
// LatestSchema is the resolver for the latestSchema field.
|
||||||
func (r *queryResolver) LatestSchema(ctx context.Context, ref string) (*model.SchemaUpdate, error) {
|
func (r *queryResolver) LatestSchema(ctx context.Context, ref string) (*model.SchemaUpdate, error) {
|
||||||
orgId := middleware.OrganizationFromContext(ctx)
|
orgId := middleware.OrganizationFromContext(ctx)
|
||||||
|
userId := middleware.UserFromContext(ctx)
|
||||||
|
|
||||||
r.Logger.Info("LatestSchema query",
|
r.Logger.Info("LatestSchema query",
|
||||||
"ref", ref,
|
"ref", ref,
|
||||||
"orgId", orgId,
|
"orgId", orgId,
|
||||||
|
"userId", userId,
|
||||||
)
|
)
|
||||||
|
|
||||||
_, err := r.apiKeyCanAccessRef(ctx, ref, false)
|
// If authenticated with API key (organization), check access
|
||||||
if err != nil {
|
if orgId != "" {
|
||||||
r.Logger.Error("API key cannot access ref", "error", err, "ref", ref)
|
_, err := r.apiKeyCanAccessRef(ctx, ref, false)
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
r.Logger.Error("API key cannot access ref", "error", err, "ref", ref)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if userId != "" {
|
||||||
|
// For user authentication, check if user has access to ref through their organizations
|
||||||
|
userOrgs := r.Cache.OrganizationsByUser(userId)
|
||||||
|
if len(userOrgs) == 0 {
|
||||||
|
r.Logger.Error("User has no organizations", "userId", userId)
|
||||||
|
return nil, fmt.Errorf("user has no access to any organizations")
|
||||||
|
}
|
||||||
|
// Use the first organization's ID for querying
|
||||||
|
// In a real-world scenario, you might want to check which org has access to this ref
|
||||||
|
orgId = userOrgs[0].ID.String()
|
||||||
|
r.Logger.Info("Using organization from user context", "orgId", orgId)
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("no authentication provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current services and schema
|
// Get current services and schema
|
||||||
@@ -280,8 +367,8 @@ func (r *queryResolver) LatestSchema(ctx context.Context, ref string) (*model.Sc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate Cosmo router config
|
// Generate Cosmo router config (concurrency-limited)
|
||||||
cosmoConfig, err := GenerateCosmoRouterConfig(subGraphs)
|
cosmoConfig, err := r.CosmoGenerator.Generate(ctx, subGraphs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Logger.Error("generate cosmo config", "error", err)
|
r.Logger.Error("generate cosmo config", "error", err)
|
||||||
cosmoConfig = "" // Return empty if generation fails
|
cosmoConfig = "" // Return empty if generation fails
|
||||||
@@ -324,9 +411,6 @@ func (r *subscriptionResolver) SchemaUpdates(ctx context.Context, ref string) (<
|
|||||||
|
|
||||||
// Send initial state immediately
|
// Send initial state immediately
|
||||||
go func() {
|
go func() {
|
||||||
// Use background context for async operation
|
|
||||||
bgCtx := context.Background()
|
|
||||||
|
|
||||||
services, lastUpdate := r.Cache.Services(orgId, ref, "")
|
services, lastUpdate := r.Cache.Services(orgId, ref, "")
|
||||||
r.Logger.Info("Preparing initial schema update",
|
r.Logger.Info("Preparing initial schema update",
|
||||||
"ref", ref,
|
"ref", ref,
|
||||||
@@ -337,24 +421,16 @@ func (r *subscriptionResolver) SchemaUpdates(ctx context.Context, ref string) (<
|
|||||||
|
|
||||||
subGraphs := make([]*model.SubGraph, len(services))
|
subGraphs := make([]*model.SubGraph, len(services))
|
||||||
for i, id := range services {
|
for i, id := range services {
|
||||||
sg, err := r.fetchSubGraph(bgCtx, id)
|
sg, err := r.fetchSubGraph(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Logger.Error("fetch subgraph for initial update", "error", err, "id", id)
|
r.Logger.Error("fetch subgraph for initial update", "error", err, "id", id)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
subGraphs[i] = &model.SubGraph{
|
subGraphs[i] = r.toGqlSubGraph(sg)
|
||||||
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
|
// Generate Cosmo router config (concurrency-limited)
|
||||||
cosmoConfig, err := GenerateCosmoRouterConfig(subGraphs)
|
cosmoConfig, err := r.CosmoGenerator.Generate(ctx, subGraphs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Logger.Error("generate cosmo config", "error", err)
|
r.Logger.Error("generate cosmo config", "error", err)
|
||||||
cosmoConfig = "" // Send empty if generation fails
|
cosmoConfig = "" // Send empty if generation fails
|
||||||
@@ -375,7 +451,11 @@ func (r *subscriptionResolver) SchemaUpdates(ctx context.Context, ref string) (<
|
|||||||
"cosmoConfigLength", len(cosmoConfig),
|
"cosmoConfigLength", len(cosmoConfig),
|
||||||
)
|
)
|
||||||
|
|
||||||
ch <- update
|
select {
|
||||||
|
case ch <- update:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Clean up subscription when context is done
|
// Clean up subscription when context is done
|
||||||
|
|||||||
+1
-1
@@ -61,7 +61,7 @@ spec:
|
|||||||
timeoutSeconds: 5
|
timeoutSeconds: 5
|
||||||
failureThreshold: 3
|
failureThreshold: 3
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
image: registry.gitlab.com/unboundsoftware/schemas:${COMMIT}
|
image: oci.unbound.se/unboundsoftware/schemas:${COMMIT}
|
||||||
ports:
|
ports:
|
||||||
- name: api
|
- name: api
|
||||||
containerPort: 8080
|
containerPort: 8080
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: schemas-ingress
|
name: schemas-ingress
|
||||||
annotations:
|
annotations:
|
||||||
kubernetes.io/ingress.class: "alb"
|
|
||||||
alb.ingress.kubernetes.io/group.name: "default"
|
alb.ingress.kubernetes.io/group.name: "default"
|
||||||
alb.ingress.kubernetes.io/scheme: internet-facing
|
alb.ingress.kubernetes.io/scheme: internet-facing
|
||||||
alb.ingress.kubernetes.io/target-type: instance
|
alb.ingress.kubernetes.io/target-type: instance
|
||||||
@@ -11,6 +10,7 @@ metadata:
|
|||||||
alb.ingress.kubernetes.io/ssl-redirect: "443"
|
alb.ingress.kubernetes.io/ssl-redirect: "443"
|
||||||
alb.ingress.kubernetes.io/healthcheck-path: '/health'
|
alb.ingress.kubernetes.io/healthcheck-path: '/health'
|
||||||
spec:
|
spec:
|
||||||
|
ingressClassName: "alb"
|
||||||
rules:
|
rules:
|
||||||
- host: "schemas.unbound.se"
|
- host: "schemas.unbound.se"
|
||||||
http:
|
http:
|
||||||
|
|||||||
+55
-14
@@ -6,9 +6,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/99designs/gqlgen/graphql"
|
"github.com/99designs/gqlgen/graphql"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/domain"
|
"gitea.unbound.se/unboundsoftware/schemas/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -33,14 +32,9 @@ type AuthMiddleware struct {
|
|||||||
func (m *AuthMiddleware) Handler(next http.Handler) http.Handler {
|
func (m *AuthMiddleware) Handler(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
token, err := TokenFromContext(r.Context())
|
claims := ClaimsFromContext(r.Context())
|
||||||
if err != nil {
|
if claims != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
ctx = context.WithValue(ctx, UserKey, claims.RegisteredClaims.Subject)
|
||||||
_, _ = w.Write([]byte("Invalid JWT token format"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if token != nil {
|
|
||||||
ctx = context.WithValue(ctx, UserKey, token.Claims.(jwt.MapClaims)["sub"])
|
|
||||||
}
|
}
|
||||||
apiKey, err := ApiKeyFromContext(r.Context())
|
apiKey, err := ApiKeyFromContext(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -67,6 +61,26 @@ func UserFromContext(ctx context.Context) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UserHasRole(ctx context.Context, role string) bool {
|
||||||
|
claims := ClaimsFromContext(ctx)
|
||||||
|
if claims == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
customClaims, ok := claims.CustomClaims.(*CustomClaims)
|
||||||
|
if !ok || customClaims == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range customClaims.Roles {
|
||||||
|
if r == role {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func OrganizationFromContext(ctx context.Context) string {
|
func OrganizationFromContext(ctx context.Context) string {
|
||||||
if value := ctx.Value(OrganizationKey); value != nil {
|
if value := ctx.Value(OrganizationKey); value != nil {
|
||||||
if u, ok := value.(domain.Organization); ok {
|
if u, ok := value.(domain.Organization); ok {
|
||||||
@@ -77,15 +91,42 @@ func OrganizationFromContext(ctx context.Context) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *AuthMiddleware) Directive(ctx context.Context, _ interface{}, next graphql.Resolver, user *bool, organization *bool) (res interface{}, err error) {
|
func (m *AuthMiddleware) Directive(ctx context.Context, _ interface{}, next graphql.Resolver, user *bool, organization *bool) (res interface{}, err error) {
|
||||||
if user != nil && *user {
|
userRequired := user != nil && *user
|
||||||
if u := UserFromContext(ctx); u == "" {
|
orgRequired := organization != nil && *organization
|
||||||
|
|
||||||
|
u := UserFromContext(ctx)
|
||||||
|
orgId := OrganizationFromContext(ctx)
|
||||||
|
|
||||||
|
fmt.Printf("[Auth Directive] userRequired=%v, orgRequired=%v, hasUser=%v, hasOrg=%v\n",
|
||||||
|
userRequired, orgRequired, u != "", orgId != "")
|
||||||
|
|
||||||
|
// If both are required, it means EITHER is acceptable (OR logic)
|
||||||
|
if userRequired && orgRequired {
|
||||||
|
if u == "" && orgId == "" {
|
||||||
|
fmt.Printf("[Auth Directive] REJECTED: Neither user nor organization available\n")
|
||||||
|
return nil, fmt.Errorf("authentication required: provide either user token or organization API key")
|
||||||
|
}
|
||||||
|
fmt.Printf("[Auth Directive] ACCEPTED: Has user=%v OR organization=%v\n", u != "", orgId != "")
|
||||||
|
return next(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only user required
|
||||||
|
if userRequired {
|
||||||
|
if u == "" {
|
||||||
|
fmt.Printf("[Auth Directive] REJECTED: No user available\n")
|
||||||
return nil, fmt.Errorf("no user available in request")
|
return nil, fmt.Errorf("no user available in request")
|
||||||
}
|
}
|
||||||
|
fmt.Printf("[Auth Directive] ACCEPTED: User authenticated\n")
|
||||||
}
|
}
|
||||||
if organization != nil && *organization {
|
|
||||||
if orgId := OrganizationFromContext(ctx); orgId == "" {
|
// Only organization required
|
||||||
|
if orgRequired {
|
||||||
|
if orgId == "" {
|
||||||
|
fmt.Printf("[Auth Directive] REJECTED: No organization available\n")
|
||||||
return nil, fmt.Errorf("no organization available in request")
|
return nil, fmt.Errorf("no organization available in request")
|
||||||
}
|
}
|
||||||
|
fmt.Printf("[Auth Directive] ACCEPTED: Organization authenticated\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
return next(ctx)
|
return next(ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-141
@@ -2,39 +2,34 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"log"
|
||||||
"strings"
|
"net/url"
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
mw "github.com/auth0/go-jwt-middleware/v2"
|
jwtmiddleware "github.com/auth0/go-jwt-middleware/v3"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/auth0/go-jwt-middleware/v3/jwks"
|
||||||
"github.com/pkg/errors"
|
"github.com/auth0/go-jwt-middleware/v3/validator"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CustomClaims contains custom claims from the JWT token.
|
||||||
|
type CustomClaims struct {
|
||||||
|
Roles []string `json:"https://unbound.se/roles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate implements the validator.CustomClaims interface.
|
||||||
|
func (c CustomClaims) Validate(_ context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type Auth0 struct {
|
type Auth0 struct {
|
||||||
domain string
|
domain string
|
||||||
audience string
|
audience string
|
||||||
client *http.Client
|
|
||||||
cache JwksCache
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuth0(audience, domain string, strictSsl bool) *Auth0 {
|
func NewAuth0(audience, domain string, _ bool) *Auth0 {
|
||||||
customTransport := http.DefaultTransport.(*http.Transport).Clone()
|
|
||||||
customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: !strictSsl}
|
|
||||||
client := &http.Client{Transport: customTransport}
|
|
||||||
|
|
||||||
return &Auth0{
|
return &Auth0{
|
||||||
domain: domain,
|
domain: domain,
|
||||||
audience: audience,
|
audience: audience,
|
||||||
client: client,
|
|
||||||
cache: JwksCache{
|
|
||||||
RWMutex: &sync.RWMutex{},
|
|
||||||
cache: make(map[string]cacheItem),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,133 +37,47 @@ type Response struct {
|
|||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Jwks struct {
|
func (a *Auth0) Middleware() *jwtmiddleware.JWTMiddleware {
|
||||||
Keys []JSONWebKeys `json:"keys"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type JSONWebKeys struct {
|
|
||||||
Kty string `json:"kty"`
|
|
||||||
Kid string `json:"kid"`
|
|
||||||
Use string `json:"use"`
|
|
||||||
N string `json:"n"`
|
|
||||||
E string `json:"e"`
|
|
||||||
X5c []string `json:"x5c"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Auth0) ValidationKeyGetter() func(token *jwt.Token) (interface{}, error) {
|
|
||||||
return func(token *jwt.Token) (interface{}, error) {
|
|
||||||
// Verify 'aud' claim
|
|
||||||
|
|
||||||
cert, err := a.getPemCert(token)
|
|
||||||
if err != nil {
|
|
||||||
panic(err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Auth0) Middleware() *mw.JWTMiddleware {
|
|
||||||
issuer := fmt.Sprintf("https://%s/", a.domain)
|
issuer := fmt.Sprintf("https://%s/", a.domain)
|
||||||
jwtMiddleware := mw.New(func(ctx context.Context, token string) (interface{}, error) {
|
|
||||||
jwtToken, err := jwt.Parse(token, a.ValidationKeyGetter(), jwt.WithAudience(a.audience), jwt.WithIssuer(issuer))
|
issuerURL, err := url.Parse(issuer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
log.Fatalf("failed to parse issuer URL: %v", err)
|
||||||
}
|
}
|
||||||
if _, ok := jwtToken.Method.(*jwt.SigningMethodRSA); !ok {
|
|
||||||
return nil, fmt.Errorf("unexpected signing method: %v", jwtToken.Header["alg"])
|
provider, err := jwks.NewCachingProvider(jwks.WithIssuerURL(issuerURL))
|
||||||
}
|
if err != nil {
|
||||||
return jwtToken, nil
|
log.Fatalf("failed to create JWKS provider: %v", err)
|
||||||
},
|
}
|
||||||
mw.WithTokenExtractor(func(r *http.Request) (string, error) {
|
|
||||||
token := r.Header.Get("Authorization")
|
jwtValidator, err := validator.New(
|
||||||
if strings.HasPrefix(token, "Bearer ") {
|
validator.WithKeyFunc(provider.KeyFunc),
|
||||||
return token[7:], nil
|
validator.WithAlgorithm(validator.RS256),
|
||||||
}
|
validator.WithIssuer(issuer),
|
||||||
return "", nil
|
validator.WithAudience(a.audience),
|
||||||
|
validator.WithCustomClaims(func() validator.CustomClaims {
|
||||||
|
return &CustomClaims{}
|
||||||
}),
|
}),
|
||||||
mw.WithCredentialsOptional(true),
|
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create JWT validator: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jwtMiddleware, err := jwtmiddleware.New(
|
||||||
|
jwtmiddleware.WithValidator(jwtValidator),
|
||||||
|
jwtmiddleware.WithCredentialsOptional(true),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to create JWT middleware: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return jwtMiddleware
|
return jwtMiddleware
|
||||||
}
|
}
|
||||||
|
|
||||||
func TokenFromContext(ctx context.Context) (*jwt.Token, error) {
|
func ClaimsFromContext(ctx context.Context) *validator.ValidatedClaims {
|
||||||
if value := ctx.Value(mw.ContextKey{}); value != nil {
|
claims, err := jwtmiddleware.GetClaims[*validator.ValidatedClaims](ctx)
|
||||||
if u, ok := value.(*jwt.Token); ok {
|
|
||||||
return u, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("token is in wrong format")
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Auth0) cacheGetWellknown(url string) (*Jwks, error) {
|
|
||||||
if value := a.cache.get(url); value != nil {
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
jwks := &Jwks{}
|
|
||||||
resp, err := a.client.Get(url)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return jwks, err
|
return nil
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = resp.Body.Close()
|
|
||||||
}()
|
|
||||||
err = json.NewDecoder(resp.Body).Decode(jwks)
|
|
||||||
if err == nil && jwks != nil {
|
|
||||||
a.cache.put(url, jwks)
|
|
||||||
}
|
|
||||||
return jwks, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Auth0) getPemCert(token *jwt.Token) (string, error) {
|
|
||||||
jwks, err := a.cacheGetWellknown(fmt.Sprintf("https://%s/.well-known/jwks.json", a.domain))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
var cert string
|
|
||||||
for k := range jwks.Keys {
|
|
||||||
if token.Header["kid"] == jwks.Keys[k].Kid {
|
|
||||||
cert = "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if cert == "" {
|
|
||||||
err := errors.New("Unable to find appropriate key.")
|
|
||||||
return cert, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return cert, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type JwksCache struct {
|
|
||||||
*sync.RWMutex
|
|
||||||
cache map[string]cacheItem
|
|
||||||
}
|
|
||||||
type cacheItem struct {
|
|
||||||
data *Jwks
|
|
||||||
expiration time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *JwksCache) get(url string) *Jwks {
|
|
||||||
c.RLock()
|
|
||||||
defer c.RUnlock()
|
|
||||||
if value, ok := c.cache[url]; ok {
|
|
||||||
if time.Now().After(value.expiration) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return value.data
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *JwksCache) put(url string, jwks *Jwks) {
|
|
||||||
c.Lock()
|
|
||||||
defer c.Unlock()
|
|
||||||
c.cache[url] = cacheItem{
|
|
||||||
data: jwks,
|
|
||||||
expiration: time.Now().Add(time.Minute * 60),
|
|
||||||
}
|
}
|
||||||
|
return claims
|
||||||
}
|
}
|
||||||
|
|||||||
+126
-24
@@ -6,15 +6,15 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
mw "github.com/auth0/go-jwt-middleware/v2"
|
"github.com/auth0/go-jwt-middleware/v3/core"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/auth0/go-jwt-middleware/v3/validator"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
"gitlab.com/unboundsoftware/eventsourced/eventsourced"
|
||||||
|
|
||||||
"gitlab.com/unboundsoftware/schemas/domain"
|
"gitea.unbound.se/unboundsoftware/schemas/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockCache is a mock implementation of the Cache interface
|
// MockCache is a mock implementation of the Cache interface
|
||||||
@@ -155,9 +155,11 @@ func TestAuthMiddleware_Handler_WithValidJWT(t *testing.T) {
|
|||||||
mockCache.On("OrganizationByAPIKey", "").Return(nil)
|
mockCache.On("OrganizationByAPIKey", "").Return(nil)
|
||||||
|
|
||||||
userID := "user-123"
|
userID := "user-123"
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
claims := &validator.ValidatedClaims{
|
||||||
"sub": userID,
|
RegisteredClaims: validator.RegisteredClaims{
|
||||||
})
|
Subject: userID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// Create a test handler that checks the context
|
// Create a test handler that checks the context
|
||||||
var capturedUser string
|
var capturedUser string
|
||||||
@@ -170,9 +172,9 @@ func TestAuthMiddleware_Handler_WithValidJWT(t *testing.T) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create request with JWT token in context
|
// Create request with JWT claims in context
|
||||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
ctx := context.WithValue(req.Context(), mw.ContextKey{}, token)
|
ctx := core.SetClaims(req.Context(), claims)
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
@@ -209,28 +211,35 @@ func TestAuthMiddleware_Handler_APIKeyErrorHandling(t *testing.T) {
|
|||||||
assert.Contains(t, rec.Body.String(), "Invalid API Key format")
|
assert.Contains(t, rec.Body.String(), "Invalid API Key format")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthMiddleware_Handler_JWTErrorHandling(t *testing.T) {
|
func TestAuthMiddleware_Handler_JWTMissingClaims(t *testing.T) {
|
||||||
// Setup
|
// Setup
|
||||||
mockCache := new(MockCache)
|
mockCache := new(MockCache)
|
||||||
authMiddleware := NewAuth(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 capturedUser string
|
||||||
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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)
|
w.WriteHeader(http.StatusOK)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create request with invalid JWT token type in context
|
// Create request without JWT claims - user should not be set
|
||||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
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()
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
authMiddleware.Handler(testHandler).ServeHTTP(rec, req)
|
authMiddleware.Handler(testHandler).ServeHTTP(rec, req)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assert.Equal(t, http.StatusInternalServerError, rec.Code)
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
assert.Contains(t, rec.Body.String(), "Invalid JWT token format")
|
assert.Empty(t, capturedUser, "User should not be set when no claims in context")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthMiddleware_Handler_BothJWTAndAPIKey(t *testing.T) {
|
func TestAuthMiddleware_Handler_BothJWTAndAPIKey(t *testing.T) {
|
||||||
@@ -249,9 +258,11 @@ func TestAuthMiddleware_Handler_BothJWTAndAPIKey(t *testing.T) {
|
|||||||
userID := "user-123"
|
userID := "user-123"
|
||||||
apiKey := "test-api-key-123"
|
apiKey := "test-api-key-123"
|
||||||
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
claims := &validator.ValidatedClaims{
|
||||||
"sub": userID,
|
RegisteredClaims: validator.RegisteredClaims{
|
||||||
})
|
Subject: userID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// Mock expects plaintext key (cache handles hashing internally)
|
// Mock expects plaintext key (cache handles hashing internally)
|
||||||
mockCache.On("OrganizationByAPIKey", apiKey).Return(expectedOrg)
|
mockCache.On("OrganizationByAPIKey", apiKey).Return(expectedOrg)
|
||||||
@@ -273,9 +284,9 @@ func TestAuthMiddleware_Handler_BothJWTAndAPIKey(t *testing.T) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create request with both JWT and API key in context
|
// Create request with both JWT claims and API key in context
|
||||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||||
ctx := context.WithValue(req.Context(), mw.ContextKey{}, token)
|
ctx := core.SetClaims(req.Context(), claims)
|
||||||
ctx = context.WithValue(ctx, ApiKey, apiKey)
|
ctx = context.WithValue(ctx, ApiKey, apiKey)
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
@@ -427,7 +438,10 @@ func TestAuthMiddleware_Directive_RequiresBoth(t *testing.T) {
|
|||||||
Name: "Test Org",
|
Name: "Test Org",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test with both present
|
// When both user and organization are marked as acceptable,
|
||||||
|
// the directive uses OR logic - either one is sufficient
|
||||||
|
|
||||||
|
// Test with both present - should succeed
|
||||||
ctx := context.WithValue(context.Background(), UserKey, "user-123")
|
ctx := context.WithValue(context.Background(), UserKey, "user-123")
|
||||||
ctx = context.WithValue(ctx, OrganizationKey, org)
|
ctx = context.WithValue(ctx, OrganizationKey, org)
|
||||||
_, err := authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
_, err := authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
||||||
@@ -435,19 +449,27 @@ func TestAuthMiddleware_Directive_RequiresBoth(t *testing.T) {
|
|||||||
}, &requireUser, &requireOrg)
|
}, &requireUser, &requireOrg)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Test with only user
|
// Test with only user - should succeed (OR logic)
|
||||||
ctx = context.WithValue(context.Background(), UserKey, "user-123")
|
ctx = context.WithValue(context.Background(), UserKey, "user-123")
|
||||||
_, err = authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
_, err = authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
||||||
return "success", nil
|
return "success", nil
|
||||||
}, &requireUser, &requireOrg)
|
}, &requireUser, &requireOrg)
|
||||||
assert.Error(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Test with only organization
|
// Test with only organization - should succeed (OR logic)
|
||||||
ctx = context.WithValue(context.Background(), OrganizationKey, org)
|
ctx = context.WithValue(context.Background(), OrganizationKey, org)
|
||||||
_, err = authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
_, err = authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
||||||
return "success", nil
|
return "success", nil
|
||||||
}, &requireUser, &requireOrg)
|
}, &requireUser, &requireOrg)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test with neither - should fail
|
||||||
|
ctx = context.Background()
|
||||||
|
_, err = authMiddleware.Directive(ctx, nil, func(ctx context.Context) (interface{}, error) {
|
||||||
|
return "success", nil
|
||||||
|
}, &requireUser, &requireOrg)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "authentication required")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthMiddleware_Directive_NoRequirements(t *testing.T) {
|
func TestAuthMiddleware_Directive_NoRequirements(t *testing.T) {
|
||||||
@@ -462,3 +484,83 @@ func TestAuthMiddleware_Directive_NoRequirements(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "success", result)
|
assert.Equal(t, "success", result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUserHasRole_WithValidRole(t *testing.T) {
|
||||||
|
// Create claims with roles
|
||||||
|
claims := &validator.ValidatedClaims{
|
||||||
|
RegisteredClaims: validator.RegisteredClaims{
|
||||||
|
Subject: "user-123",
|
||||||
|
},
|
||||||
|
CustomClaims: &CustomClaims{
|
||||||
|
Roles: []string{"admin", "user"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := core.SetClaims(context.Background(), claims)
|
||||||
|
|
||||||
|
// Test for existing role
|
||||||
|
hasRole := UserHasRole(ctx, "admin")
|
||||||
|
assert.True(t, hasRole)
|
||||||
|
|
||||||
|
hasRole = UserHasRole(ctx, "user")
|
||||||
|
assert.True(t, hasRole)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserHasRole_WithoutRole(t *testing.T) {
|
||||||
|
// Create claims with roles
|
||||||
|
claims := &validator.ValidatedClaims{
|
||||||
|
RegisteredClaims: validator.RegisteredClaims{
|
||||||
|
Subject: "user-123",
|
||||||
|
},
|
||||||
|
CustomClaims: &CustomClaims{
|
||||||
|
Roles: []string{"user"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := core.SetClaims(context.Background(), claims)
|
||||||
|
|
||||||
|
// Test for non-existing role
|
||||||
|
hasRole := UserHasRole(ctx, "admin")
|
||||||
|
assert.False(t, hasRole)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserHasRole_WithoutRolesClaim(t *testing.T) {
|
||||||
|
// Create claims without custom claims
|
||||||
|
claims := &validator.ValidatedClaims{
|
||||||
|
RegisteredClaims: validator.RegisteredClaims{
|
||||||
|
Subject: "user-123",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := core.SetClaims(context.Background(), claims)
|
||||||
|
|
||||||
|
// Test should return false when custom claims is missing
|
||||||
|
hasRole := UserHasRole(ctx, "admin")
|
||||||
|
assert.False(t, hasRole)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserHasRole_WithoutClaims(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Test should return false when no claims in context
|
||||||
|
hasRole := UserHasRole(ctx, "admin")
|
||||||
|
assert.False(t, hasRole)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserHasRole_WithEmptyRoles(t *testing.T) {
|
||||||
|
// Create claims with empty roles
|
||||||
|
claims := &validator.ValidatedClaims{
|
||||||
|
RegisteredClaims: validator.RegisteredClaims{
|
||||||
|
Subject: "user-123",
|
||||||
|
},
|
||||||
|
CustomClaims: &CustomClaims{
|
||||||
|
Roles: []string{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := core.SetClaims(context.Background(), claims)
|
||||||
|
|
||||||
|
// Test should return false when roles array is empty
|
||||||
|
hasRole := UserHasRole(ctx, "admin")
|
||||||
|
assert.False(t, hasRole)
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
"kubernetes"
|
"kubernetes"
|
||||||
],
|
],
|
||||||
"matchPackageNames": [
|
"matchPackageNames": [
|
||||||
"registry.gitlab.com/unboundsoftware/schemas"
|
"oci.unbound.se/unboundsoftware/schemas"
|
||||||
],
|
],
|
||||||
"enabled": false
|
"enabled": false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package sdlmerge
|
package sdlmerge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -61,12 +62,13 @@ func MergeSDLs(SDLs ...string) (string, error) {
|
|||||||
return "", fmt.Errorf("merge ast: %w", err)
|
return "", fmt.Errorf("merge ast: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := astprinter.PrintString(&doc)
|
// Format with indentation for better readability
|
||||||
if err != nil {
|
buf := &bytes.Buffer{}
|
||||||
|
if err := astprinter.PrintIndent(&doc, []byte(" "), buf); err != nil {
|
||||||
return "", fmt.Errorf("stringify schema: %w", err)
|
return "", fmt.Errorf("stringify schema: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return out, nil
|
return buf.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateSubgraphs(subgraphs []string) error {
|
func validateSubgraphs(subgraphs []string) error {
|
||||||
|
|||||||
@@ -0,0 +1,434 @@
|
|||||||
|
package sdlmerge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMergeSDLs_Success(t *testing.T) {
|
||||||
|
// Both types need to be in the same subgraph or properly federated
|
||||||
|
sdl1 := `
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Post {
|
||||||
|
id: ID!
|
||||||
|
title: String!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "User")
|
||||||
|
assert.Contains(t, result, "Post")
|
||||||
|
assert.Contains(t, result, "id")
|
||||||
|
assert.Contains(t, result, "name")
|
||||||
|
assert.Contains(t, result, "title")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_SingleSchema(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
type Query {
|
||||||
|
hello: String
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "Query")
|
||||||
|
assert.Contains(t, result, "hello")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_EmptySchemas(t *testing.T) {
|
||||||
|
result, err := MergeSDLs()
|
||||||
|
require.NoError(t, err)
|
||||||
|
// With no schemas, result will be empty after processing
|
||||||
|
// This is valid - just verifies no crash
|
||||||
|
_ = result
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_InvalidSyntax(t *testing.T) {
|
||||||
|
invalidSDL := `
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
// Missing closing brace
|
||||||
|
`
|
||||||
|
|
||||||
|
_, err := MergeSDLs(invalidSDL)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "parse graphql document string")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_UnknownType(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
profile: UnknownType
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
_, err := MergeSDLs(sdl)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "validate schema")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_DuplicateTypes_DifferentFields(t *testing.T) {
|
||||||
|
// Same type with different fields in different subgraphs - should fail
|
||||||
|
// In federation, shared types must be identical
|
||||||
|
sdl1 := `
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
sdl2 := `
|
||||||
|
type User {
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
_, err := MergeSDLs(sdl1, sdl2)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "shared type")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_ExtendType(t *testing.T) {
|
||||||
|
sdl1 := `
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
sdl2 := `
|
||||||
|
extend type User {
|
||||||
|
email: String!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl1, sdl2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "User")
|
||||||
|
assert.Contains(t, result, "id")
|
||||||
|
assert.Contains(t, result, "email")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_Scalars(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
scalar DateTime
|
||||||
|
|
||||||
|
type Event {
|
||||||
|
id: ID!
|
||||||
|
createdAt: DateTime!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "DateTime")
|
||||||
|
assert.Contains(t, result, "Event")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_Enums(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
enum Role {
|
||||||
|
ADMIN
|
||||||
|
USER
|
||||||
|
GUEST
|
||||||
|
}
|
||||||
|
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
role: Role!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "Role")
|
||||||
|
assert.Contains(t, result, "ADMIN")
|
||||||
|
assert.Contains(t, result, "USER")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_Interfaces(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
interface Node {
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
type User implements Node {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "Node")
|
||||||
|
assert.Contains(t, result, "implements")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_Unions(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bot {
|
||||||
|
id: ID!
|
||||||
|
version: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
union Actor = User | Bot
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "Actor")
|
||||||
|
assert.Contains(t, result, "User")
|
||||||
|
assert.Contains(t, result, "Bot")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_InputTypes(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
input CreateUserInput {
|
||||||
|
name: String!
|
||||||
|
email: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
createUser(input: CreateUserInput!): User
|
||||||
|
}
|
||||||
|
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "CreateUserInput")
|
||||||
|
assert.Contains(t, result, "createUser")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_Directives(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
name: String! @deprecated(reason: "Use fullName instead")
|
||||||
|
fullName: String!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "User")
|
||||||
|
assert.Contains(t, result, "name")
|
||||||
|
assert.Contains(t, result, "fullName")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_FederationKeys(t *testing.T) {
|
||||||
|
// Federation @key directive
|
||||||
|
sdl := `
|
||||||
|
type User @key(fields: "id") {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "User")
|
||||||
|
// @key directive should be removed during merge
|
||||||
|
assert.NotContains(t, result, "@key")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_ExternalFields(t *testing.T) {
|
||||||
|
// Federation @external directive
|
||||||
|
sdl1 := `
|
||||||
|
type User @key(fields: "id") {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
sdl2 := `
|
||||||
|
extend type User @key(fields: "id") {
|
||||||
|
id: ID! @external
|
||||||
|
posts: [Post!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Post {
|
||||||
|
id: ID!
|
||||||
|
title: String!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl1, sdl2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "User")
|
||||||
|
assert.Contains(t, result, "Post")
|
||||||
|
// @external fields should be removed
|
||||||
|
assert.NotContains(t, result, "@external")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_ComplexSchema(t *testing.T) {
|
||||||
|
// Multiple subgraphs with various types - simplified to avoid cross-references
|
||||||
|
users := `
|
||||||
|
type User @key(fields: "id") {
|
||||||
|
id: ID!
|
||||||
|
username: String!
|
||||||
|
email: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
user(id: ID!): User
|
||||||
|
users: [User!]!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
posts := `
|
||||||
|
extend type User @key(fields: "id") {
|
||||||
|
id: ID! @external
|
||||||
|
posts: [Post!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Post @key(fields: "id") {
|
||||||
|
id: ID!
|
||||||
|
title: String!
|
||||||
|
content: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
extend type Query {
|
||||||
|
post(id: ID!): Post
|
||||||
|
posts: [Post!]!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
comments := `
|
||||||
|
extend type Post @key(fields: "id") {
|
||||||
|
id: ID! @external
|
||||||
|
comments: [Comment!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Comment {
|
||||||
|
id: ID!
|
||||||
|
text: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
extend type Query {
|
||||||
|
comment(id: ID!): Comment
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(users, posts, comments)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify all types are present
|
||||||
|
assert.Contains(t, result, "User")
|
||||||
|
assert.Contains(t, result, "Post")
|
||||||
|
assert.Contains(t, result, "Comment")
|
||||||
|
assert.Contains(t, result, "Query")
|
||||||
|
|
||||||
|
// Verify fields from all subgraphs
|
||||||
|
assert.Contains(t, result, "username")
|
||||||
|
assert.Contains(t, result, "posts")
|
||||||
|
assert.Contains(t, result, "comments")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_EmptyTypeDefinition(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
type Empty {}
|
||||||
|
`
|
||||||
|
|
||||||
|
_, err := MergeSDLs(sdl)
|
||||||
|
require.Error(t, err)
|
||||||
|
// Empty types are invalid in GraphQL
|
||||||
|
assert.Contains(t, err.Error(), "empty body")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_MultipleValidationErrors(t *testing.T) {
|
||||||
|
// Schema with multiple errors
|
||||||
|
sdl := `
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
profile: NonExistentType1
|
||||||
|
settings: NonExistentType2
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
_, err := MergeSDLs(sdl)
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_ListTypes(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
tags: [String!]!
|
||||||
|
friends: [User!]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "User")
|
||||||
|
assert.Contains(t, result, "tags")
|
||||||
|
assert.Contains(t, result, "friends")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_NonNullTypes(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
type User {
|
||||||
|
id: ID!
|
||||||
|
name: String!
|
||||||
|
email: String
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "User")
|
||||||
|
assert.Contains(t, result, "id")
|
||||||
|
assert.Contains(t, result, "name")
|
||||||
|
assert.Contains(t, result, "email")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_Comments(t *testing.T) {
|
||||||
|
sdl := `
|
||||||
|
# This is a user type
|
||||||
|
type User {
|
||||||
|
# User ID
|
||||||
|
id: ID!
|
||||||
|
# User name
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdl)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, result, "User")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeSDLs_LargeSchema(t *testing.T) {
|
||||||
|
// Test with a reasonably large schema to ensure performance
|
||||||
|
var sdlBuilder strings.Builder
|
||||||
|
for i := 0; i < 50; i++ {
|
||||||
|
sdlBuilder.WriteString("type Type")
|
||||||
|
sdlBuilder.WriteString(strings.Repeat(string(rune('A'+i%26)), 1))
|
||||||
|
sdlBuilder.WriteString(string(rune('0' + i/26)))
|
||||||
|
sdlBuilder.WriteString(" { id: ID }\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := MergeSDLs(sdlBuilder.String())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify some types are present
|
||||||
|
assert.Contains(t, result, "TypeA0")
|
||||||
|
assert.Contains(t, result, "TypeB0")
|
||||||
|
assert.Contains(t, result, "TypeC0")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user