mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-03 12:01:58 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9235292586 | ||
|
|
23f10387c5 | ||
|
|
0d3ad9e4fd | ||
|
|
f5d232d971 |
@@ -1,6 +1,3 @@
|
|||||||
# This workflow will build a golang project
|
|
||||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
|
|
||||||
|
|
||||||
name: Create Go Release (Tag Versioning)
|
name: Create Go Release (Tag Versioning)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -26,7 +23,9 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Git
|
- name: Set up Git
|
||||||
run: |
|
run: |
|
||||||
@@ -38,7 +37,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
git fetch --tags
|
git fetch --tags
|
||||||
latest_tag=$(git describe --tags `git rev-list --tags --max-count=1`)
|
latest_tag=$(git describe --tags `git rev-list --tags --max-count=1`)
|
||||||
echo "::set-output name=tag::$latest_tag"
|
echo "tag=${latest_tag}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
- name: Determine new tag version
|
- name: Determine new tag version
|
||||||
id: new_tag
|
id: new_tag
|
||||||
@@ -57,7 +56,7 @@ jobs:
|
|||||||
((minor++))
|
((minor++))
|
||||||
patch=0
|
patch=0
|
||||||
;;
|
;;
|
||||||
"release")
|
"major")
|
||||||
((major++))
|
((major++))
|
||||||
minor=0
|
minor=0
|
||||||
patch=0
|
patch=0
|
||||||
@@ -68,15 +67,11 @@ jobs:
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
new_tag="v$major.$minor.$patch"
|
new_tag="v$major.$minor.$patch"
|
||||||
echo "::set-output name=tag::$new_tag"
|
echo "tag=${new_tag}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
- name: Create tag
|
- name: Create tag
|
||||||
run: |
|
run: |
|
||||||
git tag -a ${{ steps.new_tag.outputs.tag }} -m "Tagging ${{ steps.new_tag.outputs.tag }} for release"
|
git tag -a ${{ steps.new_tag.outputs.tag }} -m "Tagging ${{ steps.new_tag.outputs.tag }} for release"
|
||||||
|
|
||||||
- name: Push changes
|
- name: Push tag
|
||||||
uses: ad-m/github-push-action@master
|
run: git push origin ${{ steps.new_tag.outputs.tag }}
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.BITECH_GITHUB_TOKEN }}
|
|
||||||
force: true
|
|
||||||
tags: true
|
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
name: Release Clients
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Client version (e.g. 1.4.0)"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
publish:
|
||||||
|
description: "Publish packages to Gitea (untick for a build/test dry run)"
|
||||||
|
required: true
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
env:
|
||||||
|
VERSION_INPUT: ${{ github.event.inputs.version }}
|
||||||
|
PUBLISH: ${{ github.event.inputs.publish }}
|
||||||
|
SERVER_URL: ${{ github.server_url }}
|
||||||
|
OWNER: ${{ github.repository_owner }}
|
||||||
|
REGISTRY_USER: ${{ secrets.PACKAGE_REGISTRY_USERNAME || vars.PACKAGE_REGISTRY_USERNAME }}
|
||||||
|
TOKEN: ${{ secrets.PACKAGE_REGISTRY_TOKEN || vars.PACKAGE_REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
name: Validate version
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.v.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- id: v
|
||||||
|
run: |
|
||||||
|
version="${VERSION_INPUT#v}"
|
||||||
|
if ! [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
|
||||||
|
echo "Invalid version: $VERSION_INPUT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "version=${version}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
js:
|
||||||
|
name: JS (npm)
|
||||||
|
needs: validate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
working-directory: clients/resolvespec-js
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.validate.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
|
||||||
|
- name: Enable pnpm
|
||||||
|
run: corepack enable
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: pnpm test
|
||||||
|
|
||||||
|
- name: Set version
|
||||||
|
run: npm version "$VERSION" --no-git-tag-version --allow-same-version
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
if: ${{ env.PUBLISH == 'true' }}
|
||||||
|
run: |
|
||||||
|
host="${SERVER_URL#*://}"
|
||||||
|
registry="${SERVER_URL}/api/packages/${OWNER}/npm/"
|
||||||
|
npm config set "@warkypublic:registry" "$registry"
|
||||||
|
npm config set "//${host}/api/packages/${OWNER}/npm/:_authToken" "$TOKEN"
|
||||||
|
npm publish --registry "$registry"
|
||||||
|
|
||||||
|
python:
|
||||||
|
name: Python (PyPI)
|
||||||
|
needs: validate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
working-directory: clients/resolvespec-python
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.validate.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: pip install -e ".[dev]" build twine
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: pytest
|
||||||
|
|
||||||
|
- name: Set version
|
||||||
|
run: sed -i -E "s/^version = \".*\"/version = \"${VERSION}\"/" pyproject.toml
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: python -m build
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
if: ${{ env.PUBLISH == 'true' }}
|
||||||
|
run: |
|
||||||
|
twine upload \
|
||||||
|
--repository-url "${SERVER_URL}/api/packages/${OWNER}/pypi" \
|
||||||
|
-u "$REGISTRY_USER" -p "$TOKEN" \
|
||||||
|
dist/*
|
||||||
|
|
||||||
|
rust:
|
||||||
|
name: Rust (Cargo)
|
||||||
|
needs: validate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
working-directory: clients/resolvespec-rs
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.validate.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: cargo test
|
||||||
|
|
||||||
|
- name: Set version
|
||||||
|
run: sed -i -E '0,/^version = ".*"/s//version = "'"${VERSION}"'"/' Cargo.toml
|
||||||
|
|
||||||
|
- name: Package
|
||||||
|
run: cargo package --allow-dirty
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
if: ${{ env.PUBLISH == 'true' }}
|
||||||
|
env:
|
||||||
|
CARGO_REGISTRIES_GITEA_INDEX: sparse+${{ github.server_url }}/api/packages/${{ github.repository_owner }}/cargo/
|
||||||
|
run: |
|
||||||
|
export CARGO_REGISTRIES_GITEA_TOKEN="Bearer ${TOKEN}"
|
||||||
|
cargo publish --registry gitea --allow-dirty
|
||||||
|
|
||||||
|
dotnet:
|
||||||
|
name: C# (NuGet)
|
||||||
|
needs: validate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
working-directory: clients/resolvespec-cs
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.validate.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: "8.0.x"
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: dotnet test tests/ResolveSpec.Tests.csproj
|
||||||
|
|
||||||
|
- name: Pack
|
||||||
|
run: dotnet pack src/ResolveSpec.csproj -c Release -p:Version="$VERSION" -o out
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
if: ${{ env.PUBLISH == 'true' }}
|
||||||
|
run: |
|
||||||
|
dotnet nuget push out/*.nupkg \
|
||||||
|
--source "${SERVER_URL}/api/packages/${OWNER}/nuget/index.json" \
|
||||||
|
--api-key "$TOKEN" \
|
||||||
|
--skip-duplicate
|
||||||
|
|
||||||
|
go:
|
||||||
|
name: Go (Go registry)
|
||||||
|
needs: validate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
working-directory: clients/resolvespec-go
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.validate.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: clients/resolvespec-go/go.mod
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test ./...
|
||||||
|
|
||||||
|
- name: Build module zip
|
||||||
|
run: |
|
||||||
|
python3 - <<'PY'
|
||||||
|
import os, re, zipfile
|
||||||
|
version = "v" + os.environ["VERSION"]
|
||||||
|
module = re.search(r"^module\s+(\S+)", open("go.mod").read(), re.M).group(1)
|
||||||
|
prefix = f"{module}@{version}/"
|
||||||
|
with zipfile.ZipFile("../resolvespec-go.zip", "w", zipfile.ZIP_DEFLATED) as z:
|
||||||
|
for root, dirs, files in os.walk("."):
|
||||||
|
dirs[:] = [d for d in dirs if d != ".git"]
|
||||||
|
for f in files:
|
||||||
|
path = os.path.join(root, f)
|
||||||
|
z.write(path, prefix + os.path.relpath(path, "."))
|
||||||
|
PY
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
if: ${{ env.PUBLISH == 'true' }}
|
||||||
|
run: |
|
||||||
|
curl -f -X PUT \
|
||||||
|
--user "${REGISTRY_USER}:${TOKEN}" \
|
||||||
|
--upload-file ../resolvespec-go.zip \
|
||||||
|
"${SERVER_URL}/api/packages/${OWNER}/go/upload"
|
||||||
|
|
||||||
|
dart:
|
||||||
|
name: Dart (Pub)
|
||||||
|
needs: validate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
working-directory: clients/resolvespec-dart
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.validate.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: dart-lang/setup-dart@v1
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: dart pub get
|
||||||
|
|
||||||
|
- name: Analyze
|
||||||
|
run: dart analyze
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: dart test
|
||||||
|
|
||||||
|
- name: Set version and registry
|
||||||
|
run: |
|
||||||
|
sed -i -E "s/^version: .*/version: ${VERSION}/" pubspec.yaml
|
||||||
|
sed -i -E "s#^publish_to: .*#publish_to: ${SERVER_URL}/api/packages/${OWNER}/pub#" pubspec.yaml
|
||||||
|
if ! grep -q "^## ${VERSION}\$" CHANGELOG.md; then
|
||||||
|
{ head -n 1 CHANGELOG.md; printf '\n## %s\n\n- Release %s.\n' "$VERSION" "$VERSION"; tail -n +2 CHANGELOG.md; } > CHANGELOG.tmp
|
||||||
|
mv CHANGELOG.tmp CHANGELOG.md
|
||||||
|
fi
|
||||||
|
# pub warns about a dirty git tree; commit the stamped files locally (never pushed)
|
||||||
|
git -c user.name=ci -c user.email=ci@localhost commit -q -am "ci: stamp dart version ${VERSION}"
|
||||||
|
|
||||||
|
- name: Dry run
|
||||||
|
if: ${{ env.PUBLISH != 'true' }}
|
||||||
|
run: dart pub publish --dry-run
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
if: ${{ env.PUBLISH == 'true' }}
|
||||||
|
run: |
|
||||||
|
dart pub token add "${SERVER_URL}/api/packages/${OWNER}/pub" --env-var TOKEN
|
||||||
|
dart pub publish --force
|
||||||
@@ -9,9 +9,9 @@ jobs:
|
|||||||
name: Unit Tests
|
name: Unit Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version: "1.24"
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
go test ./pkg/resolvespec ./pkg/restheadspec -coverprofile=coverage.out
|
go test ./pkg/resolvespec ./pkg/restheadspec -coverprofile=coverage.out
|
||||||
go tool cover -html=coverage.out -o coverage.html
|
go tool cover -html=coverage.out -o coverage.html
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v3
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
name: coverage-report
|
name: coverage-report
|
||||||
@@ -31,15 +31,16 @@ jobs:
|
|||||||
name: Race Detector
|
name: Race Detector
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version: "1.24"
|
||||||
- name: Run unit tests with the race detector
|
- name: Run unit tests with the race detector
|
||||||
run: go test -race -count=1 ./pkg/...
|
run: go test -race -count=1 ./pkg/...
|
||||||
integration-tests:
|
integration-tests:
|
||||||
name: Integration Tests
|
name: Integration Tests
|
||||||
|
if: false # disabled for now
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
@@ -56,46 +57,51 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version: "1.24"
|
||||||
|
- name: Install PostgreSQL client
|
||||||
|
run: |
|
||||||
|
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
|
||||||
|
$SUDO apt-get update -qq
|
||||||
|
$SUDO apt-get install -y -qq postgresql-client
|
||||||
- name: Create test databases
|
- name: Create test databases
|
||||||
env:
|
env:
|
||||||
PGPASSWORD: postgres
|
PGPASSWORD: postgres
|
||||||
run: |
|
run: |
|
||||||
psql -h localhost -U postgres -c "CREATE DATABASE resolvespec_test;"
|
psql -h postgres -U postgres -c "CREATE DATABASE resolvespec_test;"
|
||||||
psql -h localhost -U postgres -c "CREATE DATABASE restheadspec_test;"
|
psql -h postgres -U postgres -c "CREATE DATABASE restheadspec_test;"
|
||||||
- name: Run resolvespec integration tests
|
- name: Run resolvespec integration tests
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
env:
|
env:
|
||||||
TEST_DATABASE_URL: "host=localhost user=postgres password=postgres dbname=resolvespec_test port=5432 sslmode=disable"
|
TEST_DATABASE_URL: "host=postgres user=postgres password=postgres dbname=resolvespec_test port=5432 sslmode=disable"
|
||||||
run: go test -tags=integration ./pkg/resolvespec -v -coverprofile=coverage-resolvespec-integration.out
|
run: go test -tags=integration ./pkg/resolvespec -v -coverprofile=coverage-resolvespec-integration.out
|
||||||
- name: Run restheadspec integration tests
|
- name: Run restheadspec integration tests
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
env:
|
env:
|
||||||
TEST_DATABASE_URL: "host=localhost user=postgres password=postgres dbname=restheadspec_test port=5432 sslmode=disable"
|
TEST_DATABASE_URL: "host=postgres user=postgres password=postgres dbname=restheadspec_test port=5432 sslmode=disable"
|
||||||
run: go test -tags=integration ./pkg/restheadspec -v -coverprofile=coverage-restheadspec-integration.out
|
run: go test -tags=integration ./pkg/restheadspec -v -coverprofile=coverage-restheadspec-integration.out
|
||||||
- name: Generate integration coverage
|
- name: Generate integration coverage
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
env:
|
env:
|
||||||
TEST_DATABASE_URL: "host=localhost user=postgres password=postgres dbname=resolvespec_test port=5432 sslmode=disable"
|
TEST_DATABASE_URL: "host=postgres user=postgres password=postgres dbname=resolvespec_test port=5432 sslmode=disable"
|
||||||
run: |
|
run: |
|
||||||
go tool cover -html=coverage-resolvespec-integration.out -o coverage-resolvespec-integration.html
|
go tool cover -html=coverage-resolvespec-integration.out -o coverage-resolvespec-integration.html
|
||||||
go tool cover -html=coverage-restheadspec-integration.out -o coverage-restheadspec-integration.html
|
go tool cover -html=coverage-restheadspec-integration.out -o coverage-restheadspec-integration.html
|
||||||
|
|
||||||
- name: Upload resolvespec integration coverage
|
- name: Upload resolvespec integration coverage
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v3
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
name: resolvespec-integration-coverage-report
|
name: resolvespec-integration-coverage-report
|
||||||
path: coverage-resolvespec-integration.html
|
path: coverage-resolvespec-integration.html
|
||||||
|
|
||||||
- name: Upload restheadspec integration coverage
|
- name: Upload restheadspec integration coverage
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v3
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
with:
|
with:
|
||||||
name: integration-coverage-restheadspec-report
|
name: integration-coverage-restheadspec-report
|
||||||
path: coverage-restheadspec-integration
|
path: coverage-restheadspec-integration.html
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 0.1.0
|
||||||
|
|
||||||
|
- Initial release: ResolveSpec (JSON body) and FunctionSpec client.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Hein
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
name: resolvespec
|
name: resolvespec
|
||||||
description: Client for ResolveSpec (JSON body) and FunctionSpec endpoints.
|
description: Client for ResolveSpec (JSON body) and FunctionSpec endpoints.
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
|
repository: https://git.warky.dev/wdevs/ResolveSpec
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# resolvespec-go
|
# resolvespec-go
|
||||||
|
|
||||||
Go client for ResolveSpec (JSON body) and FunctionSpec. Module: `github.com/bitechdev/ResolveSpec/clients/resolvespec-go`. Stdlib only.
|
Go client for ResolveSpec (JSON body) and FunctionSpec. Module: `git.warky.dev/wdevs/ResolveSpec/clients/resolvespec-go`. Stdlib only.
|
||||||
|
|
||||||
## Clients
|
## Clients
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
module github.com/bitechdev/ResolveSpec/clients/resolvespec-go
|
module git.warky.dev/wdevs/ResolveSpec/clients/resolvespec-go
|
||||||
|
|
||||||
go 1.22
|
go 1.22
|
||||||
|
|||||||
@@ -1507,6 +1507,13 @@ func (b *BunInsertQuery) OnConflict(action string) common.InsertQuery {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BunInsertQuery) ExcludeColumn(columns ...string) common.InsertQuery {
|
||||||
|
if len(columns) > 0 {
|
||||||
|
b.query = b.query.ExcludeColumn(columns...)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
func (b *BunInsertQuery) Returning(columns ...string) common.InsertQuery {
|
func (b *BunInsertQuery) Returning(columns ...string) common.InsertQuery {
|
||||||
if len(columns) > 0 {
|
if len(columns) > 0 {
|
||||||
b.query = b.query.Returning(strings.Join(columns, ", "))
|
b.query = b.query.Returning(strings.Join(columns, ", "))
|
||||||
@@ -1619,6 +1626,13 @@ func (b *BunUpdateQuery) SetMap(values map[string]interface{}) common.UpdateQuer
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BunUpdateQuery) ExcludeColumn(columns ...string) common.UpdateQuery {
|
||||||
|
if len(columns) > 0 {
|
||||||
|
b.query = b.query.ExcludeColumn(columns...)
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
func (b *BunUpdateQuery) Where(query string, args ...interface{}) common.UpdateQuery {
|
func (b *BunUpdateQuery) Where(query string, args ...interface{}) common.UpdateQuery {
|
||||||
b.query = b.query.Where(query, args...)
|
b.query = b.query.Where(query, args...)
|
||||||
return b
|
return b
|
||||||
|
|||||||
@@ -751,6 +751,13 @@ func (g *GormInsertQuery) OnConflict(action string) common.InsertQuery {
|
|||||||
return g
|
return g
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *GormInsertQuery) ExcludeColumn(columns ...string) common.InsertQuery {
|
||||||
|
if len(columns) > 0 {
|
||||||
|
g.db = g.db.Omit(columns...)
|
||||||
|
}
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
func (g *GormInsertQuery) Returning(columns ...string) common.InsertQuery {
|
func (g *GormInsertQuery) Returning(columns ...string) common.InsertQuery {
|
||||||
g.returningColumns = columns
|
g.returningColumns = columns
|
||||||
return g
|
return g
|
||||||
@@ -930,6 +937,13 @@ func (g *GormUpdateQuery) SetMap(values map[string]interface{}) common.UpdateQue
|
|||||||
return g
|
return g
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *GormUpdateQuery) ExcludeColumn(columns ...string) common.UpdateQuery {
|
||||||
|
if len(columns) > 0 {
|
||||||
|
g.db = g.db.Omit(columns...)
|
||||||
|
}
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
func (g *GormUpdateQuery) Where(query string, args ...interface{}) common.UpdateQuery {
|
func (g *GormUpdateQuery) Where(query string, args ...interface{}) common.UpdateQuery {
|
||||||
g.db = g.db.Where(query, args...)
|
g.db = g.db.Where(query, args...)
|
||||||
return g
|
return g
|
||||||
|
|||||||
@@ -691,6 +691,13 @@ func (p *PgSQLInsertQuery) OnConflict(action string) common.InsertQuery {
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *PgSQLInsertQuery) ExcludeColumn(columns ...string) common.InsertQuery {
|
||||||
|
for _, col := range columns {
|
||||||
|
delete(p.values, col)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
func (p *PgSQLInsertQuery) Returning(columns ...string) common.InsertQuery {
|
func (p *PgSQLInsertQuery) Returning(columns ...string) common.InsertQuery {
|
||||||
p.returning = columns
|
p.returning = columns
|
||||||
return p
|
return p
|
||||||
@@ -850,6 +857,13 @@ func (p *PgSQLUpdateQuery) Set(column string, value interface{}) common.UpdateQu
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *PgSQLUpdateQuery) ExcludeColumn(columns ...string) common.UpdateQuery {
|
||||||
|
for _, col := range columns {
|
||||||
|
delete(p.sets, col)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
func (p *PgSQLUpdateQuery) SetMap(values map[string]interface{}) common.UpdateQuery {
|
func (p *PgSQLUpdateQuery) SetMap(values map[string]interface{}) common.UpdateQuery {
|
||||||
pkName := ""
|
pkName := ""
|
||||||
if p.model != nil {
|
if p.model != nil {
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ type InsertQuery interface {
|
|||||||
Table(table string) InsertQuery
|
Table(table string) InsertQuery
|
||||||
Value(column string, value interface{}) InsertQuery
|
Value(column string, value interface{}) InsertQuery
|
||||||
OnConflict(action string) InsertQuery
|
OnConflict(action string) InsertQuery
|
||||||
|
// ExcludeColumn omits columns from a Model()-based INSERT (e.g. generated columns).
|
||||||
|
ExcludeColumn(columns ...string) InsertQuery
|
||||||
Returning(columns ...string) InsertQuery
|
Returning(columns ...string) InsertQuery
|
||||||
|
|
||||||
// Execution
|
// Execution
|
||||||
@@ -94,6 +96,8 @@ type UpdateQuery interface {
|
|||||||
Table(table string) UpdateQuery
|
Table(table string) UpdateQuery
|
||||||
Set(column string, value interface{}) UpdateQuery
|
Set(column string, value interface{}) UpdateQuery
|
||||||
SetMap(values map[string]interface{}) UpdateQuery
|
SetMap(values map[string]interface{}) UpdateQuery
|
||||||
|
// ExcludeColumn omits columns from a Model()-based UPDATE (e.g. generated columns).
|
||||||
|
ExcludeColumn(columns ...string) UpdateQuery
|
||||||
Where(query string, args ...interface{}) UpdateQuery
|
Where(query string, args ...interface{}) UpdateQuery
|
||||||
Returning(columns ...string) UpdateQuery
|
Returning(columns ...string) UpdateQuery
|
||||||
|
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ func (p *NestedCUDProcessor) ProcessNestedCUD(
|
|||||||
case "insert", "create", "add":
|
case "insert", "create", "add":
|
||||||
// Only perform insert if we have data to insert
|
// Only perform insert if we have data to insert
|
||||||
if hasData {
|
if hasData {
|
||||||
id, err := p.processInsert(ctx, regularData, tableName)
|
id, err := p.processInsert(ctx, regularData, model, tableName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Insert failed for table=%s, data=%+v, error=%v", tableName, regularData, err)
|
logger.Error("Insert failed for table=%s, data=%+v, error=%v", tableName, regularData, err)
|
||||||
return nil, fmt.Errorf("insert failed: %w", err)
|
return nil, fmt.Errorf("insert failed: %w", err)
|
||||||
@@ -148,7 +148,7 @@ func (p *NestedCUDProcessor) ProcessNestedCUD(
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
if hasData {
|
if hasData {
|
||||||
rows, err := p.processUpdate(ctx, regularData, tableName, data[pkName])
|
rows, err := p.processUpdate(ctx, regularData, model, tableName, data[pkName])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Update failed for table=%s, id=%v, data=%+v, error=%v", tableName, data[pkName], regularData, err)
|
logger.Error("Update failed for table=%s, id=%v, data=%+v, error=%v", tableName, data[pkName], regularData, err)
|
||||||
return nil, fmt.Errorf("update failed: %w", err)
|
return nil, fmt.Errorf("update failed: %w", err)
|
||||||
@@ -295,10 +295,12 @@ func (p *NestedCUDProcessor) injectForeignKeys(data map[string]interface{}, mode
|
|||||||
func (p *NestedCUDProcessor) processInsert(
|
func (p *NestedCUDProcessor) processInsert(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
data map[string]interface{},
|
data map[string]interface{},
|
||||||
|
model interface{},
|
||||||
tableName string,
|
tableName string,
|
||||||
) (interface{}, error) {
|
) (interface{}, error) {
|
||||||
logger.Debug("Inserting into %s with data: %+v", tableName, data)
|
logger.Debug("Inserting into %s with data: %+v", tableName, data)
|
||||||
|
|
||||||
|
reflection.RemoveNonWritableColumns(model, data)
|
||||||
query := p.db.NewInsert().Table(tableName)
|
query := p.db.NewInsert().Table(tableName)
|
||||||
|
|
||||||
for key, value := range data {
|
for key, value := range data {
|
||||||
@@ -335,6 +337,7 @@ func (p *NestedCUDProcessor) processSelect(ctx context.Context, tableName string
|
|||||||
func (p *NestedCUDProcessor) processUpdate(
|
func (p *NestedCUDProcessor) processUpdate(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
data map[string]interface{},
|
data map[string]interface{},
|
||||||
|
model interface{},
|
||||||
tableName string,
|
tableName string,
|
||||||
id interface{},
|
id interface{},
|
||||||
) (int64, error) {
|
) (int64, error) {
|
||||||
@@ -345,6 +348,7 @@ func (p *NestedCUDProcessor) processUpdate(
|
|||||||
|
|
||||||
logger.Debug("Updating %s with ID %v, data: %+v", tableName, id, data)
|
logger.Debug("Updating %s with ID %v, data: %+v", tableName, id, data)
|
||||||
|
|
||||||
|
reflection.RemoveNonWritableColumns(model, data)
|
||||||
query := p.db.NewUpdate().Table(tableName).SetMap(data).Where(fmt.Sprintf("%s = ?", QuoteIdent(reflection.GetPrimaryKeyName(tableName))), id)
|
query := p.db.NewUpdate().Table(tableName).SetMap(data).Where(fmt.Sprintf("%s = ?", QuoteIdent(reflection.GetPrimaryKeyName(tableName))), id)
|
||||||
|
|
||||||
result, err := query.Exec(ctx)
|
result, err := query.Exec(ctx)
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ func (m *mockInsertQuery) Value(column string, value interface{}) InsertQuery {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
func (m *mockInsertQuery) OnConflict(action string) InsertQuery { return m }
|
func (m *mockInsertQuery) OnConflict(action string) InsertQuery { return m }
|
||||||
|
func (m *mockInsertQuery) ExcludeColumn(columns ...string) InsertQuery { return m }
|
||||||
func (m *mockInsertQuery) Returning(columns ...string) InsertQuery { return m }
|
func (m *mockInsertQuery) Returning(columns ...string) InsertQuery { return m }
|
||||||
func (m *mockInsertQuery) Exec(ctx context.Context) (Result, error) {
|
func (m *mockInsertQuery) Exec(ctx context.Context) (Result, error) {
|
||||||
m.db.insertCalls = append(m.db.insertCalls, m.values)
|
m.db.insertCalls = append(m.db.insertCalls, m.values)
|
||||||
@@ -131,6 +132,7 @@ func (m *mockUpdateQuery) SetMap(values map[string]interface{}) UpdateQuery {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
func (m *mockUpdateQuery) Where(condition string, args ...interface{}) UpdateQuery { return m }
|
func (m *mockUpdateQuery) Where(condition string, args ...interface{}) UpdateQuery { return m }
|
||||||
|
func (m *mockUpdateQuery) ExcludeColumn(columns ...string) UpdateQuery { return m }
|
||||||
func (m *mockUpdateQuery) Returning(columns ...string) UpdateQuery { return m }
|
func (m *mockUpdateQuery) Returning(columns ...string) UpdateQuery { return m }
|
||||||
func (m *mockUpdateQuery) Exec(ctx context.Context) (Result, error) {
|
func (m *mockUpdateQuery) Exec(ctx context.Context) (Result, error) {
|
||||||
// Record the update call
|
// Record the update call
|
||||||
|
|||||||
@@ -895,6 +895,9 @@ func (h *Handler) create(hookCtx *HookContext) (interface{}, error) {
|
|||||||
|
|
||||||
// Insert record
|
// Insert record
|
||||||
query := hookCtx.Tx.NewInsert().Model(hookCtx.ModelPtr).Table(hookCtx.TableName)
|
query := hookCtx.Tx.NewInsert().Model(hookCtx.ModelPtr).Table(hookCtx.TableName)
|
||||||
|
if generated := reflection.NonWritableColumns(hookCtx.Model); len(generated) > 0 {
|
||||||
|
query = query.ExcludeColumn(generated...)
|
||||||
|
}
|
||||||
if _, err := query.Exec(hookCtx.Context); err != nil {
|
if _, err := query.Exec(hookCtx.Context); err != nil {
|
||||||
return nil, fmt.Errorf("failed to create record: %w", err)
|
return nil, fmt.Errorf("failed to create record: %w", err)
|
||||||
}
|
}
|
||||||
@@ -924,6 +927,8 @@ func (h *Handler) update(hookCtx *HookContext) error {
|
|||||||
// the stored value unless disallowNulls is set, in which case null is skipped.
|
// the stored value unless disallowNulls is set, in which case null is skipped.
|
||||||
values := common.MergeUpdateValues(make(map[string]interface{}, len(updates)), updates, h.disallowNulls)
|
values := common.MergeUpdateValues(make(map[string]interface{}, len(updates)), updates, h.disallowNulls)
|
||||||
|
|
||||||
|
reflection.RemoveNonWritableColumns(hookCtx.Model, values)
|
||||||
|
|
||||||
if len(values) > 0 {
|
if len(values) > 0 {
|
||||||
query := hookCtx.Tx.NewUpdate().Table(hookCtx.TableName).SetMap(values).
|
query := hookCtx.Tx.NewUpdate().Table(hookCtx.TableName).SetMap(values).
|
||||||
Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), hookCtx.ID)
|
Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), hookCtx.ID)
|
||||||
|
|||||||
@@ -656,7 +656,7 @@ func isColumnWritableInType(typ reflect.Type, columnName string) (found bool, wr
|
|||||||
// Check bun tag for scanonly
|
// Check bun tag for scanonly
|
||||||
bunTag := field.Tag.Get("bun")
|
bunTag := field.Tag.Get("bun")
|
||||||
if bunTag != "" {
|
if bunTag != "" {
|
||||||
if isBunFieldScanOnly(bunTag) {
|
if isBunFieldScanOnly(bunTag) || isBunFieldGenerated(bunTag) {
|
||||||
return true, false
|
return true, false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -689,6 +689,70 @@ func isBunFieldScanOnly(tag string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isBunFieldGenerated checks if a bun tag marks the column as database-generated
|
||||||
|
// (GENERATED ALWAYS AS ... STORED), which can be read but never written.
|
||||||
|
// Example: "email_normalized,generated" -> true
|
||||||
|
func isBunFieldGenerated(tag string) bool {
|
||||||
|
for _, part := range strings.Split(tag, ",") {
|
||||||
|
if strings.TrimSpace(part) == "generated" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveNonWritableColumns deletes from values every key that maps to a
|
||||||
|
// non-writable model column (bun scanonly/generated, gorm read-only). Used
|
||||||
|
// before writing a read-merged record back with UPDATE ... SET.
|
||||||
|
func RemoveNonWritableColumns(model any, values map[string]interface{}) {
|
||||||
|
for key := range values {
|
||||||
|
if !IsColumnWritable(model, key) {
|
||||||
|
delete(values, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NonWritableColumns returns the column names of the model that cannot be
|
||||||
|
// written (bun scanonly/generated, gorm read-only), including embedded structs.
|
||||||
|
func NonWritableColumns(model any) []string {
|
||||||
|
t := reflect.TypeOf(model)
|
||||||
|
for t != nil && (t.Kind() == reflect.Pointer || t.Kind() == reflect.Slice || t.Kind() == reflect.Array) {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
if t == nil || t.Kind() != reflect.Struct {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var cols []string
|
||||||
|
collectNonWritable(t, &cols)
|
||||||
|
return cols
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectNonWritable(typ reflect.Type, cols *[]string) {
|
||||||
|
for i := 0; i < typ.NumField(); i++ {
|
||||||
|
field := typ.Field(i)
|
||||||
|
if field.Anonymous {
|
||||||
|
ft := field.Type
|
||||||
|
if ft.Kind() == reflect.Pointer {
|
||||||
|
ft = ft.Elem()
|
||||||
|
}
|
||||||
|
if ft.Kind() == reflect.Struct {
|
||||||
|
collectNonWritable(ft, cols)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bunTag, gormTag := field.Tag.Get("bun"), field.Tag.Get("gorm")
|
||||||
|
if bunTag == "-" || gormTag == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (bunTag != "" && (isBunFieldScanOnly(bunTag) || isBunFieldGenerated(bunTag))) ||
|
||||||
|
(gormTag != "" && isGormFieldReadOnly(gormTag)) {
|
||||||
|
if name := getColumnNameFromField(field); name != "" {
|
||||||
|
*cols = append(*cols, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// isGormFieldReadOnly checks if a gorm tag indicates the field is read-only
|
// isGormFieldReadOnly checks if a gorm tag indicates the field is read-only
|
||||||
// Examples:
|
// Examples:
|
||||||
// - "<-:false" -> true (no writes allowed)
|
// - "<-:false" -> true (no writes allowed)
|
||||||
|
|||||||
@@ -1920,3 +1920,46 @@ func TestMapToStruct_Errors(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRemoveNonWritableColumns_Generated(t *testing.T) {
|
||||||
|
type m struct {
|
||||||
|
ID int `bun:"id,pk"`
|
||||||
|
Email string `bun:"email"`
|
||||||
|
Norm string `bun:"email_normalized,generated"`
|
||||||
|
Scan string `bun:"scan_col,scanonly"`
|
||||||
|
}
|
||||||
|
vals := map[string]interface{}{"id": 1, "email": "A", "email_normalized": "a", "scan_col": "x", "dynamic": 1}
|
||||||
|
RemoveNonWritableColumns(&m{}, vals)
|
||||||
|
if _, ok := vals["email_normalized"]; ok {
|
||||||
|
t.Error("generated column not removed")
|
||||||
|
}
|
||||||
|
if _, ok := vals["scan_col"]; ok {
|
||||||
|
t.Error("scanonly column not removed")
|
||||||
|
}
|
||||||
|
if len(vals) != 3 {
|
||||||
|
t.Errorf("unexpected keys: %v", vals)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNonWritableColumns(t *testing.T) {
|
||||||
|
type base struct {
|
||||||
|
Created string `bun:"created_at,scanonly"`
|
||||||
|
}
|
||||||
|
type m struct {
|
||||||
|
base
|
||||||
|
ID int `bun:"id,pk"`
|
||||||
|
Email string `bun:"email"`
|
||||||
|
Norm string `bun:"email_normalized,generated"`
|
||||||
|
Ro string `gorm:"column:ro;->"`
|
||||||
|
}
|
||||||
|
got := NonWritableColumns(&m{})
|
||||||
|
want := map[string]bool{"created_at": true, "email_normalized": true, "ro": true}
|
||||||
|
if len(got) != len(want) {
|
||||||
|
t.Fatalf("got %v", got)
|
||||||
|
}
|
||||||
|
for _, c := range got {
|
||||||
|
if !want[c] {
|
||||||
|
t.Errorf("unexpected %s", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -559,6 +559,7 @@ func (h *Handler) executeCreate(ctx context.Context, schema, entity string, data
|
|||||||
if len(cols) == 0 {
|
if len(cols) == 0 {
|
||||||
return invalidArg("no writable fields in data")
|
return invalidArg("no writable fields in data")
|
||||||
}
|
}
|
||||||
|
reflection.RemoveNonWritableColumns(model, cols)
|
||||||
q := tx.NewInsert().Table(tableName)
|
q := tx.NewInsert().Table(tableName)
|
||||||
for key, value := range cols {
|
for key, value := range cols {
|
||||||
q = q.Value(key, value)
|
q = q.Value(key, value)
|
||||||
@@ -726,6 +727,7 @@ func (h *Handler) executeUpdate(ctx context.Context, schema, entity, id string,
|
|||||||
existingMap[key] = v
|
existingMap[key] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reflection.RemoveNonWritableColumns(model, setCols)
|
||||||
q := tx.NewUpdate().Table(tableName).SetMap(setCols).
|
q := tx.NewUpdate().Table(tableName).SetMap(setCols).
|
||||||
Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), id)
|
Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), id)
|
||||||
res, err := q.Exec(ctx)
|
res, err := q.Exec(ctx)
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ func (h *Handler) executeWhere(ctx context.Context, req whereRequest) (_ *whereR
|
|||||||
cond := fmt.Sprintf("%s IN (%s)", common.QuoteIdent(pkName), strings.Join(inList, ", "))
|
cond := fmt.Sprintf("%s IN (%s)", common.QuoteIdent(pkName), strings.Join(inList, ", "))
|
||||||
var affected int64
|
var affected int64
|
||||||
if req.op == "update" {
|
if req.op == "update" {
|
||||||
|
reflection.RemoveNonWritableColumns(model, setCols)
|
||||||
r, err := tx.NewUpdate().Table(tableName).SetMap(setCols).Where(cond, ids...).Exec(ctx)
|
r, err := tx.NewUpdate().Table(tableName).SetMap(setCols).Where(cond, ids...).Exec(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error updating records: %w", err)
|
return fmt.Errorf("error updating records: %w", err)
|
||||||
|
|||||||
@@ -824,6 +824,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
}
|
}
|
||||||
responseData = v
|
responseData = v
|
||||||
|
|
||||||
|
reflection.RemoveNonWritableColumns(model, v)
|
||||||
query := tx.NewInsert().Table(tableName)
|
query := tx.NewInsert().Table(tableName)
|
||||||
for key, value := range v {
|
for key, value := range v {
|
||||||
query = query.Value(key, common.ConvertSliceForBun(value))
|
query = query.Value(key, common.ConvertSliceForBun(value))
|
||||||
@@ -971,6 +972,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
item = modifiedData
|
item = modifiedData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reflection.RemoveNonWritableColumns(model, item)
|
||||||
txQuery := tx.NewInsert().Table(tableName)
|
txQuery := tx.NewInsert().Table(tableName)
|
||||||
for key, value := range item {
|
for key, value := range item {
|
||||||
txQuery = txQuery.Value(key, common.ConvertSliceForBun(value))
|
txQuery = txQuery.Value(key, common.ConvertSliceForBun(value))
|
||||||
@@ -1127,6 +1129,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
itemMap = modifiedData
|
itemMap = modifiedData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reflection.RemoveNonWritableColumns(model, itemMap)
|
||||||
txQuery := tx.NewInsert().Table(tableName)
|
txQuery := tx.NewInsert().Table(tableName)
|
||||||
for key, value := range itemMap {
|
for key, value := range itemMap {
|
||||||
txQuery = txQuery.Value(key, common.ConvertSliceForBun(value))
|
txQuery = txQuery.Value(key, common.ConvertSliceForBun(value))
|
||||||
@@ -1322,6 +1325,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
|||||||
|
|
||||||
// Overwrite with every key present in the request (including "" and null unless disallowed)
|
// Overwrite with every key present in the request (including "" and null unless disallowed)
|
||||||
common.MergeUpdateValues(existingMap, updates, h.disallowNulls)
|
common.MergeUpdateValues(existingMap, updates, h.disallowNulls)
|
||||||
|
reflection.RemoveNonWritableColumns(model, existingMap)
|
||||||
|
|
||||||
// Build update query with merged data
|
// Build update query with merged data
|
||||||
query := tx.NewUpdate().Table(tableName).SetMap(existingMap)
|
query := tx.NewUpdate().Table(tableName).SetMap(existingMap)
|
||||||
@@ -1507,6 +1511,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
|||||||
|
|
||||||
// Overwrite with every key present in the request (including "" and null unless disallowed)
|
// Overwrite with every key present in the request (including "" and null unless disallowed)
|
||||||
common.MergeUpdateValues(existingMap, item, h.disallowNulls)
|
common.MergeUpdateValues(existingMap, item, h.disallowNulls)
|
||||||
|
reflection.RemoveNonWritableColumns(model, existingMap)
|
||||||
|
|
||||||
txQuery := tx.NewUpdate().Table(tableName).SetMap(existingMap).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID)
|
txQuery := tx.NewUpdate().Table(tableName).SetMap(existingMap).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID)
|
||||||
if _, err := txQuery.Exec(ctx); err != nil {
|
if _, err := txQuery.Exec(ctx); err != nil {
|
||||||
@@ -1662,6 +1667,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
|
|||||||
|
|
||||||
// Overwrite with every key present in the request (including "" and null unless disallowed)
|
// Overwrite with every key present in the request (including "" and null unless disallowed)
|
||||||
common.MergeUpdateValues(existingMap, itemMap, h.disallowNulls)
|
common.MergeUpdateValues(existingMap, itemMap, h.disallowNulls)
|
||||||
|
reflection.RemoveNonWritableColumns(model, existingMap)
|
||||||
|
|
||||||
txQuery := tx.NewUpdate().Table(tableName).SetMap(existingMap).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID)
|
txQuery := tx.NewUpdate().Table(tableName).SetMap(existingMap).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), itemID)
|
||||||
if _, err := txQuery.Exec(ctx); err != nil {
|
if _, err := txQuery.Exec(ctx); err != nil {
|
||||||
|
|||||||
@@ -1410,6 +1410,9 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
if provider, ok := modelValue.(common.TableNameProvider); !ok || provider.TableName() == "" {
|
if provider, ok := modelValue.(common.TableNameProvider); !ok || provider.TableName() == "" {
|
||||||
query = query.Table(tableName)
|
query = query.Table(tableName)
|
||||||
}
|
}
|
||||||
|
if generated := reflection.NonWritableColumns(model); len(generated) > 0 {
|
||||||
|
query = query.ExcludeColumn(generated...)
|
||||||
|
}
|
||||||
fields := reflection.GetSQLModelColumns(model)
|
fields := reflection.GetSQLModelColumns(model)
|
||||||
query = query.Returning(fields...)
|
query = query.Returning(fields...)
|
||||||
|
|
||||||
@@ -1657,6 +1660,9 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
|
|||||||
|
|
||||||
// Create update query using Model() to preserve custom types and driver.Valuer interfaces
|
// Create update query using Model() to preserve custom types and driver.Valuer interfaces
|
||||||
query := tx.NewUpdate().Model(modelInstance)
|
query := tx.NewUpdate().Model(modelInstance)
|
||||||
|
if generated := reflection.NonWritableColumns(model); len(generated) > 0 {
|
||||||
|
query = query.ExcludeColumn(generated...)
|
||||||
|
}
|
||||||
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID)
|
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID)
|
||||||
|
|
||||||
// Execute BeforeScan hooks - pass query chain so hooks can modify it
|
// Execute BeforeScan hooks - pass query chain so hooks can modify it
|
||||||
|
|||||||
@@ -758,6 +758,9 @@ func (h *Handler) create(hookCtx *HookContext) (interface{}, error) {
|
|||||||
|
|
||||||
// Insert record
|
// Insert record
|
||||||
query := hookCtx.Tx.NewInsert().Model(hookCtx.ModelPtr).Table(hookCtx.TableName)
|
query := hookCtx.Tx.NewInsert().Model(hookCtx.ModelPtr).Table(hookCtx.TableName)
|
||||||
|
if generated := reflection.NonWritableColumns(hookCtx.Model); len(generated) > 0 {
|
||||||
|
query = query.ExcludeColumn(generated...)
|
||||||
|
}
|
||||||
if _, err := query.Exec(hookCtx.Context); err != nil {
|
if _, err := query.Exec(hookCtx.Context); err != nil {
|
||||||
return nil, fmt.Errorf("failed to create record: %w", err)
|
return nil, fmt.Errorf("failed to create record: %w", err)
|
||||||
}
|
}
|
||||||
@@ -786,6 +789,8 @@ func (h *Handler) update(hookCtx *HookContext) error {
|
|||||||
// the stored value unless disallowNulls is set, in which case null is skipped.
|
// the stored value unless disallowNulls is set, in which case null is skipped.
|
||||||
values := common.MergeUpdateValues(make(map[string]interface{}, len(updates)), updates, h.disallowNulls)
|
values := common.MergeUpdateValues(make(map[string]interface{}, len(updates)), updates, h.disallowNulls)
|
||||||
|
|
||||||
|
reflection.RemoveNonWritableColumns(hookCtx.Model, values)
|
||||||
|
|
||||||
if len(values) > 0 {
|
if len(values) > 0 {
|
||||||
query := hookCtx.Tx.NewUpdate().Table(hookCtx.TableName).SetMap(values).
|
query := hookCtx.Tx.NewUpdate().Table(hookCtx.TableName).SetMap(values).
|
||||||
Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), hookCtx.ID)
|
Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), hookCtx.ID)
|
||||||
|
|||||||
@@ -226,6 +226,11 @@ func (m *MockInsertQuery) OnConflict(action string) common.InsertQuery {
|
|||||||
return args.Get(0).(common.InsertQuery)
|
return args.Get(0).(common.InsertQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockInsertQuery) ExcludeColumn(columns ...string) common.InsertQuery {
|
||||||
|
args := m.Called(columns)
|
||||||
|
return args.Get(0).(common.InsertQuery)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MockInsertQuery) Returning(columns ...string) common.InsertQuery {
|
func (m *MockInsertQuery) Returning(columns ...string) common.InsertQuery {
|
||||||
args := m.Called(columns)
|
args := m.Called(columns)
|
||||||
return args.Get(0).(common.InsertQuery)
|
return args.Get(0).(common.InsertQuery)
|
||||||
@@ -254,6 +259,11 @@ func (m *MockUpdateQuery) Model(model interface{}) common.UpdateQuery {
|
|||||||
return args.Get(0).(common.UpdateQuery)
|
return args.Get(0).(common.UpdateQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockUpdateQuery) ExcludeColumn(columns ...string) common.UpdateQuery {
|
||||||
|
args := m.Called(columns)
|
||||||
|
return args.Get(0).(common.UpdateQuery)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MockUpdateQuery) Table(table string) common.UpdateQuery {
|
func (m *MockUpdateQuery) Table(table string) common.UpdateQuery {
|
||||||
args := m.Called(table)
|
args := m.Called(table)
|
||||||
return args.Get(0).(common.UpdateQuery)
|
return args.Get(0).(common.UpdateQuery)
|
||||||
|
|||||||
Reference in New Issue
Block a user