diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..6f17100d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,6 @@ +{ + "dockerComposeFile": "../docker-compose.yml", + "service": "dev", + "workspaceFolder": "/src", + "overrideCommand": true +} diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 6b8710a7..00000000 --- a/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -.git diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 00000000..46a6073c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,18 @@ +--- +name: "🐞 Bug report" +about: Report broken functionality or incorrect documentation +labels: "bug" +--- + +**Description** + + +- Version: +- Database: +- Operating System: + +**Steps To Reproduce** + + + +**Expected Behavior** diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..33d1f150 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: "🙋‍♀️ Question" + url: https://github.com/amacneil/dbmate/discussions + about: Search discussions or ask our community for help + - name: "🚀 Feature request" + url: https://github.com/amacneil/dbmate/discussions + about: Search existing feature requests or share a new idea diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..04efacea --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: docker + directory: / + schedule: + interval: weekly + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 34eb6daf..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: CI - -on: - push: - branches: [ master ] - tags: 'v*' - pull_request: - branches: [ master ] - -jobs: - build: - name: Build & Test - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Environment - run: | - set -x - docker version - docker-compose version - - - name: Cache - uses: actions/cache@v2 - with: - key: cache - path: .cache - - - name: Build docker image - run: | - set -x - docker-compose build - docker-compose run --rm --no-deps dbmate --version - - - name: Build binaries - run: | - set -x - docker-compose run --rm --no-deps dev make build-all - dist/dbmate-linux-amd64 --version - - - name: Lint - run: docker-compose run --rm --no-deps dev make lint - - - name: Start test dependencies - run: | - set -x - docker-compose pull --quiet - docker-compose up --detach - - - name: Run tests - run: | - set -x - docker-compose run --rm dev make wait - docker-compose run --rm dev make test - - - name: Release - uses: softprops/action-gh-release@v1 - if: ${{ startsWith(github.ref, 'refs/tags/v') }} - with: - files: dist/* - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..86ea6932 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,232 @@ +name: CI + +on: + push: + branches: [main] + tags: ["*"] + pull_request: + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - os: linux + image: ubuntu-latest + arch: 386 + setup: sudo apt-get update && sudo apt-get install -qq gcc-i686-linux-gnu + env: + CC: i686-linux-gnu-gcc + CXX: i686-linux-gnu-g++ + - os: linux + image: ubuntu-latest + arch: amd64 + env: {} + - os: linux + image: ubuntu-latest + arch: arm + setup: sudo apt-get update && sudo apt-get install -qq gcc-arm-linux-gnueabi + env: + CC: arm-linux-gnueabi-gcc + CXX: arm-linux-gnueabi-g++ + GOARM: 6 + - os: linux + image: ubuntu-latest + arch: arm64 + setup: sudo apt-get update && sudo apt-get install -qq gcc-aarch64-linux-gnu + env: + CC: aarch64-linux-gnu-gcc + CXX: aarch64-linux-gnu-g++ + - os: macos + image: macos-latest + arch: amd64 + env: {} + - os: macos + image: macos-latest + arch: arm64 + env: {} + - os: windows + image: windows-latest + arch: amd64 + setup: choco install sqlite + env: {} + + name: Build (${{ matrix.os }}/${{ matrix.arch }}) + runs-on: ${{ matrix.image }} + env: ${{ matrix.env }} + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Setup environment + run: ${{ matrix.setup }} + + - run: go mod download + + - run: make build ls + env: + GOARCH: ${{ matrix.arch }} + OUTPUT: dbmate-${{ matrix.os }}-${{ matrix.arch }} + + - run: make test + if: ${{ matrix.arch == 'amd64' }} + env: + GOARCH: ${{ matrix.arch }} + + - run: dist/dbmate-${{ matrix.os }}-${{ matrix.arch }} --help + if: ${{ matrix.arch == 'amd64' }} + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dbmate-${{ matrix.os }}-${{ matrix.arch }} + path: dist/dbmate-* + + - name: Publish binaries + uses: softprops/action-gh-release@v2 + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + with: + files: dist/dbmate-* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + docker: + name: Docker Test (linux/amd64) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Configure QEMU + uses: docker/setup-qemu-action@v3 + + - name: Configure Buildx + uses: docker/setup-buildx-action@v3 + + - name: Check Docker environment + run: | + set -x + docker version + docker buildx version + docker compose version + + - name: Build Docker image + run: | + set -x + docker compose build + docker compose run --rm --no-deps dbmate --version + + - name: Run make build + run: docker compose run --rm --no-deps dev make build ls + + - name: Run make lint + run: docker compose run --rm --no-deps dev make lint + + - name: Start test dependencies + run: | + set -x + docker compose pull --ignore-buildable --quiet + docker compose up --detach + docker compose run --rm dev make wait + + - name: Run make test + run: docker compose run --rm dev make test + + - name: Login to Docker Hub + uses: docker/login-action@v3 + if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }} + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }} + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate Docker image tags + id: meta + uses: docker/metadata-action@v5 + if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }} + with: + images: | + ${{ github.repository }} + ghcr.io/${{ github.repository }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + + - name: Publish Docker image + uses: docker/build-push-action@v6 + if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }} + with: + context: . + target: release + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + npm: + name: NPM + runs-on: ubuntu-latest + needs: build + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://registry.npmjs.org + cache: npm + cache-dependency-path: typescript/package-lock.json + + - uses: actions/download-artifact@v4 + with: + path: dist + + - run: find dist + + - run: npm ci + working-directory: typescript + + - run: npm run lint:ci + working-directory: typescript + + - run: npm run generate + working-directory: typescript + + - run: npm run publish + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + working-directory: typescript + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} + + dependabot: + name: Dependabot + runs-on: ubuntu-latest + permissions: + pull-requests: write + if: github.event.pull_request.user.login == 'dependabot[bot]' + steps: + - name: Automatically approve dependabot PRs + uses: octokit/request-action@v2.x + with: + route: POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews + owner: ${{ github.event.repository.owner.login }} + repo: ${{ github.event.repository.name }} + pull_number: ${{ github.event.pull_request.number }} + event: APPROVE + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 6c1953e1..00000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,62 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. - -name: "CodeQL" - -on: - push: - branches: [ master ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ master ] - schedule: - - cron: '0 0 * * 4' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - language: [ 'go' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more... - # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f5446544..2f56e530 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,14 +2,14 @@ name: Release on: push: - tags: 'v*' + tags: ["v*"] jobs: homebrew: name: Bump Homebrew formula runs-on: ubuntu-latest steps: - - uses: mislav/bump-homebrew-formula-action@v1 + - uses: mislav/bump-homebrew-formula-action@v3 with: formula-name: dbmate env: diff --git a/.gitignore b/.gitignore index 44a09fe5..c85cb141 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,14 @@ .DS_Store .env +.idea +*.log +*.sqlite3 +*.tsbuildinfo /.cache /db /dbmate -/dist /testdata/db/schema.sql /vendor +dist +docker-compose.override.yml +node_modules diff --git a/.golangci.yml b/.golangci.yml index ac76638a..74eb31f2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,24 +1,20 @@ linters: enable: - bodyclose - - deadcode - - depguard - errcheck - goimports - - golint - gosimple - govet - ineffassign - misspell - nakedret + - revive - rowserrcheck - staticcheck - - structcheck - typecheck - unconvert - unparam - unused - - varcheck - whitespace linters-settings: diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..828715a4 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +// -*- jsonc -*- +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "golang.go" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..60374046 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,29 @@ +// -*- jsonc -*- +{ + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.detectIndentation": false, + "editor.formatOnSave": true, + "editor.insertSpaces": true, + "editor.tabSize": 2, + "eslint.options": { + "reportUnusedDisableDirectives": "error" + }, + "files.eol": "\n", + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + "files.trimTrailingWhitespace": true, + "prettier.prettierPath": "./typescript/node_modules/prettier/index.cjs", + "go.formatTool": "goimports", + "[go]": { + "editor.defaultFormatter": "golang.go", + "editor.insertSpaces": false, + "editor.tabSize": 4 + }, + "[makefile]": { + "editor.insertSpaces": false, + "editor.tabSize": 4 + } +} diff --git a/Dockerfile b/Dockerfile index ebdbdf9c..1bc9f82c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,43 +1,38 @@ -# development image -FROM techknowlogick/xgo:go-1.15.x as dev +# development stage +FROM golang:1.24.0 as dev WORKDIR /src -ENV GOCACHE /src/.cache/go-build +ENV PATH="/src/typescript/node_modules/.bin:${PATH}" +RUN git config --global --add safe.directory /src -# enable cgo to build sqlite -ENV CGO_ENABLED 1 - -# install database clients +# install development tools RUN apt-get update \ - && apt-get install -qq --no-install-recommends \ - curl \ - mysql-client \ - postgresql-client \ - sqlite3 \ - && rm -rf /var/lib/apt/lists/* + && apt-get install -qq --no-install-recommends \ + curl \ + file \ + mariadb-client \ + postgresql-client \ + sqlite3 \ + nodejs \ + npm \ + && rm -rf /var/lib/apt/lists/* # golangci-lint -RUN curl -fsSL -o /tmp/lint-install.sh https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh \ - && chmod +x /tmp/lint-install.sh \ - && /tmp/lint-install.sh -b /usr/local/bin v1.32.2 \ - && rm -f /tmp/lint-install.sh +RUN curl -fsSL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh \ + | sh -s -- -b /usr/local/bin v1.64.5 # download modules -COPY go.* ./ +COPY go.* /src/ RUN go mod download - -ENTRYPOINT [] -CMD ["/bin/bash"] - -# build stage -FROM dev as build -COPY . ./ +COPY . /src/ RUN make build # release stage -FROM alpine as release +FROM alpine:3.21.3 as release RUN apk add --no-cache \ - mariadb-client \ - postgresql-client \ - sqlite -COPY --from=build /src/dist/dbmate-linux-amd64 /usr/local/bin/dbmate -ENTRYPOINT ["dbmate"] + mariadb-client \ + mariadb-connector-c \ + postgresql-client \ + sqlite \ + tzdata +COPY --from=dev /src/dist/dbmate /usr/local/bin/dbmate +ENTRYPOINT ["/usr/local/bin/dbmate"] diff --git a/Makefile b/Makefile index 70963e06..794520fc 100644 --- a/Makefile +++ b/Makefile @@ -1,59 +1,81 @@ -# no static linking for macos -LDFLAGS := -ldflags '-s' -# statically link binaries (to support alpine + scratch containers) -STATICLDFLAGS := -ldflags '-s -extldflags "-static"' -# avoid building code that is incompatible with static linking -TAGS := -tags netgo,osusergo,sqlite_omit_load_extension,sqlite_json +# enable cgo to build sqlite +export CGO_ENABLED = 1 + +# default output file +OUTPUT ?= dbmate + +# platform-specific settings +GOOS := $(shell go env GOOS) +ifeq ($(GOOS),linux) + # statically link binaries to support alpine linux + override FLAGS := -tags netgo,osusergo,sqlite_omit_load_extension,sqlite_fts5,sqlite_json -ldflags '-s -extldflags "-static"' $(FLAGS) +else + # strip binaries + override FLAGS := -tags sqlite_omit_load_extension,sqlite_fts5,sqlite_json -ldflags '-s' $(FLAGS) +endif +ifeq ($(GOOS),darwin) + export SDKROOT ?= $(shell xcrun --sdk macosx --show-sdk-path) +endif +ifeq ($(GOOS),windows) + ifneq ($(suffix $(OUTPUT)),.exe) + OUTPUT := $(addsuffix .exe,$(OUTPUT)) + endif +endif .PHONY: all -all: build test lint +all: fix build wait test + +.PHONY: clean +clean: + rm -rf dist + +.PHONY: build +build: clean + go build -o dist/$(OUTPUT) $(FLAGS) . + +.PHONY: ls +ls: + ls -lh dist/$(OUTPUT) + file dist/$(OUTPUT) .PHONY: test test: - go test -p 1 $(TAGS) $(STATICLDFLAGS) ./... - -.PHONY: fix -fix: - golangci-lint run --fix + go test -v -p 1 $(FLAGS) ./... .PHONY: lint lint: - golangci-lint run + golangci-lint run --timeout 5m + +.PHONY: fix +fix: + golangci-lint run --fix --timeout 5m .PHONY: wait wait: - dist/dbmate-linux-amd64 -e CLICKHOUSE_TEST_URL wait - dist/dbmate-linux-amd64 -e MYSQL_TEST_URL wait - dist/dbmate-linux-amd64 -e POSTGRES_TEST_URL wait + dist/dbmate -e CLICKHOUSE_TEST_URL wait + dist/dbmate -e MYSQL_TEST_URL wait + dist/dbmate -e POSTGRES_TEST_URL wait -.PHONY: clean -clean: - rm -rf dist/* +.PHONY: update-deps +update-deps: + go get -u ./... + go mod tidy + go mod verify + cd typescript && \ + rm -f package-lock.json && \ + ./node_modules/.bin/npm-check-updates --upgrade && \ + npm install && \ + npm dedupe -.PHONY: build -build: clean build-linux-amd64 - ls -lh dist - -.PHONY: build-linux-amd64 -build-linux-amd64: - GOOS=linux GOARCH=amd64 \ - go build $(TAGS) $(STATICLDFLAGS) -o dist/dbmate-linux-amd64 . - -.PHONY: build-all -build-all: clean build-linux-amd64 - GOOS=linux GOARCH=arm64 CC=aarch64-linux-gnu-gcc-5 CXX=aarch64-linux-gnu-g++-5 \ - go build $(TAGS) $(STATICLDFLAGS) -o dist/dbmate-linux-arm64 . - GOOS=darwin GOARCH=amd64 CC=o64-clang CXX=o64-clang++ \ - go build $(TAGS) $(LDFLAGS) -o dist/dbmate-macos-amd64 . - GOOS=windows GOARCH=amd64 CC=x86_64-w64-mingw32-gcc-posix CXX=x86_64-w64-mingw32-g++-posix \ - go build $(TAGS) $(STATICLDFLAGS) -o dist/dbmate-windows-amd64.exe . - ls -lh dist - -.PHONY: docker-make -docker-make: - docker-compose build - docker-compose run --rm dev make +.PHONY: docker-build +docker-build: + docker compose pull --ignore-buildable + docker compose build dev + +.PHONY: docker-all +docker-all: docker-build + docker compose run --rm dev make all .PHONY: docker-sh docker-sh: - -docker-compose run --rm dev + -docker compose run --rm dev diff --git a/README.md b/README.md index cca110db..e0d97a60 100644 --- a/README.md +++ b/README.md @@ -1,83 +1,117 @@ # Dbmate -[![GitHub Build](https://img.shields.io/github/workflow/status/amacneil/dbmate/CI/master)](https://github.com/amacneil/dbmate/actions?query=branch%3Amaster+event%3Apush+workflow%3ACI) -[![Go Report Card](https://goreportcard.com/badge/github.com/amacneil/dbmate)](https://goreportcard.com/report/github.com/amacneil/dbmate) -[![GitHub Release](https://img.shields.io/github/release/amacneil/dbmate.svg)](https://github.com/amacneil/dbmate/releases) - -Dbmate is a database migration tool, to keep your database schema in sync across multiple developers and your production servers. - -It is a standalone command line tool, which can be used with Go, Node.js, Python, Ruby, PHP, or any other language or framework you are using to write database-backed applications. This is especially helpful if you are writing many services in different languages, and want to maintain some sanity with consistent development tools. - -For a comparison between dbmate and other popular database schema migration tools, please see the [Alternatives](#alternatives) table. +[![Release](https://img.shields.io/github/release/amacneil/dbmate.svg)](https://github.com/amacneil/dbmate/releases) +[![Go Report](https://goreportcard.com/badge/github.com/amacneil/dbmate)](https://goreportcard.com/report/github.com/amacneil/dbmate) +[![Reference](https://img.shields.io/badge/go.dev-reference-blue?logo=go&logoColor=white)](https://pkg.go.dev/github.com/amacneil/dbmate/v2/pkg/dbmate) + +Dbmate is a database migration tool that will keep your database schema in sync across multiple developers and your production servers. + +It is a standalone command line tool that can be used with Go, Node.js, Python, Ruby, PHP, Rust, C++, or any other language or framework you are using to write database-backed applications. This is especially helpful if you are writing multiple services in different languages, and want to maintain some sanity with consistent development tools. + +For a comparison between dbmate and other popular database schema migration tools, please see [Alternatives](#alternatives). + +## Table of Contents + +- [Features](#features) +- [Installation](#installation) +- [Commands](#commands) + - [Command Line Options](#command-line-options) +- [Usage](#usage) + - [Connecting to the Database](#connecting-to-the-database) + - [PostgreSQL](#postgresql) + - [MySQL](#mysql) + - [SQLite](#sqlite) + - [ClickHouse](#clickhouse) + - [BigQuery](#bigquery) + - [Spanner](#spanner) + - [Creating Migrations](#creating-migrations) + - [Running Migrations](#running-migrations) + - [Rolling Back Migrations](#rolling-back-migrations) + - [Migration Options](#migration-options) + - [Waiting For The Database](#waiting-for-the-database) + - [Exporting Schema File](#exporting-schema-file) +- [Library](#library) + - [Use dbmate as a library](#use-dbmate-as-a-library) + - [Embedding migrations](#embedding-migrations) +- [Concepts](#concepts) + - [Migration files](#migration-files) + - [Schema file](#schema-file) + - [Schema migrations table](#schema-migrations-table) +- [Alternatives](#alternatives) +- [Contributing](#contributing) ## Features -* Supports MySQL, PostgreSQL, SQLite, and ClickHouse. -* Uses plain SQL for writing schema migrations. -* Migrations are timestamp-versioned, to avoid version number conflicts with multiple developers. -* Migrations are run atomically inside a transaction. -* Supports creating and dropping databases (handy in development/test). -* Supports saving a `schema.sql` file to easily diff schema changes in git. -* Database connection URL is definied using an environment variable (`DATABASE_URL` by default), or specified on the command line. -* Built-in support for reading environment variables from your `.env` file. -* Easy to distribute, single self-contained binary. +- Supports MySQL, PostgreSQL, SQLite, and ClickHouse +- Uses plain SQL for writing schema migrations +- Migrations are timestamp-versioned, to avoid version number conflicts with multiple developers +- Migrations are run atomically inside a transaction +- Supports creating and dropping databases (handy in development/test) +- Supports saving a `schema.sql` file to easily diff schema changes in git +- Database connection URL is defined using an environment variable (`DATABASE_URL` by default), or specified on the command line +- Built-in support for reading environment variables from your `.env` file +- Easy to distribute, single self-contained binary +- Doesn't try to upsell you on a SaaS service ## Installation -**macOS** +**NPM** -Install using Homebrew: +Install using [NPM](https://www.npmjs.com/): ```sh -$ brew install dbmate +npm install --save-dev dbmate +npx dbmate --help ``` -**Linux** +**macOS** -Download the binary directly: +Install using [Homebrew](https://brew.sh/): ```sh -$ sudo curl -fsSL -o /usr/local/bin/dbmate https://github.com/amacneil/dbmate/releases/latest/download/dbmate-linux-amd64 -$ sudo chmod +x /usr/local/bin/dbmate +brew install dbmate +dbmate --help ``` -**Docker** +**Linux** -You can run dbmate using the official docker image (remember to set `--network=host` or see [this comment](https://github.com/amacneil/dbmate/issues/128#issuecomment-615924611) for more tips on using dbmate with docker networking): +Install the binary directly: ```sh -$ docker run --rm -it --network=host amacneil/dbmate --help +sudo curl -fsSL -o /usr/local/bin/dbmate https://github.com/amacneil/dbmate/releases/latest/download/dbmate-linux-amd64 +sudo chmod +x /usr/local/bin/dbmate +/usr/local/bin/dbmate --help ``` -If you wish to create or apply migrations, you will need to use Docker's [bind mount](https://docs.docker.com/storage/bind-mounts/) feature to make your local working directory (`pwd`) available inside the dbmate container: +**Windows** -```sh -$ docker run --rm -it --network=host -v "$(pwd)/db:/db" amacneil/dbmate new create_users_table +Install using [Scoop](https://scoop.sh) + +```pwsh +scoop install dbmate +dbmate --help ``` -**Heroku** +**Docker** + +Docker images are published to GitHub Container Registry ([`ghcr.io/amacneil/dbmate`](https://ghcr.io/amacneil/dbmate)). -To use dbmate on Heroku, the easiest method is to store the linux binary in your git repository: +Remember to set `--network=host` or see [this comment](https://github.com/amacneil/dbmate/issues/128#issuecomment-615924611) for more tips on using dbmate with docker networking): ```sh -$ mkdir -p bin -$ curl -fsSL -o bin/dbmate https://github.com/amacneil/dbmate/releases/latest/download/dbmate-linux-amd64 -$ chmod +x bin/dbmate -$ git add bin/dbmate -$ git commit -m "Add dbmate binary" -$ git push heroku master +docker run --rm -it --network=host ghcr.io/amacneil/dbmate --help ``` -You can then run dbmate on heroku: +If you wish to create or apply migrations, you will need to use Docker's [bind mount](https://docs.docker.com/storage/bind-mounts/) feature to make your local working directory (`pwd`) available inside the dbmate container: ```sh -$ heroku run bin/dbmate up +docker run --rm -it --network=host -v "$(pwd)/db:/db" ghcr.io/amacneil/dbmate new create_users_table ``` ## Commands ```sh -dbmate # print help +dbmate --help # print usage help dbmate new # generate a new migration file dbmate up # create the database (if it does not already exist) and run any pending migrations dbmate create # create the database @@ -87,11 +121,29 @@ dbmate rollback # roll back the most recent migration dbmate down # alias for rollback dbmate status # show the status of all migrations (supports --exit-code and --quiet) dbmate dump # write the database schema.sql file +dbmate load # load schema.sql file to the database dbmate wait # wait for the database server to become available ``` +### Command Line Options + +The following options are available with all commands. You must use command line arguments in the order `dbmate [global options] command [command options]`. Most options can also be configured via environment variables (and loaded from your `.env` file, which is helpful to share configuration between team members). + +- `--url, -u "protocol://host:port/dbname"` - specify the database url directly. _(env: `DATABASE_URL`)_ +- `--env, -e "DATABASE_URL"` - specify an environment variable to read the database connection URL from. +- `--env-file ".env"` - specify an alternate environment variables file(s) to load. +- `--migrations-dir, -d "./db/migrations"` - where to keep the migration files. _(env: `DBMATE_MIGRATIONS_DIR`)_ +- `--migrations-table "schema_migrations"` - database table to record migrations in. _(env: `DBMATE_MIGRATIONS_TABLE`)_ +- `--schema-file, -s "./db/schema.sql"` - a path to keep the schema.sql file. _(env: `DBMATE_SCHEMA_FILE`)_ +- `--no-dump-schema` - don't auto-update the schema.sql file on migrate/rollback _(env: `DBMATE_NO_DUMP_SCHEMA`)_ +- `--strict` - fail if migrations would be applied out of order _(env: `DBMATE_STRICT`)_ +- `--wait` - wait for the db to become available before executing the subsequent command _(env: `DBMATE_WAIT`)_ +- `--wait-timeout 60s` - timeout for --wait flag _(env: `DBMATE_WAIT_TIMEOUT`)_ + ## Usage +### Connecting to the Database + Dbmate locates your database using the `DATABASE_URL` environment variable by default. If you are writing a [twelve-factor app](http://12factor.net/), you should be storing all connection strings in environment variables. To make this easy in development, dbmate looks for a `.env` file in the current directory, and treats any variables listed there as if they were specified in the current environment (existing environment variables take preference, however). @@ -109,23 +161,39 @@ DATABASE_URL="postgres://postgres@127.0.0.1:5432/myapp_development?sslmode=disab protocol://username:password@host:port/database_name?options ``` -* `protocol` must be one of `mysql`, `postgres`, `postgresql`, `sqlite`, `sqlite3`, `clickhouse` -* `host` can be either a hostname or IP address -* `options` are driver-specific (refer to the underlying Go SQL drivers if you wish to use these) +- `protocol` must be one of `mysql`, `postgres`, `postgresql`, `sqlite`, `sqlite3`, `clickhouse` +- `username` and `password` must be URL encoded (you will get an error if you use special charactors) +- `host` can be either a hostname or IP address +- `options` are driver-specific (refer to the underlying Go SQL drivers if you wish to use these) -**MySQL** +Dbmate can also load the connection URL from a different environment variable. For example, before running your test suite, you may wish to drop and recreate the test database. One easy way to do this is to store your test database connection URL in the `TEST_DATABASE_URL` environment variable: ```sh -DATABASE_URL="mysql://username:password@127.0.0.1:3306/database_name" +$ cat .env +DATABASE_URL="postgres://postgres@127.0.0.1:5432/myapp_dev?sslmode=disable" +TEST_DATABASE_URL="postgres://postgres@127.0.0.1:5432/myapp_test?sslmode=disable" ``` -A `socket` parameter can be specified to connect through a unix socket: +You can then specify this environment variable in your test script (Makefile or similar): ```sh -DATABASE_URL="mysql://username:password@/database_name?socket=/var/run/mysqld/mysqld.sock" +$ dbmate -e TEST_DATABASE_URL drop +Dropping: myapp_test +$ dbmate -e TEST_DATABASE_URL --no-dump-schema up +Creating: myapp_test +Applying: 20151127184807_create_users_table.sql +Applied: 20151127184807_create_users_table.sql in 123µs +``` + +Alternatively, you can specify the url directly on the command line: + +```sh +$ dbmate -u "postgres://postgres@127.0.0.1:5432/myapp_test?sslmode=disable" up ``` -**PostgreSQL** +The only advantage of using `dbmate -e TEST_DATABASE_URL` over `dbmate -u $TEST_DATABASE_URL` is that the former takes advantage of dbmate's automatic `.env` file loading. + +#### PostgreSQL When connecting to Postgres, you may need to add the `sslmode=disable` option to your connection string, as dbmate by default requires a TLS connection (some other frameworks/languages allow unencrypted connections by default). @@ -150,7 +218,19 @@ DATABASE_URL="postgres://username:password@127.0.0.1:5432/database_name?search_p DATABASE_URL="postgres://username:password@127.0.0.1:5432/database_name?search_path=myschema,public" ``` -**SQLite** +#### MySQL + +```sh +DATABASE_URL="mysql://username:password@127.0.0.1:3306/database_name" +``` + +A `socket` parameter can be specified to connect through a unix socket: + +```sh +DATABASE_URL="mysql://username:password@/database_name?socket=/var/run/mysqld/mysqld.sock" +``` + +#### SQLite SQLite databases are stored on the filesystem, so you do not need to specify a host. By default, files are relative to the current directory. For example, the following will create a database at `./db/database.sqlite3`: @@ -164,20 +244,92 @@ To specify an absolute path, add a forward slash to the path. The following will DATABASE_URL="sqlite:/tmp/database.sqlite3" ``` -**ClickHouse** +#### ClickHouse ```sh DATABASE_URL="clickhouse://username:password@127.0.0.1:9000/database_name" ``` -or +To work with ClickHouse cluster, there are 4 connection query parameters that can be supplied: + +- `on_cluster` - Indicataion to use cluster statements and replicated migration table. (default: `false`) If this parameter is not supplied, other cluster related query parameters are ignored. + +```sh +DATABASE_URL="clickhouse://username:password@127.0.0.1:9000/database_name?on_cluster" + +DATABASE_URL="clickhouse://username:password@127.0.0.1:9000/database_name?on_cluster=true" +``` + +- `cluster_macro` (Optional) - Macro value to be used for ON CLUSTER statements and for the replciated migration table engine zookeeper path. (default: `{cluster}`) + +```sh +DATABASE_URL="clickhouse://username:password@127.0.0.1:9000/database_name?on_cluster&cluster_macro={my_cluster}" +``` + +- `replica_macro` (Optional) - Macro value to be used for the replica name in the replciated migration table engine. (default: `{replica}`) + +```sh +DATABASE_URL="clickhouse://username:password@127.0.0.1:9000/database_name?on_cluster&replica_macro={my_replica}" +``` + +- `zoo_path` (Optional) - The path to the table migration in ClickHouse/Zoo Keeper. (default: `/clickhouse/tables//{table}`) ```sh -DATABASE_URL="clickhouse://127.0.0.1:9000?username=username&password=password&database=database_name" +DATABASE_URL="clickhouse://username:password@127.0.0.1:9000/database_name?on_cluster&zoo_path=/zk/path/tables" ``` [See other supported connection options](https://github.com/ClickHouse/clickhouse-go#dsn). +#### BigQuery + +Follow the following format for `DATABASE_URL` when connecting to actual BigQuery in GCP: + +``` +bigquery://projectid/location/dataset +``` + +`projectid` (mandatory) - Project ID + +`dataset` (mandatory) - Dataset name within the Project + +`location` (optional) - Where Dataset is created + +_NOTE: Follow [this doc](https://cloud.google.com/docs/authentication/provide-credentials-adc) on how to set `GOOGLE_APPLICATION_CREDENTIALS` environment variable for proper Authentication_ + +Follow the following format if trying to connect to a custom endpoint e.g. [BigQuery Emulator](https://github.com/goccy/bigquery-emulator) + +``` +bigquery://host:port/projectid/location/dataset?disable_auth=true +``` + +`disable_auth` (optional) - Pass `true` to skip Authentication, use only for testing and connecting to emulator. + +#### Spanner + +Spanner support is currently limited to databases using the [PostgreSQL Dialect](https://cloud.google.com/spanner/docs/postgresql-interface), which must be chosen during database creation. For future Spanner with GoogleSQL support, see [this discussion](https://github.com/amacneil/dbmate/discussions/369). + +Spanner with the Postgres interface requires that the [PGAdapter](https://cloud.google.com/spanner/docs/pgadapter) is running. Use the following format for `DATABASE_URL`, with the host and port set to where the PGAdapter is running: + +```shell +DATABASE_URL="spanner-postgres://127.0.0.1:5432/database_name?sslmode=disable" +``` + +Note that specifying a username and password is not necessary, as authentication is handled by the PGAdapter (they will be ignored by the PGAdapter if specified). + +Other options of the [postgres driver](#postgresql) are supported. + +Spanner also doesn't allow DDL to be executed inside explicit transactions. You must therefore specify `transaction:false` on migrations that include DDL: + +```sql +-- migrate:up transaction:false +CREATE TABLE ... + +-- migrate:down transaction:false +DROP TABLE ... +``` + +Schema dumps are not currently supported, as `pg_dump` uses functions that are not provided by Spanner. + ### Creating Migrations To create a new migration, run `dbmate new create_users_table`. You can name the migration anything you like. This will create a file `db/migrations/20151127184807_create_users_table.sql` in the current directory: @@ -211,6 +363,7 @@ Run `dbmate up` to run any pending migrations. $ dbmate up Creating: myapp_development Applying: 20151127184807_create_users_table.sql +Applied: 20151127184807_create_users_table.sql in 123µs Writing: ./db/schema.sql ``` @@ -239,6 +392,7 @@ Run `dbmate rollback` to roll back the most recent migration: ```sh $ dbmate rollback Rolling back: 20151127184807_create_users_table.sql +Rolled back: 20151127184807_create_users_table.sql in 123µs Writing: ./db/schema.sql ``` @@ -246,11 +400,11 @@ Writing: ./db/schema.sql dbmate supports options passed to a migration block in the form of `key:value` pairs. List of supported options: -* `transaction` +- `transaction` -#### transaction +**transaction** -`transaction` is useful if you need to run some SQL which cannot be executed from within a transaction. For example, in Postgres, you would need to disable transactions for migrations that alter an enum type to add a value: +`transaction` is useful if you do not want to run SQL inside a transaction: ```sql -- migrate:up transaction:false @@ -259,23 +413,6 @@ ALTER TYPE colors ADD VALUE 'orange' AFTER 'red'; `transaction` will default to `true` if your database supports it. -### Schema File - -When you run the `up`, `migrate`, or `rollback` commands, dbmate will automatically create a `./db/schema.sql` file containing a complete representation of your database schema. Dbmate keeps this file up to date for you, so you should not manually edit it. - -It is recommended to check this file into source control, so that you can easily review changes to the schema in commits or pull requests. It's also possible to use this file when you want to quickly load a database schema, without running each migration sequentially (for example in your test harness). However, if you do not wish to save this file, you could add it to `.gitignore`, or pass the `--no-dump-schema` command line option. - -To dump the `schema.sql` file without performing any other actions, run `dbmate dump`. Unlike other dbmate actions, this command relies on the respective `pg_dump`, `mysqldump`, or `sqlite3` commands being available in your PATH. If these tools are not available, dbmate will silenty skip the schema dump step during `up`, `migrate`, or `rollback` actions. You can diagnose the issue by running `dbmate dump` and looking at the output: - -```sh -$ dbmate dump -exec: "pg_dump": executable file not found in $PATH -``` - -On Ubuntu or Debian systems, you can fix this by installing `postgresql-client`, `mysql-client`, or `sqlite3` respectively. Ensure that the package version you install is greater than or equal to the version running on your database server. - -> Note: The `schema.sql` file will contain a complete schema for your database, even if some tables or columns were created outside of dbmate migrations. - ### Waiting For The Database If you use a Docker development environment for your project, you may encounter issues with the database not being immediately ready when running migrations or unit tests. This can be due to the database server having only just started. @@ -313,74 +450,169 @@ Error: unable to connect to database: dial tcp 127.0.0.1:5432: connect: connecti Please note that the `wait` command does not verify whether your specified database exists, only that the server is available and ready (so it will return success if the database server is available, but your database has not yet been created). -### Options +### Exporting Schema File -The following command line options are available with all commands. You must use command line arguments in the order `dbmate [global options] command [command options]`. Most options can also be configured via environment variables (and loaded from your `.env` file, which is helpful to share configuration between team members). +When you run the `up`, `migrate`, or `rollback` commands, dbmate will automatically create a `./db/schema.sql` file containing a complete representation of your database schema. Dbmate keeps this file up to date for you, so you should not manually edit it. -* `--url, -u "protocol://host:port/dbname"` - specify the database url directly. _(env: `$DATABASE_URL`)_ -* `--env, -e "DATABASE_URL"` - specify an environment variable to read the database connection URL from. -* `--migrations-dir, -d "./db/migrations"` - where to keep the migration files. _(env: `$DBMATE_MIGRATIONS_DIR`)_ -* `--schema-file, -s "./db/schema.sql"` - a path to keep the schema.sql file. _(env: `$DBMATE_SCHEMA_FILE`)_ -* `--no-dump-schema` - don't auto-update the schema.sql file on migrate/rollback _(env: `$DBMATE_NO_DUMP_SCHEMA`)_ -* `--wait` - wait for the db to become available before executing the subsequent command _(env: `$DBMATE_WAIT`)_ -* `--wait-timeout 60s` - timeout for --wait flag _(env: `$DBMATE_WAIT_TIMEOUT`)_ +It is recommended to check this file into source control, so that you can easily review changes to the schema in commits or pull requests. It's also possible to use this file when you want to quickly load a database schema, without running each migration sequentially (for example in your test harness). However, if you do not wish to save this file, you could add it to your `.gitignore`, or pass the `--no-dump-schema` command line option. -For example, before running your test suite, you may wish to drop and recreate the test database. One easy way to do this is to store your test database connection URL in the `TEST_DATABASE_URL` environment variable: +To dump the `schema.sql` file without performing any other actions, run `dbmate dump`. Unlike other dbmate actions, this command relies on the respective `pg_dump`, `mysqldump`, or `sqlite3` commands being available in your PATH. If these tools are not available, dbmate will silently skip the schema dump step during `up`, `migrate`, or `rollback` actions. You can diagnose the issue by running `dbmate dump` and looking at the output: ```sh -$ cat .env -TEST_DATABASE_URL="postgres://postgres@127.0.0.1:5432/myapp_test?sslmode=disable" +$ dbmate dump +exec: "pg_dump": executable file not found in $PATH ``` -You can then specify this environment variable in your test script (Makefile or similar): +On Ubuntu or Debian systems, you can fix this by installing `postgresql-client`, `mysql-client`, or `sqlite3` respectively. Ensure that the package version you install is greater than or equal to the version running on your database server. -```sh -$ dbmate -e TEST_DATABASE_URL drop -Dropping: myapp_test -$ dbmate -e TEST_DATABASE_URL --no-dump-schema up -Creating: myapp_test -Applying: 20151127184807_create_users_table.sql +> Note: The `schema.sql` file will contain a complete schema for your database, even if some tables or columns were created outside of dbmate migrations. + +## Library + +### Use dbmate as a library + +Dbmate is designed to be used as a CLI with any language or framework, but it can also be used as a library in a Go application. + +Here is a simple example. Remember to import the driver you need! + +```go +package main + +import ( + "net/url" + + "github.com/assetnote/dbmate/pkg/dbmate" + _ "github.com/assetnote/dbmate/pkg/driver/sqlite" +) + +func main() { + u, _ := url.Parse("sqlite:foo.sqlite3") + db := dbmate.New(u) + + err := db.CreateAndMigrate() + if err != nil { + panic(err) + } +} ``` -Alternatively, you can specify the url directly on the command line: +See the [reference documentation](https://pkg.go.dev/github.com/amacneil/dbmate/v2/pkg/dbmate) for more options. -```sh -$ dbmate -u "postgres://postgres@127.0.0.1:5432/myapp_test?sslmode=disable" up +### Embedding migrations + +Migrations can be embedded into your application binary using Go's [embed](https://pkg.go.dev/embed) functionality. + +Use `db.FS` to specify the filesystem used for reading migrations: + +```go +package main + +import ( + "embed" + "fmt" + "net/url" + + "github.com/assetnote/dbmate/pkg/dbmate" + _ "github.com/assetnote/dbmate/pkg/driver/sqlite" +) + +//go:embed db/migrations/*.sql +var fs embed.FS + +func main() { + u, _ := url.Parse("sqlite:foo.sqlite3") + db := dbmate.New(u) + db.FS = fs + + fmt.Println("Migrations:") + migrations, err := db.FindMigrations() + if err != nil { + panic(err) + } + for _, m := range migrations { + fmt.Println(m.Version, m.FilePath) + } + + fmt.Println("\nApplying...") + err = db.CreateAndMigrate() + if err != nil { + panic(err) + } +} ``` -The only advantage of using `dbmate -e TEST_DATABASE_URL` over `dbmate -u $TEST_DATABASE_URL` is that the former takes advantage of dbmate's automatic `.env` file loading. +## Concepts + +### Migration files + +Migration files are very simple, and are stored in `./db/migrations` by default. You can create a new migration file named `[date]_create_users.sql` by running `dbmate new create_users`. +Here is an example: + +```sql +-- migrate:up +create table users ( + id integer, + name varchar(255), +); + +-- migrate:down +drop table if exists users; +``` + +Both up and down migrations are stored in the same file, for ease of editing. Both up and down directives are required, even if you choose not to implement the down migration. + +When you apply a migration dbmate only stores the version number, not the contents, so you should always rollback a migration before modifying its contents. For this reason, you can safely rename a migration file without affecting its applied status, as long as you keep the version number intact. + +### Schema file + +The schema file is written to `./db/schema.sql` by default. It is a complete dump of your database schema, including any applied migrations, and any other modifications you have made. + +This file should be checked in to source control, so that you can easily compare the diff of a migration. You can use the schema file to quickly restore your database without needing to run all migrations. + +### Schema migrations table + +Dbmate stores a record of each applied migration in table named `schema_migrations`. This table will be created for you automatically if it does not already exist. + +The table is very simple: + +```sql +CREATE TABLE IF NOT EXISTS schema_migrations ( + version VARCHAR(255) PRIMARY KEY +) +``` + +You can customize the name of this table using the `--migrations-table` flag or `DBMATE_MIGRATIONS_TABLE` environment variable. ## Alternatives Why another database schema migration tool? Dbmate was inspired by many other tools, primarily [Active Record Migrations](http://guides.rubyonrails.org/active_record_migrations.html), with the goals of being trivial to configure, and language & framework independent. Here is a comparison between dbmate and other popular migration tools. -| | [goose](https://bitbucket.org/liamstask/goose/) | [sql-migrate](https://github.com/rubenv/sql-migrate) | [golang-migrate/migrate](https://github.com/golang-migrate/migrate) | [activerecord](http://guides.rubyonrails.org/active_record_migrations.html) | [sequelize](http://docs.sequelizejs.com/manual/tutorial/migrations.html) | [dbmate](https://github.com/amacneil/dbmate) | -| --- |:---:|:---:|:---:|:---:|:---:|:---:| -| **Features** | -|Plain SQL migration files|:white_check_mark:|:white_check_mark:|:white_check_mark:|||:white_check_mark:| -|Support for creating and dropping databases||||:white_check_mark:||:white_check_mark:| -|Support for saving schema dump files||||:white_check_mark:||:white_check_mark:| -|Timestamp-versioned migration files|:white_check_mark:|||:white_check_mark:|:white_check_mark:|:white_check_mark:| -|Ability to wait for database to become ready||||||:white_check_mark:| -|Database connection string loaded from environment variables||||||:white_check_mark:| -|Automatically load .env file||||||:white_check_mark:| -|No separate configuration file||||:white_check_mark:|:white_check_mark:|:white_check_mark:| -|Language/framework independent|:eight_pointed_black_star:|:eight_pointed_black_star:|:eight_pointed_black_star:|||:white_check_mark:| -| **Drivers** | -|PostgreSQL|:white_check_mark:|:white_check_mark:|:white_check_mark:|:white_check_mark:|:white_check_mark:|:white_check_mark:| -|MySQL|:white_check_mark:|:white_check_mark:|:white_check_mark:|:white_check_mark:|:white_check_mark:|:white_check_mark:| -|SQLite|:white_check_mark:|:white_check_mark:|:white_check_mark:|:white_check_mark:|:white_check_mark:|:white_check_mark:| -|CliсkHouse|||:white_check_mark:|:white_check_mark:|:white_check_mark:|:white_check_mark:| - -> :eight_pointed_black_star: In theory these tools could be used with other languages, but a Go development environment is required because binary builds are not provided. - -*If you notice any inaccuracies in this table, please [propose a change](https://github.com/amacneil/dbmate/edit/master/README.md).* +| | [dbmate](https://github.com/amacneil/dbmate) | [goose](https://github.com/pressly/goose) | [sql-migrate](https://github.com/rubenv/sql-migrate) | [golang-migrate](https://github.com/golang-migrate/migrate) | [activerecord](http://guides.rubyonrails.org/active_record_migrations.html) | [sequelize](http://docs.sequelizejs.com/manual/tutorial/migrations.html) | [flyway](https://flywaydb.org/) | [sqitch](https://sqitch.org/) | +| ------------------------------------------------------------ | :------------------------------------------: | :---------------------------------------: | :--------------------------------------------------: | :---------------------------------------------------------: | :-------------------------------------------------------------------------: | :----------------------------------------------------------------------: | :-----------------------------: | :---------------------------: | +| **Features** | +| Plain SQL migration files | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | :white_check_mark: | :white_check_mark: | +| Support for creating and dropping databases | :white_check_mark: | | | | :white_check_mark: | | | +| Support for saving schema dump files | :white_check_mark: | | | | :white_check_mark: | | | +| Timestamp-versioned migration files | :white_check_mark: | :white_check_mark: | | :white_check_mark: | :white_check_mark: | :white_check_mark: | | +| Custom schema migrations table | :white_check_mark: | | :white_check_mark: | | | :white_check_mark: | :white_check_mark: | +| Ability to wait for database to become ready | :white_check_mark: | | | | | | | +| Database connection string loaded from environment variables | :white_check_mark: | | | | | | :white_check_mark: | +| Automatically load .env file | :white_check_mark: | | | | | | | +| No separate configuration file | :white_check_mark: | :white_check_mark: | | :white_check_mark: | :white_check_mark: | :white_check_mark: | | +| Language/framework independent | :white_check_mark: | :white_check_mark: | | :white_check_mark: | | | :white_check_mark: | :white_check_mark: | +| **Drivers** | +| PostgreSQL | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| MySQL | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| SQLite | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| CliсkHouse | :white_check_mark: | | | :white_check_mark: | :white_check_mark: | :white_check_mark: | | + +_If you notice any inaccuracies in this table, please [propose a change](https://github.com/amacneil/dbmate/edit/main/README.md)._ ## Contributing Dbmate is written in Go, pull requests are welcome. -Tests are run against a real database using docker-compose. To build a docker image and run the tests: +Tests are run against a real database using docker compose. To build a docker image and run the tests: ```sh $ make docker-all @@ -389,5 +621,5 @@ $ make docker-all To start a development shell: ```sh -$ make docker-bash +$ make docker-sh ``` diff --git a/RELEASING.md b/RELEASING.md index ab49b263..2d2e84ce 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -4,4 +4,4 @@ The following steps should be followed to publish a new version of dbmate (requi 1. Update [version.go](/pkg/dbmate/version.go) with new version number ([example PR](https://github.com/amacneil/dbmate/pull/146/files)) 2. Create new release on [releases page](https://github.com/amacneil/dbmate/releases) and write release notes -3. GitHub Actions will automatically publish release binaries and submit Homebrew PR +3. GitHub Actions will do the rest (publish binaries, NPM package, and Homebrew PR) diff --git a/docker-compose.yml b/docker-compose.yml index f2a47541..4a8e49ed 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: '2.3' services: dev: build: @@ -10,20 +9,29 @@ services: - mysql - postgres - clickhouse + - clickhouse-cluster-01 + - clickhouse-cluster-02 + - bigquery + - spanner-emulator environment: - CLICKHOUSE_TEST_URL: clickhouse://clickhouse:9000?database=dbmate_test + CLICKHOUSE_TEST_URL: clickhouse://clickhouse:9000/dbmate_test + CLICKHOUSE_CLUSTER_01_TEST_URL: clickhouse://ch-cluster-01:9000/dbmate_test + CLICKHOUSE_CLUSTER_02_TEST_URL: clickhouse://ch-cluster-02:9000/dbmate_test MYSQL_TEST_URL: mysql://root:root@mysql/dbmate_test POSTGRES_TEST_URL: postgres://postgres:postgres@postgres/dbmate_test?sslmode=disable - SQLITE_TEST_URL: sqlite3:/tmp/dbmate_test.sqlite3 + BIGQUERY_TEST_URL: bigquery://test/us-east5/dbmate_test?disable_auth=true&endpoint=http%3A%2F%2Fbigquery%3A9050 + SPANNER_POSTGRES_TEST_URL: spanner-postgres://spanner-emulator/dbmate_test?sslmode=disable dbmate: build: context: . target: release + image: dbmate_release mysql: - image: mysql:5.7 + image: mysql/mysql-server:8.0 environment: + MYSQL_ROOT_HOST: "%" MYSQL_ROOT_PASSWORD: root postgres: @@ -32,4 +40,36 @@ services: POSTGRES_PASSWORD: postgres clickhouse: - image: yandex/clickhouse-server:19.16 + image: clickhouse/clickhouse-server:22.8 + + zookeeper: + image: zookeeper:3.8 + hostname: zookeeper + + clickhouse-cluster-01: + image: clickhouse/clickhouse-server:22.8 + hostname: ch-cluster-01 + environment: + - CLICKHOUSE_CONFIG=/etc/clickhouse-server/config.xml + depends_on: + - zookeeper + volumes: + - ./pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01:/etc/clickhouse-server + + clickhouse-cluster-02: + image: clickhouse/clickhouse-server:22.8 + hostname: ch-cluster-02 + environment: + - CLICKHOUSE_CONFIG=/etc/clickhouse-server/config.xml + depends_on: + - zookeeper + volumes: + - ./pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02:/etc/clickhouse-server + + bigquery: + image: ghcr.io/goccy/bigquery-emulator:0.4.4 + command: | + --project=test --dataset=dbmate_test + + spanner-emulator: + image: gcr.io/cloud-spanner-pg-adapter/pgadapter-emulator diff --git a/fixtures/bigquery/.gitignore b/fixtures/bigquery/.gitignore new file mode 100644 index 00000000..83f6e397 --- /dev/null +++ b/fixtures/bigquery/.gitignore @@ -0,0 +1 @@ +credentials.json diff --git a/fixtures/bigquery/README.md b/fixtures/bigquery/README.md new file mode 100644 index 00000000..de25c5dd --- /dev/null +++ b/fixtures/bigquery/README.md @@ -0,0 +1,49 @@ +# Google BigQuery Test Fixtures + +## Creating a service account for testing + +From the `dbmate` top-level directory: + +```sh +$ PROJECT_ID=your-google-cloud-project-id +$ LOCATION=us-east5 +$ DATASET=test_dataset +$ SERVICE_ACCOUNT=dbmate-test-sa + +$ gcloud auth login + +$ gcloud iam service-accounts create $SERVICE_ACCOUNT + +$ gcloud projects add-iam-policy-binding $PROJECT_ID \ + --role="roles/bigquery.dataEditor" \ + --member=serviceAccount:${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com + +$ gcloud projects add-iam-policy-binding $PROJECT_ID \ + --role="roles/bigquery.jobUser" \ + --member=serviceAccount:${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com + +$ gcloud iam service-accounts keys create \ + fixtures/bigquery/credentials.json \ + --iam-account=${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com + +## WARNING: Only do this on a private machine, as anyone else with +## access to the system will also be able to read the credentials file's +## contents once it's made world-readable and use it to access Google as +## the service account. This is necessary for dbmate running as root +## inside the Docker container to be able to read the file, though. + +$ chmod a+r fixtures/bigquery/credentials.json + +$ docker compose run --rm dev + +## The rest of these commands should be executed from inside the Docker +## container: + +$ make build + +$ make test \ + GOOGLE_APPLICATION_CREDENTIALS=/src/fixtures/bigquery/credentials.json \ + GOOGLE_BIGQUERY_TEST_URL=bigquery://$PROJECT_ID/$LOCATION/$DATASET \ + FLAGS+="-count 1 -v ./pkg/driver/bigquery #" + +``` diff --git a/fixtures/bigquery/migrations/20151129054053_test_migration.sql b/fixtures/bigquery/migrations/20151129054053_test_migration.sql new file mode 100644 index 00000000..e93dce59 --- /dev/null +++ b/fixtures/bigquery/migrations/20151129054053_test_migration.sql @@ -0,0 +1,9 @@ +-- migrate:up +create table users ( + id int64, + name string +); +insert into users (id, name) values (1, 'alice'); + +-- migrate:down +drop table users; diff --git a/fixtures/bigquery/migrations/20200227231541_test_posts.sql b/fixtures/bigquery/migrations/20200227231541_test_posts.sql new file mode 100644 index 00000000..7bdc88b2 --- /dev/null +++ b/fixtures/bigquery/migrations/20200227231541_test_posts.sql @@ -0,0 +1,8 @@ +-- migrate:up +create table posts ( + id int64, + name string +); + +-- migrate:down +drop table posts; diff --git a/fixtures/loadEnvFiles/.env b/fixtures/loadEnvFiles/.env new file mode 100644 index 00000000..11b4e5ca --- /dev/null +++ b/fixtures/loadEnvFiles/.env @@ -0,0 +1 @@ +TEST_DOTENV=default diff --git a/fixtures/loadEnvFiles/.gitignore b/fixtures/loadEnvFiles/.gitignore new file mode 100644 index 00000000..8e0776e8 --- /dev/null +++ b/fixtures/loadEnvFiles/.gitignore @@ -0,0 +1 @@ +!.env diff --git a/fixtures/loadEnvFiles/first.txt b/fixtures/loadEnvFiles/first.txt new file mode 100644 index 00000000..71c5e94e --- /dev/null +++ b/fixtures/loadEnvFiles/first.txt @@ -0,0 +1 @@ +FIRST=one diff --git a/fixtures/loadEnvFiles/invalid.txt b/fixtures/loadEnvFiles/invalid.txt new file mode 100644 index 00000000..7524c931 --- /dev/null +++ b/fixtures/loadEnvFiles/invalid.txt @@ -0,0 +1 @@ +INVALID ENV FILE diff --git a/fixtures/loadEnvFiles/second.txt b/fixtures/loadEnvFiles/second.txt new file mode 100644 index 00000000..0e1c837b --- /dev/null +++ b/fixtures/loadEnvFiles/second.txt @@ -0,0 +1 @@ +SECOND=two diff --git a/go.mod b/go.mod index 1c6f6820..56853b91 100644 --- a/go.mod +++ b/go.mod @@ -1,26 +1,91 @@ module github.com/assetnote/dbmate -go 1.15 +go 1.23.0 + +toolchain go1.24.0 + +require ( + cloud.google.com/go/bigquery v1.66.2 + github.com/ClickHouse/clickhouse-go/v2 v2.32.1 + github.com/go-sql-driver/mysql v1.9.0 + github.com/joho/godotenv v1.5.1 + github.com/lib/pq v1.10.9 + github.com/mattn/go-sqlite3 v1.14.24 + github.com/stretchr/testify v1.10.0 + github.com/urfave/cli/v2 v2.27.5 + github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04 + google.golang.org/api v0.222.0 + gorm.io/driver/bigquery v1.2.0 +) require ( - github.com/ClickHouse/clickhouse-go v1.4.3 - github.com/ashwanthkumar/slack-go-webhook v0.0.0-20200209025033-430dd4e66960 - github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect + cel.dev/expr v0.20.0 // indirect + cloud.google.com/go v0.118.2 // indirect + cloud.google.com/go/auth v0.14.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect + cloud.google.com/go/compute/metadata v0.6.0 // indirect + cloud.google.com/go/iam v1.4.0 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/ClickHouse/ch-go v0.65.1 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 // indirect + github.com/andybalholm/brotli v1.1.1 // indirect + github.com/apache/arrow/go/v15 v15.0.2 // indirect + github.com/ashwanthkumar/slack-go-webhook v0.0.0-20200209025033-430dd4e66960 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 // indirect - github.com/go-sql-driver/mysql v1.5.0 - github.com/joho/godotenv v1.3.0 - github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d - github.com/kr/pretty v0.1.0 // indirect - github.com/lib/pq v1.10.7 - github.com/mattn/go-sqlite3 v1.14.4 - github.com/parnurzeal/gorequest v0.2.16 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-faster/city v1.0.1 // indirect + github.com/go-faster/errors v0.7.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/google/flatbuffers v25.2.10+incompatible // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/googleapis/gax-go/v2 v2.14.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/moul/http2curl v1.0.0 // indirect + github.com/parnurzeal/gorequest v0.3.0 // indirect + github.com/paulmach/orb v0.11.1 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/smartystreets/goconvey v1.7.2 // indirect - github.com/stretchr/testify v1.4.0 - github.com/urfave/cli/v2 v2.3.0 - golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect - moul.io/http2curl v1.0.0 // indirect + github.com/segmentio/asm v1.2.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/oauth2 v0.26.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + golang.org/x/time v0.10.0 // indirect + golang.org/x/tools v0.30.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect + google.golang.org/genproto v0.0.0-20250219182151-9fdb1cabc7b2 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect + google.golang.org/grpc v1.70.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/gorm v1.25.12 // indirect ) diff --git a/go.sum b/go.sum index 9a2f32c0..530d15ef 100644 --- a/go.sum +++ b/go.sum @@ -1,88 +1,1038 @@ +cel.dev/expr v0.20.0 h1:OunBvVCfvpWlt4dN7zg3FM6TDkzOePe1+foGJ9AXeeI= +cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= +cloud.google.com/go v0.118.2 h1:bKXO7RXMFDkniAAvvuMrAPtQ/VHrs9e7J5UT3yrGdTY= +cloud.google.com/go v0.118.2/go.mod h1:CFO4UPEPi8oV21xoezZCrd3d81K4fFkDTEJu4R8K+9M= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/auth v0.14.1 h1:AwoJbzUdxA/whv1qj3TLKwh3XX5sikny2fc40wUl+h0= +cloud.google.com/go/auth v0.14.1/go.mod h1:4JHUxlGXisL0AW8kXPtUF6ztuOksyfUQNFjfsOCXkPM= +cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= +cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/bigquery v1.66.2 h1:EKOSqjtO7jPpJoEzDmRctGea3c2EOGoexy8VyY9dNro= +cloud.google.com/go/bigquery v1.66.2/go.mod h1:+Yd6dRyW8D/FYEjUGodIbu0QaoEmgav7Lwhotup6njo= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/datacatalog v1.24.3 h1:3bAfstDB6rlHyK0TvqxEwaeOvoN9UgCs2bn03+VXmss= +cloud.google.com/go/datacatalog v1.24.3/go.mod h1:Z4g33XblDxWGHngDzcpfeOU0b1ERlDPTuQoYG6NkF1s= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= +cloud.google.com/go/iam v1.4.0 h1:ZNfy/TYfn2uh/ukvhp783WhnbVluqf/tzOaqVUPlIPA= +cloud.google.com/go/iam v1.4.0/go.mod h1:gMBgqPaERlriaOV0CUl//XUzDhSfXevn4OEUbg6VRs4= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/longrunning v0.6.4 h1:3tyw9rO3E2XVXzSApn1gyEEnH2K9SynNQjMlBi3uHLg= +cloud.google.com/go/longrunning v0.6.4/go.mod h1:ttZpLCe6e7EXvn9OxpBRx7kZEB0efv8yBO6YnVMfhJs= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/monitoring v1.24.0 h1:csSKiCJ+WVRgNkRzzz3BPoGjFhjPY23ZTcaenToJxMM= +cloud.google.com/go/monitoring v1.24.0/go.mod h1:Bd1PRK5bmQBQNnuGwHBfUamAV1ys9049oEPHnn4pcsc= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= +cloud.google.com/go/storage v1.50.0 h1:3TbVkzTooBvnZsk7WaAQfOsNrdoM8QHusXA1cpk6QJs= +cloud.google.com/go/storage v1.50.0/go.mod h1:l7XeiD//vx5lfqE3RavfmU9yvk5Pp0Zhcv482poyafY= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/ClickHouse/clickhouse-go v1.4.3 h1:iAFMa2UrQdR5bHJ2/yaSLffZkxpcOYQMCUuKeNXGdqc= -github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ClickHouse/ch-go v0.65.1 h1:SLuxmLl5Mjj44/XbINsK2HFvzqup0s6rwKLFH347ZhU= +github.com/ClickHouse/ch-go v0.65.1/go.mod h1:bsodgURwmrkvkBe5jw1qnGDgyITsYErfONKAHn05nv4= +github.com/ClickHouse/clickhouse-go/v2 v2.32.1 h1:RLhkxA6iH/bLTXeDtEj/u4yUx9Q03Y95P+cjHScQK78= +github.com/ClickHouse/clickhouse-go/v2 v2.32.1/go.mod h1:YtaiIFlHCGNPbOpAvFGYobtcVnmgYvD/WmzitixxWYc= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0 h1:f2Qw/Ehhimh5uO1fayV0QIW7DShEQqhtUfhYc+cBPlw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.26.0/go.mod h1:2bIszWvQRlJVmJLiuLhukLImRjKPcYdzzsx6darK02A= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 h1:5IT7xOdq17MtcdtL/vtl6mGfzhaq4m4vpollPRmlsBQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0/go.mod h1:ZV4VOm0/eHR06JLrXWe09068dHpr3TRpY9Uo7T+anuA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 h1:ig/FpDD2JofP/NExKQUbn7uOSZzJAQqogfqluZK4ed4= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= +github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= +github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= github.com/ashwanthkumar/slack-go-webhook v0.0.0-20200209025033-430dd4e66960 h1:MIEURpsIpyLyy+dZ+GnL8T5P49Tco0ik9cYaUQNnAxE= github.com/ashwanthkumar/slack-go-webhook v0.0.0-20200209025033-430dd4e66960/go.mod h1:97O1qkjJBHSSaWJxsTShRIeFy0HWiygk+jnugO9aX3I= -github.com/bkaradzic/go-lz4 v1.0.0 h1:RXc4wYsyz985CkXXeX04y4VnZFGG8Rd43pRaHsOXAKk= -github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4= -github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY/k9Os/UFtwERei2/XzGemhpGnBKNg= -github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk= +github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0= -github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= -github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= -github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= -github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= -github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d h1:cVtBfNW5XTHiKQe7jDaDBSh/EVM4XLPutLAGboIXuM0= -github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d/go.mod h1:P2viExyCEfeWGU259JnaQ34Inuec4R38JCyBx2edgD0= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= +github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= +github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= +github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo= +github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +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.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= -github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= -github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI= -github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= -github.com/parnurzeal/gorequest v0.2.16 h1:T/5x+/4BT+nj+3eSknXmCTnEVGSzFzPGdpqmUVVZXHQ= -github.com/parnurzeal/gorequest v0.2.16/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE= -github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/parnurzeal/gorequest v0.3.0 h1:SoFyqCDC9COr1xuS6VA8fC8RU7XyrJZN2ona1kEX7FI= +github.com/parnurzeal/gorequest v0.3.0/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE= +github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= +github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= +github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= 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/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= -github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= -github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= -github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04 h1:qXafrlZL1WsJW5OokjraLLRURHiw0OzKHD/RNdspp4w= +github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04/go.mod h1:FiwNQxz6hGoNFBC4nIx+CxZhI3nne5RmIOlT/MXcSD4= +go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao= +go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/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.1-0.20180807135948-17ff2d5776d2/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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o= +gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= +google.golang.org/api v0.222.0 h1:Aiewy7BKLCuq6cUCeOUrsAlzjXPqBkEeQ/iwGHVQa/4= +google.golang.org/api v0.222.0/go.mod h1:efZia3nXpWELrwMlN5vyQrD4GmJN1Vw0x68Et3r+a9c= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20250219182151-9fdb1cabc7b2 h1:2v3FMY0zK1tvBifGo6n93tzG4Bt6ovwccxvaAMbg4y0= +google.golang.org/genproto v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:8gW3cF0R9yLr/iTl4DCcRcZuuTmm/ohUb1kauVvE354= +google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2 h1:35ZFtrCgaAjF7AFAK0+lRSf+4AyYnWRbH7og13p7rZ4= +google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:W9ynFDP/shebLB1Hl/ESTOap2jHd6pmLXPNZC7SVDbA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 h1:DMTIbak9GhdaSxEjvVzAeNZvyc03I61duqNbnm3SU0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3 h1:fvjTMHxHEw/mxHbtzPi3JCcKXQRAnQTBRo6YCJSVHKI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8= -moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/bigquery v1.2.0 h1:E94oEXErYb4uImcR8oiCjE1SP2VdnrL5f3d78PtFWNk= +gorm.io/driver/bigquery v1.2.0/go.mod h1:/5kcyb6RVIk/seff6YANAjB5aisE4oqY35x0Ix9iwXY= +gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/main.go b/main.go index fcf3b7ec..77ea1b80 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,8 @@ package main import ( + "errors" "fmt" - "log" "net/url" "os" "regexp" @@ -10,6 +10,7 @@ import ( "github.com/ashwanthkumar/slack-go-webhook" "github.com/assetnote/dbmate/pkg/dbmate" + _ "github.com/assetnote/dbmate/pkg/driver/bigquery" _ "github.com/assetnote/dbmate/pkg/driver/clickhouse" _ "github.com/assetnote/dbmate/pkg/driver/mysql" _ "github.com/assetnote/dbmate/pkg/driver/postgres" @@ -19,10 +20,14 @@ import ( ) func main() { - loadDotEnv() + err := loadEnvFiles(os.Args[1:]) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(3) + } app := NewApp() - err := app.Run(os.Args) + err = app.Run(os.Args) if err != nil { errText := redactLogString(fmt.Sprintf("Error: %s\n", err)) _, _ = fmt.Fprint(os.Stderr, errText) @@ -52,6 +57,8 @@ func NewApp() *cli.App { app.Usage = "A lightweight, framework-independent database migration tool." app.Version = dbmate.Version + defaultDB := dbmate.New(nil) + app.Flags = []cli.Flag{ &cli.StringFlag{ Name: "url", @@ -64,24 +71,29 @@ func NewApp() *cli.App { Value: "DATABASE_URL", Usage: "specify an environment variable containing the database URL", }, - &cli.StringFlag{ + &cli.StringSliceFlag{ + Name: "env-file", + Value: cli.NewStringSlice(".env"), + Usage: "specify a file to load environment variables from", + }, + &cli.StringSliceFlag{ Name: "migrations-dir", Aliases: []string{"d"}, EnvVars: []string{"DBMATE_MIGRATIONS_DIR"}, - Value: dbmate.DefaultMigrationsDir, + Value: cli.NewStringSlice(defaultDB.MigrationsDir[0]), Usage: "specify the directory containing migration files", }, &cli.StringFlag{ Name: "migrations-table", EnvVars: []string{"DBMATE_MIGRATIONS_TABLE"}, - Value: dbmate.DefaultMigrationsTableName, + Value: defaultDB.MigrationsTableName, Usage: "specify the database table to record migrations in", }, &cli.StringFlag{ Name: "schema-file", Aliases: []string{"s"}, EnvVars: []string{"DBMATE_SCHEMA_FILE"}, - Value: dbmate.DefaultSchemaFile, + Value: defaultDB.SchemaFile, Usage: "specify the schema file location", }, &cli.BoolFlag{ @@ -98,7 +110,7 @@ func NewApp() *cli.App { Name: "wait-timeout", EnvVars: []string{"DBMATE_WAIT_TIMEOUT"}, Usage: "timeout for --wait flag", - Value: dbmate.DefaultWaitTimeout, + Value: defaultDB.WaitTimeout, }, } @@ -116,6 +128,11 @@ func NewApp() *cli.App { Name: "up", Usage: "Create database (if necessary) and migrate to the latest version", Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "strict", + EnvVars: []string{"DBMATE_STRICT"}, + Usage: "fail if migrations would be applied out of order", + }, &cli.BoolFlag{ Name: "verbose", Aliases: []string{"v"}, @@ -124,6 +141,7 @@ func NewApp() *cli.App { }, }, Action: action(func(db *dbmate.DB, c *cli.Context) error { + db.Strict = c.Bool("strict") db.Verbose = c.Bool("verbose") return db.CreateAndMigrate() }), @@ -131,14 +149,14 @@ func NewApp() *cli.App { { Name: "create", Usage: "Create database", - Action: action(func(db *dbmate.DB, c *cli.Context) error { + Action: action(func(db *dbmate.DB, _ *cli.Context) error { return db.Create() }), }, { Name: "drop", Usage: "Drop database (if it exists)", - Action: action(func(db *dbmate.DB, c *cli.Context) error { + Action: action(func(db *dbmate.DB, _ *cli.Context) error { return db.Drop() }), }, @@ -146,6 +164,11 @@ func NewApp() *cli.App { Name: "migrate", Usage: "Migrate to the latest version", Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "strict", + EnvVars: []string{"DBMATE_STRICT"}, + Usage: "fail if migrations would be applied out of order", + }, &cli.BoolFlag{ Name: "verbose", Aliases: []string{"v"}, @@ -154,6 +177,7 @@ func NewApp() *cli.App { }, }, Action: action(func(db *dbmate.DB, c *cli.Context) error { + db.Strict = c.Bool("strict") db.Verbose = c.Bool("verbose") return db.Migrate() }), @@ -189,6 +213,7 @@ func NewApp() *cli.App { }, }, Action: action(func(db *dbmate.DB, c *cli.Context) error { + db.Strict = c.Bool("strict") setExitCode := c.Bool("exit-code") quiet := c.Bool("quiet") if quiet { @@ -210,14 +235,21 @@ func NewApp() *cli.App { { Name: "dump", Usage: "Write the database schema to disk", - Action: action(func(db *dbmate.DB, c *cli.Context) error { + Action: action(func(db *dbmate.DB, _ *cli.Context) error { return db.DumpSchema() }), }, + { + Name: "load", + Usage: "Load schema file to the database", + Action: action(func(db *dbmate.DB, _ *cli.Context) error { + return db.LoadSchema() + }), + }, { Name: "wait", Usage: "Wait for the database to become available", - Action: action(func(db *dbmate.DB, c *cli.Context) error { + Action: action(func(db *dbmate.DB, _ *cli.Context) error { return db.Wait() }), }, @@ -226,15 +258,46 @@ func NewApp() *cli.App { return app } -// load environment variables from .env file -func loadDotEnv() { - if _, err := os.Stat(".env"); err != nil { - return +// load environment variables from file(s) +func loadEnvFiles(args []string) error { + var envFiles []string + + for i := 0; i < len(args); i++ { + if args[i] == "--env-file" { + if i+1 >= len(args) { + // returning nil here, even though it's an error + // because we want the caller to proceed anyway, + // and produce the actual arg parsing error response + return nil + } + + envFiles = append(envFiles, args[i+1]) + i++ + } + } + + if len(envFiles) == 0 { + envFiles = []string{".env"} } - if err := godotenv.Load(); err != nil { - log.Fatalf("Error loading .env file: %s", err.Error()) + // try to load all files in sequential order, + // ignoring any that do not exist + for _, file := range envFiles { + err := godotenv.Load([]string{file}...) + if err == nil { + continue + } + + var perr *os.PathError + if errors.As(err, &perr) && errors.Is(perr, os.ErrNotExist) { + // Ignoring file not found error + continue + } + + return fmt.Errorf("loading env file(s) %v: %v", envFiles, err) } + + return nil } // action wraps a cli.ActionFunc with dbmate initialization logic @@ -246,13 +309,13 @@ func action(f func(*dbmate.DB, *cli.Context) error) cli.ActionFunc { } db := dbmate.New(u) db.AutoDumpSchema = !c.Bool("no-dump-schema") - db.MigrationsDir = c.String("migrations-dir") + db.MigrationsDir = c.StringSlice("migrations-dir") db.MigrationsTableName = c.String("migrations-table") db.SchemaFile = c.String("schema-file") db.WaitBefore = c.Bool("wait") - overrideTimeout := c.Duration("wait-timeout") - if overrideTimeout != 0 { - db.WaitTimeout = overrideTimeout + waitTimeout := c.Duration("wait-timeout") + if waitTimeout != 0 { + db.WaitTimeout = waitTimeout } return f(db, c) diff --git a/main_cgo.go b/main_cgo.go new file mode 100644 index 00000000..7a5ef22b --- /dev/null +++ b/main_cgo.go @@ -0,0 +1,8 @@ +//go:build cgo +// +build cgo + +package main + +import ( + _ "github.com/assetnote/dbmate/pkg/driver/sqlite" +) diff --git a/main_test.go b/main_test.go index df1cab52..5d184978 100644 --- a/main_test.go +++ b/main_test.go @@ -3,6 +3,7 @@ package main import ( "flag" "os" + "strings" "testing" "github.com/stretchr/testify/require" @@ -58,3 +59,122 @@ func TestRedactLogString(t *testing.T) { require.Equal(t, ex.expected, redactLogString(ex.in)) } } + +func TestLoadEnvFiles(t *testing.T) { + setup := func(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + env := os.Environ() + os.Clearenv() + + err = os.Chdir("fixtures/loadEnvFiles") + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { + err := os.Chdir(cwd) + if err != nil { + t.Fatal(err) + } + + os.Clearenv() + + for _, e := range env { + pair := strings.SplitN(e, "=", 2) + os.Setenv(pair[0], pair[1]) + } + }) + } + + t.Run("default file is .env", func(t *testing.T) { + setup(t) + + err := loadEnvFiles([]string{}) + require.NoError(t, err) + + require.Equal(t, 1, len(os.Environ())) + require.Equal(t, "default", os.Getenv("TEST_DOTENV")) + }) + + t.Run("valid file", func(t *testing.T) { + setup(t) + + err := loadEnvFiles([]string{"--env-file", "first.txt"}) + require.NoError(t, err) + require.Equal(t, 1, len(os.Environ())) + require.Equal(t, "one", os.Getenv("FIRST")) + }) + + t.Run("two valid files", func(t *testing.T) { + setup(t) + + err := loadEnvFiles([]string{"--env-file", "first.txt", "--env-file", "second.txt"}) + require.NoError(t, err) + require.Equal(t, 2, len(os.Environ())) + require.Equal(t, "one", os.Getenv("FIRST")) + require.Equal(t, "two", os.Getenv("SECOND")) + }) + + t.Run("nonexistent file", func(t *testing.T) { + setup(t) + + err := loadEnvFiles([]string{"--env-file", "nonexistent.txt"}) + require.NoError(t, err) + require.Equal(t, 0, len(os.Environ())) + }) + + t.Run("no overload", func(t *testing.T) { + setup(t) + + // we do not load values over existing values + os.Setenv("FIRST", "not one") + + err := loadEnvFiles([]string{"--env-file", "first.txt"}) + require.NoError(t, err) + require.Equal(t, 1, len(os.Environ())) + require.Equal(t, "not one", os.Getenv("FIRST")) + }) + + t.Run("invalid file", func(t *testing.T) { + setup(t) + + err := loadEnvFiles([]string{"--env-file", "invalid.txt"}) + require.Error(t, err) + require.Contains(t, err.Error(), "unexpected character \"\\n\" in variable name near \"INVALID ENV FILE\\n\"") + require.Equal(t, 0, len(os.Environ())) + }) + + t.Run("invalid file followed by a valid file", func(t *testing.T) { + setup(t) + + err := loadEnvFiles([]string{"--env-file", "invalid.txt", "--env-file", "first.txt"}) + require.Error(t, err) + require.Contains(t, err.Error(), "unexpected character \"\\n\" in variable name near \"INVALID ENV FILE\\n\"") + require.Equal(t, 0, len(os.Environ())) + }) + + t.Run("valid file followed by an invalid file", func(t *testing.T) { + setup(t) + + err := loadEnvFiles([]string{"--env-file", "first.txt", "--env-file", "invalid.txt"}) + require.Error(t, err) + require.Contains(t, err.Error(), "unexpected character \"\\n\" in variable name near \"INVALID ENV FILE\\n\"") + require.Equal(t, 1, len(os.Environ())) + require.Equal(t, "one", os.Getenv("FIRST")) + }) + + t.Run("valid file followed by an invalid file followed by a valid file", func(t *testing.T) { + setup(t) + + err := loadEnvFiles([]string{"--env-file", "first.txt", "--env-file", "invalid.txt", "--env-file", "second.txt"}) + require.Error(t, err) + require.Contains(t, err.Error(), "unexpected character \"\\n\" in variable name near \"INVALID ENV FILE\\n\"") + // files after an invalid file should not get loaded + require.Equal(t, 1, len(os.Environ())) + require.Equal(t, "one", os.Getenv("FIRST")) + }) +} diff --git a/pkg/dbmate/db.go b/pkg/dbmate/db.go index 2cb2a43d..7d6efeb7 100644 --- a/pkg/dbmate/db.go +++ b/pkg/dbmate/db.go @@ -4,9 +4,11 @@ import ( "database/sql" "errors" "fmt" - "io/ioutil" + "io" + "io/fs" "net/url" "os" + "path" "path/filepath" "regexp" "sort" @@ -15,37 +17,51 @@ import ( "github.com/assetnote/dbmate/pkg/dbutil" ) -// DefaultMigrationsDir specifies default directory to find migration files -const DefaultMigrationsDir = "./db/migrations" - -// DefaultMigrationsTableName specifies default database tables to record migraitons in -const DefaultMigrationsTableName = "schema_migrations" - -// DefaultSchemaFile specifies default location for schema.sql -const DefaultSchemaFile = "./db/schema.sql" - -// DefaultWaitInterval specifies length of time between connection attempts -const DefaultWaitInterval = time.Second +// Error codes +var ( + ErrNoMigrationFiles = errors.New("no migration files found") + ErrInvalidURL = errors.New("invalid url, have you set your --url flag or DATABASE_URL environment variable?") + ErrNoRollback = errors.New("can't rollback: no migrations have been applied") + ErrCantConnect = errors.New("unable to connect to database") + ErrUnsupportedDriver = errors.New("unsupported driver") + ErrNoMigrationName = errors.New("please specify a name for the new migration") + ErrMigrationAlreadyExist = errors.New("file already exists") + ErrMigrationDirNotFound = errors.New("could not find migrations directory") + ErrMigrationNotFound = errors.New("can't find migration file") + ErrCreateDirectory = errors.New("unable to create directory") +) -// DefaultWaitTimeout specifies maximum time for connection attempts -const DefaultWaitTimeout = 60 * time.Second +// migrationFileRegexp pattern for valid migration files +var migrationFileRegexp = regexp.MustCompile(`^(\d+).*\.sql$`) // DB allows dbmate actions to be performed on a specified database type DB struct { - AutoDumpSchema bool - DatabaseURL *url.URL - MigrationsDir string + // AutoDumpSchema generates schema.sql after each action + AutoDumpSchema bool + // DatabaseURL is the database connection string + DatabaseURL *url.URL + // FS specifies the filesystem, or nil for OS filesystem + FS fs.FS + // Log is the interface to write stdout + Log io.Writer + // MigrationsDir specifies the directory or directories to find migration files + MigrationsDir []string + // MigrationsTableName specifies the database table to record migrations in MigrationsTableName string - SchemaFile string - Verbose bool - WaitBefore bool - WaitInterval time.Duration - WaitTimeout time.Duration + // SchemaFile specifies the location for schema.sql file + SchemaFile string + // Fail if migrations would be applied out of order + Strict bool + // Verbose prints the result of each statement execution + Verbose bool + // WaitBefore will wait for database to become available before running any actions + WaitBefore bool + // WaitInterval specifies length of time between connection attempts + WaitInterval time.Duration + // WaitTimeout specifies maximum time for connection attempts + WaitTimeout time.Duration } -// migrationFileRegexp pattern for valid migration files -var migrationFileRegexp = regexp.MustCompile(`^\d.*\.sql$`) - // StatusResult represents an available migration status type StatusResult struct { Filename string @@ -57,43 +73,44 @@ func New(databaseURL *url.URL) *DB { return &DB{ AutoDumpSchema: true, DatabaseURL: databaseURL, - MigrationsDir: DefaultMigrationsDir, - MigrationsTableName: DefaultMigrationsTableName, - SchemaFile: DefaultSchemaFile, + FS: nil, + Log: os.Stdout, + MigrationsDir: []string{"./db/migrations"}, + MigrationsTableName: "schema_migrations", + SchemaFile: "./db/schema.sql", + Strict: false, + Verbose: false, WaitBefore: false, - WaitInterval: DefaultWaitInterval, - WaitTimeout: DefaultWaitTimeout, + WaitInterval: time.Second, + WaitTimeout: 60 * time.Second, } } -// GetDriver initializes the appropriate database driver -func (db *DB) GetDriver() (Driver, error) { +// Driver initializes the appropriate database driver +func (db *DB) Driver() (Driver, error) { if db.DatabaseURL == nil || db.DatabaseURL.Scheme == "" { - return nil, errors.New("invalid url") + return nil, ErrInvalidURL } driverFunc := drivers[db.DatabaseURL.Scheme] if driverFunc == nil { - return nil, fmt.Errorf("unsupported driver: %s", db.DatabaseURL.Scheme) + return nil, fmt.Errorf("%w: %s", ErrUnsupportedDriver, db.DatabaseURL.Scheme) } config := DriverConfig{ DatabaseURL: db.DatabaseURL, + Log: db.Log, MigrationsTableName: db.MigrationsTableName, } + drv := driverFunc(config) - return driverFunc(config), nil -} - -// Wait blocks until the database server is available. It does not verify that -// the specified database exists, only that the host is ready to accept connections. -func (db *DB) Wait() error { - drv, err := db.GetDriver() - if err != nil { - return err + if db.WaitBefore { + if err := db.wait(drv); err != nil { + return nil, err + } } - return db.wait(drv) + return drv, nil } func (db *DB) wait(drv Driver) error { @@ -104,37 +121,42 @@ func (db *DB) wait(drv Driver) error { return nil } - fmt.Print("Waiting for database") + fmt.Fprint(db.Log, "Waiting for database") for i := 0 * time.Second; i < db.WaitTimeout; i += db.WaitInterval { - fmt.Print(".") + fmt.Fprint(db.Log, ".") time.Sleep(db.WaitInterval) // attempt connection to database server err = drv.Ping() if err == nil { // connection successful - fmt.Print("\n") + fmt.Fprint(db.Log, "\n") return nil } } // if we find outselves here, we could not connect within the timeout - fmt.Print("\n") - return fmt.Errorf("unable to connect to database: %s", err) + fmt.Fprint(db.Log, "\n") + return fmt.Errorf("%w: %s", ErrCantConnect, err) } -// CreateAndMigrate creates the database (if necessary) and runs migrations -func (db *DB) CreateAndMigrate() error { - drv, err := db.GetDriver() +// Wait blocks until the database server is available. It does not verify that +// the specified database exists, only that the host is ready to accept connections. +func (db *DB) Wait() error { + drv, err := db.Driver() if err != nil { return err } - if db.WaitBefore { - err := db.wait(drv) - if err != nil { - return err - } + // if db.WaitBefore is true, wait() will get called twice, no harm + return db.wait(drv) +} + +// CreateAndMigrate creates the database (if necessary) and runs migrations +func (db *DB) CreateAndMigrate() error { + drv, err := db.Driver() + if err != nil { + return err } // create database if it does not already exist @@ -148,61 +170,36 @@ func (db *DB) CreateAndMigrate() error { } // migrate - return db.migrate(drv) + return db.Migrate() } // Create creates the current database func (db *DB) Create() error { - drv, err := db.GetDriver() + drv, err := db.Driver() if err != nil { return err } - if db.WaitBefore { - err := db.wait(drv) - if err != nil { - return err - } - } - return drv.CreateDatabase() } // Drop drops the current database (if it exists) func (db *DB) Drop() error { - drv, err := db.GetDriver() + drv, err := db.Driver() if err != nil { return err } - if db.WaitBefore { - err := db.wait(drv) - if err != nil { - return err - } - } - return drv.DropDatabase() } // DumpSchema writes the current database schema to a file func (db *DB) DumpSchema() error { - drv, err := db.GetDriver() + drv, err := db.Driver() if err != nil { return err } - return db.dumpSchema(drv) -} - -func (db *DB) dumpSchema(drv Driver) error { - if db.WaitBefore { - err := db.wait(drv) - if err != nil { - return err - } - } - sqlDB, err := db.openDatabaseForMigration(drv) if err != nil { return err @@ -214,7 +211,7 @@ func (db *DB) dumpSchema(drv Driver) error { return err } - fmt.Printf("Writing: %s\n", db.SchemaFile) + fmt.Fprintf(db.Log, "Writing: %s\n", db.SchemaFile) // ensure schema directory exists if err = ensureDir(filepath.Dir(db.SchemaFile)); err != nil { @@ -222,13 +219,48 @@ func (db *DB) dumpSchema(drv Driver) error { } // write schema to file - return ioutil.WriteFile(db.SchemaFile, schema, 0644) + return os.WriteFile(db.SchemaFile, schema, 0o644) +} + +// LoadSchema loads schema file to the current database +func (db *DB) LoadSchema() error { + drv, err := db.Driver() + if err != nil { + return err + } + + sqlDB, err := drv.Open() + if err != nil { + return err + } + defer dbutil.MustClose(sqlDB) + + _, err = os.Stat(db.SchemaFile) + if err != nil { + return err + } + + fmt.Fprintf(db.Log, "Reading: %s\n", db.SchemaFile) + + bytes, err := os.ReadFile(db.SchemaFile) + if err != nil { + return err + } + + result, err := sqlDB.Exec(string(bytes)) + if err != nil { + return err + } else if db.Verbose { + db.printVerbose(result) + } + + return nil } // ensureDir creates a directory if it does not already exist func ensureDir(dir string) error { - if err := os.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("unable to create directory `%s`", dir) + if err := os.MkdirAll(dir, 0o755); err != nil { + return fmt.Errorf("%w `%s`", ErrCreateDirectory, dir) } return nil @@ -241,21 +273,21 @@ func (db *DB) NewMigration(name string) error { // new migration name timestamp := time.Now().UTC().Format("20060102150405") if name == "" { - return fmt.Errorf("please specify a name for the new migration") + return ErrNoMigrationName } name = fmt.Sprintf("%s_%s.sql", timestamp, name) // create migrations dir if missing - if err := ensureDir(db.MigrationsDir); err != nil { + if err := ensureDir(db.MigrationsDir[0]); err != nil { return err } // check file does not already exist - path := filepath.Join(db.MigrationsDir, name) - fmt.Printf("Creating migration: %s\n", path) + path := filepath.Join(db.MigrationsDir[0], name) + fmt.Fprintf(db.Log, "Creating migration: %s\n", path) if _, err := os.Stat(path); !os.IsNotExist(err) { - return fmt.Errorf("file already exists") + return ErrMigrationAlreadyExist } // write new migration @@ -302,70 +334,70 @@ func (db *DB) openDatabaseForMigration(drv Driver) (*sql.DB, error) { // Migrate migrates database to the latest version func (db *DB) Migrate() error { - drv, err := db.GetDriver() + drv, err := db.Driver() if err != nil { return err } - return db.migrate(drv) -} - -func (db *DB) migrate(drv Driver) error { - files, err := findMigrationFiles(db.MigrationsDir, migrationFileRegexp) + migrations, err := db.FindMigrations() if err != nil { return err } - if len(files) == 0 { - return fmt.Errorf("no migration files found") + if len(migrations) == 0 { + return ErrNoMigrationFiles } - if db.WaitBefore { - err := db.wait(drv) - if err != nil { - return err + highestAppliedMigrationVersion := "" + pendingMigrations := []Migration{} + for _, migration := range migrations { + if migration.Applied { + if db.Strict && highestAppliedMigrationVersion <= migration.Version { + highestAppliedMigrationVersion = migration.Version + } + } else { + pendingMigrations = append(pendingMigrations, migration) } } - sqlDB, err := db.openDatabaseForMigration(drv) - if err != nil { - return err + if len(pendingMigrations) > 0 && db.Strict && pendingMigrations[0].Version <= highestAppliedMigrationVersion { + return fmt.Errorf( + "migration `%s` is out of order with already applied migrations, the version number has to be higher than the applied migration `%s` in --strict mode", + pendingMigrations[0].Version, + highestAppliedMigrationVersion, + ) } - defer dbutil.MustClose(sqlDB) - applied, err := drv.SelectMigrations(sqlDB, -1) + sqlDB, err := db.openDatabaseForMigration(drv) if err != nil { return err } + defer dbutil.MustClose(sqlDB) - for _, filename := range files { - ver := migrationVersion(filename) - if ok := applied[ver]; ok { - // migration already applied - continue - } + for _, migration := range pendingMigrations { + fmt.Fprintf(db.Log, "Applying: %s\n", migration.FileName) - fmt.Printf("Applying: %s\n", filename) + start := time.Now() - up, _, err := parseMigration(filepath.Join(db.MigrationsDir, filename)) + parsed, err := migration.Parse() if err != nil { return err } execMigration := func(tx dbutil.Transaction) error { // run actual migration - result, err := tx.Exec(up.Contents) + result, err := tx.Exec(parsed.Up) if err != nil { - return err + return drv.QueryError(parsed.Up, err) } else if db.Verbose { - printVerbose(result) + db.printVerbose(result) } // record migration - return drv.InsertMigration(tx, ver) + return drv.InsertMigration(tx, migration.Version) } - if up.Options.Transaction() { + if parsed.UpOptions.Transaction() { // begin transaction err = doTransaction(sqlDB, execMigration) } else { @@ -373,6 +405,9 @@ func (db *DB) migrate(drv Driver) error { err = execMigration(sqlDB) } + elapsed := time.Since(start) + fmt.Fprintf(db.Log, "Applied: %s in %s\n", migration.FileName, elapsed) + if err != nil { return err } @@ -380,132 +415,158 @@ func (db *DB) migrate(drv Driver) error { // automatically update schema file, silence errors if db.AutoDumpSchema { - _ = db.dumpSchema(drv) + _ = db.DumpSchema() } return nil } -func printVerbose(result sql.Result) { +func (db *DB) printVerbose(result sql.Result) { lastInsertID, err := result.LastInsertId() if err == nil { - fmt.Printf("Last insert ID: %d\n", lastInsertID) + fmt.Fprintf(db.Log, "Last insert ID: %d\n", lastInsertID) } rowsAffected, err := result.RowsAffected() if err == nil { - fmt.Printf("Rows affected: %d\n", rowsAffected) + fmt.Fprintf(db.Log, "Rows affected: %d\n", rowsAffected) } } -func findMigrationFiles(dir string, re *regexp.Regexp) ([]string, error) { - files, err := ioutil.ReadDir(dir) - if err != nil { - return nil, fmt.Errorf("could not find migrations directory `%s`", dir) - } - - matches := []string{} - for _, file := range files { - if file.IsDir() { - continue - } - - name := file.Name() - if !re.MatchString(name) { - continue - } +func (db *DB) readMigrationsDir(dir string) ([]fs.DirEntry, error) { + path := path.Clean(dir) - matches = append(matches, name) + // We use nil instead of os.DirFS() because DirFS cannot support both relative and absolute + // directory paths - it must be anchored at either "." or "/", which we do not know in advance. + // See: https://github.com/amacneil/dbmate/issues/403 + if db.FS == nil { + return os.ReadDir(path) } - sort.Strings(matches) - - return matches, nil + return fs.ReadDir(db.FS, path) } -func findMigrationFile(dir string, ver string) (string, error) { - if ver == "" { - panic("migration version is required") +// FindMigrations lists all available migrations +func (db *DB) FindMigrations() ([]Migration, error) { + drv, err := db.Driver() + if err != nil { + return nil, err } - ver = regexp.QuoteMeta(ver) - re := regexp.MustCompile(fmt.Sprintf(`^%s.*\.sql$`, ver)) + sqlDB, err := drv.Open() + if err != nil { + return nil, err + } + defer dbutil.MustClose(sqlDB) - files, err := findMigrationFiles(dir, re) + // find applied migrations + appliedMigrations := map[string]bool{} + migrationsTableExists, err := drv.MigrationsTableExists(sqlDB) if err != nil { - return "", err + return nil, err } - if len(files) == 0 { - return "", fmt.Errorf("can't find migration file: %s*.sql", ver) + if migrationsTableExists { + appliedMigrations, err = drv.SelectMigrations(sqlDB, -1) + if err != nil { + return nil, err + } } - return files[0], nil -} + migrations := []Migration{} + for _, dir := range db.MigrationsDir { + // find filesystem migrations + files, err := db.readMigrationsDir(dir) + if err != nil { + return nil, fmt.Errorf("%w `%s`", ErrMigrationDirNotFound, dir) + } + + for _, file := range files { + if file.IsDir() { + continue + } + + matches := migrationFileRegexp.FindStringSubmatch(file.Name()) + if len(matches) < 2 { + continue + } + + migration := Migration{ + Applied: false, + FileName: matches[0], + FilePath: path.Join(dir, matches[0]), + FS: db.FS, + Version: matches[1], + } + if ok := appliedMigrations[migration.Version]; ok { + migration.Applied = true + } + + migrations = append(migrations, migration) + } + } + + sort.Slice( + migrations, func(i, j int) bool { + return migrations[i].FileName < migrations[j].FileName + }, + ) -func migrationVersion(filename string) string { - return regexp.MustCompile(`^\d+`).FindString(filename) + return migrations, nil } // Rollback rolls back the most recent migration func (db *DB) Rollback() error { - drv, err := db.GetDriver() + drv, err := db.Driver() if err != nil { return err } - if db.WaitBefore { - err := db.wait(drv) - if err != nil { - return err - } - } - sqlDB, err := db.openDatabaseForMigration(drv) if err != nil { return err } defer dbutil.MustClose(sqlDB) - applied, err := drv.SelectMigrations(sqlDB, 1) + // find last applied migration + var latest *Migration + migrations, err := db.FindMigrations() if err != nil { return err } - // grab most recent applied migration (applied has len=1) - latest := "" - for ver := range applied { - latest = ver - } - if latest == "" { - return fmt.Errorf("can't rollback: no migrations have been applied") + for i, migration := range migrations { + if migration.Applied { + latest = &migrations[i] + } } - filename, err := findMigrationFile(db.MigrationsDir, latest) - if err != nil { - return err + if latest == nil { + return ErrNoRollback } - fmt.Printf("Rolling back: %s\n", filename) + fmt.Fprintf(db.Log, "Rolling back: %s\n", latest.FileName) + + start := time.Now() - _, down, err := parseMigration(filepath.Join(db.MigrationsDir, filename)) + parsed, err := latest.Parse() if err != nil { return err } execMigration := func(tx dbutil.Transaction) error { // rollback migration - result, err := tx.Exec(down.Contents) + result, err := tx.Exec(parsed.Down) if err != nil { - return err + return drv.QueryError(parsed.Down, err) } else if db.Verbose { - printVerbose(result) + db.printVerbose(result) } // remove migration record - return drv.DeleteMigration(tx, latest) + return drv.DeleteMigration(tx, latest.Version) } - if down.Options.Transaction() { + if parsed.DownOptions.Transaction() { // begin transaction err = doTransaction(sqlDB, execMigration) } else { @@ -513,13 +574,16 @@ func (db *DB) Rollback() error { err = execMigration(sqlDB) } + elapsed := time.Since(start) + fmt.Fprintf(db.Log, "Rolled back: %s in %s\n", latest.FileName, elapsed) + if err != nil { return err } // automatically update schema file, silence errors if db.AutoDumpSchema { - _ = db.dumpSchema(drv) + _ = db.DumpSchema() } return nil @@ -527,12 +591,7 @@ func (db *DB) Rollback() error { // Status shows the status of all migrations func (db *DB) Status(quiet bool) (int, error) { - drv, err := db.GetDriver() - if err != nil { - return -1, err - } - - results, err := db.CheckMigrationsStatus(drv) + results, err := db.FindMigrations() if err != nil { return -1, err } @@ -542,61 +601,22 @@ func (db *DB) Status(quiet bool) (int, error) { for _, res := range results { if res.Applied { - line = fmt.Sprintf("[X] %s", res.Filename) + line = fmt.Sprintf("[X] %s", res.FileName) totalApplied++ } else { - line = fmt.Sprintf("[ ] %s", res.Filename) + line = fmt.Sprintf("[ ] %s", res.FileName) } if !quiet { - fmt.Println(line) + fmt.Fprintln(db.Log, line) } } totalPending := len(results) - totalApplied if !quiet { - fmt.Println() - fmt.Printf("Applied: %d\n", totalApplied) - fmt.Printf("Pending: %d\n", totalPending) + fmt.Fprintln(db.Log) + fmt.Fprintf(db.Log, "Applied: %d\n", totalApplied) + fmt.Fprintf(db.Log, "Pending: %d\n", totalPending) } return totalPending, nil } - -// CheckMigrationsStatus returns the status of all available mgirations -func (db *DB) CheckMigrationsStatus(drv Driver) ([]StatusResult, error) { - files, err := findMigrationFiles(db.MigrationsDir, migrationFileRegexp) - if err != nil { - return nil, err - } - - if len(files) == 0 { - return nil, fmt.Errorf("no migration files found") - } - - sqlDB, err := db.openDatabaseForMigration(drv) - if err != nil { - return nil, err - } - defer dbutil.MustClose(sqlDB) - - applied, err := drv.SelectMigrations(sqlDB, -1) - if err != nil { - return nil, err - } - - var results []StatusResult - - for _, filename := range files { - ver := migrationVersion(filename) - res := StatusResult{Filename: filename} - if ok := applied[ver]; ok { - res.Applied = true - } else { - res.Applied = false - } - - results = append(results, res) - } - - return results, nil -} diff --git a/pkg/dbmate/db_test.go b/pkg/dbmate/db_test.go index 0b5d2470..b3d50593 100644 --- a/pkg/dbmate/db_test.go +++ b/pkg/dbmate/db_test.go @@ -1,37 +1,48 @@ package dbmate_test import ( - "io/ioutil" "net/url" "os" "path/filepath" + "regexp" + "strings" "testing" + "testing/fstest" "time" "github.com/assetnote/dbmate/pkg/dbmate" + "github.com/assetnote/dbmate/pkg/dbtest" "github.com/assetnote/dbmate/pkg/dbutil" _ "github.com/assetnote/dbmate/pkg/driver/mysql" _ "github.com/assetnote/dbmate/pkg/driver/postgres" _ "github.com/assetnote/dbmate/pkg/driver/sqlite" - "github.com/kami-zh/go-capturer" "github.com/stretchr/testify/require" + "github.com/zenizh/go-capturer" ) -var testdataDir string +var rootDir string + +func sqliteTestURL(t *testing.T) *url.URL { + return dbtest.MustParseURL(t, "sqlite:dbmate_test.sqlite3") +} + +func sqliteBrokenTestURL(t *testing.T) *url.URL { + return dbtest.MustParseURL(t, "sqlite:/doesnotexist/dbmate_test.sqlite3") +} func newTestDB(t *testing.T, u *url.URL) *dbmate.DB { var err error - // only chdir once, because testdata is relative to current directory - if testdataDir == "" { - testdataDir, err = filepath.Abs("../../testdata") - require.NoError(t, err) - - err = os.Chdir(testdataDir) + // find root directory relative to current directory + if rootDir == "" { + rootDir, err = filepath.Abs("../..") require.NoError(t, err) } + err = os.Chdir(rootDir + "/testdata") + require.NoError(t, err) + db := dbmate.New(u) db.AutoDumpSchema = false @@ -39,10 +50,10 @@ func newTestDB(t *testing.T, u *url.URL) *dbmate.DB { } func TestNew(t *testing.T) { - db := dbmate.New(dbutil.MustParseURL("foo:test")) + db := dbmate.New(dbtest.MustParseURL(t, "foo:test")) require.True(t, db.AutoDumpSchema) require.Equal(t, "foo:test", db.DatabaseURL.String()) - require.Equal(t, "./db/migrations", db.MigrationsDir) + require.Equal(t, []string{"./db/migrations"}, db.MigrationsDir) require.Equal(t, "schema_migrations", db.MigrationsTableName) require.Equal(t, "./db/schema.sql", db.SchemaFile) require.False(t, db.WaitBefore) @@ -53,61 +64,54 @@ func TestNew(t *testing.T) { func TestGetDriver(t *testing.T) { t.Run("missing URL", func(t *testing.T) { db := dbmate.New(nil) - drv, err := db.GetDriver() + drv, err := db.Driver() require.Nil(t, drv) - require.EqualError(t, err, "invalid url") + require.EqualError(t, err, "invalid url, have you set your --url flag or DATABASE_URL environment variable?") }) t.Run("missing schema", func(t *testing.T) { - db := dbmate.New(dbutil.MustParseURL("//hi")) - drv, err := db.GetDriver() + db := dbmate.New(dbtest.MustParseURL(t, "//hi")) + drv, err := db.Driver() require.Nil(t, drv) - require.EqualError(t, err, "invalid url") + require.EqualError(t, err, "invalid url, have you set your --url flag or DATABASE_URL environment variable?") }) t.Run("invalid driver", func(t *testing.T) { - db := dbmate.New(dbutil.MustParseURL("foo://bar")) - drv, err := db.GetDriver() + db := dbmate.New(dbtest.MustParseURL(t, "foo://bar")) + drv, err := db.Driver() require.EqualError(t, err, "unsupported driver: foo") require.Nil(t, drv) }) } func TestWait(t *testing.T) { - u := dbutil.MustParseURL(os.Getenv("POSTGRES_TEST_URL")) - db := newTestDB(t, u) + db := newTestDB(t, sqliteTestURL(t)) - // speed up our retry loop for testing + // speed up retry loop for testing db.WaitInterval = time.Millisecond db.WaitTimeout = 5 * time.Millisecond - // drop database - err := db.Drop() - require.NoError(t, err) + t.Run("valid connection", func(t *testing.T) { + err := db.Wait() + require.NoError(t, err) + }) - // test wait - err = db.Wait() - require.NoError(t, err) + t.Run("invalid connection", func(t *testing.T) { + db.DatabaseURL = sqliteBrokenTestURL(t) - // test invalid connection - u.Host = "postgres:404" - err = db.Wait() - require.Error(t, err) - require.Contains(t, err.Error(), "unable to connect to database: dial tcp") - require.Contains(t, err.Error(), "connect: connection refused") + err := db.Wait() + require.Error(t, err) + require.Contains(t, err.Error(), "unable to connect to database: unable to open database file:") + }) } func TestDumpSchema(t *testing.T) { - u := dbutil.MustParseURL(os.Getenv("POSTGRES_TEST_URL")) - db := newTestDB(t, u) + db := newTestDB(t, sqliteTestURL(t)) // create custom schema file directory - dir, err := ioutil.TempDir("", "dbmate") + dir, err := os.MkdirTemp("", "dbmate") require.NoError(t, err) - defer func() { - err := os.RemoveAll(dir) - require.NoError(t, err) - }() + defer os.RemoveAll(dir) // create schema.sql in subdirectory to test creating directory db.SchemaFile = filepath.Join(dir, "/schema/schema.sql") @@ -129,23 +133,19 @@ func TestDumpSchema(t *testing.T) { require.NoError(t, err) // verify schema - schema, err := ioutil.ReadFile(db.SchemaFile) + schema, err := os.ReadFile(db.SchemaFile) require.NoError(t, err) - require.Contains(t, string(schema), "-- PostgreSQL database dump") + require.Contains(t, string(schema), "-- Dbmate schema migrations") } func TestAutoDumpSchema(t *testing.T) { - u := dbutil.MustParseURL(os.Getenv("POSTGRES_TEST_URL")) - db := newTestDB(t, u) + db := newTestDB(t, sqliteTestURL(t)) db.AutoDumpSchema = true // create custom schema file directory - dir, err := ioutil.TempDir("", "dbmate") + dir, err := os.MkdirTemp("", "dbmate") require.NoError(t, err) - defer func() { - err := os.RemoveAll(dir) - require.NoError(t, err) - }() + defer os.RemoveAll(dir) // create schema.sql in subdirectory to test creating directory db.SchemaFile = filepath.Join(dir, "/schema/schema.sql") @@ -163,9 +163,9 @@ func TestAutoDumpSchema(t *testing.T) { require.NoError(t, err) // verify schema - schema, err := ioutil.ReadFile(db.SchemaFile) + schema, err := os.ReadFile(db.SchemaFile) require.NoError(t, err) - require.Contains(t, string(schema), "-- PostgreSQL database dump") + require.Contains(t, string(schema), "-- Dbmate schema migrations") // remove schema err = os.Remove(db.SchemaFile) @@ -176,26 +176,92 @@ func TestAutoDumpSchema(t *testing.T) { require.NoError(t, err) // schema should be recreated - schema, err = ioutil.ReadFile(db.SchemaFile) + schema, err = os.ReadFile(db.SchemaFile) require.NoError(t, err) - require.Contains(t, string(schema), "-- PostgreSQL database dump") + require.Contains(t, string(schema), "-- Dbmate schema migrations") } -func checkWaitCalled(t *testing.T, u *url.URL, command func() error) { - oldHost := u.Host - u.Host = "postgres:404" +func TestLoadSchema(t *testing.T) { + db := newTestDB(t, sqliteTestURL(t)) + drv, err := db.Driver() + require.NoError(t, err) + + // create custom schema file directory + dir, err := os.MkdirTemp("", "dbmate") + require.NoError(t, err) + defer os.RemoveAll(dir) + + // create schema.sql in subdirectory to test creating directory + db.SchemaFile = filepath.Join(dir, "/schema/schema.sql") + + // prepare database state + err = db.Drop() + require.NoError(t, err) + err = db.CreateAndMigrate() + require.NoError(t, err) + + // schema.sql should not exist + _, err = os.Stat(db.SchemaFile) + require.True(t, os.IsNotExist(err)) + + // load schema should return error + err = db.LoadSchema() + require.Error(t, err) + require.Regexp(t, "(no such file or directory|system cannot find the path specified)", err.Error()) + + // create schema file + err = db.DumpSchema() + require.NoError(t, err) + + // schema.sql should exist + _, err = os.Stat(db.SchemaFile) + require.NoError(t, err) + + // drop and create database + err = db.Drop() + require.NoError(t, err) + err = db.Create() + require.NoError(t, err) + + // load schema.sql into database + err = db.LoadSchema() + require.NoError(t, err) + + // verify result + sqlDB, err := drv.Open() + require.NoError(t, err) + defer dbutil.MustClose(sqlDB) + + // check applied migrations + appliedMigrations, err := drv.SelectMigrations(sqlDB, -1) + require.NoError(t, err) + require.Equal(t, map[string]bool{"20200227231541": true, "20151129054053": true}, appliedMigrations) + + // users and posts tables have been created + var count int + err = sqlDB.QueryRow("select count(*) from users").Scan(&count) + require.NoError(t, err) + err = sqlDB.QueryRow("select count(*) from posts").Scan(&count) + require.NoError(t, err) +} + +func checkWaitCalled(t *testing.T, db *dbmate.DB, command func() error) { + oldDatabaseURL := db.DatabaseURL + db.DatabaseURL = sqliteBrokenTestURL(t) + err := command() require.Error(t, err) - require.Contains(t, err.Error(), "unable to connect to database: dial tcp") - require.Contains(t, err.Error(), "connect: connection refused") - u.Host = oldHost + require.Contains(t, err.Error(), "unable to connect to database: unable to open database file:") + + db.DatabaseURL = oldDatabaseURL } func testWaitBefore(t *testing.T, verbose bool) { - u := dbutil.MustParseURL(os.Getenv("POSTGRES_TEST_URL")) + u := sqliteTestURL(t) db := newTestDB(t, u) db.Verbose = verbose db.WaitBefore = true + // so that checkWaitCalled returns quickly db.WaitInterval = time.Millisecond db.WaitTimeout = 5 * time.Millisecond @@ -203,32 +269,43 @@ func testWaitBefore(t *testing.T, verbose bool) { // drop database err := db.Drop() require.NoError(t, err) - checkWaitCalled(t, u, db.Drop) + checkWaitCalled(t, db, db.Drop) // create err = db.Create() require.NoError(t, err) - checkWaitCalled(t, u, db.Create) + checkWaitCalled(t, db, db.Create) // create and migrate err = db.CreateAndMigrate() require.NoError(t, err) - checkWaitCalled(t, u, db.CreateAndMigrate) + checkWaitCalled(t, db, db.CreateAndMigrate) // migrate err = db.Migrate() require.NoError(t, err) - checkWaitCalled(t, u, db.Migrate) + checkWaitCalled(t, db, db.Migrate) // rollback err = db.Rollback() require.NoError(t, err) - checkWaitCalled(t, u, db.Rollback) + checkWaitCalled(t, db, db.Rollback) // dump err = db.DumpSchema() require.NoError(t, err) - checkWaitCalled(t, u, db.DumpSchema) + checkWaitCalled(t, db, db.DumpSchema) + + // drop and recreate database before load + err = db.Drop() + require.NoError(t, err) + err = db.Create() + require.NoError(t, err) + + // load + err = db.LoadSchema() + require.NoError(t, err) + checkWaitCalled(t, db, db.LoadSchema) } func TestWaitBefore(t *testing.T) { @@ -239,183 +316,539 @@ func TestWaitBeforeVerbose(t *testing.T) { output := capturer.CaptureOutput(func() { testWaitBefore(t, true) }) - require.Contains(t, output, + re := regexp.MustCompile(`((Applied|Rolled back): .* in) ([\w.,µ]+)`) + maskedOutput := re.ReplaceAllString(output, "$1 ELAPSED") + require.Contains(t, maskedOutput, `Applying: 20151129054053_test_migration.sql +Last insert ID: 1 Rows affected: 1 +Applied: 20151129054053_test_migration.sql in ELAPSED Applying: 20200227231541_test_posts.sql -Rows affected: 0`) - require.Contains(t, output, +Last insert ID: 1 +Rows affected: 1 +Applied: 20200227231541_test_posts.sql in ELAPSED`) + require.Contains(t, maskedOutput, `Rolling back: 20200227231541_test_posts.sql -Rows affected: 0`) +Last insert ID: 0 +Rows affected: 0 +Rolled back: 20200227231541_test_posts.sql in ELAPSED`) } -func testURLs() []*url.URL { - return []*url.URL{ - dbutil.MustParseURL(os.Getenv("MYSQL_TEST_URL")), - dbutil.MustParseURL(os.Getenv("POSTGRES_TEST_URL")), - dbutil.MustParseURL(os.Getenv("SQLITE_TEST_URL")), +func testEachURL(t *testing.T, fn func(*testing.T, *url.URL)) { + t.Run("sqlite", func(t *testing.T) { + fn(t, sqliteTestURL(t)) + }) + + optionalTestURLs := []string{"MYSQL_TEST_URL", "POSTGRES_TEST_URL"} + for _, varname := range optionalTestURLs { + // split on underscore and take first part + testname := strings.ToLower(strings.Split(varname, "_")[0]) + t.Run(testname, func(t *testing.T) { + u := dbtest.GetenvURLOrSkip(t, varname) + fn(t, u) + }) } } func TestMigrate(t *testing.T) { - for _, u := range testURLs() { - t.Run(u.Scheme, func(t *testing.T) { - db := newTestDB(t, u) - drv, err := db.GetDriver() - require.NoError(t, err) - - // drop and recreate database - err = db.Drop() - require.NoError(t, err) - err = db.Create() - require.NoError(t, err) - - // migrate - err = db.Migrate() - require.NoError(t, err) - - // verify results - sqlDB, err := drv.Open() - require.NoError(t, err) - defer dbutil.MustClose(sqlDB) - - count := 0 - err = sqlDB.QueryRow(`select count(*) from schema_migrations - where version = '20151129054053'`).Scan(&count) - require.NoError(t, err) - require.Equal(t, 1, count) - - err = sqlDB.QueryRow("select count(*) from users").Scan(&count) - require.NoError(t, err) - require.Equal(t, 1, count) - }) - } + testEachURL(t, func(t *testing.T, u *url.URL) { + db := newTestDB(t, u) + drv, err := db.Driver() + require.NoError(t, err) + + // drop and recreate database + err = db.Drop() + require.NoError(t, err) + err = db.Create() + require.NoError(t, err) + + // migrate + err = db.Migrate() + require.NoError(t, err) + + // verify results + sqlDB, err := drv.Open() + require.NoError(t, err) + defer dbutil.MustClose(sqlDB) + + // check applied migrations + appliedMigrations, err := drv.SelectMigrations(sqlDB, -1) + require.NoError(t, err) + require.Equal(t, map[string]bool{"20200227231541": true, "20151129054053": true}, appliedMigrations) + + // users table have records + count := 0 + err = sqlDB.QueryRow("select count(*) from users").Scan(&count) + require.NoError(t, err) + require.Equal(t, 1, count) + }) } func TestUp(t *testing.T) { - for _, u := range testURLs() { - t.Run(u.Scheme, func(t *testing.T) { - db := newTestDB(t, u) - drv, err := db.GetDriver() - require.NoError(t, err) - - // drop database - err = db.Drop() - require.NoError(t, err) - - // create and migrate - err = db.CreateAndMigrate() - require.NoError(t, err) - - // verify results - sqlDB, err := drv.Open() - require.NoError(t, err) - defer dbutil.MustClose(sqlDB) - - count := 0 - err = sqlDB.QueryRow(`select count(*) from schema_migrations - where version = '20151129054053'`).Scan(&count) - require.NoError(t, err) - require.Equal(t, 1, count) - - err = sqlDB.QueryRow("select count(*) from users").Scan(&count) - require.NoError(t, err) - require.Equal(t, 1, count) - }) - } + testEachURL(t, func(t *testing.T, u *url.URL) { + db := newTestDB(t, u) + drv, err := db.Driver() + require.NoError(t, err) + + // drop database + err = db.Drop() + require.NoError(t, err) + + // create and migrate + err = db.CreateAndMigrate() + require.NoError(t, err) + + // verify results + sqlDB, err := drv.Open() + require.NoError(t, err) + defer dbutil.MustClose(sqlDB) + + // check applied migrations + appliedMigrations, err := drv.SelectMigrations(sqlDB, -1) + require.NoError(t, err) + require.Equal(t, map[string]bool{"20200227231541": true, "20151129054053": true}, appliedMigrations) + + // users table have records + count := 0 + err = sqlDB.QueryRow("select count(*) from users").Scan(&count) + require.NoError(t, err) + require.Equal(t, 1, count) + }) } func TestRollback(t *testing.T) { - for _, u := range testURLs() { - t.Run(u.Scheme, func(t *testing.T) { - db := newTestDB(t, u) - drv, err := db.GetDriver() - require.NoError(t, err) - - // drop, recreate, and migrate database - err = db.Drop() - require.NoError(t, err) - err = db.Create() - require.NoError(t, err) - err = db.Migrate() - require.NoError(t, err) - - // verify migration - sqlDB, err := drv.Open() - require.NoError(t, err) - defer dbutil.MustClose(sqlDB) - - count := 0 - err = sqlDB.QueryRow(`select count(*) from schema_migrations - where version = '20151129054053'`).Scan(&count) - require.NoError(t, err) - require.Equal(t, 1, count) - - err = sqlDB.QueryRow("select count(*) from posts").Scan(&count) - require.Nil(t, err) - - // rollback - err = db.Rollback() - require.NoError(t, err) - - // verify rollback - err = sqlDB.QueryRow("select count(*) from schema_migrations").Scan(&count) - require.NoError(t, err) - require.Equal(t, 1, count) - - err = sqlDB.QueryRow("select count(*) from posts").Scan(&count) - require.NotNil(t, err) - require.Regexp(t, "(does not exist|doesn't exist|no such table)", err.Error()) - }) + testEachURL(t, func(t *testing.T, u *url.URL) { + db := newTestDB(t, u) + drv, err := db.Driver() + require.NoError(t, err) + + // drop and create database + err = db.Drop() + require.NoError(t, err) + err = db.Create() + require.NoError(t, err) + + // rollback should return error + err = db.Rollback() + require.Error(t, err) + require.ErrorContains(t, err, "can't rollback: no migrations have been applied") + + // migrate database + err = db.Migrate() + require.NoError(t, err) + + // verify migration + sqlDB, err := drv.Open() + require.NoError(t, err) + defer dbutil.MustClose(sqlDB) + + // check applied migrations + appliedMigrations, err := drv.SelectMigrations(sqlDB, -1) + require.NoError(t, err) + require.Equal(t, map[string]bool{"20200227231541": true, "20151129054053": true}, appliedMigrations) + + // users and posts tables have been created + var count int + err = sqlDB.QueryRow("select count(*) from users").Scan(&count) + require.NoError(t, err) + err = sqlDB.QueryRow("select count(*) from posts").Scan(&count) + require.NoError(t, err) + + // rollback second migration + err = db.Rollback() + require.NoError(t, err) + + // one migration remaining + err = sqlDB.QueryRow("select count(*) from schema_migrations").Scan(&count) + require.NoError(t, err) + require.Equal(t, 1, count) + + // posts table was deleted + err = sqlDB.QueryRow("select count(*) from posts").Scan(&count) + require.NotNil(t, err) + require.Regexp(t, "(does not exist|doesn't exist|no such table)", err.Error()) + + // users table still exists + err = sqlDB.QueryRow("select count(*) from users").Scan(&count) + require.Nil(t, err) + + // rollback first migration + err = db.Rollback() + require.NoError(t, err) + + // no migrations remaining + err = sqlDB.QueryRow("select count(*) from schema_migrations").Scan(&count) + require.NoError(t, err) + require.Equal(t, 0, count) + + // posts table was deleted + err = sqlDB.QueryRow("select count(*) from posts").Scan(&count) + require.NotNil(t, err) + require.Regexp(t, "(does not exist|doesn't exist|no such table)", err.Error()) + + // users table was deleted + err = sqlDB.QueryRow("select count(*) from users").Scan(&count) + require.NotNil(t, err) + require.Regexp(t, "(does not exist|doesn't exist|no such table)", err.Error()) + }) +} + +func TestFindMigrations(t *testing.T) { + testEachURL(t, func(t *testing.T, u *url.URL) { + db := newTestDB(t, u) + drv, err := db.Driver() + require.NoError(t, err) + + // drop, recreate, and migrate database + err = db.Drop() + require.NoError(t, err) + err = db.Create() + require.NoError(t, err) + + // verify migration + sqlDB, err := drv.Open() + require.NoError(t, err) + defer dbutil.MustClose(sqlDB) + + // two pending + results, err := db.FindMigrations() + require.NoError(t, err) + require.Len(t, results, 2) + require.False(t, results[0].Applied) + require.False(t, results[1].Applied) + migrationsTableExists, err := drv.MigrationsTableExists(sqlDB) + require.NoError(t, err) + require.False(t, migrationsTableExists) + + // run migrations + err = db.Migrate() + require.NoError(t, err) + + // two applied + results, err = db.FindMigrations() + require.NoError(t, err) + require.Len(t, results, 2) + require.True(t, results[0].Applied) + require.True(t, results[1].Applied) + + // rollback last migration + err = db.Rollback() + require.NoError(t, err) + + // one applied, one pending + results, err = db.FindMigrations() + require.NoError(t, err) + require.Len(t, results, 2) + require.True(t, results[0].Applied) + require.False(t, results[1].Applied) + }) +} + +func TestFindMigrationsAbsolute(t *testing.T) { + t.Run("relative path", func(t *testing.T) { + db := newTestDB(t, sqliteTestURL(t)) + db.MigrationsDir = []string{"db/migrations"} + + migrations, err := db.FindMigrations() + require.NoError(t, err) + + require.Equal(t, "db/migrations/20151129054053_test_migration.sql", migrations[0].FilePath) + }) + + t.Run("absolute path", func(t *testing.T) { + dir, err := os.MkdirTemp("", "dbmate") + require.NoError(t, err) + defer os.RemoveAll(dir) + require.True(t, filepath.IsAbs(dir)) + + file, err := os.Create(filepath.Join(dir, "1234_example.sql")) + require.NoError(t, err) + defer file.Close() + + db := newTestDB(t, sqliteTestURL(t)) + db.MigrationsDir = []string{dir} + require.Nil(t, db.FS) + + migrations, err := db.FindMigrations() + require.NoError(t, err) + require.Len(t, migrations, 1) + require.Equal(t, dir+"/1234_example.sql", migrations[0].FilePath) + require.True(t, filepath.IsAbs(migrations[0].FilePath)) + require.Nil(t, migrations[0].FS) + require.Equal(t, "1234_example.sql", migrations[0].FileName) + require.Equal(t, "1234", migrations[0].Version) + require.False(t, migrations[0].Applied) + }) +} + +func TestFindMigrationsFS(t *testing.T) { + mapFS := fstest.MapFS{ + "db/migrations/20151129054053_test_migration.sql": {}, + "db/migrations/001_test_migration.sql": { + Data: []byte(`-- migrate:up +create table users (id serial, name text); +-- migrate:down +drop table users; +`), + }, + "db/migrations/002_test_migration.sql": {}, + "db/migrations/003_not_sql.txt": {}, + "db/migrations/missing_version.sql": {}, + "db/not_migrations/20151129054053_test_migration.sql": {}, } + + db := newTestDB(t, sqliteTestURL(t)) + db.FS = mapFS + + // drop and recreate database + err := db.Drop() + require.NoError(t, err) + err = db.Create() + require.NoError(t, err) + + actual, err := db.FindMigrations() + require.NoError(t, err) + + // test migrations are correct and in order + require.Equal(t, "001_test_migration.sql", actual[0].FileName) + require.Equal(t, "db/migrations/001_test_migration.sql", actual[0].FilePath) + require.Equal(t, "001", actual[0].Version) + require.Equal(t, false, actual[0].Applied) + + require.Equal(t, "002_test_migration.sql", actual[1].FileName) + require.Equal(t, "db/migrations/002_test_migration.sql", actual[1].FilePath) + require.Equal(t, "002", actual[1].Version) + require.Equal(t, false, actual[1].Applied) + + require.Equal(t, "20151129054053_test_migration.sql", actual[2].FileName) + require.Equal(t, "db/migrations/20151129054053_test_migration.sql", actual[2].FilePath) + require.Equal(t, "20151129054053", actual[2].Version) + require.Equal(t, false, actual[2].Applied) + + // test parsing first migration + parsed, err := actual[0].Parse() + require.Nil(t, err) + require.Equal(t, "-- migrate:up\ncreate table users (id serial, name text);\n", parsed.Up) + require.True(t, parsed.UpOptions.Transaction()) + require.Equal(t, "-- migrate:down\ndrop table users;\n", parsed.Down) + require.True(t, parsed.DownOptions.Transaction()) } -func TestStatus(t *testing.T) { - for _, u := range testURLs() { - t.Run(u.Scheme, func(t *testing.T) { - db := newTestDB(t, u) - drv, err := db.GetDriver() - require.NoError(t, err) - - // drop, recreate, and migrate database - err = db.Drop() - require.NoError(t, err) - err = db.Create() - require.NoError(t, err) - - // verify migration - sqlDB, err := drv.Open() - require.NoError(t, err) - defer dbutil.MustClose(sqlDB) - - // two pending - results, err := db.CheckMigrationsStatus(drv) - require.NoError(t, err) - require.Len(t, results, 2) - require.False(t, results[0].Applied) - require.False(t, results[1].Applied) - - // run migrations - err = db.Migrate() - require.NoError(t, err) - - // two applied - results, err = db.CheckMigrationsStatus(drv) - require.NoError(t, err) - require.Len(t, results, 2) - require.True(t, results[0].Applied) - require.True(t, results[1].Applied) - - // rollback last migration - err = db.Rollback() - require.NoError(t, err) - - // one applied, one pending - results, err = db.CheckMigrationsStatus(drv) - require.NoError(t, err) - require.Len(t, results, 2) - require.True(t, results[0].Applied) - require.False(t, results[1].Applied) - }) +func TestFindMigrationsFSMultipleDirs(t *testing.T) { + mapFS := fstest.MapFS{ + "db/migrations_a/001_test_migration_a.sql": {}, + "db/migrations_a/005_test_migration_a.sql": {}, + "db/migrations_b/003_test_migration_b.sql": {}, + "db/migrations_b/004_test_migration_b.sql": {}, + "db/migrations_c/002_test_migration_c.sql": {}, + "db/migrations_c/006_test_migration_c.sql": {}, } + + db := newTestDB(t, sqliteTestURL(t)) + db.FS = mapFS + db.MigrationsDir = []string{"./db/migrations_a", "./db/migrations_b", "./db/migrations_c"} + + // drop and recreate database + err := db.Drop() + require.NoError(t, err) + err = db.Create() + require.NoError(t, err) + + actual, err := db.FindMigrations() + require.NoError(t, err) + + // test migrations are correct and in order + require.Equal(t, "db/migrations_a/001_test_migration_a.sql", actual[0].FilePath) + require.Equal(t, "db/migrations_c/002_test_migration_c.sql", actual[1].FilePath) + require.Equal(t, "db/migrations_b/003_test_migration_b.sql", actual[2].FilePath) + require.Equal(t, "db/migrations_b/004_test_migration_b.sql", actual[3].FilePath) + require.Equal(t, "db/migrations_a/005_test_migration_a.sql", actual[4].FilePath) + require.Equal(t, "db/migrations_c/006_test_migration_c.sql", actual[5].FilePath) +} + +func TestMigrateUnrestrictedOrder(t *testing.T) { + emptyMigration := []byte("-- migrate:up\n-- migrate:down") + + // initialize database + db := newTestDB(t, sqliteTestURL(t)) + + err := db.Drop() + require.NoError(t, err) + err = db.Create() + require.NoError(t, err) + + // test to apply new migrations on empty database + db.FS = fstest.MapFS{ + "db/migrations/001_test_migration_a.sql": {Data: emptyMigration}, + "db/migrations/100_test_migration_b.sql": {Data: emptyMigration}, + } + + err = db.Migrate() + require.NoError(t, err) + + // test to apply an out of order migration + db.FS = fstest.MapFS{ + "db/migrations/001_test_migration_a.sql": {Data: emptyMigration}, + "db/migrations/100_test_migration_b.sql": {Data: emptyMigration}, + "db/migrations/010_test_migration_c.sql": {Data: emptyMigration}, + } + + err = db.Migrate() + require.NoError(t, err) +} + +func TestMigrateStrictOrder(t *testing.T) { + emptyMigration := []byte("-- migrate:up\n-- migrate:down") + + // initialize database + db := newTestDB(t, sqliteTestURL(t)) + db.Strict = true + + err := db.Drop() + require.NoError(t, err) + err = db.Create() + require.NoError(t, err) + + // test to apply new migrations on empty database + db.FS = fstest.MapFS{ + "db/migrations/001_test_migration_a.sql": {Data: emptyMigration}, + "db/migrations/010_test_migration_b.sql": {Data: emptyMigration}, + } + + err = db.Migrate() + require.NoError(t, err) + + // test to apply an in order migration + db.FS = fstest.MapFS{ + "db/migrations/001_test_migration_a.sql": {Data: emptyMigration}, + "db/migrations/010_test_migration_b.sql": {Data: emptyMigration}, + "db/migrations/100_test_migration_c.sql": {Data: emptyMigration}, + } + + err = db.Migrate() + require.NoError(t, err) + + // test to apply an out of order migration + db.FS = fstest.MapFS{ + "db/migrations/001_test_migration_a.sql": {Data: emptyMigration}, + "db/migrations/010_test_migration_b.sql": {Data: emptyMigration}, + "db/migrations/100_test_migration_c.sql": {Data: emptyMigration}, + "db/migrations/050_test_migration_d.sql": {Data: emptyMigration}, + } + + err = db.Migrate() + require.Error(t, err) +} + +func TestMigrateQueryErrorMessage(t *testing.T) { + db := newTestDB(t, sqliteTestURL(t)) + + err := db.Drop() + require.NoError(t, err) + err = db.Create() + require.NoError(t, err) + + t.Run("ASCII SQL, error in migrate up", func(t *testing.T) { + db.FS = fstest.MapFS{ + "db/migrations/001_ascii_error_up.sql": { + Data: []byte("-- migrate:up\n-- line 2\nnot_valid_sql_ascii_up;\n-- migrate:down"), + }, + } + + err = db.Migrate() + require.Error(t, err) + require.Contains(t, err.Error(), "near \"not_valid_sql_ascii_up\": syntax error") + }) + + t.Run("ASCII SQL, error in migrate down", func(t *testing.T) { + db.FS = fstest.MapFS{ + "db/migrations/002_ascii_error_down.sql": { + Data: []byte("-- migrate:up\n--migrate:down\n not_valid_sql_ascii_down; -- indented"), + }, + } + + err = db.Migrate() + require.NoError(t, err) + + err = db.Rollback() + require.Error(t, err) + require.Contains(t, err.Error(), "near \"not_valid_sql_ascii_down\": syntax error") + }) + + t.Run("UTF-8 SQL, error in migrate up", func(t *testing.T) { + db.FS = fstest.MapFS{ + "db/migrations/003_utf8_error_up.sql": { + Data: []byte("-- migrate:up\n-- line 2\n/* สวัสดี hello */ not_valid_sql_utf8_up;\n--migrate:down"), + }, + } + + err = db.Migrate() + require.Error(t, err) + require.Contains(t, err.Error(), "near \"not_valid_sql_utf8_up\": syntax error") + }) + + t.Run("UTF-8 SQL, error in migrate down", func(t *testing.T) { + db.FS = fstest.MapFS{ + "db/migrations/004_utf8_error_up.sql": { + Data: []byte("-- migrate:up\n-- migrate:down\n/* สวัสดี hello */ not_valid_sql_utf8_down;"), + }, + } + + err = db.Migrate() + require.NoError(t, err) + + err = db.Rollback() + require.Error(t, err) + require.Contains(t, err.Error(), "near \"not_valid_sql_utf8_down\": syntax error") + }) + + t.Run("correctly count with CR-LF line endings present", func(t *testing.T) { + db.FS = fstest.MapFS{ + "db/migrations/005_cr_lf_line_endings.sql": { + Data: []byte("-- migrate:up\r\n-- line 2\r\n not_valid_sql_crlf_up; -- indented\r\n-- migrate:down"), + }, + } + + err = db.Migrate() + require.Error(t, err) + require.Contains(t, err.Error(), "near \"not_valid_sql_crlf_up\": syntax error") + }) +} + +func TestMigrationContents(t *testing.T) { + // ensure Windows CR/LF line endings in migration files work + testEachURL(t, func(t *testing.T, u *url.URL) { + db := newTestDB(t, u) + drv, err := db.Driver() + require.NoError(t, err) + + err = db.Drop() + require.NoError(t, err) + err = db.Create() + require.NoError(t, err) + + sqlDB, err := drv.Open() + require.NoError(t, err) + defer dbutil.MustClose(sqlDB) + + db.FS = fstest.MapFS{ + "db/migrations/001_win_crlf_migration_empty.sql": { + Data: []byte("-- migrate:up\r\n-- migrate:down\r\n"), + }, + "db/migrations/002_win_crlf_migration_basic.sql": { + Data: []byte("-- migrate:up\r\ncreate table test_win_crlf_basic (\r\n id integer,\r\n name varchar(255)\r\n);\r\n-- migrate:down\r\ndrop table test_win_crlf_basic;\r\n"), + }, + "db/migrations/003_win_crlf_migration_options.sql": { + Data: []byte("-- migrate:up transaction:true\r\ncreate table test_win_crlf_options (\r\n id integer,\r\n name varchar(255)\r\n);\r\n-- migrate:down transaction:true\r\ndrop table test_win_crlf_options;\r\n"), + }, + } + + // run migrations + err = db.Migrate() + require.NoError(t, err) + + // rollback last migration + err = db.Rollback() + require.NoError(t, err) + }) } diff --git a/pkg/dbmate/driver.go b/pkg/dbmate/driver.go index 6323c3d4..99f33d92 100644 --- a/pkg/dbmate/driver.go +++ b/pkg/dbmate/driver.go @@ -2,6 +2,8 @@ package dbmate import ( "database/sql" + "fmt" + "io" "net/url" "github.com/assetnote/dbmate/pkg/dbutil" @@ -14,22 +16,58 @@ type Driver interface { CreateDatabase() error DropDatabase() error DumpSchema(*sql.DB) ([]byte, error) + MigrationsTableExists(*sql.DB) (bool, error) CreateMigrationsTable(*sql.DB) error SelectMigrations(*sql.DB, int) (map[string]bool, error) InsertMigration(dbutil.Transaction, string) error DeleteMigration(dbutil.Transaction, string) error Ping() error + QueryError(string, error) error } // DriverConfig holds configuration passed to driver constructors type DriverConfig struct { DatabaseURL *url.URL + Log io.Writer MigrationsTableName string } // DriverFunc represents a driver constructor type DriverFunc func(DriverConfig) Driver +type QueryError struct { + Err error + Query string + Position int +} + +func (e *QueryError) Error() string { + if e.Position > 0 { + line := 1 + column := 1 + offset := 0 + for _, ch := range e.Query { + offset++ + if offset >= e.Position { + break + } + // don't count CR as a column in CR/LF sequences + if ch == '\r' { + continue + } + if ch == '\n' { + line++ + column = 1 + continue + } + column++ + } + return fmt.Sprintf("line: %d, column: %d, position: %d: %s", line, column, e.Position, e.Err.Error()) + } + + return e.Err.Error() +} + var drivers = map[string]DriverFunc{} // RegisterDriver registers a driver constructor for a given URL scheme diff --git a/pkg/dbmate/migration.go b/pkg/dbmate/migration.go index 2c0f7b6f..ec67f7cf 100644 --- a/pkg/dbmate/migration.go +++ b/pkg/dbmate/migration.go @@ -1,14 +1,52 @@ package dbmate import ( - "fmt" - "io/ioutil" + "errors" + "io/fs" + "os" "regexp" "strings" ) -// MigrationOptions is an interface for accessing migration options -type MigrationOptions interface { +// Migration represents an available migration and status +type Migration struct { + Applied bool + FileName string + FilePath string + FS fs.FS + Version string +} + +func (m *Migration) readFile() (string, error) { + if m.FS == nil { + bytes, err := os.ReadFile(m.FilePath) + return string(bytes), err + } + + bytes, err := fs.ReadFile(m.FS, m.FilePath) + return string(bytes), err +} + +// Parse a migration +func (m *Migration) Parse() (*ParsedMigration, error) { + contents, err := m.readFile() + if err != nil { + return nil, err + } + + return parseMigrationContents(contents) +} + +// ParsedMigration contains the migration contents and options +type ParsedMigration struct { + Up string + UpOptions ParsedMigrationOptions + Down string + DownOptions ParsedMigrationOptions +} + +// ParsedMigrationOptions is an interface for accessing migration options +type ParsedMigrationOptions interface { Transaction() bool } @@ -20,74 +58,56 @@ func (m migrationOptions) Transaction() bool { return m["transaction"] != "false" } -// Migration contains the migration contents and options -type Migration struct { - Contents string - Options MigrationOptions -} - -// NewMigration constructs a Migration object -func NewMigration() Migration { - return Migration{Contents: "", Options: make(migrationOptions)} -} - -// parseMigration reads a migration file and returns (up Migration, down Migration, error) -func parseMigration(path string) (Migration, Migration, error) { - data, err := ioutil.ReadFile(path) - if err != nil { - return NewMigration(), NewMigration(), err - } - up, down, err := parseMigrationContents(string(data)) - return up, down, err -} +var ( + upRegExp = regexp.MustCompile(`(?m)^--\s*migrate:up(\s*$|\s+\S+)`) + downRegExp = regexp.MustCompile(`(?m)^--\s*migrate:down(\s*$|\s+\S+)`) + emptyLineRegExp = regexp.MustCompile(`^\s*$`) + commentLineRegExp = regexp.MustCompile(`^\s*--`) + whitespaceRegExp = regexp.MustCompile(`\s+`) + optionSeparatorRegExp = regexp.MustCompile(`:`) + blockDirectiveRegExp = regexp.MustCompile(`^--\s*migrate:(up|down)`) +) -var upRegExp = regexp.MustCompile(`(?m)^--\s*migrate:up(\s*$|\s+\S+)`) -var downRegExp = regexp.MustCompile(`(?m)^--\s*migrate:down(\s*$|\s+\S+)$`) -var emptyLineRegExp = regexp.MustCompile(`^\s*$`) -var commentLineRegExp = regexp.MustCompile(`^\s*--`) -var whitespaceRegExp = regexp.MustCompile(`\s+`) -var optionSeparatorRegExp = regexp.MustCompile(`:`) -var blockDirectiveRegExp = regexp.MustCompile(`^--\s*migrate:[up|down]]`) +// Error codes +var ( + ErrParseMissingUp = errors.New("dbmate requires each migration to define an up block with '-- migrate:up'") + ErrParseMissingDown = errors.New("dbmate requires each migration to define a down block with '-- migrate:down'") + ErrParseWrongOrder = errors.New("dbmate requires '-- migrate:up' to appear before '-- migrate:down'") + ErrParseUnexpectedStmt = errors.New("dbmate does not support statements preceding the '-- migrate:up' block") +) // parseMigrationContents parses the string contents of a migration. // It will return two Migration objects, the first representing the "up" // block and the second representing the "down" block. This function // requires that at least an up block was defined and will otherwise // return an error. -func parseMigrationContents(contents string) (Migration, Migration, error) { - up := NewMigration() - down := NewMigration() - - upDirectiveStart, upDirectiveEnd, hasDefinedUpBlock := getMatchPositions(contents, upRegExp) - downDirectiveStart, downDirectiveEnd, hasDefinedDownBlock := getMatchPositions(contents, downRegExp) +func parseMigrationContents(contents string) (*ParsedMigration, error) { + upDirectiveStart, hasDefinedUpBlock := getMatchPosition(contents, upRegExp) + downDirectiveStart, hasDefinedDownBlock := getMatchPosition(contents, downRegExp) if !hasDefinedUpBlock { - return up, down, fmt.Errorf("dbmate requires each migration to define an up bock with '-- migrate:up'") - } else if statementsPrecedeMigrateBlocks(contents, upDirectiveStart, downDirectiveStart) { - return up, down, fmt.Errorf("dbmate does not support statements defined outside of the '-- migrate:up' or '-- migrate:down' blocks") + return nil, ErrParseMissingUp } - - upEnd := len(contents) - downEnd := len(contents) - - if hasDefinedDownBlock && upDirectiveStart < downDirectiveStart { - upEnd = downDirectiveStart - } else if hasDefinedDownBlock && upDirectiveStart > downDirectiveStart { - downEnd = upDirectiveStart - } else { - downEnd = -1 + if !hasDefinedDownBlock { + return nil, ErrParseMissingDown + } + if upDirectiveStart > downDirectiveStart { + return nil, ErrParseWrongOrder + } + if statementsPrecedeMigrateBlocks(contents, upDirectiveStart) { + return nil, ErrParseUnexpectedStmt } - upDirective := substring(contents, upDirectiveStart, upDirectiveEnd) - downDirective := substring(contents, downDirectiveStart, downDirectiveEnd) - - up.Options = parseMigrationOptions(upDirective) - up.Contents = substring(contents, upDirectiveStart, upEnd) - - down.Options = parseMigrationOptions(downDirective) - down.Contents = substring(contents, downDirectiveStart, downEnd) + upBlock := substring(contents, upDirectiveStart, downDirectiveStart) + downBlock := substring(contents, downDirectiveStart, len(contents)) - return up, down, nil + parsed := ParsedMigration{ + Up: upBlock, + UpOptions: parseMigrationOptions(upBlock), + Down: downBlock, + DownOptions: parseMigrationOptions(downBlock), + } + return &parsed, nil } // parseMigrationOptions parses the migration options out of a block @@ -95,12 +115,14 @@ func parseMigrationContents(contents string) (Migration, Migration, error) { // // For example: // -// fmt.Printf("%#v", parseMigrationOptions("-- migrate:up transaction:false")) -// // migrationOptions{"transaction": "false"} -// -func parseMigrationOptions(contents string) MigrationOptions { +// fmt.Printf("%#v", parseMigrationOptions("-- migrate:up transaction:false")) +// // migrationOptions{"transaction": "false"} +func parseMigrationOptions(contents string) ParsedMigrationOptions { options := make(migrationOptions) + // remove everything after first newline + contents = strings.SplitN(contents, "\n", 2)[0] + // strip away the -- migrate:[up|down] part contents = blockDirectiveRegExp.ReplaceAllString(contents, "") @@ -146,15 +168,8 @@ func parseMigrationOptions(contents string) MigrationOptions { // -- migrate:up // create table users (id serial, status status_type); // `, 54, -1) -// -func statementsPrecedeMigrateBlocks(contents string, upDirectiveStart, downDirectiveStart int) bool { - until := upDirectiveStart - - if downDirectiveStart > -1 { - until = min(upDirectiveStart, downDirectiveStart) - } - - lines := strings.Split(contents[0:until], "\n") +func statementsPrecedeMigrateBlocks(contents string, upDirectiveStart int) bool { + lines := strings.Split(contents[0:upDirectiveStart], "\n") for _, line := range lines { if isEmptyLine(line) || isCommentLine(line) { @@ -177,12 +192,12 @@ func isCommentLine(s string) bool { return commentLineRegExp.MatchString(s) } -func getMatchPositions(s string, re *regexp.Regexp) (int, int, bool) { +func getMatchPosition(s string, re *regexp.Regexp) (int, bool) { match := re.FindStringIndex(s) if match == nil { - return -1, -1, false + return -1, false } - return match[0], match[1], true + return match[0], true } func substring(s string, begin, end int) string { @@ -191,10 +206,3 @@ func substring(s string, begin, end int) string { } return s[begin:end] } - -func min(a, b int) int { - if a < b { - return a - } - return b -} diff --git a/pkg/dbmate/migration_test.go b/pkg/dbmate/migration_test.go index e01d3900..fe53cb22 100644 --- a/pkg/dbmate/migration_test.go +++ b/pkg/dbmate/migration_test.go @@ -2,28 +2,57 @@ package dbmate import ( "testing" + "testing/fstest" "github.com/stretchr/testify/require" ) +func TestParse(t *testing.T) { + fs := fstest.MapFS{ + "bar/123_foo.sql": { + Data: []byte(`-- migrate:up +create table users (id serial, name text); +-- migrate:down +drop table users; +`), + }, + } + + migration := &Migration{ + Applied: false, + FileName: "123_foo.sql", + FilePath: "bar/123_foo.sql", + FS: fs, + Version: "123", + } + + parsed, err := migration.Parse() + require.Nil(t, err) + require.Equal(t, "-- migrate:up\ncreate table users (id serial, name text);\n", parsed.Up) + require.True(t, parsed.UpOptions.Transaction()) + require.Equal(t, "-- migrate:down\ndrop table users;\n", parsed.Down) + require.True(t, parsed.DownOptions.Transaction()) +} + func TestParseMigrationContents(t *testing.T) { - // It supports the typical use case. - migration := `-- migrate:up + t.Run("support the typical use case", func(t *testing.T) { + migration := `-- migrate:up create table users (id serial, name text); -- migrate:down drop table users;` - up, down, err := parseMigrationContents(migration) - require.Nil(t, err) + parsed, err := parseMigrationContents(migration) + require.Nil(t, err) - require.Equal(t, "-- migrate:up\ncreate table users (id serial, name text);\n", up.Contents) - require.Equal(t, true, up.Options.Transaction()) + require.Equal(t, "-- migrate:up\ncreate table users (id serial, name text);\n", parsed.Up) + require.Equal(t, true, parsed.UpOptions.Transaction()) - require.Equal(t, "-- migrate:down\ndrop table users;", down.Contents) - require.Equal(t, true, down.Options.Transaction()) + require.Equal(t, "-- migrate:down\ndrop table users;", parsed.Down) + require.Equal(t, true, parsed.DownOptions.Transaction()) + }) - // It does not require space between the '--' and 'migrate' - migration = ` + t.Run("do not require space between '--' and 'migrate'", func(t *testing.T) { + migration := ` --migrate:up create table users (id serial, name text); @@ -31,58 +60,76 @@ create table users (id serial, name text); drop table users; ` - up, down, err = parseMigrationContents(migration) - require.Nil(t, err) + parsed, err := parseMigrationContents(migration) + require.Nil(t, err) - require.Equal(t, "--migrate:up\ncreate table users (id serial, name text);\n\n", up.Contents) - require.Equal(t, true, up.Options.Transaction()) + require.Equal(t, "--migrate:up\ncreate table users (id serial, name text);\n\n", parsed.Up) + require.Equal(t, true, parsed.UpOptions.Transaction()) - require.Equal(t, "--migrate:down\ndrop table users;\n", down.Contents) - require.Equal(t, true, down.Options.Transaction()) + require.Equal(t, "--migrate:down\ndrop table users;\n", parsed.Down) + require.Equal(t, true, parsed.DownOptions.Transaction()) + }) - // It is acceptable for down to be defined before up - migration = `-- migrate:down + t.Run("require up before down", func(t *testing.T) { + migration := `-- migrate:down drop table users; -- migrate:up create table users (id serial, name text); ` - up, down, err = parseMigrationContents(migration) - require.Nil(t, err) - - require.Equal(t, "-- migrate:up\ncreate table users (id serial, name text);\n", up.Contents) - require.Equal(t, true, up.Options.Transaction()) + _, err := parseMigrationContents(migration) + require.Error(t, err, "dbmate requires '-- migrate:up' to appear before '-- migrate:down'") + }) - require.Equal(t, "-- migrate:down\ndrop table users;\n", down.Contents) - require.Equal(t, true, down.Options.Transaction()) - - // It supports turning transactions off for a given migration block, - // e.g., the below would not work in Postgres inside a transaction. - // It also supports omitting the down block. - migration = `-- migrate:up transaction:false + t.Run("support disabling transactions", func(t *testing.T) { + // e.g., the below would not work in Postgres inside a transaction. + // It also supports omitting the down block. + migration := `-- migrate:up transaction:false +ALTER TYPE colors ADD VALUE 'orange' AFTER 'red'; +-- migrate:down transaction:false ALTER TYPE colors ADD VALUE 'orange' AFTER 'red'; ` - up, down, err = parseMigrationContents(migration) - require.Nil(t, err) + parsed, err := parseMigrationContents(migration) + require.Nil(t, err) + + require.Equal(t, "-- migrate:up transaction:false\nALTER TYPE colors ADD VALUE 'orange' AFTER 'red';\n", parsed.Up) + require.Equal(t, false, parsed.UpOptions.Transaction()) - require.Equal(t, "-- migrate:up transaction:false\nALTER TYPE colors ADD VALUE 'orange' AFTER 'red';\n", up.Contents) - require.Equal(t, false, up.Options.Transaction()) + require.Equal(t, "-- migrate:down transaction:false\nALTER TYPE colors ADD VALUE 'orange' AFTER 'red';\n", parsed.Down) + require.Equal(t, false, parsed.DownOptions.Transaction()) + }) + + t.Run("require migrate blocks", func(t *testing.T) { + migration := ` +ALTER TABLE users +ADD COLUMN status status_type DEFAULT 'active'; +` - require.Equal(t, "", down.Contents) - require.Equal(t, true, down.Options.Transaction()) + _, err := parseMigrationContents(migration) + require.Error(t, err, "dbmate requires each migration to define an up block with '-- migrate:up'") + }) - // It does *not* support omitting the up block. - migration = `-- migrate:down + t.Run("require an up block", func(t *testing.T) { + migration := `-- migrate:down drop table users; ` - _, _, err = parseMigrationContents(migration) - require.NotNil(t, err) - require.Equal(t, "dbmate requires each migration to define an up bock with '-- migrate:up'", err.Error()) + _, err := parseMigrationContents(migration) + require.Error(t, err, "dbmate requires each migration to define an up block with '-- migrate:up'") + }) + + t.Run("require a down block", func(t *testing.T) { + migration := `-- migrate:up +create table users (id serial, name text); +` + + _, err := parseMigrationContents(migration) + require.Error(t, err, "dbmate requires each migration to define a down block with '-- migrate:down'") + }) - // It allows leading comments and whitespace preceding the migrate blocks - migration = ` + t.Run("allow leading comments and whitespace preceding the migrate blocks", func(t *testing.T) { + migration := ` -- This migration creates the users table. -- It'll drop it in the event of a rollback. @@ -93,17 +140,18 @@ create table users (id serial, name text); drop table users; ` - up, down, err = parseMigrationContents(migration) - require.Nil(t, err) + parsed, err := parseMigrationContents(migration) + require.Nil(t, err) - require.Equal(t, "-- migrate:up\ncreate table users (id serial, name text);\n\n", up.Contents) - require.Equal(t, true, up.Options.Transaction()) + require.Equal(t, "-- migrate:up\ncreate table users (id serial, name text);\n\n", parsed.Up) + require.Equal(t, true, parsed.UpOptions.Transaction()) - require.Equal(t, "-- migrate:down\ndrop table users;\n", down.Contents) - require.Equal(t, true, down.Options.Transaction()) + require.Equal(t, "-- migrate:down\ndrop table users;\n", parsed.Down) + require.Equal(t, true, parsed.DownOptions.Transaction()) + }) - // It does *not* allow arbitrary statements preceding the migrate blocks - migration = ` + t.Run("do not allow arbitrary statements preceding the migrate blocks", func(t *testing.T) { + migration := ` -- create status_type CREATE TYPE status_type AS ENUM ('active', 'inactive'); @@ -116,17 +164,39 @@ ALTER TABLE users DROP COLUMN status; ` - _, _, err = parseMigrationContents(migration) - require.NotNil(t, err) - require.Equal(t, "dbmate does not support statements defined outside of the '-- migrate:up' or '-- migrate:down' blocks", err.Error()) + _, err := parseMigrationContents(migration) + require.Error(t, err, "dbmate does not support statements preceding the '-- migrate:up' block") + }) - // It requires an at least an up block - migration = ` -ALTER TABLE users -ADD COLUMN status status_type DEFAULT 'active'; -` + t.Run("ensure Windows CR/LF line endings in migration files are parsed correctly", func(t *testing.T) { + t.Run("without migration options", func(t *testing.T) { + migration := "-- migrate:up\r\ncreate table users (id serial, name text);\r\n-- migrate:down\r\ndrop table users;\r\n" + + parsed, err := parseMigrationContents(migration) + require.Nil(t, err) + + require.Equal(t, "-- migrate:up\r\ncreate table users (id serial, name text);\r\n", parsed.Up) + require.Equal(t, migrationOptions{}, parsed.UpOptions) + require.Equal(t, true, parsed.UpOptions.Transaction()) + + require.Equal(t, "-- migrate:down\r\ndrop table users;\r\n", parsed.Down) + require.Equal(t, migrationOptions{}, parsed.DownOptions) + require.Equal(t, true, parsed.DownOptions.Transaction()) + }) + + t.Run("with migration options", func(t *testing.T) { + migration := "-- migrate:up transaction:true\r\ncreate table users (id serial, name text);\r\n-- migrate:down transaction:true\r\ndrop table users;\r\n" + + parsed, err := parseMigrationContents(migration) + require.Nil(t, err) + + require.Equal(t, "-- migrate:up transaction:true\r\ncreate table users (id serial, name text);\r\n", parsed.Up) + require.Equal(t, migrationOptions{"transaction": "true"}, parsed.UpOptions) + require.Equal(t, true, parsed.UpOptions.Transaction()) - _, _, err = parseMigrationContents(migration) - require.NotNil(t, err) - require.Equal(t, "dbmate requires each migration to define an up bock with '-- migrate:up'", err.Error()) + require.Equal(t, "-- migrate:down transaction:true\r\ndrop table users;\r\n", parsed.Down) + require.Equal(t, migrationOptions{"transaction": "true"}, parsed.DownOptions) + require.Equal(t, true, parsed.DownOptions.Transaction()) + }) + }) } diff --git a/pkg/dbmate/version.go b/pkg/dbmate/version.go index 5d20b510..62a2cc7e 100644 --- a/pkg/dbmate/version.go +++ b/pkg/dbmate/version.go @@ -1,4 +1,4 @@ package dbmate // Version of dbmate -const Version = "1.11.1" +const Version = "2.26.0" diff --git a/pkg/dbtest/dbtest.go b/pkg/dbtest/dbtest.go new file mode 100644 index 00000000..4c21feba --- /dev/null +++ b/pkg/dbtest/dbtest.go @@ -0,0 +1,36 @@ +// Helper package that should only be used in test files +package dbtest + +import ( + "net/url" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +// MustParseURL parses a URL from string, and fails the test if the URL is invalid. +func MustParseURL(t *testing.T, s string) *url.URL { + require.NotEmpty(t, s) + + u, err := url.Parse(s) + require.NoError(t, err) + + return u +} + +// GetenvOrSkip gets an environment variable, and skips the test if it is empty. +func GetenvOrSkip(t *testing.T, key string) string { + value := os.Getenv(key) + if value == "" { + t.Skipf("no %s provided", key) + } + + return value +} + +// GetenvURLOrSkip gets an environment variable, parses it as a URL, +// fails the test if the URL is invalid, and skips the test if empty. +func GetenvURLOrSkip(t *testing.T, key string) *url.URL { + return MustParseURL(t, GetenvOrSkip(t, key)) +} diff --git a/pkg/dbutil/dbutil.go b/pkg/dbutil/dbutil.go index e0bb13a5..311ef882 100644 --- a/pkg/dbutil/dbutil.go +++ b/pkg/dbutil/dbutil.go @@ -137,17 +137,17 @@ func QueryValue(db Transaction, query string, args ...interface{}) (string, erro return result.String, nil } -// MustParseURL parses a URL from string, and panics if it fails. -// It is used during testing and in cases where we are parsing a generated URL. -func MustParseURL(s string) *url.URL { +// MustUnescapePath unescapes a URL path, and panics if it fails. +// It is used during in cases where we are parsing a generated path. +func MustUnescapePath(s string) string { if s == "" { - panic("missing url") + panic("missing path") } - u, err := url.Parse(s) + path, err := url.PathUnescape(s) if err != nil { panic(err) } - return u + return path } diff --git a/pkg/dbutil/dbutil_test.go b/pkg/dbutil/dbutil_test.go index 2ad7162c..157436ed 100644 --- a/pkg/dbutil/dbutil_test.go +++ b/pkg/dbutil/dbutil_test.go @@ -4,6 +4,7 @@ import ( "database/sql" "testing" + "github.com/assetnote/dbmate/pkg/dbtest" "github.com/assetnote/dbmate/pkg/dbutil" _ "github.com/mattn/go-sqlite3" // database/sql driver @@ -12,13 +13,13 @@ import ( func TestDatabaseName(t *testing.T) { t.Run("valid", func(t *testing.T) { - u := dbutil.MustParseURL("foo://host/dbname?query") + u := dbtest.MustParseURL(t, "foo://host/dbname?query") name := dbutil.DatabaseName(u) require.Equal(t, "dbname", name) }) t.Run("empty", func(t *testing.T) { - u := dbutil.MustParseURL("foo://host") + u := dbtest.MustParseURL(t, "foo://host") name := dbutil.DatabaseName(u) require.Equal(t, "", name) }) diff --git a/pkg/driver/bigquery/bigquery.go b/pkg/driver/bigquery/bigquery.go new file mode 100644 index 00000000..0359d62e --- /dev/null +++ b/pkg/driver/bigquery/bigquery.go @@ -0,0 +1,505 @@ +package bigquery + +import ( + "bytes" + "context" + "database/sql" + "fmt" + "io" + "net/url" + "reflect" + "strings" + "unsafe" + + "cloud.google.com/go/bigquery" + "google.golang.org/api/googleapi" + "google.golang.org/api/iterator" + _ "gorm.io/driver/bigquery" // database/sql driver + + "github.com/assetnote/dbmate/pkg/dbmate" + "github.com/assetnote/dbmate/pkg/dbutil" +) + +func init() { + dbmate.RegisterDriver(NewDriver, "bigquery") +} + +type Driver struct { + migrationsTableName string + databaseURL *url.URL + log io.Writer +} + +func NewDriver(config dbmate.DriverConfig) dbmate.Driver { + return &Driver{ + migrationsTableName: config.MigrationsTableName, + databaseURL: config.DatabaseURL, + log: config.Log, + } +} + +func (drv *Driver) CreateDatabase() error { + db, err := drv.Open() + if err != nil { + return err + } + defer dbutil.MustClose(db) + + exists, err := drv.DatabaseExists() + if err != nil { + return err + } + + if exists { + return nil + } + + ctx := context.Background() + conn, err := db.Conn(ctx) + if err != nil { + return err + } + defer conn.Close() + + return conn.Raw(func(driverConn any) error { + client := getClient(driverConn) + config := getConfig(driverConn) + + return client.Dataset(config.dataSet).Create(ctx, &bigquery.DatasetMetadata{ + Location: config.location, + }) + }) +} + +func (drv *Driver) CreateMigrationsTable(db *sql.DB) error { + ctx := context.Background() + conn, err := db.Conn(ctx) + if err != nil { + return err + } + defer conn.Close() + + return conn.Raw(func(driverConn any) error { + client := getClient(driverConn) + config := getConfig(driverConn) + + exists, err := tableExists(client, config.dataSet, drv.migrationsTableName) + if err != nil { + return err + } + if exists { + return nil + } + + return client.Dataset(config.dataSet).Table(drv.migrationsTableName).Create(ctx, &bigquery.TableMetadata{ + Schema: bigquery.Schema{ + { + Name: "version", + Type: bigquery.StringFieldType, + }, + }, + }) + }) +} + +func (drv *Driver) DatabaseExists() (bool, error) { + db, err := drv.Open() + if err != nil { + return false, err + } + defer dbutil.MustClose(db) + + ctx := context.Background() + conn, err := db.Conn(ctx) + if err != nil { + return false, err + } + defer conn.Close() + + var exists bool + err = conn.Raw(func(driverConn any) error { + client := getClient(driverConn) + config := getConfig(driverConn) + + it := client.Datasets(ctx) + for { + dataset, err := it.Next() + if err == iterator.Done { + exists = false + return nil + } + if err != nil { + return err + } + if dataset.DatasetID == config.dataSet { + exists = true + return nil + } + } + }) + + return exists, err +} + +func (drv *Driver) DropDatabase() error { + db, err := drv.Open() + if err != nil { + return err + } + defer dbutil.MustClose(db) + + exists, err := drv.DatabaseExists() + if err != nil { + return err + } + if !exists { + return nil + } + + ctx := context.Background() + conn, err := db.Conn(ctx) + if err != nil { + return err + } + defer conn.Close() + + return conn.Raw(func(driverConn any) error { + client := getClient(driverConn) + config := getConfig(driverConn) + + return client.Dataset(config.dataSet).DeleteWithContents(ctx) + }) +} + +func (drv *Driver) schemaDump(db *sql.DB) ([]byte, error) { + // build schema migrations table data + var buf bytes.Buffer + buf.WriteString("\n--\n-- Database schema\n--\n\n") + + config, err := drv.getConfig(db) + if err != nil { + return nil, err + } + + query := fmt.Sprintf( + `SELECT table_name AS object_name, 'TABLE' AS object_type, ddl + FROM `+"`%s.%s.INFORMATION_SCHEMA.TABLES`"+` + UNION ALL + SELECT routine_name AS object_name, 'FUNCTION' AS object_type, ddl + FROM `+"`%s.%s.INFORMATION_SCHEMA.ROUTINES`"+` + ORDER BY CASE object_type + WHEN 'TABLE' THEN 1 + WHEN 'FUNCTION' THEN 2 + ELSE 3 + END;`, + config.projectID, config.dataSet, + config.projectID, config.dataSet, + ) + + // Execute the query + rows, err := db.Query(query) + if err != nil { + return nil, fmt.Errorf("error querying objects: %v", err) + } + defer dbutil.MustClose(rows) + + // Iterate over the results and generate DDL for each object + for rows.Next() { + var objectName, objectType, ddl string + if err := rows.Scan(&objectName, &objectType, &ddl); err != nil { + return nil, fmt.Errorf("error scanning object: %v", err) + } + + buf.WriteString(ddl + "\n") + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("error iterating objects: %v", err) + } + + return buf.Bytes(), nil +} + +func (drv *Driver) schemaMigrationsDump(db *sql.DB) ([]byte, error) { + migrationsTable := drv.migrationsTableName + + // load applied migrations + migrations, err := dbutil.QueryColumn(db, + fmt.Sprintf("select version from %s order by version asc", migrationsTable)) + if err != nil { + return nil, err + } + + // build schema migrations table data + var buf bytes.Buffer + buf.WriteString("\n--\n-- Dbmate schema migrations\n--\n\n") + + if len(migrations) > 0 { + buf.WriteString( + fmt.Sprintf("INSERT INTO %s (version) VALUES\n ('", migrationsTable) + + strings.Join(migrations, "'),\n ('") + + "');\n") + } + + return buf.Bytes(), nil +} + +func (drv *Driver) DumpSchema(db *sql.DB) ([]byte, error) { + schema, err := drv.schemaDump(db) + if err != nil { + return nil, err + } + + migrations, err := drv.schemaMigrationsDump(db) + if err != nil { + return nil, err + } + + return append(schema, migrations...), nil +} + +func (drv *Driver) MigrationsTableExists(db *sql.DB) (bool, error) { + ctx := context.Background() + conn, err := db.Conn(ctx) + if err != nil { + return false, err + } + defer conn.Close() + + var exists bool + err = conn.Raw(func(driverConn any) error { + client := getClient(driverConn) + config := getConfig(driverConn) + exists, err = tableExists(client, config.dataSet, drv.migrationsTableName) + if err != nil { + return err + } + return nil + }) + if err != nil { + return exists, err + } + + return exists, nil +} + +func (drv *Driver) DeleteMigration(util dbutil.Transaction, version string) error { + db, err := drv.Open() + if err != nil { + return err + } + defer dbutil.MustClose(db) + + config, err := drv.getConfig(db) + if err != nil { + return err + } + + query := fmt.Sprintf("DELETE FROM %s.%s WHERE version = '%s';", config.dataSet, drv.migrationsTableName, version) + _, err = util.Exec(query) + if err != nil { + return err + } + + return nil +} + +func (drv *Driver) InsertMigration(_ dbutil.Transaction, version string) error { + db, err := drv.Open() + if err != nil { + return err + } + defer dbutil.MustClose(db) + + config, err := drv.getConfig(db) + if err != nil { + return err + } + + queryTemplate := `INSERT INTO %s.%s (version) VALUES ('%s');` + queryString := fmt.Sprintf(queryTemplate, config.dataSet, drv.migrationsTableName, version) + _, err = db.Exec(queryString, version) + if err != nil { + return err + } + + return nil +} + +func (drv *Driver) Open() (*sql.DB, error) { + return sql.Open("bigquery", connectionString(drv.databaseURL)) +} + +func (drv *Driver) Ping() error { + db, err := drv.Open() + if err != nil { + return err + } + defer dbutil.MustClose(db) + + return db.Ping() +} + +func (*Driver) QueryError(query string, err error) error { + return &dbmate.QueryError{Err: err, Query: query} +} + +func (drv *Driver) SelectMigrations(db *sql.DB, limit int) (map[string]bool, error) { + config, err := drv.getConfig(db) + if err != nil { + return nil, err + } + + query := fmt.Sprintf("SELECT version FROM %s.%s ORDER BY version DESC", config.dataSet, drv.migrationsTableName) + if limit >= 0 { + query = fmt.Sprintf("%s limit %d", query, limit) + } + rows, err := db.Query(query) + if err != nil { + return nil, err + } + defer dbutil.MustClose(rows) + + migrations := map[string]bool{} + for rows.Next() { + var version string + if err := rows.Scan(&version); err != nil { + return nil, err + } + + migrations[version] = true + } + + if err = rows.Err(); err != nil { + return nil, err + } + + return migrations, nil +} + +// Helper function to check whether a table exists or not in a dataset +func tableExists(client *bigquery.Client, datasetID, tableName string) (bool, error) { + table := client.Dataset(datasetID).Table(tableName) + _, err := table.Metadata(context.Background()) + if err == nil { + return true, nil + } + if gError, ok := err.(*googleapi.Error); ok && gError.Code == 404 { + return false, nil + } + return false, err +} + +func connectionString(u *url.URL) string { + return u.String() +} + +// nolint:unused +func connectionStringOld(u *url.URL) string { + //if connecting to emulator with host:port format + if u.Port() != "" { + paths := strings.Split(strings.TrimPrefix(u.Path, "/"), "/") + + newURL := &url.URL{ + Scheme: u.Scheme, + Host: paths[0], + } + + params := url.Values{} + if u.Query().Get("disable_auth") == "true" { + params.Set("disable_auth", "true") + } + params.Set("endpoint", fmt.Sprintf("http://%s:%s", u.Hostname(), u.Port())) + + if len(paths) == 3 { + // bigquery://host:port/project/location/dataset + newURL.Path += "/" + paths[1] + newURL.Path += "/" + paths[2] + } else { + // bigquery://host:port/project/dataset + newURL.Path += "/" + paths[1] + } + + newURL.RawQuery = params.Encode() + + return newURL.String() + } + + //connecting to GCP BigQuery, drop all query strings + newURL := &url.URL{ + Scheme: u.Scheme, + Host: u.Host, + Path: u.Path, + } + + return newURL.String() +} + +func (drv *Driver) getClient(db *sql.DB) (*bigquery.Client, error) { + conn, err := db.Conn(context.Background()) + if err != nil { + return nil, err + } + defer conn.Close() + + var client *bigquery.Client + + err = conn.Raw(func(driverConn any) error { + client = getClient(driverConn) + return nil + }) + if err != nil { + return nil, err + } + + return client, nil +} + +func getClient(driverConn any) *bigquery.Client { + value := reflect.ValueOf(driverConn).Elem().FieldByName("client") + value = reflect.NewAt(value.Type(), unsafe.Pointer(value.UnsafeAddr())) + return value.Elem().Interface().(*bigquery.Client) +} + +// As the `bigQueryConfig` struct is unexported from `go-gorm/bigquery`, +// we need to maintain a copy here and access it through reflection. +// +// See: https://github.com/go-gorm/bigquery/blob/74582cba0726b82b8a59990fee4064e059e88c9b/driver/driver.go#L18-L27 +// +// nolint:unused +type bigQueryConfig struct { + projectID string + location string + dataSet string + scopes []string + endpoint string + disableAuth bool + credentialFile string + credentialJSON []byte +} + +func (drv *Driver) getConfig(db *sql.DB) (*bigQueryConfig, error) { + conn, err := db.Conn(context.Background()) + if err != nil { + return nil, err + } + defer conn.Close() + + var config *bigQueryConfig + + err = conn.Raw(func(driverConn any) error { + config = getConfig(driverConn) + return nil + }) + if err != nil { + return nil, err + } + + return config, nil +} + +func getConfig(driverConn any) *bigQueryConfig { + value := reflect.ValueOf(driverConn).Elem().FieldByName("config") + value = reflect.NewAt(reflect.TypeOf(bigQueryConfig{}), unsafe.Pointer(value.UnsafeAddr())) + return value.Interface().(*bigQueryConfig) +} diff --git a/pkg/driver/bigquery/bigquery_test.go b/pkg/driver/bigquery/bigquery_test.go new file mode 100644 index 00000000..6c2c4941 --- /dev/null +++ b/pkg/driver/bigquery/bigquery_test.go @@ -0,0 +1,375 @@ +package bigquery + +import ( + "database/sql" + "fmt" + "net/url" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/assetnote/dbmate/pkg/dbmate" + "github.com/assetnote/dbmate/pkg/dbtest" + "github.com/assetnote/dbmate/pkg/dbutil" +) + +func testBigQueryDriver(t *testing.T) *Driver { + u := dbtest.GetenvURLOrSkip(t, "BIGQUERY_TEST_URL") + drv, err := dbmate.New(u).Driver() + require.NoError(t, err) + + return drv.(*Driver) +} + +func testGoogleBigQueryDriver(t *testing.T) *Driver { + u := dbtest.GetenvURLOrSkip(t, "GOOGLE_BIGQUERY_TEST_URL") + + endpoint := u.Query().Get("endpoint") + if endpoint != "" { + endpointURL, err := url.Parse(endpoint) + require.NoError(t, err) + + if endpointURL.Hostname() != "bigquery.googleapis.com" { + t.Skipf("skipping test, GOOGLE_BIGQUERY_TEST_URL endpoint is %s and not bigquery.googleapis.com", endpointURL.Hostname()) + } + } + + drv, err := dbmate.New(u).Driver() + require.NoError(t, err) + + return drv.(*Driver) +} + +func prepTestBigQueryDB(t *testing.T) *sql.DB { + drv := testBigQueryDriver(t) + + // drop any existing database + err := drv.DropDatabase() + require.NoError(t, err) + + // create database + err = drv.CreateDatabase() + require.NoError(t, err) + + // connect database + db, err := drv.Open() + require.NoError(t, err) + + return db +} + +func prepTestGoogleBigQueryDB(t *testing.T) *sql.DB { + drv := testGoogleBigQueryDriver(t) + + // drop any existing database + err := drv.DropDatabase() + require.NoError(t, err) + + // create database + err = drv.CreateDatabase() + require.NoError(t, err) + + // connect database + db, err := drv.Open() + require.NoError(t, err) + + return db +} + +func TestGetDriver(t *testing.T) { + db := dbmate.New(dbtest.MustParseURL(t, "bigquery://")) + drvInterface, err := db.Driver() + require.NoError(t, err) + + // driver should have URL and default migrations table set + drv, ok := drvInterface.(*Driver) + require.True(t, ok) + require.Equal(t, db.DatabaseURL.String(), drv.databaseURL.String()) + require.Equal(t, "schema_migrations", drv.migrationsTableName) +} + +func TestGetClient(t *testing.T) { + drv := testBigQueryDriver(t) + + db, err := drv.Open() + require.NoError(t, err) + defer dbutil.MustClose(db) + + client, err := drv.getClient(db) + require.NoError(t, err) + require.Equal(t, "test", client.Project()) +} + +func TestGetConfig(t *testing.T) { + drv := testBigQueryDriver(t) + + db, err := drv.Open() + require.NoError(t, err) + defer dbutil.MustClose(db) + + config, err := drv.getConfig(db) + require.NoError(t, err) + require.Equal(t, "test", config.projectID) + require.Equal(t, "us-east5", config.location) + require.Equal(t, "dbmate_test", config.dataSet) +} + +func TestConnectionString(t *testing.T) { + cases := []struct { + input string + expected string + }{ + {"bigquery://projectid/dataset", "bigquery://projectid/dataset"}, + {"bigquery://projectid/location/dataset", "bigquery://projectid/location/dataset"}, + {"bigquery://projectid/location/dataset?disable_auth=false", "bigquery://projectid/location/dataset?disable_auth=false"}, + {"bigquery://projectid/location/dataset?disable_auth=true", "bigquery://projectid/location/dataset?disable_auth=true"}, + {"bigquery://projectid/location/dataset?endpoint=https%3A%2F%2Fbigquery.googleapis.com", "bigquery://projectid/location/dataset?endpoint=https%3A%2F%2Fbigquery.googleapis.com"}, + {"bigquery://projectid/location/dataset?endpoint=http%3A%2F%2F0.0.0.0%3A9050", "bigquery://projectid/location/dataset?endpoint=http%3A%2F%2F0.0.0.0%3A9050"}, + {"bigquery://projectid/location/dataset?endpoint=http%3A%2F%2F0.0.0.0%3A9050&disable_auth=true", "bigquery://projectid/location/dataset?endpoint=http%3A%2F%2F0.0.0.0%3A9050&disable_auth=true"}, + } + + for _, c := range cases { + t.Run(c.input, func(t *testing.T) { + u, err := url.Parse(c.input) + require.NoError(t, err) + + actual := connectionString(u) + require.Equal(t, c.expected, actual) + }) + } +} +func TestBigQueryCreateDropDatabase(t *testing.T) { + drv := testBigQueryDriver(t) + + // drop any existing database + err := drv.DropDatabase() + require.NoError(t, err) + + // create database + err = drv.CreateDatabase() + require.NoError(t, err) + + // check that database exists and we can connect to it + func() { + db, err := drv.Open() + require.NoError(t, err) + defer dbutil.MustClose(db) + + err = db.Ping() + require.NoError(t, err) + }() + + // drop the database + err = drv.DropDatabase() + require.NoError(t, err) + + // check that database no longer exists + func() { + db, err := drv.Open() + require.NoError(t, err) + defer dbutil.MustClose(db) + + err = db.Ping() + require.Error(t, err) + require.Regexp(t, "dataset dbmate_test is not found", err.Error()) + }() +} + +func TestBigQueryDatabaseExists(t *testing.T) { + drv := testBigQueryDriver(t) + + // drop any existing database + err := drv.DropDatabase() + require.NoError(t, err) + + // DatabaseExists should return false + exists, err := drv.DatabaseExists() + require.NoError(t, err) + require.Equal(t, false, exists) + + // create database + err = drv.CreateDatabase() + require.NoError(t, err) + + // DatabaseExists should return true + exists, err = drv.DatabaseExists() + require.NoError(t, err) + require.Equal(t, true, exists) +} + +func TestBigQueryCreateMigrationsTable(t *testing.T) { + drv := testBigQueryDriver(t) + drv.migrationsTableName = "test_migrations" + + db := prepTestBigQueryDB(t) + defer dbutil.MustClose(db) + + // migrations table should not exist + count := 0 + err := db.QueryRow("select count(*) from test_migrations").Scan(&count) + require.Error(t, err) + require.Regexp(t, "Table not found: test_migrations", err.Error()) + + // create table + err = drv.CreateMigrationsTable(db) + require.NoError(t, err) + + // migrations table should exist + err = db.QueryRow("select count(*) from test_migrations").Scan(&count) + require.NoError(t, err) + + // create table should be idempotent + err = drv.CreateMigrationsTable(db) + require.NoError(t, err) +} + +func TestBigQuerySelectMigrations(t *testing.T) { + drv := testBigQueryDriver(t) + drv.migrationsTableName = "test_migrations" + + db := prepTestBigQueryDB(t) + defer dbutil.MustClose(db) + + err := drv.CreateMigrationsTable(db) + require.NoError(t, err) + + _, err = db.Exec(`insert into test_migrations (version) + values ('abc2'), ('abc1'), ('abc3')`) + require.NoError(t, err) + + migrations, err := drv.SelectMigrations(db, -1) + require.NoError(t, err) + require.Equal(t, true, migrations["abc1"]) + require.Equal(t, true, migrations["abc2"]) + require.Equal(t, true, migrations["abc2"]) + + // test limit param + migrations, err = drv.SelectMigrations(db, 1) + require.NoError(t, err) + require.Equal(t, true, migrations["abc3"]) + require.Equal(t, false, migrations["abc1"]) + require.Equal(t, false, migrations["abc2"]) +} + +func TestBigQueryInsertMigration(t *testing.T) { + drv := testBigQueryDriver(t) + drv.migrationsTableName = "test_migrations" + + db := prepTestBigQueryDB(t) + defer dbutil.MustClose(db) + + err := drv.CreateMigrationsTable(db) + require.NoError(t, err) + + count := 0 + err = db.QueryRow("select count(*) from test_migrations").Scan(&count) + require.NoError(t, err) + require.Equal(t, 0, count) + + // insert migration + err = drv.InsertMigration(db, "abc1") + require.NoError(t, err) + + err = db.QueryRow("select count(*) from test_migrations where version = 'abc1'"). + Scan(&count) + require.NoError(t, err) + require.Equal(t, 1, count) +} + +func TestBigQueryDeleteMigration(t *testing.T) { + drv := testBigQueryDriver(t) + drv.migrationsTableName = "test_migrations" + + db := prepTestBigQueryDB(t) + defer dbutil.MustClose(db) + + err := drv.CreateMigrationsTable(db) + require.NoError(t, err) + + _, err = db.Exec(`insert into test_migrations (version) + values ('abc1'), ('abc2')`) + require.NoError(t, err) + + err = drv.DeleteMigration(db, "abc2") + require.NoError(t, err) + + count := 0 + err = db.QueryRow("select count(*) from test_migrations").Scan(&count) + require.NoError(t, err) + require.Equal(t, 1, count) +} + +func TestBigQueryPingError(t *testing.T) { + drv := testBigQueryDriver(t) + + // drop any existing database + err := drv.DropDatabase() + require.NoError(t, err) + + // ping database + err = drv.Ping() + require.Error(t, err) + require.Contains(t, err.Error(), "dataset dbmate_test is not found") +} + +func TestBigQueryPingSuccess(t *testing.T) { + drv := testBigQueryDriver(t) + + db := prepTestBigQueryDB(t) + defer dbutil.MustClose(db) + + // ping database + err := drv.Ping() + require.NoError(t, err) +} + +func TestBigQueryMigrationsTableExists(t *testing.T) { + drv := testBigQueryDriver(t) + drv.migrationsTableName = "test_migrations" + + db := prepTestBigQueryDB(t) + defer dbutil.MustClose(db) + + exists, err := drv.MigrationsTableExists(db) + require.NoError(t, err) + require.Equal(t, false, exists) + + err = drv.CreateMigrationsTable(db) + require.NoError(t, err) + + exists, err = drv.MigrationsTableExists(db) + require.NoError(t, err) + require.Equal(t, true, exists) +} + +func TestGoogleBigQueryDumpSchema(t *testing.T) { + t.Run("default migrations table", func(t *testing.T) { + drv := testGoogleBigQueryDriver(t) + + // prepare database + db := prepTestGoogleBigQueryDB(t) + defer dbutil.MustClose(db) + err := drv.CreateMigrationsTable(db) + require.NoError(t, err) + + // insert migration + err = drv.InsertMigration(db, "abc1") + require.NoError(t, err) + err = drv.InsertMigration(db, "abc2") + require.NoError(t, err) + + // DumpSchema should return schema + config, err := drv.getConfig(db) + require.NoError(t, err) + + schema, err := drv.DumpSchema(db) + require.NoError(t, err) + require.Contains(t, string(schema), fmt.Sprintf("CREATE TABLE `%s.%s.schema_migrations`", config.projectID, config.dataSet)) + require.Contains(t, string(schema), "\n--\n"+ + "-- Dbmate schema migrations\n"+ + "--\n\n"+ + "INSERT INTO schema_migrations (version) VALUES\n"+ + " ('abc1'),\n"+ + " ('abc2');\n") + }) +} diff --git a/pkg/driver/clickhouse/clickhouse.go b/pkg/driver/clickhouse/clickhouse.go index 80423b15..51736629 100644 --- a/pkg/driver/clickhouse/clickhouse.go +++ b/pkg/driver/clickhouse/clickhouse.go @@ -4,6 +4,7 @@ import ( "bytes" "database/sql" "fmt" + "io" "net/url" "regexp" "sort" @@ -12,7 +13,7 @@ import ( "github.com/assetnote/dbmate/pkg/dbmate" "github.com/assetnote/dbmate/pkg/dbutil" - "github.com/ClickHouse/clickhouse-go" + "github.com/ClickHouse/clickhouse-go/v2" ) func init() { @@ -23,6 +24,8 @@ func init() { type Driver struct { migrationsTableName string databaseURL *url.URL + log io.Writer + clusterParameters *ClusterParameters } // NewDriver initializes the driver @@ -30,13 +33,15 @@ func NewDriver(config dbmate.DriverConfig) dbmate.Driver { return &Driver{ migrationsTableName: config.MigrationsTableName, databaseURL: config.DatabaseURL, + log: config.Log, + clusterParameters: ExtractClusterParametersFromURL(config.DatabaseURL), } } func connectionString(initialURL *url.URL) string { - u := *initialURL + // clone url + u, _ := url.Parse(initialURL.String()) - u.Scheme = "tcp" host := u.Host if u.Port() == "" { host = fmt.Sprintf("%s:9000", host) @@ -44,24 +49,35 @@ func connectionString(initialURL *url.URL) string { u.Host = host query := u.Query() - if query.Get("username") == "" && u.User.Username() != "" { - query.Set("username", u.User.Username()) + username := u.User.Username() + password, _ := u.User.Password() + + if query.Get("username") != "" { + username = query.Get("username") + query.Del("username") } - password, passwordSet := u.User.Password() - if query.Get("password") == "" && passwordSet { - query.Set("password", password) + if query.Get("password") != "" { + password = query.Get("password") + query.Del("password") } - u.User = nil - if query.Get("database") == "" { - path := strings.Trim(u.Path, "/") - if path != "" { - query.Set("database", path) - u.Path = "" + if username != "" { + if password == "" { + u.User = url.User(username) + } else { + u.User = url.UserPassword(username, password) } } + + if query.Get("database") != "" { + u.Path = fmt.Sprintf("/%s", query.Get("database")) + query.Del("database") + } + u.RawQuery = query.Encode() + u = ClearClusterParametersFromURL(u) + return u.String() } @@ -78,15 +94,27 @@ func (drv *Driver) openClickHouseDB() (*sql.DB, error) { } // connect to clickhouse database - values := clickhouseURL.Query() - values.Set("database", "default") - clickhouseURL.RawQuery = values.Encode() + clickhouseURL.Path = "/default" return sql.Open("clickhouse", clickhouseURL.String()) } +func (drv *Driver) onClusterClause() string { + clusterClause := "" + if drv.clusterParameters.OnCluster { + escapedClusterMacro := drv.escapeString(drv.clusterParameters.ClusterMacro) + clusterClause = fmt.Sprintf(" ON CLUSTER '%s'", escapedClusterMacro) + } + return clusterClause +} + func (drv *Driver) databaseName() string { - name := dbutil.MustParseURL(connectionString(drv.databaseURL)).Query().Get("database") + u, err := url.Parse(connectionString(drv.databaseURL)) + if err != nil { + panic(err) + } + + name := strings.TrimLeft(u.Path, "/") if name == "" { name = "default" } @@ -105,10 +133,16 @@ func (drv *Driver) quoteIdentifier(str string) string { return fmt.Sprintf(`"%s"`, str) } +func (drv *Driver) escapeString(str string) string { + quoteEscaper := strings.NewReplacer(`'`, `\'`, `\`, `\\`) + str = quoteEscaper.Replace(str) + return str +} + // CreateDatabase creates the specified database func (drv *Driver) CreateDatabase() error { name := drv.databaseName() - fmt.Printf("Creating: %s\n", name) + fmt.Fprintf(drv.log, "Creating: %s\n", name) db, err := drv.openClickHouseDB() if err != nil { @@ -116,7 +150,9 @@ func (drv *Driver) CreateDatabase() error { } defer dbutil.MustClose(db) - _, err = db.Exec("create database " + drv.quoteIdentifier(name)) + q := fmt.Sprintf("CREATE DATABASE %s%s", drv.quoteIdentifier(name), drv.onClusterClause()) + + _, err = db.Exec(q) return err } @@ -124,7 +160,7 @@ func (drv *Driver) CreateDatabase() error { // DropDatabase drops the specified database (if it exists) func (drv *Driver) DropDatabase() error { name := drv.databaseName() - fmt.Printf("Dropping: %s\n", name) + fmt.Fprintf(drv.log, "Dropping: %s\n", name) db, err := drv.openClickHouseDB() if err != nil { @@ -132,15 +168,16 @@ func (drv *Driver) DropDatabase() error { } defer dbutil.MustClose(db) - _, err = db.Exec("drop database if exists " + drv.quoteIdentifier(name)) + q := fmt.Sprintf("DROP DATABASE IF EXISTS %s%s", drv.quoteIdentifier(name), drv.onClusterClause()) + + _, err = db.Exec(q) return err } func (drv *Driver) schemaDump(db *sql.DB, buf *bytes.Buffer, databaseName string) error { buf.WriteString("\n--\n-- Database schema\n--\n\n") - - buf.WriteString("CREATE DATABASE " + drv.quoteIdentifier(databaseName) + " IF NOT EXISTS;\n\n") + buf.WriteString(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s%s;\n\n", drv.quoteIdentifier(databaseName), drv.onClusterClause())) tables, err := dbutil.QueryColumn(db, "show tables") if err != nil { @@ -227,17 +264,36 @@ func (drv *Driver) DatabaseExists() (bool, error) { return exists, err } +// MigrationsTableExists checks if the schema_migrations table exists +func (drv *Driver) MigrationsTableExists(db *sql.DB) (bool, error) { + exists := false + err := db.QueryRow(fmt.Sprintf("EXISTS TABLE %s", drv.quotedMigrationsTableName())). + Scan(&exists) + if err == sql.ErrNoRows { + return false, nil + } + + return exists, err +} + // CreateMigrationsTable creates the schema migrations table func (drv *Driver) CreateMigrationsTable(db *sql.DB) error { + engineClause := "ReplacingMergeTree(ts)" + if drv.clusterParameters.OnCluster { + escapedZooPath := drv.escapeString(drv.clusterParameters.ZooPath) + escapedReplicaMacro := drv.escapeString(drv.clusterParameters.ReplicaMacro) + engineClause = fmt.Sprintf("ReplicatedReplacingMergeTree('%s', '%s', ts)", escapedZooPath, escapedReplicaMacro) + } + _, err := db.Exec(fmt.Sprintf(` - create table if not exists %s ( + create table if not exists %s%s ( version String, ts DateTime default now(), applied UInt8 default 1 - ) engine = ReplacingMergeTree(ts) + ) engine = %s primary key version order by version - `, drv.quotedMigrationsTableName())) + `, drv.quotedMigrationsTableName(), drv.onClusterClause(), engineClause)) return err } @@ -298,9 +354,6 @@ func (drv *Driver) DeleteMigration(db dbutil.Transaction, version string) error // Ping verifies a connection to the database server. It does not verify whether the // specified database exists. func (drv *Driver) Ping() error { - // attempt connection to primary database, not "clickhouse" database - // to support servers with no "clickhouse" database - // (see https://github.com/amacneil/dbmate/issues/78) db, err := drv.Open() if err != nil { return err @@ -321,6 +374,11 @@ func (drv *Driver) Ping() error { return err } +// Return a normalized version of the driver-specific error type. +func (drv *Driver) QueryError(query string, err error) error { + return &dbmate.QueryError{Err: err, Query: query} +} + func (drv *Driver) quotedMigrationsTableName() string { return drv.quoteIdentifier(drv.migrationsTableName) } diff --git a/pkg/driver/clickhouse/clickhouse_cluster_test.go b/pkg/driver/clickhouse/clickhouse_cluster_test.go new file mode 100644 index 00000000..60444c6a --- /dev/null +++ b/pkg/driver/clickhouse/clickhouse_cluster_test.go @@ -0,0 +1,341 @@ +package clickhouse + +import ( + "database/sql" + "testing" + + "github.com/assetnote/dbmate/pkg/dbmate" + "github.com/assetnote/dbmate/pkg/dbtest" + "github.com/assetnote/dbmate/pkg/dbutil" + + "github.com/stretchr/testify/require" +) + +func assertDatabaseExists(t *testing.T, drv *Driver, shouldExist bool) { + db, err := sql.Open("clickhouse", drv.databaseURL.String()) + require.NoError(t, err) + defer dbutil.MustClose(db) + + err = db.Ping() + if shouldExist { + require.NoError(t, err) + } else { + require.EqualError(t, err, "code: 81, message: Database dbmate_test doesn't exist") + } +} + +// Makes sure driver creatinon is atomic +func TestDriverCreationSanity(t *testing.T) { + u := dbtest.GetenvURLOrSkip(t, "CLICKHOUSE_CLUSTER_01_TEST_URL") + u.RawQuery = "on_cluster" + dbm := dbmate.New(u) + + drv, err := dbm.Driver() + require.NoError(t, err) + + drvAgain, err := dbm.Driver() + require.NoError(t, err) + + require.Equal(t, drv, drvAgain) +} + +func TestOnClusterClause(t *testing.T) { + cases := []struct { + input string + expected string + }{ + // not on cluster + {"clickhouse://myhost:9000", ""}, + // on_cluster supplied + {"clickhouse://myhost:9000?on_cluster", " ON CLUSTER '{cluster}'"}, + // on_cluster with supplied macro + {"clickhouse://myhost:9000?on_cluster&cluster_macro={cluster2}", " ON CLUSTER '{cluster2}'"}, + } + + for _, c := range cases { + t.Run(c.input, func(t *testing.T) { + drv := testClickHouseDriverURL(t, dbtest.MustParseURL(t, c.input)) + actual := drv.onClusterClause() + require.Equal(t, c.expected, actual) + }) + } +} + +func TestClickHouseCreateDropDatabaseOnCluster(t *testing.T) { + drv01 := testClickHouseDriverCluster01(t) + drv02 := testClickHouseDriverCluster02(t) + + // drop any existing database + err := drv01.DropDatabase() + require.NoError(t, err) + + // create database + err = drv01.CreateDatabase() + require.NoError(t, err) + + // check that database exists and we can connect to it + assertDatabaseExists(t, drv01, true) + // check that database exists on the other clickhouse node and we can connect to it + assertDatabaseExists(t, drv02, true) + + // drop the database + err = drv01.DropDatabase() + require.NoError(t, err) + + // check that database no longer exists + assertDatabaseExists(t, drv01, false) + // check that database no longer exists on the other clickhouse node + assertDatabaseExists(t, drv02, false) +} + +func TestClickHouseDumpSchemaOnCluster(t *testing.T) { + drv := testClickHouseDriverCluster01(t) + drv.migrationsTableName = "test_migrations" + + // prepare database + db := prepTestClickHouseDB(t, drv) + defer dbutil.MustClose(db) + err := drv.CreateMigrationsTable(db) + require.NoError(t, err) + + // insert migration + tx, err := db.Begin() + require.NoError(t, err) + err = drv.InsertMigration(tx, "abc1") + require.NoError(t, err) + err = tx.Commit() + require.NoError(t, err) + tx, err = db.Begin() + require.NoError(t, err) + err = drv.InsertMigration(tx, "abc2") + require.NoError(t, err) + err = tx.Commit() + require.NoError(t, err) + + // DumpSchema should return schema + schema, err := drv.DumpSchema(db) + require.NoError(t, err) + require.Contains(t, string(schema), "CREATE TABLE "+drv.databaseName()+".test_migrations") + require.Contains(t, string(schema), "ENGINE = ReplicatedReplacingMergeTree") + require.Contains(t, string(schema), "--\n"+ + "-- Dbmate schema migrations\n"+ + "--\n\n"+ + "INSERT INTO test_migrations (version) VALUES\n"+ + " ('abc1'),\n"+ + " ('abc2');\n") + + // DumpSchema should return error if command fails + drv.databaseURL.Path = "/fakedb" + db, err = sql.Open("clickhouse", drv.databaseURL.String()) + require.NoError(t, err) + + schema, err = drv.DumpSchema(db) + require.Nil(t, schema) + require.EqualError(t, err, "code: 81, message: Database fakedb doesn't exist") +} + +func TestClickHouseCreateMigrationsTableOnCluster(t *testing.T) { + testCases := []struct { + name string + migrationsTable string + expectedTableName string + }{ + { + name: "default table", + migrationsTable: "", + expectedTableName: "schema_migrations", + }, + { + name: "custom table", + migrationsTable: "testMigrations", + expectedTableName: "\"testMigrations\"", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + drv01 := testClickHouseDriverCluster01(t) + drv02 := testClickHouseDriverCluster02(t) + if tc.migrationsTable != "" { + drv01.migrationsTableName = tc.migrationsTable + drv02.migrationsTableName = tc.migrationsTable + } + + db01 := prepTestClickHouseDB(t, drv01) + defer dbutil.MustClose(db01) + + db02 := prepTestClickHouseDB(t, drv02) + defer dbutil.MustClose(db02) + + // migrations table should not exist + exists, err := drv01.MigrationsTableExists(db01) + require.NoError(t, err) + require.Equal(t, false, exists) + + // migrations table should not exist on the other node + exists, err = drv02.MigrationsTableExists(db02) + require.NoError(t, err) + require.Equal(t, false, exists) + + // create table + err = drv01.CreateMigrationsTable(db01) + require.NoError(t, err) + + // migrations table should exist + exists, err = drv01.MigrationsTableExists(db01) + require.NoError(t, err) + require.Equal(t, true, exists) + + // migrations table should exist on other node + exists, err = drv02.MigrationsTableExists(db02) + require.NoError(t, err) + require.Equal(t, true, exists) + + // create table should be idempotent + err = drv01.CreateMigrationsTable(db01) + require.NoError(t, err) + }) + } +} + +func TestClickHouseSelectMigrationsOnCluster(t *testing.T) { + drv01 := testClickHouseDriverCluster01(t) + drv02 := testClickHouseDriverCluster02(t) + drv01.migrationsTableName = "test_migrations" + drv02.migrationsTableName = "test_migrations" + + db01 := prepTestClickHouseDB(t, drv01) + defer dbutil.MustClose(db01) + + db02 := prepTestClickHouseDB(t, drv02) + defer dbutil.MustClose(db02) + + err := drv01.CreateMigrationsTable(db01) + require.NoError(t, err) + + tx, err := db01.Begin() + require.NoError(t, err) + stmt, err := tx.Prepare("insert into test_migrations (version) values (?)") + require.NoError(t, err) + _, err = stmt.Exec("abc2") + require.NoError(t, err) + _, err = stmt.Exec("abc1") + require.NoError(t, err) + _, err = stmt.Exec("abc3") + require.NoError(t, err) + err = tx.Commit() + require.NoError(t, err) + + migrations01, err := drv01.SelectMigrations(db01, -1) + require.NoError(t, err) + require.Equal(t, true, migrations01["abc1"]) + require.Equal(t, true, migrations01["abc2"]) + require.Equal(t, true, migrations01["abc3"]) + + // Assert select on other node + migrations02, err := drv02.SelectMigrations(db02, -1) + require.NoError(t, err) + require.Equal(t, true, migrations02["abc1"]) + require.Equal(t, true, migrations02["abc2"]) + require.Equal(t, true, migrations02["abc3"]) + + // test limit param + migrations01, err = drv01.SelectMigrations(db01, 1) + require.NoError(t, err) + require.Equal(t, true, migrations01["abc3"]) + require.Equal(t, false, migrations01["abc1"]) + require.Equal(t, false, migrations01["abc2"]) + + // test limit param on other node + migrations02, err = drv02.SelectMigrations(db02, 1) + require.NoError(t, err) + require.Equal(t, true, migrations02["abc3"]) + require.Equal(t, false, migrations02["abc1"]) + require.Equal(t, false, migrations02["abc2"]) +} + +func TestClickHouseInsertMigrationOnCluster(t *testing.T) { + drv01 := testClickHouseDriverCluster01(t) + drv02 := testClickHouseDriverCluster02(t) + drv01.migrationsTableName = "test_migrations" + drv02.migrationsTableName = "test_migrations" + + db01 := prepTestClickHouseDB(t, drv01) + defer dbutil.MustClose(db01) + + db02 := prepTestClickHouseDB(t, drv02) + defer dbutil.MustClose(db02) + + err := drv01.CreateMigrationsTable(db01) + require.NoError(t, err) + + count01 := 0 + err = db01.QueryRow("select count(*) from test_migrations").Scan(&count01) + require.NoError(t, err) + require.Equal(t, 0, count01) + + count02 := 0 + err = db02.QueryRow("select count(*) from test_migrations").Scan(&count02) + require.NoError(t, err) + require.Equal(t, 0, count02) + + // insert migration + tx, err := db01.Begin() + require.NoError(t, err) + err = drv01.InsertMigration(tx, "abc1") + require.NoError(t, err) + err = tx.Commit() + require.NoError(t, err) + + err = db01.QueryRow("select count(*) from test_migrations where version = 'abc1'").Scan(&count01) + require.NoError(t, err) + require.Equal(t, 1, count01) + + err = db02.QueryRow("select count(*) from test_migrations where version = 'abc1'").Scan(&count02) + require.NoError(t, err) + require.Equal(t, 1, count02) +} + +func TestClickHouseDeleteMigrationOnCluster(t *testing.T) { + drv01 := testClickHouseDriverCluster01(t) + drv02 := testClickHouseDriverCluster02(t) + drv01.migrationsTableName = "test_migrations" + drv02.migrationsTableName = "test_migrations" + + db01 := prepTestClickHouseDB(t, drv01) + defer dbutil.MustClose(db01) + + db02 := prepTestClickHouseDB(t, drv02) + defer dbutil.MustClose(db02) + + err := drv01.CreateMigrationsTable(db01) + require.NoError(t, err) + + tx, err := db01.Begin() + require.NoError(t, err) + stmt, err := tx.Prepare("insert into test_migrations (version) values (?)") + require.NoError(t, err) + _, err = stmt.Exec("abc2") + require.NoError(t, err) + _, err = stmt.Exec("abc1") + require.NoError(t, err) + err = tx.Commit() + require.NoError(t, err) + + tx, err = db01.Begin() + require.NoError(t, err) + err = drv01.DeleteMigration(tx, "abc2") + require.NoError(t, err) + err = tx.Commit() + require.NoError(t, err) + + count01 := 0 + err = db01.QueryRow("select count(*) from test_migrations final where applied").Scan(&count01) + require.NoError(t, err) + require.Equal(t, 1, count01) + + count02 := 0 + err = db02.QueryRow("select count(*) from test_migrations final where applied").Scan(&count02) + require.NoError(t, err) + require.Equal(t, 1, count02) +} diff --git a/pkg/driver/clickhouse/clickhouse_test.go b/pkg/driver/clickhouse/clickhouse_test.go index 704d817e..603794a2 100644 --- a/pkg/driver/clickhouse/clickhouse_test.go +++ b/pkg/driver/clickhouse/clickhouse_test.go @@ -3,44 +3,18 @@ package clickhouse import ( "database/sql" "net/url" - "os" "testing" "github.com/assetnote/dbmate/pkg/dbmate" + "github.com/assetnote/dbmate/pkg/dbtest" "github.com/assetnote/dbmate/pkg/dbutil" "github.com/stretchr/testify/require" ) -func testClickHouseDriver(t *testing.T) *Driver { - u := dbutil.MustParseURL(os.Getenv("CLICKHOUSE_TEST_URL")) - drv, err := dbmate.New(u).GetDriver() - require.NoError(t, err) - - return drv.(*Driver) -} - -func prepTestClickHouseDB(t *testing.T) *sql.DB { - drv := testClickHouseDriver(t) - - // drop any existing database - err := drv.DropDatabase() - require.NoError(t, err) - - // create database - err = drv.CreateDatabase() - require.NoError(t, err) - - // connect database - db, err := sql.Open("clickhouse", drv.databaseURL.String()) - require.NoError(t, err) - - return db -} - func TestGetDriver(t *testing.T) { - db := dbmate.New(dbutil.MustParseURL("clickhouse://")) - drvInterface, err := db.GetDriver() + db := dbmate.New(dbtest.MustParseURL(t, "clickhouse://")) + drvInterface, err := db.Driver() require.NoError(t, err) // driver should have URL and default migrations table set @@ -51,21 +25,35 @@ func TestGetDriver(t *testing.T) { } func TestConnectionString(t *testing.T) { - t.Run("simple", func(t *testing.T) { - u, err := url.Parse("clickhouse://user:pass@host/db") - require.NoError(t, err) - - s := connectionString(u) - require.Equal(t, "tcp://host:9000?database=db&password=pass&username=user", s) - }) - - t.Run("canonical", func(t *testing.T) { - u, err := url.Parse("clickhouse://host:9000?database=db&password=pass&username=user") - require.NoError(t, err) - - s := connectionString(u) - require.Equal(t, "tcp://host:9000?database=db&password=pass&username=user", s) - }) + cases := []struct { + input string + expected string + }{ + // defaults + {"clickhouse://myhost", "clickhouse://myhost:9000"}, + // custom port + {"clickhouse://myhost:1234/mydb", "clickhouse://myhost:1234/mydb"}, + // database parameter + {"clickhouse://myhost?database=mydb", "clickhouse://myhost:9000/mydb"}, + // username & password + {"clickhouse://abc:123@myhost/mydb", "clickhouse://abc:123@myhost:9000/mydb"}, + {"clickhouse://abc:@myhost/mydb", "clickhouse://abc@myhost:9000/mydb"}, + // username & password parameter + {"clickhouse://myhost/mydb?username=abc&password=123", "clickhouse://abc:123@myhost:9000/mydb"}, + {"clickhouse://aaa:111@myhost/mydb?username=bbb&password=222", "clickhouse://bbb:222@myhost:9000/mydb"}, + // custom parameters + {"clickhouse://myhost/mydb?dial_timeout=200ms", "clickhouse://myhost:9000/mydb?dial_timeout=200ms"}, + } + + for _, c := range cases { + t.Run(c.input, func(t *testing.T) { + u, err := url.Parse(c.input) + require.NoError(t, err) + + actual := connectionString(u) + require.Equal(t, c.expected, actual) + }) + } } func TestClickHouseCreateDropDatabase(t *testing.T) { @@ -109,7 +97,7 @@ func TestClickHouseDumpSchema(t *testing.T) { drv.migrationsTableName = "test_migrations" // prepare database - db := prepTestClickHouseDB(t) + db := prepTestClickHouseDB(t, drv) defer dbutil.MustClose(db) err := drv.CreateMigrationsTable(db) require.NoError(t, err) @@ -140,9 +128,7 @@ func TestClickHouseDumpSchema(t *testing.T) { " ('abc2');\n") // DumpSchema should return error if command fails - values := drv.databaseURL.Query() - values.Set("database", "fakedb") - drv.databaseURL.RawQuery = values.Encode() + drv.databaseURL.Path = "/fakedb" db, err = sql.Open("clickhouse", drv.databaseURL.String()) require.NoError(t, err) @@ -180,20 +166,33 @@ func TestClickHouseDatabaseExists_Error(t *testing.T) { drv.databaseURL.RawQuery = values.Encode() exists, err := drv.DatabaseExists() - require.EqualError(t, err, "code: 192, message: Unknown user invalid") + require.EqualError( + t, + err, + "code: 516, message: invalid: Authentication failed: password is incorrect or there is no user with such name", + ) require.Equal(t, false, exists) } func TestClickHouseCreateMigrationsTable(t *testing.T) { t.Run("default table", func(t *testing.T) { drv := testClickHouseDriver(t) - db := prepTestClickHouseDB(t) + db := prepTestClickHouseDB(t, drv) defer dbutil.MustClose(db) // migrations table should not exist count := 0 err := db.QueryRow("select count(*) from schema_migrations").Scan(&count) - require.EqualError(t, err, "code: 60, message: Table dbmate_test.schema_migrations doesn't exist.") + require.EqualError( + t, + err, + "code: 60, message: Table dbmate_test.schema_migrations doesn't exist", + ) + + // use driver function to check the same as above + exists, err := drv.MigrationsTableExists(db) + require.NoError(t, err) + require.Equal(t, false, exists) // create table err = drv.CreateMigrationsTable(db) @@ -203,6 +202,11 @@ func TestClickHouseCreateMigrationsTable(t *testing.T) { err = db.QueryRow("select count(*) from schema_migrations").Scan(&count) require.NoError(t, err) + // use driver function to check the same as above + exists, err = drv.MigrationsTableExists(db) + require.NoError(t, err) + require.Equal(t, true, exists) + // create table should be idempotent err = drv.CreateMigrationsTable(db) require.NoError(t, err) @@ -212,13 +216,22 @@ func TestClickHouseCreateMigrationsTable(t *testing.T) { drv := testClickHouseDriver(t) drv.migrationsTableName = "testMigrations" - db := prepTestClickHouseDB(t) + db := prepTestClickHouseDB(t, drv) defer dbutil.MustClose(db) // migrations table should not exist count := 0 err := db.QueryRow("select count(*) from \"testMigrations\"").Scan(&count) - require.EqualError(t, err, "code: 60, message: Table dbmate_test.testMigrations doesn't exist.") + require.EqualError( + t, + err, + "code: 60, message: Table dbmate_test.testMigrations doesn't exist", + ) + + // use driver function to check the same as above + exists, err := drv.MigrationsTableExists(db) + require.NoError(t, err) + require.Equal(t, false, exists) // create table err = drv.CreateMigrationsTable(db) @@ -228,6 +241,11 @@ func TestClickHouseCreateMigrationsTable(t *testing.T) { err = db.QueryRow("select count(*) from \"testMigrations\"").Scan(&count) require.NoError(t, err) + // use driver function to check the same as above + exists, err = drv.MigrationsTableExists(db) + require.NoError(t, err) + require.Equal(t, true, exists) + // create table should be idempotent err = drv.CreateMigrationsTable(db) require.NoError(t, err) @@ -238,7 +256,7 @@ func TestClickHouseSelectMigrations(t *testing.T) { drv := testClickHouseDriver(t) drv.migrationsTableName = "test_migrations" - db := prepTestClickHouseDB(t) + db := prepTestClickHouseDB(t, drv) defer dbutil.MustClose(db) err := drv.CreateMigrationsTable(db) @@ -275,7 +293,7 @@ func TestClickHouseInsertMigration(t *testing.T) { drv := testClickHouseDriver(t) drv.migrationsTableName = "test_migrations" - db := prepTestClickHouseDB(t) + db := prepTestClickHouseDB(t, drv) defer dbutil.MustClose(db) err := drv.CreateMigrationsTable(db) @@ -303,7 +321,7 @@ func TestClickHouseDeleteMigration(t *testing.T) { drv := testClickHouseDriver(t) drv.migrationsTableName = "test_migrations" - db := prepTestClickHouseDB(t) + db := prepTestClickHouseDB(t, drv) defer dbutil.MustClose(db) err := drv.CreateMigrationsTable(db) @@ -374,3 +392,26 @@ func TestClickHouseQuotedMigrationsTableName(t *testing.T) { require.Equal(t, `"bizarre""$name"`, name) }) } + +func TestEscapeString(t *testing.T) { + cases := []struct { + input string + expected string + }{ + // nothig to escape + {`lets go`, `lets go`}, + // escape ' + {`let's go`, `let\'s go`}, + // escape \ + {`let\s go`, `let\\s go`}, + } + + for _, c := range cases { + t.Run(c.input, func(t *testing.T) { + drv := testClickHouseDriver(t) + + actual := drv.escapeString(c.input) + require.Equal(t, c.expected, actual) + }) + } +} diff --git a/pkg/driver/clickhouse/clickhouse_testutils_test.go b/pkg/driver/clickhouse/clickhouse_testutils_test.go new file mode 100644 index 00000000..7a1a3b4f --- /dev/null +++ b/pkg/driver/clickhouse/clickhouse_testutils_test.go @@ -0,0 +1,52 @@ +package clickhouse + +import ( + "database/sql" + "net/url" + "testing" + + "github.com/assetnote/dbmate/pkg/dbmate" + "github.com/assetnote/dbmate/pkg/dbtest" + + "github.com/stretchr/testify/require" +) + +func testClickHouseDriverURL(t *testing.T, u *url.URL) *Driver { + drv, err := dbmate.New(u).Driver() + require.NoError(t, err) + + return drv.(*Driver) +} + +func testClickHouseDriver(t *testing.T) *Driver { + u := dbtest.GetenvURLOrSkip(t, "CLICKHOUSE_TEST_URL") + return testClickHouseDriverURL(t, u) +} + +func testClickHouseDriverCluster01(t *testing.T) *Driver { + u := dbtest.GetenvURLOrSkip(t, "CLICKHOUSE_CLUSTER_01_TEST_URL") + u.RawQuery = "on_cluster" + return testClickHouseDriverURL(t, u) +} + +func testClickHouseDriverCluster02(t *testing.T) *Driver { + u := dbtest.GetenvURLOrSkip(t, "CLICKHOUSE_CLUSTER_02_TEST_URL") + u.RawQuery = "on_cluster" + return testClickHouseDriverURL(t, u) +} + +func prepTestClickHouseDB(t *testing.T, drv *Driver) *sql.DB { + // drop any existing database + err := drv.DropDatabase() + require.NoError(t, err) + + // create database + err = drv.CreateDatabase() + require.NoError(t, err) + + // connect database + db, err := drv.Open() + require.NoError(t, err) + + return db +} diff --git a/pkg/driver/clickhouse/cluster_parameters.go b/pkg/driver/clickhouse/cluster_parameters.go new file mode 100644 index 00000000..3bd3212d --- /dev/null +++ b/pkg/driver/clickhouse/cluster_parameters.go @@ -0,0 +1,83 @@ +package clickhouse + +import ( + "fmt" + "net/url" +) + +const ( + OnClusterQueryParam = "on_cluster" + ZooPathQueryParam = "zoo_path" + ClusterMacroQueryParam = "cluster_macro" + ReplicaMacroQueryParam = "replica_macro" +) + +type ClusterParameters struct { + OnCluster bool + ZooPath string + ClusterMacro string + ReplicaMacro string +} + +func ClearClusterParametersFromURL(u *url.URL) *url.URL { + q := u.Query() + q.Del(OnClusterQueryParam) + q.Del(ClusterMacroQueryParam) + q.Del(ReplicaMacroQueryParam) + q.Del(ZooPathQueryParam) + u.RawQuery = q.Encode() + + return u +} + +func ExtractClusterParametersFromURL(u *url.URL) *ClusterParameters { + onCluster := extractOnCluster(u) + clusterMacro := extractClusterMacro(u) + replicaMacro := extractReplicaMacro(u) + zookeeperPath := extractZookeeperPath(u) + + r := &ClusterParameters{ + OnCluster: onCluster, + ZooPath: zookeeperPath, + ClusterMacro: clusterMacro, + ReplicaMacro: replicaMacro, + } + + return r +} + +func extractOnCluster(u *url.URL) bool { + v := u.Query() + hasOnCluster := v.Has(OnClusterQueryParam) + onClusterValue := v.Get(OnClusterQueryParam) + onCluster := hasOnCluster && (onClusterValue == "" || onClusterValue == "true") + return onCluster +} + +func extractClusterMacro(u *url.URL) string { + v := u.Query() + clusterMacro := v.Get(ClusterMacroQueryParam) + if clusterMacro == "" { + clusterMacro = "{cluster}" + } + return clusterMacro +} + +func extractReplicaMacro(u *url.URL) string { + v := u.Query() + replicaMacro := v.Get(ReplicaMacroQueryParam) + if replicaMacro == "" { + replicaMacro = "{replica}" + } + return replicaMacro +} + +func extractZookeeperPath(u *url.URL) string { + v := u.Query() + clusterMacro := extractClusterMacro(u) + zookeeperPath := v.Get(ZooPathQueryParam) + if zookeeperPath == "" { + zookeeperPath = fmt.Sprintf("/clickhouse/tables/%s/{table}", clusterMacro) + } + return zookeeperPath +} diff --git a/pkg/driver/clickhouse/cluster_parameters_test.go b/pkg/driver/clickhouse/cluster_parameters_test.go new file mode 100644 index 00000000..860c7f8b --- /dev/null +++ b/pkg/driver/clickhouse/cluster_parameters_test.go @@ -0,0 +1,97 @@ +package clickhouse + +import ( + "testing" + + "github.com/assetnote/dbmate/pkg/dbtest" + + "github.com/stretchr/testify/require" +) + +func TestOnCluster(t *testing.T) { + cases := []struct { + input string + expected bool + }{ + // param not supplied + {"clickhouse://myhost:9000", false}, + // empty on_cluster parameter + {"clickhouse://myhost:9000?on_cluster", true}, + // true on_cluster parameter + {"clickhouse://myhost:9000?on_cluster=true", true}, + // any other value on_cluster parameter + {"clickhouse://myhost:9000?on_cluster=falsy", false}, + } + + for _, c := range cases { + t.Run(c.input, func(t *testing.T) { + u := dbtest.MustParseURL(t, c.input) + + actual := extractOnCluster(u) + require.Equal(t, c.expected, actual) + }) + } +} + +func TestClusterMacro(t *testing.T) { + cases := []struct { + input string + expected string + }{ + // cluster_macro not supplied + {"clickhouse://myhost:9000", "{cluster}"}, + // cluster_macro supplied + {"clickhouse://myhost:9000?cluster_macro={cluster2}", "{cluster2}"}, + } + + for _, c := range cases { + t.Run(c.input, func(t *testing.T) { + u := dbtest.MustParseURL(t, c.input) + + actual := extractClusterMacro(u) + require.Equal(t, c.expected, actual) + }) + } +} + +func TestReplicaMacro(t *testing.T) { + cases := []struct { + input string + expected string + }{ + // replica_macro not supplied + {"clickhouse://myhost:9000", "{replica}"}, + // replica_macro supplied + {"clickhouse://myhost:9000?replica_macro={replica2}", "{replica2}"}, + } + + for _, c := range cases { + t.Run(c.input, func(t *testing.T) { + u := dbtest.MustParseURL(t, c.input) + + actual := extractReplicaMacro(u) + require.Equal(t, c.expected, actual) + }) + } +} + +func TestZookeeperPath(t *testing.T) { + cases := []struct { + input string + expected string + }{ + // zoo_path not supplied + {"clickhouse://myhost:9000", "/clickhouse/tables/{cluster}/{table}"}, + // zoo_path supplied + {"clickhouse://myhost:9000?zoo_path=/zk/path/tables", "/zk/path/tables"}, + } + + for _, c := range cases { + t.Run(c.input, func(t *testing.T) { + u := dbtest.MustParseURL(t, c.input) + + actual := extractZookeeperPath(u) + require.Equal(t, c.expected, actual) + }) + } +} diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/config.d/database_atomic.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/config.d/database_atomic.xml new file mode 100644 index 00000000..f0a41568 --- /dev/null +++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/config.d/database_atomic.xml @@ -0,0 +1,3 @@ + + 0 + diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/config.d/docker_related_config.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/config.d/docker_related_config.xml new file mode 100644 index 00000000..3025dc26 --- /dev/null +++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/config.d/docker_related_config.xml @@ -0,0 +1,12 @@ + + + :: + 0.0.0.0 + 1 + + + diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/config.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/config.xml new file mode 100644 index 00000000..23a7c8b4 --- /dev/null +++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/config.xml @@ -0,0 +1,127 @@ + + + + + trace + /var/log/clickhouse-server/clickhouse-server.log + /var/log/clickhouse-server/clickhouse-server.err.log + 100M + 2 + + + 8123 + 9000 + 9009 + + 4096 + 3 + 0 + 100 + 0 + 10000 + 0.9 + 4194304 + 0 + 8589934592 + 5368709120 + 1000 + 134217728 + 10000 + + /var/lib/clickhouse/ + /var/lib/clickhouse/tmp/ + /var/lib/clickhouse/user_files/ + + + + users.xml + + + /var/lib/clickhouse/access/ + + + + + false + false + false + false + + + default + default + true + + + + + + + + + + + true + + ch-cluster-01 + 9000 + + + + + + true + + ch-cluster-02 + 9000 + + + + + + + + zookeeper + 2181 + + + + + cluster-01 + shard-01 + ch_cluster-01 + + + + 3600 + 3600 + 60 + + + system + query_log
+ toYYYYMM(event_date) + + 7500 +
+ + *_dictionary.xml + + + + + /clickhouse/task_queue/ddl + + + + /var/lib/clickhouse/format_schemas/ + +
diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/users.d/database_atomic_drop_sync.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/users.d/database_atomic_drop_sync.xml new file mode 100644 index 00000000..386fce01 --- /dev/null +++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/users.d/database_atomic_drop_sync.xml @@ -0,0 +1,7 @@ + + + + 1 + + + diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/users.d/default_profile.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/users.d/default_profile.xml new file mode 100644 index 00000000..34cce837 --- /dev/null +++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/users.d/default_profile.xml @@ -0,0 +1,10 @@ + + + + + 2 + + + diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/users.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/users.xml new file mode 100644 index 00000000..96067d01 --- /dev/null +++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-01/users.xml @@ -0,0 +1,120 @@ + + + + + + + + + + random + + + + + 1 + + + + + + + + + + + + + ::/0 + + + + default + + + default + + + + + + + + + + + + + + 3600 + + + 0 + 0 + 0 + 0 + 0 + + + + diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/config.d/database_atomic.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/config.d/database_atomic.xml new file mode 100644 index 00000000..f0a41568 --- /dev/null +++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/config.d/database_atomic.xml @@ -0,0 +1,3 @@ + + 0 + diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/config.d/docker_related_config.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/config.d/docker_related_config.xml new file mode 100644 index 00000000..3025dc26 --- /dev/null +++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/config.d/docker_related_config.xml @@ -0,0 +1,12 @@ + + + :: + 0.0.0.0 + 1 + + + diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/config.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/config.xml new file mode 100644 index 00000000..b9a1e2cb --- /dev/null +++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/config.xml @@ -0,0 +1,127 @@ + + + + + trace + /var/log/clickhouse-server/clickhouse-server.log + /var/log/clickhouse-server/clickhouse-server.err.log + 100M + 2 + + + 8123 + 9000 + 9009 + + 4096 + 3 + 0 + 100 + 0 + 10000 + 0.9 + 4194304 + 0 + 8589934592 + 5368709120 + 1000 + 134217728 + 10000 + + /var/lib/clickhouse/ + /var/lib/clickhouse/tmp/ + /var/lib/clickhouse/user_files/ + + + + users.xml + + + /var/lib/clickhouse/access/ + + + + + false + false + false + false + + + default + default + true + + + + + + + + + + + true + + ch-cluster-01 + 9000 + + + + + + true + + ch-cluster-02 + 9000 + + + + + + + + zookeeper + 2181 + + + + + cluster-01 + shard-02 + ch_cluster-02 + + + + 3600 + 3600 + 60 + + + system + query_log
+ toYYYYMM(event_date) + + 7500 +
+ + *_dictionary.xml + + + + + /clickhouse/task_queue/ddl + + + + /var/lib/clickhouse/format_schemas/ + +
diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/users.d/database_atomic_drop_sync.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/users.d/database_atomic_drop_sync.xml new file mode 100644 index 00000000..386fce01 --- /dev/null +++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/users.d/database_atomic_drop_sync.xml @@ -0,0 +1,7 @@ + + + + 1 + + + diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/users.d/default_profile.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/users.d/default_profile.xml new file mode 100644 index 00000000..34cce837 --- /dev/null +++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/users.d/default_profile.xml @@ -0,0 +1,10 @@ + + + + + 2 + + + diff --git a/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/users.xml b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/users.xml new file mode 100644 index 00000000..96067d01 --- /dev/null +++ b/pkg/driver/clickhouse/testdata/cluster_config/ch-cluster-02/users.xml @@ -0,0 +1,120 @@ + + + + + + + + + + random + + + + + 1 + + + + + + + + + + + + + ::/0 + + + + default + + + default + + + + + + + + + + + + + + 3600 + + + 0 + 0 + 0 + 0 + 0 + + + + diff --git a/pkg/driver/mysql/mysql.go b/pkg/driver/mysql/mysql.go index 2a83be5f..926867bb 100644 --- a/pkg/driver/mysql/mysql.go +++ b/pkg/driver/mysql/mysql.go @@ -4,7 +4,9 @@ import ( "bytes" "database/sql" "fmt" + "io" "net/url" + "regexp" "strings" "github.com/assetnote/dbmate/pkg/dbmate" @@ -21,6 +23,7 @@ func init() { type Driver struct { migrationsTableName string databaseURL *url.URL + log io.Writer } // NewDriver initializes the driver @@ -28,6 +31,7 @@ func NewDriver(config dbmate.DriverConfig) dbmate.Driver { return &Driver{ migrationsTableName: config.MigrationsTableName, databaseURL: config.DatabaseURL, + log: config.Log, } } @@ -49,7 +53,7 @@ func connectionString(u *url.URL) string { // Get decoded user:pass userPassEncoded := u.User.String() - userPass, _ := url.QueryUnescape(userPassEncoded) + userPass, _ := url.PathUnescape(userPassEncoded) // Build DSN w/ user:pass percent-decoded normalizedString := "" @@ -92,7 +96,7 @@ func (drv *Driver) quoteIdentifier(str string) string { // CreateDatabase creates the specified database func (drv *Driver) CreateDatabase() error { name := dbutil.DatabaseName(drv.databaseURL) - fmt.Printf("Creating: %s\n", name) + fmt.Fprintf(drv.log, "Creating: %s\n", name) db, err := drv.openRootDB() if err != nil { @@ -109,7 +113,7 @@ func (drv *Driver) CreateDatabase() error { // DropDatabase drops the specified database (if it exists) func (drv *Driver) DropDatabase() error { name := dbutil.DatabaseName(drv.databaseURL) - fmt.Printf("Dropping: %s\n", name) + fmt.Fprintf(drv.log, "Dropping: %s\n", name) db, err := drv.openRootDB() if err != nil { @@ -128,17 +132,21 @@ func (drv *Driver) mysqldumpArgs() []string { args := []string{"--opt", "--routines", "--no-data", "--skip-dump-date", "--skip-add-drop-table"} - if hostname := drv.databaseURL.Hostname(); hostname != "" { - args = append(args, "--host="+hostname) - } - if port := drv.databaseURL.Port(); port != "" { - args = append(args, "--port="+port) + socket := drv.databaseURL.Query().Get("socket") + if socket != "" { + args = append(args, "--socket="+socket) + } else { + if hostname := drv.databaseURL.Hostname(); hostname != "" { + args = append(args, "--host="+hostname) + } + if port := drv.databaseURL.Port(); port != "" { + args = append(args, "--port="+port) + } } + if username := drv.databaseURL.User.Username(); username != "" { args = append(args, "--user="+username) } - // mysql recommends against using environment variables to supply password - // https://dev.mysql.com/doc/refman/5.7/en/password-security-user.html if password, set := drv.databaseURL.User.Password(); set { args = append(args, "--password="+password) } @@ -189,7 +197,17 @@ func (drv *Driver) DumpSchema(db *sql.DB) ([]byte, error) { } schema = append(schema, migrations...) - return dbutil.TrimLeadingSQLComments(schema) + schema, err = dbutil.TrimLeadingSQLComments(schema) + if err != nil { + return nil, err + } + return trimAutoincrementValues(schema), nil +} + +// trimAutoincrementValues removes AUTO_INCREMENT values from MySQL schema dumps +func trimAutoincrementValues(data []byte) []byte { + aiPattern := regexp.MustCompile(" AUTO_INCREMENT=[0-9]*") + return aiPattern.ReplaceAll(data, []byte("")) } // DatabaseExists determines whether the database exists @@ -212,10 +230,23 @@ func (drv *Driver) DatabaseExists() (bool, error) { return exists, err } +// MigrationsTableExists checks if the schema_migrations table exists +func (drv *Driver) MigrationsTableExists(db *sql.DB) (bool, error) { + match := "" + err := db.QueryRow(fmt.Sprintf("show tables like '%s'", + drv.migrationsTableName)). + Scan(&match) + if err == sql.ErrNoRows { + return false, nil + } + + return match != "", err +} + // CreateMigrationsTable creates the schema_migrations table func (drv *Driver) CreateMigrationsTable(db *sql.DB) error { - _, err := db.Exec(fmt.Sprintf("create table if not exists %s "+ - "(version varchar(255) primary key) character set latin1 collate latin1_bin", + _, err := db.Exec(fmt.Sprintf( + "create table if not exists %s (version varchar(128) primary key)", drv.quotedMigrationsTableName())) return err @@ -282,6 +313,11 @@ func (drv *Driver) Ping() error { return db.Ping() } +// Return a normalized version of the driver-specific error type. +func (drv *Driver) QueryError(query string, err error) error { + return &dbmate.QueryError{Err: err, Query: query} +} + func (drv *Driver) quotedMigrationsTableName() string { return drv.quoteIdentifier(drv.migrationsTableName) } diff --git a/pkg/driver/mysql/mysql_test.go b/pkg/driver/mysql/mysql_test.go index db086758..e62f086b 100644 --- a/pkg/driver/mysql/mysql_test.go +++ b/pkg/driver/mysql/mysql_test.go @@ -3,18 +3,18 @@ package mysql import ( "database/sql" "net/url" - "os" "testing" "github.com/assetnote/dbmate/pkg/dbmate" + "github.com/assetnote/dbmate/pkg/dbtest" "github.com/assetnote/dbmate/pkg/dbutil" "github.com/stretchr/testify/require" ) func testMySQLDriver(t *testing.T) *Driver { - u := dbutil.MustParseURL(os.Getenv("MYSQL_TEST_URL")) - drv, err := dbmate.New(u).GetDriver() + u := dbtest.GetenvURLOrSkip(t, "MYSQL_TEST_URL") + drv, err := dbmate.New(u).Driver() require.NoError(t, err) return drv.(*Driver) @@ -39,8 +39,8 @@ func prepTestMySQLDB(t *testing.T) *sql.DB { } func TestGetDriver(t *testing.T) { - db := dbmate.New(dbutil.MustParseURL("mysql://")) - drvInterface, err := db.GetDriver() + db := dbmate.New(dbtest.MustParseURL(t, "mysql://")) + drvInterface, err := db.Driver() require.NoError(t, err) // driver should have URL and default migrations table set @@ -78,6 +78,18 @@ func TestConnectionString(t *testing.T) { require.Equal(t, "duhfsd7s:123!@123!@@tcp(host:123)/foo?flag=on&multiStatements=true", s) }) + t.Run("url encoding", func(t *testing.T) { + u, err := url.Parse("mysql://bob%2Balice:secret%5E%5B%2A%28%29@host:123/foo") + require.NoError(t, err) + require.Equal(t, "bob+alice:secret%5E%5B%2A%28%29", u.User.String()) + require.Equal(t, "123", u.Port()) + + s := connectionString(u) + // ensure that '+' is correctly encoded by url.PathUnescape as '+' + // (not whitespace as url.QueryUnescape generates) + require.Equal(t, "bob+alice:secret^[*()@tcp(host:123)/foo?multiStatements=true", s) + }) + t.Run("socket", func(t *testing.T) { // test with no user/pass u, err := url.Parse("mysql:///foo?socket=/var/run/mysqld/mysqld.sock&flag=on") @@ -133,6 +145,42 @@ func TestMySQLCreateDropDatabase(t *testing.T) { }() } +func TestMySQLDumpArgs(t *testing.T) { + drv := testMySQLDriver(t) + drv.databaseURL = dbtest.MustParseURL(t, "mysql://bob/mydb") + + require.Equal(t, []string{"--opt", + "--routines", + "--no-data", + "--skip-dump-date", + "--skip-add-drop-table", + "--host=bob", + "mydb"}, drv.mysqldumpArgs()) + + drv.databaseURL = dbtest.MustParseURL(t, "mysql://alice:pw@bob:5678/mydb") + require.Equal(t, []string{"--opt", + "--routines", + "--no-data", + "--skip-dump-date", + "--skip-add-drop-table", + "--host=bob", + "--port=5678", + "--user=alice", + "--password=pw", + "mydb"}, drv.mysqldumpArgs()) + + drv.databaseURL = dbtest.MustParseURL(t, "mysql://alice:pw@bob:5678/mydb?socket=/var/run/mysqld/mysqld.sock") + require.Equal(t, []string{"--opt", + "--routines", + "--no-data", + "--skip-dump-date", + "--skip-add-drop-table", + "--socket=/var/run/mysqld/mysqld.sock", + "--user=alice", + "--password=pw", + "mydb"}, drv.mysqldumpArgs()) +} + func TestMySQLDumpSchema(t *testing.T) { drv := testMySQLDriver(t) drv.migrationsTableName = "test_migrations" @@ -167,10 +215,36 @@ func TestMySQLDumpSchema(t *testing.T) { drv.databaseURL.Path = "/fakedb" schema, err = drv.DumpSchema(db) require.Nil(t, schema) - require.EqualError(t, err, "mysqldump: [Warning] Using a password "+ - "on the command line interface can be insecure.\n"+ - "mysqldump: Got error: 1049: "+ - "Unknown database 'fakedb' when selecting the database") + require.Error(t, err) + require.Contains(t, err.Error(), "Unknown database 'fakedb'") +} + +func TestMySQLDumpSchemaContainsNoAutoIncrement(t *testing.T) { + drv := testMySQLDriver(t) + + db := prepTestMySQLDB(t) + defer dbutil.MustClose(db) + err := drv.CreateMigrationsTable(db) + require.NoError(t, err) + + // create table with AUTO_INCREMENT column + _, err = db.Exec(`create table foo_table (id int not null primary key auto_increment)`) + require.NoError(t, err) + + // create a record + _, err = db.Exec(`insert into foo_table values ()`) + require.NoError(t, err) + + // AUTO_INCREMENT should appear on the table definition + var tblName, tblCreate string + err = db.QueryRow(`show create table foo_table`).Scan(&tblName, &tblCreate) + require.NoError(t, err) + require.Contains(t, tblCreate, "AUTO_INCREMENT=") + + // AUTO_INCREMENT should not appear in the dump + schema, err := drv.DumpSchema(db) + require.NoError(t, err) + require.NotContains(t, string(schema), "AUTO_INCREMENT=") } func TestMySQLDatabaseExists(t *testing.T) { diff --git a/pkg/driver/postgres/postgres.go b/pkg/driver/postgres/postgres.go index bf542e32..bcefddf9 100644 --- a/pkg/driver/postgres/postgres.go +++ b/pkg/driver/postgres/postgres.go @@ -4,7 +4,10 @@ import ( "bytes" "database/sql" "fmt" + "io" "net/url" + "runtime" + "strconv" "strings" "github.com/assetnote/dbmate/pkg/dbmate" @@ -16,12 +19,15 @@ import ( func init() { dbmate.RegisterDriver(NewDriver, "postgres") dbmate.RegisterDriver(NewDriver, "postgresql") + dbmate.RegisterDriver(NewDriver, "redshift") + dbmate.RegisterDriver(NewDriver, "spanner-postgres") } // Driver provides top level database functions type Driver struct { migrationsTableName string databaseURL *url.URL + log io.Writer } // NewDriver initializes the driver @@ -29,6 +35,7 @@ func NewDriver(config dbmate.DriverConfig) dbmate.Driver { return &Driver{ migrationsTableName: config.MigrationsTableName, databaseURL: config.DatabaseURL, + log: config.Log, } } @@ -44,8 +51,15 @@ func connectionString(u *url.URL) string { } // default hostname - if hostname == "" { - hostname = "localhost" + if hostname == "" && query.Get("host") == "" { + switch runtime.GOOS { + case "linux": + query.Set("host", "/var/run/postgresql") + case "darwin", "freebsd", "dragonfly", "openbsd", "netbsd": + query.Set("host", "/tmp") + default: + hostname = "localhost" + } } // host param overrides url hostname @@ -59,24 +73,35 @@ func connectionString(u *url.URL) string { query.Del("port") } if port == "" { - port = "5432" + switch u.Scheme { + case "redshift": + port = "5439" + default: + port = "5432" + } } // generate output URL out, _ := url.Parse(u.String()) + // force scheme back to postgres if there was another postgres-compatible scheme + out.Scheme = "postgres" out.Host = fmt.Sprintf("%s:%s", hostname, port) out.RawQuery = query.Encode() return out.String() } -func connectionArgsForDump(u *url.URL) []string { - u = dbutil.MustParseURL(connectionString(u)) +func connectionArgsForDump(conn *url.URL) []string { + u, err := url.Parse(connectionString(conn)) + if err != nil { + panic(err) + } // find schemas from search_path query := u.Query() schemas := strings.Split(query.Get("search_path"), ",") query.Del("search_path") + query.Del("binary_parameters") u.RawQuery = query.Encode() out := []string{} @@ -103,8 +128,10 @@ func (drv *Driver) openPostgresDB() (*sql.DB, error) { return nil, err } - // connect to postgres database - postgresURL.Path = "postgres" + // connect to postgres database, unless this is a Redshift connection + if drv.databaseURL.Scheme != "redshift" { + postgresURL.Path = "postgres" + } return sql.Open("postgres", postgresURL.String()) } @@ -112,7 +139,7 @@ func (drv *Driver) openPostgresDB() (*sql.DB, error) { // CreateDatabase creates the specified database func (drv *Driver) CreateDatabase() error { name := dbutil.DatabaseName(drv.databaseURL) - fmt.Printf("Creating: %s\n", name) + fmt.Fprintf(drv.log, "Creating: %s\n", name) db, err := drv.openPostgresDB() if err != nil { @@ -129,7 +156,7 @@ func (drv *Driver) CreateDatabase() error { // DropDatabase drops the specified database (if it exists) func (drv *Driver) DropDatabase() error { name := dbutil.DatabaseName(drv.databaseURL) - fmt.Printf("Dropping: %s\n", name) + fmt.Fprintf(drv.log, "Dropping: %s\n", name) db, err := drv.openPostgresDB() if err != nil { @@ -208,6 +235,27 @@ func (drv *Driver) DatabaseExists() (bool, error) { return exists, err } +// MigrationsTableExists checks if the schema_migrations table exists +func (drv *Driver) MigrationsTableExists(db *sql.DB) (bool, error) { + schema, migrationsTableNameParts, err := drv.migrationsTableNameParts(db) + if err != nil { + return false, err + } + + migrationsTable := strings.Join(migrationsTableNameParts, ".") + exists := false + err = db.QueryRow("SELECT 1 FROM information_schema.tables "+ + "WHERE table_schema = $1 "+ + "AND table_name = $2", + schema, migrationsTable). + Scan(&exists) + if err == sql.ErrNoRows { + return false, nil + } + + return exists, err +} + // CreateMigrationsTable creates the schema_migrations table func (drv *Driver) CreateMigrationsTable(db *sql.DB) error { schema, migrationsTable, err := drv.quotedMigrationsTableNameParts(db) @@ -216,8 +264,9 @@ func (drv *Driver) CreateMigrationsTable(db *sql.DB) error { } // first attempt at creating migrations table - createTableStmt := fmt.Sprintf("create table if not exists %s.%s", schema, migrationsTable) + - " (version varchar(255) primary key)" + createTableStmt := fmt.Sprintf( + "create table if not exists %s.%s (version varchar(128) primary key)", + schema, migrationsTable) _, err = db.Exec(createTableStmt) if err == nil { // table exists or created successfully @@ -233,7 +282,7 @@ func (drv *Driver) CreateMigrationsTable(db *sql.DB) error { // in theory we could attempt to create the schema every time, but we avoid that // in case the user doesn't have permissions to create schemas - fmt.Printf("Creating schema: %s\n", schema) + fmt.Fprintf(drv.log, "Creating schema: %s\n", schema) _, err = db.Exec(fmt.Sprintf("create schema if not exists %s", schema)) if err != nil { return err @@ -330,6 +379,19 @@ func (drv *Driver) Ping() error { return err } +// Return a normalized version of the driver-specific error type. +func (drv *Driver) QueryError(query string, err error) error { + position := 0 + + if pqErr, ok := err.(*pq.Error); ok { + if pos, err := strconv.Atoi(pqErr.Position); err == nil { + position = pos + } + } + + return &dbmate.QueryError{Err: err, Query: query, Position: position} +} + func (drv *Driver) quotedMigrationsTableName(db dbutil.Transaction) (string, error) { schema, name, err := drv.quotedMigrationsTableNameParts(db) if err != nil { @@ -339,7 +401,7 @@ func (drv *Driver) quotedMigrationsTableName(db dbutil.Transaction) (string, err return schema + "." + name, nil } -func (drv *Driver) quotedMigrationsTableNameParts(db dbutil.Transaction) (string, string, error) { +func (drv *Driver) migrationsTableNameParts(db dbutil.Transaction) (string, []string, error) { schema := "" tableNameParts := strings.Split(drv.migrationsTableName, ".") if len(tableNameParts) > 1 { @@ -359,7 +421,7 @@ func (drv *Driver) quotedMigrationsTableNameParts(db dbutil.Transaction) (string // this is a hack because we don't always have the URL context available schema, err = dbutil.QueryValue(db, "select current_schema()") if err != nil { - return "", "", err + return "", nil, err } } @@ -368,6 +430,21 @@ func (drv *Driver) quotedMigrationsTableNameParts(db dbutil.Transaction) (string schema = "public" } + return schema, tableNameParts, nil +} + +func (drv *Driver) quotedMigrationsTableNameParts(db dbutil.Transaction) (string, string, error) { + schema, tableNameParts, err := drv.migrationsTableNameParts(db) + + if err != nil { + return "", "", err + } + + // Quote identifiers for Redshift and Spanner + if drv.databaseURL.Scheme == "redshift" || drv.databaseURL.Scheme == "spanner-postgres" { + return pq.QuoteIdentifier(schema), pq.QuoteIdentifier(strings.Join(tableNameParts, ".")), nil + } + // quote all parts // use server rather than client to do this to avoid unnecessary quotes // (which would change schema.sql diff) diff --git a/pkg/driver/postgres/postgres_test.go b/pkg/driver/postgres/postgres_test.go index 6a6b4763..3ca7c499 100644 --- a/pkg/driver/postgres/postgres_test.go +++ b/pkg/driver/postgres/postgres_test.go @@ -2,19 +2,38 @@ package postgres import ( "database/sql" + "fmt" "net/url" - "os" + "runtime" "testing" "github.com/assetnote/dbmate/pkg/dbmate" + "github.com/assetnote/dbmate/pkg/dbtest" "github.com/assetnote/dbmate/pkg/dbutil" "github.com/stretchr/testify/require" ) func testPostgresDriver(t *testing.T) *Driver { - u := dbutil.MustParseURL(os.Getenv("POSTGRES_TEST_URL")) - drv, err := dbmate.New(u).GetDriver() + u := dbtest.GetenvURLOrSkip(t, "POSTGRES_TEST_URL") + drv, err := dbmate.New(u).Driver() + require.NoError(t, err) + + return drv.(*Driver) +} + +func testRedshiftDriver(t *testing.T) *Driver { + u := dbtest.GetenvURLOrSkip(t, "REDSHIFT_TEST_URL") + drv, err := dbmate.New(u).Driver() + require.NoError(t, err) + + return drv.(*Driver) +} + +func testSpannerPostgresDriver(t *testing.T) *Driver { + // URL to the spanner pgadapter, or a locally-running spanner emulator with the pgadapter + u := dbtest.GetenvURLOrSkip(t, "SPANNER_POSTGRES_TEST_URL") + drv, err := dbmate.New(u).Driver() require.NoError(t, err) return drv.(*Driver) @@ -32,15 +51,46 @@ func prepTestPostgresDB(t *testing.T) *sql.DB { require.NoError(t, err) // connect database - db, err := sql.Open("postgres", drv.databaseURL.String()) + db, err := sql.Open("postgres", connectionString(drv.databaseURL)) + require.NoError(t, err) + + return db +} + +func prepRedshiftTestDB(t *testing.T, drv *Driver) *sql.DB { + // connect database + db, err := sql.Open("postgres", connectionString(drv.databaseURL)) + require.NoError(t, err) + + _, migrationsTable, err := drv.quotedMigrationsTableNameParts(db) + if err != nil { + t.Error(err) + } + + _, err = db.Exec(fmt.Sprintf("drop table if exists %s", migrationsTable)) + require.NoError(t, err) + + return db +} + +func prepTestSpannerPostgresDB(t *testing.T, drv *Driver) *sql.DB { + // Spanner doesn't allow running `drop database`, so we just drop the migrations + // table instead + db, err := sql.Open("postgres", connectionString(drv.databaseURL)) + require.NoError(t, err) + + _, migrationsTable, err := drv.quotedMigrationsTableNameParts(db) + require.NoError(t, err) + + _, err = db.Exec(fmt.Sprintf("drop table if exists %s", migrationsTable)) require.NoError(t, err) return db } func TestGetDriver(t *testing.T) { - db := dbmate.New(dbutil.MustParseURL("postgres://")) - drvInterface, err := db.GetDriver() + db := dbmate.New(dbtest.MustParseURL(t, "postgres://")) + drvInterface, err := db.Driver() require.NoError(t, err) // driver should have URL and default migrations table set @@ -50,13 +100,24 @@ func TestGetDriver(t *testing.T) { require.Equal(t, "schema_migrations", drv.migrationsTableName) } +func defaultConnString() string { + switch runtime.GOOS { + case "linux": + return "postgres://:5432/foo?host=%2Fvar%2Frun%2Fpostgresql" + case "darwin", "freebsd", "dragonfly", "openbsd", "netbsd": + return "postgres://:5432/foo?host=%2Ftmp" + default: + return "postgres://localhost:5432/foo" + } +} + func TestConnectionString(t *testing.T) { cases := []struct { input string expected string }{ // defaults - {"postgres:///foo", "postgres://localhost:5432/foo"}, + {"postgres:///foo", defaultConnString()}, // support custom url params {"postgres://bob:secret@myhost:1234/foo?bar=baz", "postgres://bob:secret@myhost:1234/foo?bar=baz"}, // support `host` and `port` via url params @@ -66,6 +127,11 @@ func TestConnectionString(t *testing.T) { {"postgres://bob:secret@myhost:1234/foo?host=/var/run/postgresql", "postgres://bob:secret@:1234/foo?host=%2Fvar%2Frun%2Fpostgresql"}, {"postgres://bob:secret@localhost/foo?socket=/var/run/postgresql", "postgres://bob:secret@:5432/foo?host=%2Fvar%2Frun%2Fpostgresql"}, {"postgres:///foo?socket=/var/run/postgresql", "postgres://:5432/foo?host=%2Fvar%2Frun%2Fpostgresql"}, + {"postgres://bob:secret@/foo?socket=/var/run/postgresql", "postgres://bob:secret@:5432/foo?host=%2Fvar%2Frun%2Fpostgresql"}, + {"postgres://bob:secret@/foo?host=/var/run/postgresql", "postgres://bob:secret@:5432/foo?host=%2Fvar%2Frun%2Fpostgresql"}, + // redshift default port is 5439, not 5432 + {"redshift://myhost/foo", "postgres://myhost:5439/foo"}, + {"spanner-postgres://myhost/foo", "postgres://myhost:5432/foo"}, } for _, c := range cases { @@ -85,11 +151,11 @@ func TestConnectionArgsForDump(t *testing.T) { expected []string }{ // defaults - {"postgres:///foo", []string{"postgres://localhost:5432/foo"}}, + {"postgres:///foo", []string{defaultConnString()}}, // support single schema - {"postgres:///foo?search_path=foo", []string{"--schema", "foo", "postgres://localhost:5432/foo"}}, + {"postgres:///foo?search_path=foo", []string{"--schema", "foo", defaultConnString()}}, // support multiple schemas - {"postgres:///foo?search_path=foo,public", []string{"--schema", "foo", "--schema", "public", "postgres://localhost:5432/foo"}}, + {"postgres:///foo?search_path=foo,public", []string{"--schema", "foo", "--schema", "public", defaultConnString()}}, } for _, c := range cases { @@ -174,8 +240,8 @@ func TestPostgresDumpSchema(t *testing.T) { drv.databaseURL.Path = "/fakedb" schema, err = drv.DumpSchema(db) require.Nil(t, schema) - require.EqualError(t, err, "pg_dump: [archiver (db)] connection to database "+ - "\"fakedb\" failed: FATAL: database \"fakedb\" does not exist") + require.Error(t, err) + require.Contains(t, err.Error(), "database \"fakedb\" does not exist") }) t.Run("custom migrations table with schema", func(t *testing.T) { @@ -353,6 +419,54 @@ func TestPostgresCreateMigrationsTable(t *testing.T) { }) } +func TestRedshiftCreateMigrationsTable(t *testing.T) { + t.Run("default schema", func(t *testing.T) { + drv := testRedshiftDriver(t) + db := prepRedshiftTestDB(t, drv) + defer dbutil.MustClose(db) + + // migrations table should not exist + count := 0 + err := db.QueryRow("select count(*) from public.schema_migrations").Scan(&count) + require.Error(t, err, "migrations table exists when it shouldn't") + require.Equal(t, "pq: relation \"public.schema_migrations\" does not exist", err.Error()) + + // create table + err = drv.CreateMigrationsTable(db) + require.NoError(t, err) + + // migrations table should exist + err = db.QueryRow("select count(*) from public.schema_migrations").Scan(&count) + require.NoError(t, err) + + // create table should be idempotent + err = drv.CreateMigrationsTable(db) + require.NoError(t, err) + }) +} + +func TestSpannerPostgresCreateMigrationsTable(t *testing.T) { + t.Run("default schema", func(t *testing.T) { + drv := testSpannerPostgresDriver(t) + db := prepTestSpannerPostgresDB(t, drv) + defer dbutil.MustClose(db) + + // migrations table should not exist + count := 0 + err := db.QueryRow("select count(*) from public.schema_migrations").Scan(&count) + require.Error(t, err, "migrations table exists when it shouldn't") + require.Contains(t, err.Error(), "pq: relation \"public.schema_migrations\" does not exist") + + // create table + err = drv.CreateMigrationsTable(db) + require.NoError(t, err) + + // migrations table should exist + err = db.QueryRow("select count(*) from public.schema_migrations").Scan(&count) + require.NoError(t, err) + }) +} + func TestPostgresSelectMigrations(t *testing.T) { drv := testPostgresDriver(t) drv.migrationsTableName = "test_migrations" @@ -567,3 +681,112 @@ func TestPostgresQuotedMigrationsTableName(t *testing.T) { require.Equal(t, "\"whyWould\".i.\"doThis\"", name) }) } + +func TestPostgresMigrationsTableExists(t *testing.T) { + t.Run("default schema", func(t *testing.T) { + drv := testPostgresDriver(t) + drv.migrationsTableName = "test_migrations" + + db := prepTestPostgresDB(t) + defer dbutil.MustClose(db) + + exists, err := drv.MigrationsTableExists(db) + require.NoError(t, err) + require.Equal(t, false, exists) + + err = drv.CreateMigrationsTable(db) + require.NoError(t, err) + + exists, err = drv.MigrationsTableExists(db) + require.NoError(t, err) + require.Equal(t, true, exists) + }) + + t.Run("custom schema", func(t *testing.T) { + drv := testPostgresDriver(t) + u, err := url.Parse(drv.databaseURL.String() + "&search_path=foo") + require.NoError(t, err) + drv.databaseURL = u + + db := prepTestPostgresDB(t) + defer dbutil.MustClose(db) + + err = drv.CreateMigrationsTable(db) + require.NoError(t, err) + + exists, err := drv.MigrationsTableExists(db) + require.NoError(t, err) + require.Equal(t, true, exists) + }) + + t.Run("custom schema with special chars", func(t *testing.T) { + drv := testPostgresDriver(t) + u, err := url.Parse(drv.databaseURL.String() + "&search_path=custom-schema") + require.NoError(t, err) + drv.databaseURL = u + + db := prepTestPostgresDB(t) + defer dbutil.MustClose(db) + + err = drv.CreateMigrationsTable(db) + require.NoError(t, err) + + exists, err := drv.MigrationsTableExists(db) + require.NoError(t, err) + require.Equal(t, true, exists) + }) + + t.Run("custom migrations table name containing schema with special chars", func(t *testing.T) { + drv := testPostgresDriver(t) + drv.migrationsTableName = "custom$schema.schema_migrations" + u, err := url.Parse(drv.databaseURL.String()) + require.NoError(t, err) + drv.databaseURL = u + + db := prepTestPostgresDB(t) + defer dbutil.MustClose(db) + + err = drv.CreateMigrationsTable(db) + require.NoError(t, err) + + exists, err := drv.MigrationsTableExists(db) + require.NoError(t, err) + require.Equal(t, true, exists) + }) + + t.Run("custom migrations table name containing table name with special chars", func(t *testing.T) { + drv := testPostgresDriver(t) + drv.migrationsTableName = "schema.custom#table#name" + u, err := url.Parse(drv.databaseURL.String()) + require.NoError(t, err) + drv.databaseURL = u + + db := prepTestPostgresDB(t) + defer dbutil.MustClose(db) + + err = drv.CreateMigrationsTable(db) + require.NoError(t, err) + + exists, err := drv.MigrationsTableExists(db) + require.NoError(t, err) + require.Equal(t, true, exists) + }) + + t.Run("custom migrations table name containing schema and table name with special chars", func(t *testing.T) { + drv := testPostgresDriver(t) + drv.migrationsTableName = "custom-schema.custom@table@name" + u, err := url.Parse(drv.databaseURL.String()) + require.NoError(t, err) + drv.databaseURL = u + + db := prepTestPostgresDB(t) + defer dbutil.MustClose(db) + + err = drv.CreateMigrationsTable(db) + require.NoError(t, err) + + exists, err := drv.MigrationsTableExists(db) + require.NoError(t, err) + require.Equal(t, true, exists) + }) +} diff --git a/pkg/driver/sqlite/sqlite.go b/pkg/driver/sqlite/sqlite.go index de22af7a..82eb1673 100644 --- a/pkg/driver/sqlite/sqlite.go +++ b/pkg/driver/sqlite/sqlite.go @@ -1,3 +1,4 @@ +//go:build cgo // +build cgo package sqlite @@ -6,6 +7,7 @@ import ( "bytes" "database/sql" "fmt" + "io" "net/url" "os" "regexp" @@ -27,6 +29,7 @@ func init() { type Driver struct { migrationsTableName string databaseURL *url.URL + log io.Writer } // NewDriver initializes the driver @@ -34,6 +37,7 @@ func NewDriver(config dbmate.DriverConfig) dbmate.Driver { return &Driver{ migrationsTableName: config.MigrationsTableName, databaseURL: config.DatabaseURL, + log: config.Log, } } @@ -43,6 +47,23 @@ func ConnectionString(u *url.URL) string { newURL := *u newURL.Scheme = "" + if newURL.Opaque == "" && newURL.Path != "" { + // When the DSN is in the form "scheme:/absolute/path" or + // "scheme://absolute/path" or "scheme:///absolute/path", url.Parse + // will consider the file path as : + // - "absolute" as the hostname + // - "path" (and the rest until "?") as the URL path. + // Instead, when the DSN is in the form "scheme:", the (relative) file + // path is stored in the "Opaque" field. + // See: https://pkg.go.dev/net/url#URL + // + // While Opaque is not escaped, the URL Path is. So, if .Path contains + // the file path, we need to un-escape it, and rebuild the full path. + + newURL.Opaque = "//" + newURL.Host + dbutil.MustUnescapePath(newURL.Path) + newURL.Path = "" + } + // trim duplicate leading slashes str := regexp.MustCompile("^//+").ReplaceAllString(newURL.String(), "/") @@ -56,7 +77,7 @@ func (drv *Driver) Open() (*sql.DB, error) { // CreateDatabase creates the specified database func (drv *Driver) CreateDatabase() error { - fmt.Printf("Creating: %s\n", ConnectionString(drv.databaseURL)) + fmt.Fprintf(drv.log, "Creating: %s\n", ConnectionString(drv.databaseURL)) db, err := drv.Open() if err != nil { @@ -70,7 +91,7 @@ func (drv *Driver) CreateDatabase() error { // DropDatabase drops the specified database (if it exists) func (drv *Driver) DropDatabase() error { path := ConnectionString(drv.databaseURL) - fmt.Printf("Dropping: %s\n", path) + fmt.Fprintf(drv.log, "Dropping: %s\n", path) exists, err := drv.DatabaseExists() if err != nil { @@ -110,7 +131,7 @@ func (drv *Driver) schemaMigrationsDump(db *sql.DB) ([]byte, error) { // DumpSchema returns the current database schema func (drv *Driver) DumpSchema(db *sql.DB) ([]byte, error) { path := ConnectionString(drv.databaseURL) - schema, err := dbutil.RunCommand("sqlite3", path, ".schema") + schema, err := dbutil.RunCommand("sqlite3", path, ".schema --nosys") if err != nil { return nil, err } @@ -137,11 +158,25 @@ func (drv *Driver) DatabaseExists() (bool, error) { return true, nil } +// MigrationsTableExists checks if the schema_migrations table exists +func (drv *Driver) MigrationsTableExists(db *sql.DB) (bool, error) { + exists := false + err := db.QueryRow("SELECT 1 FROM sqlite_master "+ + "WHERE type='table' AND name=$1", + drv.migrationsTableName). + Scan(&exists) + if err == sql.ErrNoRows { + return false, nil + } + + return exists, err +} + // CreateMigrationsTable creates the schema migrations table func (drv *Driver) CreateMigrationsTable(db *sql.DB) error { - _, err := db.Exec( - fmt.Sprintf("create table if not exists %s ", drv.quotedMigrationsTableName()) + - "(version varchar(255) primary key)") + _, err := db.Exec(fmt.Sprintf( + "create table if not exists %s (version varchar(128) primary key)", + drv.quotedMigrationsTableName())) return err } @@ -208,6 +243,11 @@ func (drv *Driver) Ping() error { return db.Ping() } +// Return a normalized version of the driver-specific error type. +func (drv *Driver) QueryError(query string, err error) error { + return &dbmate.QueryError{Err: err, Query: query} +} + func (drv *Driver) quotedMigrationsTableName() string { return drv.quoteIdentifier(drv.migrationsTableName) } diff --git a/pkg/driver/sqlite/sqlite_test.go b/pkg/driver/sqlite/sqlite_test.go index 3d61fc27..147e93f9 100644 --- a/pkg/driver/sqlite/sqlite_test.go +++ b/pkg/driver/sqlite/sqlite_test.go @@ -1,3 +1,4 @@ +//go:build cgo // +build cgo package sqlite @@ -8,14 +9,15 @@ import ( "testing" "github.com/assetnote/dbmate/pkg/dbmate" + "github.com/assetnote/dbmate/pkg/dbtest" "github.com/assetnote/dbmate/pkg/dbutil" "github.com/stretchr/testify/require" ) func testSQLiteDriver(t *testing.T) *Driver { - u := dbutil.MustParseURL(os.Getenv("SQLITE_TEST_URL")) - drv, err := dbmate.New(u).GetDriver() + u := dbtest.MustParseURL(t, "sqlite:dbmate_test.sqlite3") + drv, err := dbmate.New(u).Driver() require.NoError(t, err) return drv.(*Driver) @@ -40,8 +42,8 @@ func prepTestSQLiteDB(t *testing.T) *sql.DB { } func TestGetDriver(t *testing.T) { - db := dbmate.New(dbutil.MustParseURL("sqlite://")) - drvInterface, err := db.GetDriver() + db := dbmate.New(dbtest.MustParseURL(t, "sqlite://")) + drvInterface, err := db.Driver() require.NoError(t, err) // driver should have URL and default migrations table set @@ -53,27 +55,88 @@ func TestGetDriver(t *testing.T) { func TestConnectionString(t *testing.T) { t.Run("relative", func(t *testing.T) { - u := dbutil.MustParseURL("sqlite:foo/bar.sqlite3?mode=ro") + u := dbtest.MustParseURL(t, "sqlite:foo/bar.sqlite3?mode=ro") require.Equal(t, "foo/bar.sqlite3?mode=ro", ConnectionString(u)) }) + t.Run("relative with dot", func(t *testing.T) { + u := dbtest.MustParseURL(t, "sqlite:./foo/bar.sqlite3?mode=ro") + require.Equal(t, "./foo/bar.sqlite3?mode=ro", ConnectionString(u)) + }) + + t.Run("relative with double dot", func(t *testing.T) { + u := dbtest.MustParseURL(t, "sqlite:../foo/bar.sqlite3?mode=ro") + require.Equal(t, "../foo/bar.sqlite3?mode=ro", ConnectionString(u)) + }) + t.Run("absolute", func(t *testing.T) { - u := dbutil.MustParseURL("sqlite:/tmp/foo.sqlite3?mode=ro") + u := dbtest.MustParseURL(t, "sqlite:/tmp/foo.sqlite3?mode=ro") + require.Equal(t, "/tmp/foo.sqlite3?mode=ro", ConnectionString(u)) + }) + + t.Run("two slashes", func(t *testing.T) { + // interpreted as absolute path + u := dbtest.MustParseURL(t, "sqlite://tmp/foo.sqlite3?mode=ro") require.Equal(t, "/tmp/foo.sqlite3?mode=ro", ConnectionString(u)) }) t.Run("three slashes", func(t *testing.T) { // interpreted as absolute path - u := dbutil.MustParseURL("sqlite:///tmp/foo.sqlite3?mode=ro") + u := dbtest.MustParseURL(t, "sqlite:///tmp/foo.sqlite3?mode=ro") require.Equal(t, "/tmp/foo.sqlite3?mode=ro", ConnectionString(u)) }) t.Run("four slashes", func(t *testing.T) { // interpreted as absolute path // supported for backwards compatibility - u := dbutil.MustParseURL("sqlite:////tmp/foo.sqlite3?mode=ro") + u := dbtest.MustParseURL(t, "sqlite:////tmp/foo.sqlite3?mode=ro") require.Equal(t, "/tmp/foo.sqlite3?mode=ro", ConnectionString(u)) }) + + t.Run("relative with space", func(t *testing.T) { + u := dbtest.MustParseURL(t, "sqlite:foo bar.sqlite3?mode=ro") + require.Equal(t, "foo bar.sqlite3?mode=ro", ConnectionString(u)) + }) + + t.Run("relative with space and dot", func(t *testing.T) { + u := dbtest.MustParseURL(t, "sqlite:./foo bar.sqlite3?mode=ro") + require.Equal(t, "./foo bar.sqlite3?mode=ro", ConnectionString(u)) + }) + + t.Run("relative with space and double dot", func(t *testing.T) { + u := dbtest.MustParseURL(t, "sqlite:../foo bar.sqlite3?mode=ro") + require.Equal(t, "../foo bar.sqlite3?mode=ro", ConnectionString(u)) + }) + + t.Run("absolute with space", func(t *testing.T) { + u := dbtest.MustParseURL(t, "sqlite:/foo bar.sqlite3?mode=ro") + require.Equal(t, "/foo bar.sqlite3?mode=ro", ConnectionString(u)) + }) + + t.Run("two slashes with space in path", func(t *testing.T) { + // interpreted as absolute path + u := dbtest.MustParseURL(t, "sqlite://tmp/foo bar.sqlite3?mode=ro") + require.Equal(t, "/tmp/foo bar.sqlite3?mode=ro", ConnectionString(u)) + }) + + t.Run("three slashes with space in path", func(t *testing.T) { + // interpreted as absolute path + u := dbtest.MustParseURL(t, "sqlite:///tmp/foo bar.sqlite3?mode=ro") + require.Equal(t, "/tmp/foo bar.sqlite3?mode=ro", ConnectionString(u)) + }) + + t.Run("three slashes with space in path (1st dir)", func(t *testing.T) { + // interpreted as absolute path + u := dbtest.MustParseURL(t, "sqlite:///tm p/foo bar.sqlite3?mode=ro") + require.Equal(t, "/tm p/foo bar.sqlite3?mode=ro", ConnectionString(u)) + }) + + t.Run("four slashes with space", func(t *testing.T) { + // interpreted as absolute path + // supported for backwards compatibility + u := dbtest.MustParseURL(t, "sqlite:////tmp/foo bar.sqlite3?mode=ro") + require.Equal(t, "/tmp/foo bar.sqlite3?mode=ro", ConnectionString(u)) + }) } func TestSQLiteCreateDropDatabase(t *testing.T) { @@ -118,22 +181,29 @@ func TestSQLiteDumpSchema(t *testing.T) { err = drv.InsertMigration(db, "abc2") require.NoError(t, err) + // create a table that will trigger `sqlite_sequence` system table + _, err = db.Exec("CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT)") + require.NoError(t, err) + // DumpSchema should return schema schema, err := drv.DumpSchema(db) require.NoError(t, err) + require.Contains(t, string(schema), "CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT)") require.Contains(t, string(schema), "CREATE TABLE IF NOT EXISTS \"test_migrations\"") require.Contains(t, string(schema), ");\n-- Dbmate schema migrations\n"+ "INSERT INTO \"test_migrations\" (version) VALUES\n"+ " ('abc1'),\n"+ " ('abc2');\n") + // sqlite_* tables should not be present in the dump (.schema --nosys) + require.NotContains(t, string(schema), "sqlite_") + // DumpSchema should return error if command fails - drv.databaseURL = dbutil.MustParseURL(".") + drv.databaseURL = dbtest.MustParseURL(t, ".") schema, err = drv.DumpSchema(db) require.Nil(t, schema) require.Error(t, err) - require.EqualError(t, err, "Error: unable to open database \".\": "+ - "unable to open database file") + require.EqualError(t, err, "Error: unable to open database \"/.\": unable to open database file") } func TestSQLiteDatabaseExists(t *testing.T) { @@ -316,7 +386,8 @@ func TestSQLitePing(t *testing.T) { // ping database should fail err = drv.Ping() - require.EqualError(t, err, "unable to open database file: is a directory") + require.Error(t, err) + require.Contains(t, err.Error(), "unable to open database file") } func TestSQLiteQuotedMigrationsTableName(t *testing.T) { @@ -334,3 +405,12 @@ func TestSQLiteQuotedMigrationsTableName(t *testing.T) { require.Equal(t, `"fooMigrations"`, name) }) } + +func TestSQLiteFTS5Available(t *testing.T) { + db := prepTestSQLiteDB(t) + defer dbutil.MustClose(db) + + // this only passes if the FTS5 module is statically compiled in to the SQLite driver + _, err := db.Exec("CREATE VIRTUAL TABLE a USING fts5(b, c)") + require.NoError(t, err) +} diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 00000000..8a4c87ef --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,5 @@ +// -*- jsonc -*- +// This file is used by vscode-eslint due to different working directory +{ + "extends": "./typescript/tsconfig.eslint.json" +} diff --git a/typescript/.npmrc b/typescript/.npmrc new file mode 100644 index 00000000..cffe8cde --- /dev/null +++ b/typescript/.npmrc @@ -0,0 +1 @@ +save-exact=true diff --git a/typescript/README.md b/typescript/README.md new file mode 100644 index 00000000..004794d0 --- /dev/null +++ b/typescript/README.md @@ -0,0 +1,21 @@ +# Dbmate NPM package + +This directory contains scripts to generate and publish the dbmate npm package. + +## Generate the package + +``` +npm run generate +``` + +For local development, you can avoid copying the dbmate binaries if you don't have them available: + +``` +npm run generate -- --skip-bin +``` + +## Publish the packages (CI only) + +``` +npm run publish +``` diff --git a/typescript/eslint.config.mjs b/typescript/eslint.config.mjs new file mode 100644 index 00000000..554cf4ce --- /dev/null +++ b/typescript/eslint.config.mjs @@ -0,0 +1,21 @@ +import foxglove from "@foxglove/eslint-plugin"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { + ignores: ["**/dist"], + }, + ...foxglove.configs.base, + ...foxglove.configs.typescript.map((config) => ({ + ...config, + files: ["**/*.ts"], + })), + { + files: ["**/*.ts"], + languageOptions: { + parserOptions: { + project: "tsconfig.eslint.json", + }, + }, + }, +); diff --git a/typescript/generate.ts b/typescript/generate.ts new file mode 100644 index 00000000..26b639e9 --- /dev/null +++ b/typescript/generate.ts @@ -0,0 +1,158 @@ +import { exec } from "@actions/exec"; +import Handlebars from "handlebars"; +import { readFile, writeFile, cp, chmod, mkdir } from "node:fs/promises"; +import { parse as parseYaml } from "yaml"; + +type CiYaml = { + jobs: { + build: { + strategy: { + matrix: { + include: MatrixItem[]; + }; + }; + }; + }; +}; + +type MatrixItem = { + os: string; + arch: string; +}; + +type PackageJson = { + version: string; + optionalDependencies: Record; +}; + +// map GOOS to NPM +const OS_MAP: Record = { + linux: "linux", + macos: "darwin", + windows: "win32", +}; + +// map GOARCH to NPM +const ARCH_MAP: Record = { + 386: "ia32", + amd64: "x64", + arm: "arm", + arm64: "arm64", +}; + +// fetch version number +async function getVersion(): Promise { + const versionFile = await readFile("../pkg/dbmate/version.go", "utf8"); + const matches = /Version = "([^"]+)"/.exec(versionFile); + + if (matches?.[1]) { + return matches[1]; + } + + throw new Error("Unable to detect version from version.go"); +} + +// fetch github actions build matrix +async function getBuildMatrix() { + const contents = await readFile("../.github/workflows/ci.yml", "utf8"); + const ci = parseYaml(contents) as CiYaml; + + return ci.jobs.build.strategy.matrix.include; +} + +// copy and update template into new package +async function copyTemplate( + filename: string, + targetDir: string, + vars: Record, +) { + const source = await readFile(`packages/template/${filename}`, "utf8"); + const template = Handlebars.compile(source); + await writeFile(`${targetDir}/${filename}`, template(vars)); +} + +async function main() { + // clean output directories + await exec("npm", ["run", "clean"]); + + // build main package + await exec("npm", ["run", "build"], { + cwd: "packages/dbmate", + }); + + // parse main package.json + const version = await getVersion(); + const mainPackageJson = JSON.parse( + await readFile("packages/dbmate/package.json", "utf8"), + ) as PackageJson; + mainPackageJson.version = version; + + // generate os/arch packages + const buildMatrix = await getBuildMatrix(); + for (const build of buildMatrix) { + const jsOS = OS_MAP[build.os]; + if (!jsOS) { + throw new Error(`Unknown os ${build.os}`); + } + + const jsArch = ARCH_MAP[build.arch]; + if (!jsArch) { + throw new Error(`Unknown arch ${build.arch}`); + } + + const name = `@dbmate/${jsOS}-${jsArch}`; + const targetDir = `dist/@dbmate/${jsOS}-${jsArch}`; + const binext = jsOS === "win32" ? ".exe" : ""; + const templateVars = { jsOS, jsArch, name, version }; + + // generate package directory + console.log(`Generate ${name}`); + await mkdir(`${targetDir}/bin`, { recursive: true }); + await copyTemplate("package.json", targetDir, templateVars); + await copyTemplate("README.md", targetDir, templateVars); + + // copy binary from github actions artifact + const targetBin = `${targetDir}/bin/dbmate${binext}`; + try { + if (process.argv[2] === "--skip-bin") { + // dummy file for testing + await writeFile(targetBin, ""); + } else { + // copy os/arch binary (typically built via CI) + await cp( + `../dist/dbmate-${build.os}-${build.arch}/dbmate-${build.os}-${build.arch}${binext}`, + targetBin, + ); + } + await chmod(targetBin, 0o755); + } catch (e) { + console.error(e); + throw new Error( + "Run `npm run generate -- --skip-bin` to test generate without binaries", + ); + } + + // record dependency in main package.json + mainPackageJson.optionalDependencies[name] = version; + } + + // copy main package + await cp("packages/dbmate", "dist/dbmate", { + recursive: true, + }); + + // write package.json + await writeFile( + "dist/dbmate/package.json", + JSON.stringify(mainPackageJson, undefined, 2), + ); + + // copy readme and license + await cp("../LICENSE", "dist/dbmate/LICENSE"); + await cp("../README.md", "dist/dbmate/README.md"); +} + +main().catch((e: unknown) => { + console.error(e); + process.exit(1); +}); diff --git a/typescript/package-lock.json b/typescript/package-lock.json new file mode 100644 index 00000000..623843f4 --- /dev/null +++ b/typescript/package-lock.json @@ -0,0 +1,4298 @@ +{ + "name": "typescript", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "typescript", + "devDependencies": { + "@actions/exec": "1.1.1", + "@foxglove/eslint-plugin": "2.0.0", + "@foxglove/tsconfig": "2.0.0", + "eslint": "9.20.1", + "handlebars": "4.7.8", + "npm-check-updates": "17.1.14", + "prettier": "3.5.1", + "rimraf": "6.0.1", + "ts-node": "10.9.2", + "typescript": "5.7.3", + "typescript-eslint": "8.24.1", + "yaml": "2.7.0" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "dev": true, + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==", + "dev": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/compat": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.2.tgz", + "integrity": "sha512-jhgiIrsw+tRfcBQ4BFl2C3vCrIUw2trCY0cnDvGZpwTtKCEDmZhAtMfrEUP/KpnwM6PrO0T+Ltm+ccW74olG3Q==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.10.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", + "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.5", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/core": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", + "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.20.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", + "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", + "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", + "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.10.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@foxglove/eslint-plugin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@foxglove/eslint-plugin/-/eslint-plugin-2.0.0.tgz", + "integrity": "sha512-0HLMt84d1pE3Z5BfK3ExXCQyoo2lw4teCbYaUx10UuU6RYO3NSm9JJ09+LqPO93qyXdp80PQn5JMEKMT/iYVKQ==", + "dev": true, + "dependencies": { + "@eslint/compat": "^1", + "@eslint/js": "^9", + "@typescript-eslint/utils": "^8", + "eslint-config-prettier": "^9", + "eslint-plugin-es": "^4", + "eslint-plugin-filenames": "^1", + "eslint-plugin-import": "^2", + "eslint-plugin-jest": "^28", + "eslint-plugin-prettier": "^5", + "eslint-plugin-react": "^7", + "eslint-plugin-react-hooks": "^5", + "tsutils": "^3", + "typescript-eslint": "^8" + }, + "peerDependencies": { + "eslint": "^9" + } + }, + "node_modules/@foxglove/tsconfig": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@foxglove/tsconfig/-/tsconfig-2.0.0.tgz", + "integrity": "sha512-tifun2tMxfA0wJ4caDKnLvAm+CpNdaK2BYD78w07GdgMsoeVMu0KxAiR7ehRoR3QN6DUW7XDP5J4DqMTj3YFag==", + "dev": true + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", + "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "dev": true, + "peer": true, + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.1.tgz", + "integrity": "sha512-ll1StnKtBigWIGqvYDVuDmXJHVH4zLVot1yQ4fJtLpL7qacwkxJc1T0bptqw+miBQ/QfUbhl1TcQ4accW5KUyA==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.24.1", + "@typescript-eslint/type-utils": "8.24.1", + "@typescript-eslint/utils": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.1.tgz", + "integrity": "sha512-Tqoa05bu+t5s8CTZFaGpCH2ub3QeT9YDkXbPd3uQ4SfsLoh1/vv2GEYAioPoxCWJJNsenXlC88tRjwoHNts1oQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.24.1", + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/typescript-estree": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.1.tgz", + "integrity": "sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.1.tgz", + "integrity": "sha512-/Do9fmNgCsQ+K4rCz0STI7lYB4phTtEXqqCAs3gZW0pnK7lWNkvWd5iW545GSmApm4AzmQXmSqXPO565B4WVrw==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "8.24.1", + "@typescript-eslint/utils": "8.24.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.1.tgz", + "integrity": "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.1.tgz", + "integrity": "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/visitor-keys": "8.24.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.1.tgz", + "integrity": "sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.24.1", + "@typescript-eslint/types": "8.24.1", + "@typescript-eslint/typescript-estree": "8.24.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.1.tgz", + "integrity": "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.24.1", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.0.tgz", + "integrity": "sha512-tpxqxncxnpw3c93u8n3VOzACmRFoVmWJqbWXvX/JfKbkhBw1oslgPrUfeSt2psuqyEJFD6N/9lg5i7bsKpoq+Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.3", + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.20.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz", + "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.11.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.20.0", + "@eslint/plugin-kit": "^0.2.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-es": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", + "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", + "dev": true, + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-filenames": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-filenames/-/eslint-plugin-filenames-1.3.2.tgz", + "integrity": "sha512-tqxJTiEM5a0JmRCUYQmxw23vtTxrb2+a3Q2mMOPhFxvt7ZQQJmdiuMby9B/vUAuVMghyP7oET+nIf6EO6CBd/w==", + "dev": true, + "dependencies": { + "lodash.camelcase": "4.3.0", + "lodash.kebabcase": "4.1.1", + "lodash.snakecase": "4.1.1", + "lodash.upperfirst": "4.3.1" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dev": true, + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "28.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.9.0.tgz", + "integrity": "sha512-rLu1s1Wf96TgUUxSw6loVIkNtUjq1Re7A9QdCCHSohnvXEBAjuL420h0T/fMmkQlNsQP2GhQzEUpYHPfxBkvYQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "engines": { + "node": "^16.10.0 || ^18.12.0 || >=20.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz", + "integrity": "sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.1.0", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz", + "integrity": "sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/@eslint/core": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", + "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", + "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/iterator.prototype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.3.tgz", + "integrity": "sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jackspeak": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", + "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dev": true + }, + "node_modules/lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/npm-check-updates": { + "version": "17.1.14", + "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-17.1.14.tgz", + "integrity": "sha512-dr4bXIxETubLI1tFGeock5hN8yVjahvaVpx+lPO4/O2md3zJuxB7FgH3MIoTvQSCgsgkIRpe0skti01IEAA5tA==", + "dev": true, + "bin": { + "ncu": "build/cli.js", + "npm-check-updates": "build/cli.js" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0", + "npm": ">=8.12.1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz", + "integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", + "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "dev": true, + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.24.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.24.1.tgz", + "integrity": "sha512-cw3rEdzDqBs70TIcb0Gdzbt6h11BSs2pS0yaq7hDWDBtCCSei1pPSUXE9qUdQ/Wm9NgFg8mKtMt1b8fTHIl1jA==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.24.1", + "@typescript-eslint/parser": "8.24.1", + "@typescript-eslint/utils": "8.24.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "peer": true + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", + "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", + "dev": true, + "dependencies": { + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/typescript/package.json b/typescript/package.json new file mode 100644 index 00000000..012e9d3c --- /dev/null +++ b/typescript/package.json @@ -0,0 +1,24 @@ +{ + "private": true, + "scripts": { + "clean": "rimraf dist packages/dbmate/dist", + "lint": "eslint --report-unused-disable-directives --fix .", + "lint:ci": "eslint --report-unused-disable-directives .", + "generate": "ts-node generate.ts", + "publish": "ts-node publish.ts" + }, + "devDependencies": { + "@actions/exec": "1.1.1", + "@foxglove/eslint-plugin": "2.0.0", + "@foxglove/tsconfig": "2.0.0", + "eslint": "9.20.1", + "handlebars": "4.7.8", + "npm-check-updates": "17.1.14", + "prettier": "3.5.1", + "rimraf": "6.0.1", + "ts-node": "10.9.2", + "typescript-eslint": "8.24.1", + "typescript": "5.7.3", + "yaml": "2.7.0" + } +} diff --git a/typescript/packages/dbmate/package.json b/typescript/packages/dbmate/package.json new file mode 100644 index 00000000..56d246b5 --- /dev/null +++ b/typescript/packages/dbmate/package.json @@ -0,0 +1,27 @@ +{ + "name": "dbmate", + "version": "", + "description": "A lightweight, framework-agnostic database migration tool", + "repository": "https://github.com/amacneil/dbmate", + "homepage": "https://github.com/amacneil/dbmate#readme", + "author": "Adrian Macneil", + "license": "MIT", + "keywords": [ + "clickhouse", + "database", + "migration", + "mysql", + "postgres", + "schema", + "sqlite" + ], + "bin": "./dist/cli.js", + "main": "./dist/index.js", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc --build" + }, + "optionalDependencies": {} +} diff --git a/typescript/packages/dbmate/src/cli.ts b/typescript/packages/dbmate/src/cli.ts new file mode 100644 index 00000000..cf605e7a --- /dev/null +++ b/typescript/packages/dbmate/src/cli.ts @@ -0,0 +1,9 @@ +#!/usr/bin/env node +import { spawnSync } from "node:child_process"; + +import { resolveBinary } from "./resolveBinary.js"; + +const child = spawnSync(resolveBinary(), process.argv.slice(2), { + stdio: "inherit", +}); +process.exit(child.status ?? 0); diff --git a/typescript/packages/dbmate/src/index.ts b/typescript/packages/dbmate/src/index.ts new file mode 100644 index 00000000..1d0ccde4 --- /dev/null +++ b/typescript/packages/dbmate/src/index.ts @@ -0,0 +1,3 @@ +import { resolveBinary } from "./resolveBinary.js"; + +export { resolveBinary }; diff --git a/typescript/packages/dbmate/src/resolveBinary.ts b/typescript/packages/dbmate/src/resolveBinary.ts new file mode 100644 index 00000000..953515c6 --- /dev/null +++ b/typescript/packages/dbmate/src/resolveBinary.ts @@ -0,0 +1,24 @@ +import { arch, platform } from "node:process"; + +/** + * Resolve path to dbmate for the current platform + * */ +export function resolveBinary(): string { + const ext = platform === "win32" ? ".exe" : ""; + const path = `@dbmate/${platform}-${arch}/bin/dbmate${ext}`; + + try { + return require.resolve(path); + } catch (err) { + if ( + err != undefined && + typeof err === "object" && + "code" in err && + err.code === "MODULE_NOT_FOUND" + ) { + throw new Error(`Unable to locate dbmate binary '${path}'`); + } else { + throw err; + } + } +} diff --git a/typescript/packages/dbmate/tsconfig.json b/typescript/packages/dbmate/tsconfig.json new file mode 100644 index 00000000..7ffdb2d9 --- /dev/null +++ b/typescript/packages/dbmate/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./src/**/*"], + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + } +} diff --git a/typescript/packages/template/README.md b/typescript/packages/template/README.md new file mode 100644 index 00000000..d8ca4767 --- /dev/null +++ b/typescript/packages/template/README.md @@ -0,0 +1,3 @@ +# {{name}} + +The {{jsOS}} {{jsArch}} binary for dbmate. See https://github.com/amacneil/dbmate for details. diff --git a/typescript/packages/template/package.json b/typescript/packages/template/package.json new file mode 100644 index 00000000..06e1b1e0 --- /dev/null +++ b/typescript/packages/template/package.json @@ -0,0 +1,14 @@ +{ + "name": "{{name}}", + "version": "{{version}}", + "description": "The {{jsOS}} {{jsArch}} binary for dbmate", + "repository": "https://github.com/amacneil/dbmate", + "license": "MIT", + "preferUnplugged": true, + "os": [ + "{{jsOS}}" + ], + "cpu": [ + "{{jsArch}}" + ] +} diff --git a/typescript/publish.ts b/typescript/publish.ts new file mode 100644 index 00000000..6c0e4d93 --- /dev/null +++ b/typescript/publish.ts @@ -0,0 +1,18 @@ +import { exec } from "@actions/exec"; +import { readdir } from "node:fs/promises"; + +async function main() { + const packages = [`./dist/dbmate`]; + (await readdir("dist/@dbmate")).forEach((pkg) => + packages.push(`./dist/@dbmate/${pkg}`), + ); + + for (const pkg of packages) { + await exec("npm", ["publish", "--access", "public", pkg]); + } +} + +main().catch((e: unknown) => { + console.error(e); + process.exit(1); +}); diff --git a/typescript/tsconfig.eslint.json b/typescript/tsconfig.eslint.json new file mode 100644 index 00000000..a45469c2 --- /dev/null +++ b/typescript/tsconfig.eslint.json @@ -0,0 +1,6 @@ +// -*- jsonc -*- +// This tsconfig is used for eslint +{ + "extends": "./tsconfig.json", + "include": ["**/*"] +} diff --git a/typescript/tsconfig.json b/typescript/tsconfig.json new file mode 100644 index 00000000..7c273478 --- /dev/null +++ b/typescript/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@foxglove/tsconfig/base", + "include": ["*.ts"], + "compilerOptions": { + "rootDir": ".", + "outDir": "./dist", + "module": "commonjs" + } +}