Compare commits
1 Commits
storage@0.
...
pgupgr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58eda5bc34 |
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -22,7 +22,6 @@ Where `TYPE` is:
|
||||
|
||||
Where `PKG` is:
|
||||
|
||||
- `auth`: For changes to the Nhost Auth service
|
||||
- `ci`: For general changes to the build and/or CI/CD pipeline
|
||||
- `cli`: For changes to the Nhost CLI
|
||||
- `codegen`: For changes to the code generator
|
||||
@@ -33,7 +32,7 @@ Where `PKG` is:
|
||||
- `mintlify-openapi`: For changes to the Mintlify OpenAPI tool
|
||||
- `nhost-js`: For changes to the Nhost JavaScript SDK
|
||||
- `nixops`: For changes to the NixOps
|
||||
- `storage`: For changes to the Nhost Storage service
|
||||
- `storage`: For changes to the Nhost Storage
|
||||
|
||||
Where `SUMMARY` is a short description of what the PR does.
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ runs:
|
||||
|
||||
# Define valid types and packages
|
||||
VALID_TYPES="feat|fix|chore"
|
||||
VALID_PKGS="auth|ci|cli|codegen|dashboard|deps|docs|examples|mintlify-openapi|nhost-js|nixops|storage"
|
||||
VALID_PKGS="ci|cli|codegen|dashboard|deps|docs|examples|mintlify-openapi|nhost-js|nixops|storage"
|
||||
|
||||
# Check if title matches the pattern TYPE(PKG): SUMMARY
|
||||
if [[ ! "$PR_TITLE" =~ ^(${VALID_TYPES})\((${VALID_PKGS})\):\ .+ ]]; then
|
||||
@@ -31,11 +31,11 @@ runs:
|
||||
echo " - chore: mark this pull request as a maintenance item"
|
||||
echo ""
|
||||
echo "Valid PKGs:"
|
||||
echo " - auth, ci, cli, codegen, dashboard, deps, docs, examples,"
|
||||
echo " - ci, cli, codegen, dashboard, deps, docs, examples,"
|
||||
echo " - mintlify-openapi, nhost-js, nixops, storage"
|
||||
echo ""
|
||||
echo "Example: feat(cli): add new command for database migrations"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ PR title is valid!"
|
||||
echo "✅ PR title is valid!"
|
||||
84
.github/workflows/auth_checks.yaml
vendored
84
.github/workflows/auth_checks.yaml
vendored
@@ -1,84 +0,0 @@
|
||||
---
|
||||
name: "auth: check and build"
|
||||
on:
|
||||
# pull_request_target:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/auth_checks.yaml'
|
||||
- '.github/workflows/wf_check.yaml'
|
||||
- '.github/workflows/wf_build_artifacts.yaml'
|
||||
|
||||
# common build
|
||||
- 'flake.nix'
|
||||
- 'flake.lock'
|
||||
- 'nixops/**'
|
||||
- 'build/**'
|
||||
|
||||
# common go
|
||||
- '.golangci.yaml'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'vendor/**'
|
||||
|
||||
# auth
|
||||
- 'services/auth/**'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || format('push-{0}', github.sha) }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
check-permissions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo "github.event_name: ${{ github.event_name }}"
|
||||
echo "github.event.pull_request.author_association: ${{ github.event.pull_request.author_association }}"
|
||||
- name: "This task will run and fail if user has no permissions and label safe_to_test isn't present"
|
||||
if: "github.event_name == 'pull_request_target' && ! ( contains(github.event.pull_request.labels.*.name, 'safe_to_test') || contains(fromJson('[\"OWNER\", \"MEMBER\", \"COLLABORATOR\"]'), github.event.pull_request.author_association) )"
|
||||
run: |
|
||||
exit 1
|
||||
|
||||
tests:
|
||||
uses: ./.github/workflows/wf_check.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: auth
|
||||
PATH: services/auth
|
||||
GIT_REF: ${{ github.sha }}
|
||||
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
NHOST_PAT: ${{ secrets.NHOST_PAT }}
|
||||
|
||||
build_artifacts:
|
||||
uses: ./.github/workflows/wf_build_artifacts.yaml
|
||||
needs:
|
||||
- check-permissions
|
||||
with:
|
||||
NAME: auth
|
||||
PATH: services/auth
|
||||
GIT_REF: ${{ github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: true
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
remove_label:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check-permissions
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-remove-labels@v1
|
||||
with:
|
||||
labels: |
|
||||
safe_to_test
|
||||
if: contains(github.event.pull_request.labels.*.name, 'safe_to_test')
|
||||
60
.github/workflows/auth_wf_release.yaml
vendored
60
.github/workflows/auth_wf_release.yaml
vendored
@@ -1,60 +0,0 @@
|
||||
---
|
||||
name: "auth: release"
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
GIT_REF:
|
||||
required: true
|
||||
type: string
|
||||
VERSION:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID:
|
||||
required: true
|
||||
NIX_CACHE_PUB_KEY:
|
||||
required: true
|
||||
NIX_CACHE_PRIV_KEY:
|
||||
required: true
|
||||
DOCKER_USERNAME:
|
||||
required: true
|
||||
DOCKER_PASSWORD:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
build_artifacts:
|
||||
uses: ./.github/workflows/wf_build_artifacts.yaml
|
||||
with:
|
||||
NAME: auth
|
||||
PATH: services/auth
|
||||
GIT_REF: ${{ inputs.GIT_REF }}
|
||||
VERSION: ${{ inputs.VERSION }}
|
||||
DOCKER: true
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
push-docker-hub:
|
||||
uses: ./.github/workflows/wf_docker_push_image.yaml
|
||||
needs:
|
||||
- build_artifacts
|
||||
with:
|
||||
NAME: auth
|
||||
PATH: services/auth
|
||||
VERSION: ${{ inputs.VERSION }}
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
push-docker-ecr:
|
||||
uses: ./.github/workflows/wf_docker_push_image_ecr.yaml
|
||||
needs:
|
||||
- build_artifacts
|
||||
with:
|
||||
NAME: auth
|
||||
PATH: services/auth
|
||||
VERSION: ${{ inputs.VERSION }}
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
|
||||
CONTAINER_REGISTRY: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.eu-central-1.amazonaws.com
|
||||
14
.github/workflows/ci_release.yaml
vendored
14
.github/workflows/ci_release.yaml
vendored
@@ -33,20 +33,6 @@ jobs:
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Extracted project: $PROJECT, version: $VERSION"
|
||||
|
||||
auth:
|
||||
needs: extract-project
|
||||
if: needs.extract-project.outputs.project == 'auth'
|
||||
uses: ./.github/workflows/auth_wf_release.yaml
|
||||
with:
|
||||
GIT_REF: ${{ github.sha }}
|
||||
VERSION: ${{ needs.extract-project.outputs.version }}
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
cli:
|
||||
needs: extract-project
|
||||
if: needs.extract-project.outputs.project == 'cli'
|
||||
|
||||
2
.github/workflows/ci_update_changelog.yaml
vendored
2
.github/workflows/ci_update_changelog.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
project: [cli, dashboard, packages/nhost-js, services/auth, services/storage]
|
||||
project: [cli, dashboard, packages/nhost-js, services/storage]
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
4
.github/workflows/dashboard_checks.yaml
vendored
4
.github/workflows/dashboard_checks.yaml
vendored
@@ -126,10 +126,8 @@ jobs:
|
||||
NHOST_TEST_USER_EMAIL: ${{ secrets.NHOST_TEST_USER_EMAIL }}
|
||||
NHOST_TEST_USER_PASSWORD: ${{ secrets.NHOST_TEST_USER_PASSWORD }}
|
||||
NHOST_TEST_PROJECT_ADMIN_SECRET: ${{ secrets.NHOST_TEST_PROJECT_ADMIN_SECRET }}
|
||||
NHOST_TEST_ONBOARDING_USER: ${{ secrets.NHOST_TEST_ONBOARDING_USER }}
|
||||
NHOST_TEST_FREE_USER_EMAILS: ${{ secrets.NHOST_TEST_FREE_USER_EMAILS }}
|
||||
PLAYWRIGHT_REPORT_ENCRYPTION_KEY: ${{ secrets.PLAYWRIGHT_REPORT_ENCRYPTION_KEY }}
|
||||
NHOST_TEST_STAGING_SUBDOMAIN: ${{ secrets.NHOST_TEST_STAGING_SUBDOMAIN }}
|
||||
NHOST_TEST_STAGING_REGION: ${{ secrets.NHOST_TEST_STAGING_REGION }}
|
||||
|
||||
remove_label:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
13
.github/workflows/dashboard_wf_e2e_staging.yaml
vendored
13
.github/workflows/dashboard_wf_e2e_staging.yaml
vendored
@@ -52,16 +52,12 @@ on:
|
||||
required: true
|
||||
NHOST_TEST_USER_PASSWORD:
|
||||
required: true
|
||||
NHOST_TEST_ONBOARDING_USER:
|
||||
required: true
|
||||
NHOST_TEST_PROJECT_ADMIN_SECRET:
|
||||
required: true
|
||||
NHOST_TEST_FREE_USER_EMAILS:
|
||||
required: true
|
||||
PLAYWRIGHT_REPORT_ENCRYPTION_KEY:
|
||||
required: true
|
||||
NHOST_TEST_STAGING_SUBDOMAIN:
|
||||
required: true
|
||||
NHOST_TEST_STAGING_REGION:
|
||||
required: true
|
||||
|
||||
concurrency:
|
||||
group: dashboard-e2e-staging
|
||||
@@ -81,10 +77,7 @@ env:
|
||||
NHOST_TEST_USER_EMAIL: ${{ secrets.NHOST_TEST_USER_EMAIL }}
|
||||
NHOST_TEST_USER_PASSWORD: ${{ secrets.NHOST_TEST_USER_PASSWORD }}
|
||||
NHOST_TEST_PROJECT_ADMIN_SECRET: ${{ secrets.NHOST_TEST_PROJECT_ADMIN_SECRET }}
|
||||
NHOST_TEST_ONBOARDING_USER: ${{ secrets.NHOST_TEST_ONBOARDING_USER }}
|
||||
NHOST_TEST_STAGING_SUBDOMAIN: ${{ secrets.NHOST_TEST_STAGING_SUBDOMAIN }}
|
||||
NHOST_TEST_STAGING_REGION: ${{ secrets.NHOST_TEST_STAGING_REGION }}
|
||||
|
||||
NHOST_TEST_FREE_USER_EMAILS: ${{ secrets.NHOST_TEST_FREE_USER_EMAILS }}
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
|
||||
4
.github/workflows/docs_checks.yaml
vendored
4
.github/workflows/docs_checks.yaml
vendored
@@ -28,10 +28,6 @@ on:
|
||||
# nhost-js
|
||||
- packages/nhost-js/**
|
||||
|
||||
# apis
|
||||
- 'services/auth/docs/openapi.yaml'
|
||||
- 'services/storage/controller/openapi.yaml'
|
||||
|
||||
# cli
|
||||
- cli/**
|
||||
push:
|
||||
|
||||
1
.github/workflows/gen_ai_review.yaml
vendored
1
.github/workflows/gen_ai_review.yaml
vendored
@@ -24,5 +24,4 @@ jobs:
|
||||
config.model: ${{ vars.GEN_AI_MODEL }}
|
||||
config.model_turbo: $${{ vars.GEN_AI_MODEL_TURBO }}
|
||||
config.max_model_tokens: 200000
|
||||
config.custom_model_max_tokens: 200000
|
||||
ignore.glob: "['pnpm-lock.yaml','**/pnpm-lock.yaml', 'vendor/**','**/client_gen.go','**/models_gen.go','**/generated.go','**/*.gen.go']"
|
||||
|
||||
4
.github/workflows/nhost-js_checks.yaml
vendored
4
.github/workflows/nhost-js_checks.yaml
vendored
@@ -34,10 +34,6 @@ on:
|
||||
|
||||
# nhost-js
|
||||
- 'packages/nhost-js/**'
|
||||
|
||||
# apis
|
||||
- 'services/auth/docs/openapi.yaml'
|
||||
- 'services/storage/controller/openapi.yaml'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
2
.github/workflows/nixops_checks.yaml
vendored
2
.github/workflows/nixops_checks.yaml
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
PATH: nixops
|
||||
GIT_REF: ${{ github.sha }}
|
||||
VERSION: 0.0.0-dev # we use a fixed version here to avoid unnecessary rebuilds
|
||||
DOCKER: true
|
||||
DOCKER: false
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
|
||||
35
.github/workflows/nixops_wf_release.yaml
vendored
35
.github/workflows/nixops_wf_release.yaml
vendored
@@ -1,35 +0,0 @@
|
||||
---
|
||||
name: "nixops: release"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'flake.lock'
|
||||
- 'nixops/project.nix'
|
||||
|
||||
jobs:
|
||||
build_artifacts:
|
||||
uses: ./.github/workflows/wf_build_artifacts.yaml
|
||||
with:
|
||||
NAME: nixops
|
||||
PATH: nixops
|
||||
GIT_REF: ${{ inputs.GIT_REF }}
|
||||
VERSION: latest
|
||||
DOCKER: true
|
||||
secrets:
|
||||
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
|
||||
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}
|
||||
NIX_CACHE_PRIV_KEY: ${{ secrets.NIX_CACHE_PRIV_KEY }}
|
||||
|
||||
push-docker:
|
||||
uses: ./.github/workflows/wf_docker_push_image.yaml
|
||||
needs:
|
||||
- build_artifacts
|
||||
with:
|
||||
NAME: nixops
|
||||
PATH: nixops
|
||||
VERSION: latest
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@@ -32,7 +32,6 @@ linters:
|
||||
- linters:
|
||||
- funlen
|
||||
- ireturn
|
||||
- goconst
|
||||
path: _test\.go
|
||||
- linters:
|
||||
- lll
|
||||
|
||||
@@ -24,20 +24,28 @@ If you find an Issue that addresses the problem you're having, please add your r
|
||||
|
||||
### Pull Requests
|
||||
|
||||
Please have a look at our [developers guide](https://github.com/nhost/nhost/blob/main/DEVELOPERS.md) to start coding!
|
||||
|
||||
PRs to our libraries are always welcome and can be a quick way to get your fix or improvement slated for the next release. In general, PRs should:
|
||||
|
||||
## Monorepo Structure
|
||||
- Only fix/add the functionality in question **OR** address wide-spread whitespace/style issues, not both.
|
||||
- Add unit or integration tests for fixed or changed functionality (if a test suite exists).
|
||||
- Address a single concern in the least number of changed lines as possible.
|
||||
- Include documentation in the repo or on our [docs site](https://docs.nhost.io).
|
||||
- Be accompanied by a complete Pull Request template (loaded automatically when a PR is created).
|
||||
|
||||
This repository is a monorepo that contains multiple packages and applications. The structure is as follows:
|
||||
For changes that address core functionality or require breaking changes (e.g., a major release), it's best to open an Issue to discuss your proposal first. This is not required but can save time creating and reviewing changes.
|
||||
|
||||
- `cli` - The Nhost CLI
|
||||
- `dashboard` - The Nhost Dashboard
|
||||
- `docs` - Documentation
|
||||
- `examples` - Various example projects
|
||||
- `packages/nhost-js` - The Nhost JavaScript/TypeScript SDK
|
||||
- `services/auth` - Nhost Authentication service
|
||||
- `services/storage` - Nhost Storage service
|
||||
- `tools/codegen` - Internal code generation tool to build the SDK
|
||||
- `tools/mintlify-openapi` - Internal tool to generate reference documentation for Mintlify from an OpenAPI spec.
|
||||
In general, we follow the ["fork-and-pull" Git workflow](https://github.com/susam/gitpr)
|
||||
|
||||
For details about those projects and how to contribure, please refer to their respective `README.md` and `CONTRIBUTING.md` files.
|
||||
1. Fork the repository to your own Github account
|
||||
2. Clone the project to your machine
|
||||
3. Create a branch locally with a succinct but descriptive name. All changes should be part of a branch and submitted as a pull request - your branches should be prefixed with one of:
|
||||
- `bug/` for bug fixes
|
||||
- `feat/` for features
|
||||
- `chore/` for configuration changes
|
||||
- `docs/` for documentation changes
|
||||
4. Commit changes to the branch
|
||||
5. Following any formatting and testing guidelines specific to this repo
|
||||
6. Push changes to your fork
|
||||
7. Open a PR in our repository and follow the PR template to review the changes efficiently.
|
||||
|
||||
100
DEVELOPERS.md
Normal file
100
DEVELOPERS.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Developer Guide
|
||||
|
||||
## Requirements
|
||||
|
||||
### Node.js v20 or later
|
||||
|
||||
### [pnpm](https://pnpm.io/) package manager
|
||||
|
||||
The easiest way to install `pnpm` if it's not installed on your machine yet is to use `npm`:
|
||||
|
||||
```sh
|
||||
$ npm install -g pnpm
|
||||
```
|
||||
|
||||
### [Nhost CLI](https://docs.nhost.io/platform/cli/local-development)
|
||||
|
||||
- The CLI is primarily used for running the E2E tests
|
||||
- Please refer to the [installation guide](https://docs.nhost.io/platform/cli/local-development) if you have not installed it yet
|
||||
|
||||
## File Structure
|
||||
|
||||
The repository is organized as a monorepo, with the following structure (only relevant folders are shown):
|
||||
|
||||
```
|
||||
assets/ # Assets used in the README
|
||||
config/ # Configuration files for the monorepo
|
||||
dashboard/ # Dashboard
|
||||
docs/ # Documentation website
|
||||
examples/ # Example projects
|
||||
packages/ # Core packages
|
||||
integrations/ # These are packages that rely on the core packages
|
||||
```
|
||||
|
||||
## Get started
|
||||
|
||||
### Installation
|
||||
|
||||
First, clone this repository:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/nhost/nhost
|
||||
```
|
||||
|
||||
Then, install the dependencies with `pnpm`:
|
||||
|
||||
```sh
|
||||
$ cd nhost
|
||||
$ pnpm install
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
Although package references are correctly updated on the fly for TypeScript, example projects and the dashboard won't see the changes because they are depending on the build output. To fix this, you can run packages in development mode.
|
||||
|
||||
Running packages in development mode from the root folder is as simple as:
|
||||
|
||||
```sh
|
||||
$ pnpm dev
|
||||
```
|
||||
|
||||
Our packages are linked together using [PNPM's workspace](https://pnpm.io/workspaces) feature. Next.js and Vite automatically detect changes in the dependencies and rebuild everything, so the changes will be reflected in the examples and the dashboard.
|
||||
|
||||
**Note:** It's possible that Next.js or Vite throw an error when you run `pnpm dev`. Restarting the process should fix it.
|
||||
|
||||
### Use Examples
|
||||
|
||||
Examples are a great way to test your changes in practice. Make sure you've `pnpm dev` running in your terminal and then run an example.
|
||||
|
||||
Let's follow the instructions to run [react-apollo example](https://github.com/nhost/nhost/blob/main/examples/react-apollo/README.md).
|
||||
|
||||
## Edit Documentation
|
||||
|
||||
The easier way to contribute to our documentation is to go to the `docs` folder and follow the [instructions to start local development](https://github.com/nhost/nhost/blob/main/docs/README.md):
|
||||
|
||||
```sh
|
||||
$ cd docs
|
||||
# not necessary if you've already done this step somewhere in the repository
|
||||
$ pnpm install
|
||||
$ pnpm start
|
||||
```
|
||||
|
||||
## Run Test Suites
|
||||
|
||||
### Unit Tests
|
||||
|
||||
You can run the unit tests with the following command from the repository root:
|
||||
|
||||
```sh
|
||||
$ pnpm test
|
||||
```
|
||||
|
||||
### E2E Tests
|
||||
|
||||
Each package that defines end-to-end tests embeds their own Nhost configuration, that will be automatically when running the tests. As a result, you must make sure you are not running the Nhost CLI before running the tests.
|
||||
|
||||
You can run the e2e tests with the following command from the repository root:
|
||||
|
||||
```sh
|
||||
$ pnpm e2e
|
||||
```
|
||||
16
Makefile
16
Makefile
@@ -1,16 +0,0 @@
|
||||
.PHONY: envrc-install
|
||||
envrc-install: ## Copy envrc.sample to all project folders
|
||||
@for f in $$(find . -name "project.nix"); do \
|
||||
echo "Copying envrc.sample to $$(dirname $$f)/.envrc"; \
|
||||
cp ./envrc.sample $$(dirname $$f)/.envrc; \
|
||||
done
|
||||
|
||||
.PHONY: nixops-container-env
|
||||
nixops-container-env: ## Enter a NixOS container environment
|
||||
docker run \
|
||||
-it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ./:/build \
|
||||
-w /build \
|
||||
nixops:0.0.0-dev \
|
||||
bash
|
||||
@@ -12,7 +12,7 @@
|
||||
<span> • </span>
|
||||
<a href="https://nhost.io/blog">Blog</a>
|
||||
<span> • </span>
|
||||
<a href="https://x.com/nhost">X</a>
|
||||
<a href="https://twitter.com/nhost">Twitter</a>
|
||||
<span> • </span>
|
||||
<a href="https://nhost.io/discord">Discord</a>
|
||||
<span> • </span>
|
||||
@@ -33,10 +33,10 @@ Nhost consists of open source software:
|
||||
|
||||
- Database: [PostgreSQL](https://www.postgresql.org/)
|
||||
- Instant GraphQL API: [Hasura](https://hasura.io/)
|
||||
- Authentication: [Auth](https://github.com/nhost/nhost/tree/main/services/auth)
|
||||
- Storage: [Storage](https://github.com/nhost/nhost/tree/main/services/storage)
|
||||
- Authentication: [Hasura Auth](https://github.com/nhost/hasura-auth/)
|
||||
- Storage: [Hasura Storage](https://github.com/nhost/hasura-storage)
|
||||
- Serverless Functions: Node.js (JavaScript and TypeScript)
|
||||
- [Nhost CLI](https://github.com/nhost/nhost/tree/main/cli) for local development
|
||||
- [Nhost CLI](https://docs.nhost.io/platform/cli/local-development) for local development
|
||||
|
||||
## Architecture of Nhost
|
||||
|
||||
@@ -107,6 +107,7 @@ Nhost is frontend agnostic, which means Nhost works with all frontend frameworks
|
||||
# Resources
|
||||
|
||||
- Start developing locally with the [Nhost CLI](https://docs.nhost.io/platform/cli/local-development)
|
||||
|
||||
## Nhost Clients
|
||||
|
||||
- [JavaScript/TypeScript](https://docs.nhost.io/reference/javascript/nhost-js/main)
|
||||
|
||||
@@ -2,7 +2,5 @@
|
||||
// $schema provides code completion hints to IDEs.
|
||||
"$schema": "https://github.com/IBM/audit-ci/raw/main/docs/schema.json",
|
||||
"moderate": true,
|
||||
"allowlist": [
|
||||
"GHSA-9965-vmph-33xx" // https://github.com/advisories/GHSA-9965-vmph-33xx Update package once have a fix
|
||||
]
|
||||
"allowlist": ["vue-template-compiler", { "id": "CVE-2025-48068", "path": "next" }]
|
||||
}
|
||||
|
||||
@@ -54,11 +54,6 @@ get-version: ## Return version
|
||||
@echo $(VERSION)
|
||||
|
||||
|
||||
.PHONY: develop
|
||||
develop: ## Start a nix develop shell
|
||||
nix develop .\#$(NAME)
|
||||
|
||||
|
||||
.PHONY: _check-pre
|
||||
_check-pre: ## Pre-checks before running nix flake check
|
||||
|
||||
@@ -110,11 +105,6 @@ build-docker-image: ## Build docker container for native architecture
|
||||
skopeo copy --insecure-policy dir:./result docker-daemon:$(NAME):$(VERSION)
|
||||
|
||||
|
||||
.PHONY: build-docker-image-import-bare
|
||||
build-docker-image-import-bare:
|
||||
skopeo copy --insecure-policy dir:./result docker-daemon:$(NAME):$(VERSION)
|
||||
|
||||
|
||||
.PHONY: dev-env-up
|
||||
dev-env-up: _dev-env-build _dev-env-up ## Starts development environment
|
||||
|
||||
|
||||
@@ -2,19 +2,6 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [cli@1.34.1] - 2025-10-13
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(cli)* Remove references to mcp-nhost (#3575)
|
||||
- *(cli)* Workaround os.Rename issues when src and dst are on different partitions (#3599)
|
||||
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(auth)* Change some references to deprecated hasura-auth (#3584)
|
||||
- *(docs)* Udpated README.md and CONTRIBUTING.md (#3587)
|
||||
|
||||
## [cli@1.34.0] - 2025-10-09
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
# Developer Guide
|
||||
|
||||
## Requirements
|
||||
|
||||
We use nix to manage the development environment, the build process and for running tests.
|
||||
|
||||
### With Nix (Recommended)
|
||||
|
||||
Run `nix develop \#cli` to get a complete development environment.
|
||||
|
||||
### Without Nix
|
||||
|
||||
Check `project.nix` (checkDeps, buildInputs, buildNativeInputs) for manual dependency installation. Alternatively, you can run `make nixops-container-env` in the root of the repository to enter a Docker container with nix and all dependencies pre-installed (note it is a large image).
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Running Tests
|
||||
|
||||
**With Nix:**
|
||||
```bash
|
||||
make dev-env-up
|
||||
make check
|
||||
```
|
||||
|
||||
**Without Nix:**
|
||||
```bash
|
||||
# Start development environment
|
||||
make dev-env-up
|
||||
|
||||
# Lint Go code
|
||||
golangci-lint run ./...
|
||||
|
||||
# Run tests
|
||||
go test -v ./...
|
||||
```
|
||||
|
||||
### Formatting
|
||||
|
||||
Format code before committing:
|
||||
```bash
|
||||
golines -w --base-formatter=gofumpt .
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
### Local Build
|
||||
|
||||
Build the project (output in `./result`):
|
||||
```bash
|
||||
make build
|
||||
```
|
||||
|
||||
### Docker Image
|
||||
|
||||
Build and import Docker image with skopeo:
|
||||
```bash
|
||||
make build-docker-image
|
||||
```
|
||||
|
||||
If you run the command above inside the dockerized nixops-container-env and you get an error like:
|
||||
|
||||
```
|
||||
FATA[0000] writing blob: io: read/write on closed pipe
|
||||
```
|
||||
|
||||
then you need to run the following command outside of the container (needs skopeo installed on the host):
|
||||
|
||||
```bash
|
||||
cd cli
|
||||
make build-docker-image-import-bare
|
||||
```
|
||||
|
||||
### Multi-Platform Builds
|
||||
|
||||
Build for multiple platforms (Darwin/Linux, ARM64/AMD64):
|
||||
```bash
|
||||
make build-multiplatform
|
||||
```
|
||||
|
||||
This produces binaries for:
|
||||
- darwin/arm64
|
||||
- darwin/amd64
|
||||
- linux/arm64
|
||||
- linux/amd64
|
||||
@@ -17,7 +17,7 @@ func actionDump(_ context.Context, cmd *cli.Command) error {
|
||||
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
fmt.Println("Please, run `nhost mcp config` to configure the service.") //nolint:forbidigo
|
||||
fmt.Println("Please, run `mcp-nhost config` to configure the service.") //nolint:forbidigo
|
||||
return cli.Exit("failed to load config file "+err.Error(), 1)
|
||||
}
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ func getConfig(cmd *cli.Command) (*config.Config, error) {
|
||||
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
fmt.Println("Please, run `nhost mcp config` to configure the service.") //nolint:forbidigo
|
||||
fmt.Println("Please, run `mcp-nhost config` to configure the service.") //nolint:forbidigo
|
||||
return nil, cli.Exit("failed to load config file "+err.Error(), 1)
|
||||
}
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ func initInit(
|
||||
|
||||
getclient := &getter.Client{} //nolint:exhaustruct
|
||||
if _, err := getclient.Get(ctx, &getter.Request{ //nolint:exhaustruct
|
||||
Src: "git::https://github.com/nhost/nhost.git//services/auth/email-templates",
|
||||
Src: "git::https://github.com/nhost/hasura-auth.git//email-templates",
|
||||
Dst: "nhost/emails",
|
||||
DisableSymlinks: true,
|
||||
}); err != nil {
|
||||
|
||||
@@ -2,13 +2,10 @@ package software
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/software"
|
||||
@@ -95,8 +92,8 @@ func install(cmd *cli.Command, ce *clienv.CliEnv, tmpFile string) error {
|
||||
|
||||
ce.Infoln("Copying to %s...", curBin)
|
||||
|
||||
if err := moveOrCopyFile(tmpFile, curBin); err != nil {
|
||||
return fmt.Errorf("failed to move %s to %s: %w", tmpFile, curBin, err)
|
||||
if err := os.Rename(tmpFile, curBin); err != nil {
|
||||
return fmt.Errorf("failed to rename %s to %s: %w", tmpFile, curBin, err)
|
||||
}
|
||||
|
||||
ce.Infoln("Setting permissions...")
|
||||
@@ -107,55 +104,3 @@ func install(cmd *cli.Command, ce *clienv.CliEnv, tmpFile string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func moveOrCopyFile(src, dst string) error {
|
||||
if err := os.Rename(src, dst); err != nil {
|
||||
var linkErr *os.LinkError
|
||||
// this happens when moving across different filesystems
|
||||
if errors.As(err, &linkErr) && errors.Is(linkErr.Err, syscall.EXDEV) {
|
||||
if err := hardMove(src, dst); err != nil {
|
||||
return fmt.Errorf("failed to hard move: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to rename: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func hardMove(src, dst string) error {
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open source file: %w", err)
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
dstFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create destination file: %w", err)
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
if _, err := io.Copy(dstFile, srcFile); err != nil {
|
||||
return fmt.Errorf("failed to copy file contents: %w", err)
|
||||
}
|
||||
|
||||
fi, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat source file: %w", err)
|
||||
}
|
||||
|
||||
err = os.Chmod(dst, fi.Mode())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set file permissions: %w", err)
|
||||
}
|
||||
|
||||
if err := os.Remove(src); err != nil {
|
||||
return fmt.Errorf("failed to remove source file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func auth( //nolint:funlen
|
||||
&model.ConfigSmtp{
|
||||
User: "user",
|
||||
Password: "password",
|
||||
Sender: "auth@example.com",
|
||||
Sender: "hasura-auth@example.com",
|
||||
Host: "mailhog",
|
||||
Port: 1025, //nolint:mnd
|
||||
Secure: false,
|
||||
|
||||
@@ -143,7 +143,7 @@ func expectedAuth() *Service {
|
||||
"AUTH_SMTP_PASS": "password",
|
||||
"AUTH_SMTP_PORT": "1025",
|
||||
"AUTH_SMTP_SECURE": "false",
|
||||
"AUTH_SMTP_SENDER": "auth@example.com",
|
||||
"AUTH_SMTP_SENDER": "hasura-auth@example.com",
|
||||
"AUTH_SMTP_USER": "user",
|
||||
"AUTH_USER_DEFAULT_ALLOWED_ROLES": "user,admin",
|
||||
"AUTH_USER_DEFAULT_ROLE": "user",
|
||||
|
||||
@@ -459,7 +459,7 @@ func mailhog(subdomain, volumeName string, useTLS bool) *Service {
|
||||
"SMTP_PASS": "password",
|
||||
"SMTP_PORT": "1025",
|
||||
"SMTP_SECURE": "false",
|
||||
"SMTP_SENDER": "auth@example.com",
|
||||
"SMTP_SENDER": "hasura-auth@example.com",
|
||||
"SMTP_USER": "user",
|
||||
},
|
||||
ExtraHosts: extraHosts(subdomain),
|
||||
|
||||
@@ -44,7 +44,7 @@ if [[ "$version" == "latest" ]]; then
|
||||
release=$(curl --silent https://api.github.com/repos/nhost/nhost/releases\?per_page=100 | grep tag_name | grep \"cli\@ | head -n 1 | sed 's/.*"tag_name": "\([^"]*\)".*/\1/')
|
||||
version=$( echo $release | sed 's/.*@//')
|
||||
else
|
||||
release="cli@$version"
|
||||
release="cli@$release"
|
||||
fi
|
||||
|
||||
# check version exists
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
const (
|
||||
ToolGraphqlQueryName = "cloud-graphql-query"
|
||||
//nolint:lll
|
||||
ToolGraphqlQueryInstructions = `Execute a GraphQL query against the Nhost Cloud to perform operations on projects and organizations. It also allows configuring projects hosted on Nhost Cloud. Make sure you got the schema before attempting to execute any query. If you get an error while performing a query refresh the schema in case something has changed or you did something wrong. If you get an error indicating mutations are not allowed the user may have disabled them in the server, don't retry and ask the user they need to pass --with-cloud-mutations when starting nhost's mcp to enable them. Projects are apps.`
|
||||
ToolGraphqlQueryInstructions = `Execute a GraphQL query against the Nhost Cloud to perform operations on projects and organizations. It also allows configuring projects hosted on Nhost Cloud. Make sure you got the schema before attempting to execute any query. If you get an error while performing a query refresh the schema in case something has changed or you did something wrong. If you get an error indicating mutations are not allowed the user may have disabled them in the server, don't retry and ask the user they need to pass --with-cloud-mutations when starting mcp-nhost to enable them. Projects are apps.`
|
||||
)
|
||||
|
||||
func ptr[T any](v T) *T {
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
const (
|
||||
ToolGraphqlQueryName = "graphql-query"
|
||||
//nolint:lll
|
||||
ToolGraphqlQueryInstructions = `Execute a GraphQL query against a Nhost project. This tool is useful to query and mutate data. If you run into issues executing queries, retrieve the schema again in case the schema has changed. If you get an error indicating the query or mutation is not allowed the user may have disabled them in the server, don't retry and tell the user they need to enable them when starting nhost's mcp`
|
||||
ToolGraphqlQueryInstructions = `Execute a GraphQL query against a Nhost project. This tool is useful to query and mutate data. If you run into issues executing queries, retrieve the schema again in case the schema has changed. If you get an error indicating the query or mutation is not allowed the user may have disabled them in the server, don't retry and tell the user they need to enable them when starting mcp-nhost`
|
||||
)
|
||||
|
||||
func ptr[T any](v T) *T {
|
||||
|
||||
47
dashboard/.storybook/main.js
Normal file
47
dashboard/.storybook/main.js
Normal file
@@ -0,0 +1,47 @@
|
||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'storybook-addon-next-router',
|
||||
{
|
||||
/**
|
||||
* Fix Storybook issue with PostCSS@8
|
||||
* @see https://github.com/storybookjs/storybook/issues/12668#issuecomment-773958085
|
||||
*/
|
||||
name: '@storybook/addon-postcss',
|
||||
options: {
|
||||
postcssLoaderOptions: {
|
||||
implementation: require('postcss'),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
framework: '@storybook/react',
|
||||
core: {
|
||||
builder: '@storybook/builder-webpack5',
|
||||
},
|
||||
features: {
|
||||
emotionAlias: true,
|
||||
},
|
||||
webpackFinal: async (config) => {
|
||||
return {
|
||||
...config,
|
||||
resolve: {
|
||||
...config?.resolve,
|
||||
plugins: [
|
||||
...(config?.resolve?.plugins || []),
|
||||
new TsconfigPathsPlugin(),
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
env: (config) => ({
|
||||
...config,
|
||||
NEXT_PUBLIC_ENV: 'dev',
|
||||
NEXT_PUBLIC_NHOST_PLATFORM: 'false',
|
||||
}),
|
||||
};
|
||||
69
dashboard/.storybook/preview.js
Normal file
69
dashboard/.storybook/preview.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import { NhostProvider } from '@/providers/nhost';
|
||||
import '@fontsource/inter';
|
||||
import '@fontsource/inter/500.css';
|
||||
import '@fontsource/inter/700.css';
|
||||
import { CssBaseline, ThemeProvider } from '@mui/material';
|
||||
import { createClient } from '@nhost/nhost-js-beta';
|
||||
import { NhostApolloProvider } from '@nhost/react-apollo';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { Buffer } from 'buffer';
|
||||
import { initialize, mswDecorator } from 'msw-storybook-addon';
|
||||
import { RouterContext } from 'next/dist/shared/lib/router-context';
|
||||
import { createTheme } from '../src/components/ui/v2/createTheme';
|
||||
import '../src/styles/globals.css';
|
||||
|
||||
global.Buffer = Buffer;
|
||||
|
||||
initialize({ onUnhandledRequest: 'bypass' });
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export const parameters = {
|
||||
nextRouter: {
|
||||
Provider: RouterContext.Provider,
|
||||
isReady: true,
|
||||
},
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const decorators = [
|
||||
(Story, context) => {
|
||||
const isDarkMode = !context.globals?.backgrounds?.value
|
||||
?.toLowerCase()
|
||||
?.startsWith('#f');
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={createTheme(isDarkMode ? 'dark' : 'light')}>
|
||||
<CssBaseline />
|
||||
<Story />
|
||||
</ThemeProvider>
|
||||
);
|
||||
},
|
||||
(Story) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Story />
|
||||
</QueryClientProvider>
|
||||
),
|
||||
(Story) => (
|
||||
<NhostApolloProvider
|
||||
fetchPolicy="cache-first"
|
||||
graphqlUrl="https://local.graphql.nhost.run/v1"
|
||||
>
|
||||
<Story />
|
||||
</NhostApolloProvider>
|
||||
),
|
||||
(Story) => (
|
||||
<NhostProvider
|
||||
nhost={createClient({ subdomain: 'local', region: 'local' })}
|
||||
>
|
||||
<Story />
|
||||
</NhostProvider>
|
||||
),
|
||||
mswDecorator,
|
||||
];
|
||||
@@ -62,6 +62,20 @@ NEXT_PUBLIC_NHOST_HASURA_API_URL=https://local.hasura.local.nhost.run
|
||||
|
||||
This will connect the Nhost Dashboard to your locally running Nhost backend.
|
||||
|
||||
### Storybook
|
||||
|
||||
Components are documented using [Storybook](https://storybook.js.org/). To run Storybook, run the following command:
|
||||
|
||||
```bash
|
||||
pnpm storybook
|
||||
```
|
||||
|
||||
By default, Storybook will run on port `6006`. You can change this by passing the `--port` flag:
|
||||
|
||||
```bash
|
||||
pnpm storybook --port 6007
|
||||
```
|
||||
|
||||
### General Environment Variables
|
||||
|
||||
| Name | Description |
|
||||
|
||||
@@ -105,8 +105,8 @@ test('should create a table with nullable columns', async ({
|
||||
.locator(`li:has-text("${tableName}") #table-management-menu button`)
|
||||
.click();
|
||||
await page.getByText('Edit Table').click();
|
||||
await expect(page.locator('h2:has-text("Edit Table")')).toBeVisible();
|
||||
await expect(page.locator('div[data-testid="id"]')).toBeVisible();
|
||||
expect(page.locator('h2:has-text("Edit Table")')).toBeVisible();
|
||||
expect(page.locator('div[data-testid="id"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should create a table with an identity column', async ({
|
||||
@@ -146,13 +146,13 @@ test('should create a table with an identity column', async ({
|
||||
.locator(`li:has-text("${tableName}") #table-management-menu button`)
|
||||
.click();
|
||||
await page.getByText('Edit Table').click();
|
||||
await expect(page.locator('h2:has-text("Edit Table")')).toBeVisible();
|
||||
await expect(
|
||||
expect(page.locator('h2:has-text("Edit Table")')).toBeVisible();
|
||||
expect(
|
||||
page.locator('button#identityColumnIndex :has-text("identity_column")'),
|
||||
).toBeVisible();
|
||||
await expect(page.locator('[id="columns.3.defaultValue"]')).toBeDisabled();
|
||||
await expect(page.locator('[name="columns.3.isNullable"]')).toBeDisabled();
|
||||
await expect(page.locator('[name="columns.3.isUnique"]')).toBeDisabled();
|
||||
expect(page.locator('[id="columns.3.defaultValue"]')).toBeDisabled();
|
||||
expect(page.locator('[name="columns.3.isNullable"]')).toBeDisabled();
|
||||
expect(page.locator('[name="columns.3.isUnique"]')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should create table with foreign key constraint', async ({
|
||||
@@ -234,46 +234,6 @@ test('should create table with foreign key constraint', async ({
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('should be able to create a table with a composite key', async ({
|
||||
authenticatedNhostPage: page,
|
||||
}) => {
|
||||
await page.getByRole('button', { name: /new table/i }).click();
|
||||
await expect(page.getByText(/create a new table/i)).toBeVisible();
|
||||
|
||||
const tableName = snakeCase(faker.lorem.words(3));
|
||||
|
||||
await prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKeys: ['id', 'second_id'],
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'second_id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'name', type: 'text' },
|
||||
],
|
||||
});
|
||||
|
||||
await expect(page.locator('div[data-testid="id"]')).toBeVisible();
|
||||
await expect(page.locator('div[data-testid="second_id"]')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default/public/${tableName}`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: tableName, exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.locator(`li:has-text("${tableName}") #table-management-menu button`)
|
||||
.click();
|
||||
await page.getByText('Edit Table').click();
|
||||
await expect(page.locator('div[data-testid="id"]')).toBeVisible();
|
||||
await expect(page.locator('div[data-testid="second_id"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should not be able to create a table with a name that already exists', async ({
|
||||
authenticatedNhostPage: page,
|
||||
}) => {
|
||||
@@ -320,3 +280,40 @@ test('should not be able to create a table with a name that already exists', asy
|
||||
page.getByText(/error: a table with this name already exists/i),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('should be able to create a table with a composite key', async ({
|
||||
authenticatedNhostPage: page,
|
||||
}) => {
|
||||
await page.getByRole('button', { name: /new table/i }).click();
|
||||
await expect(page.getByText(/create a new table/i)).toBeVisible();
|
||||
|
||||
const tableName = snakeCase(faker.lorem.words(3));
|
||||
|
||||
await prepareTable({
|
||||
page,
|
||||
name: tableName,
|
||||
primaryKeys: ['id', 'second_id'],
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'second_id', type: 'uuid', defaultValue: 'gen_random_uuid()' },
|
||||
{ name: 'name', type: 'text' },
|
||||
],
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForURL(
|
||||
`/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/database/browser/default/public/${tableName}`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: tableName, exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.locator(`li:has-text("${tableName}") #table-management-menu button`)
|
||||
.click();
|
||||
await page.getByText('Edit Table').click();
|
||||
expect(page.locator('div[data-testid="id"]')).toBeVisible();
|
||||
expect(page.locator('div[data-testid="second_id"]')).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -41,13 +41,12 @@ export const TEST_USER_PASSWORD = process.env.NHOST_TEST_USER_PASSWORD!;
|
||||
|
||||
export const TEST_PERSONAL_ORG_SLUG = process.env.NHOST_TEST_PERSONAL_ORG_SLUG!;
|
||||
|
||||
export const TEST_ONBOARDING_USER = process.env.NHOST_TEST_ONBOARDING_USER!;
|
||||
const freeUserEmails = process.env.NHOST_TEST_FREE_USER_EMAILS!;
|
||||
|
||||
export const TEST_FREE_USER_EMAILS: string[] = JSON.parse(freeUserEmails);
|
||||
|
||||
/**
|
||||
* Name of the remote schema serverless function to test against.
|
||||
*/
|
||||
export const TEST_PROJECT_REMOTE_SCHEMA_NAME =
|
||||
process.env.NHOST_TEST_PROJECT_REMOTE_SCHEMA_NAME!;
|
||||
|
||||
export const TEST_STAGING_SUBDOMAIN = process.env.NHOST_TEST_STAGING_SUBDOMAIN!;
|
||||
export const TEST_STAGING_REGION = process.env.NHOST_TEST_STAGING_REGION!;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { expect, test } from '@/e2e/fixtures/auth-hook';
|
||||
import {
|
||||
cleanupOnboardingTestIfNeeded,
|
||||
getCardExpiration,
|
||||
getOrgSlugFromUrl,
|
||||
getProjectSlugFromUrl,
|
||||
gotoUrl,
|
||||
loginWithFreeUser,
|
||||
setFreeUserStarterOrgSlug,
|
||||
setNewProjectName,
|
||||
setNewProjectSlug,
|
||||
} from '@/e2e/utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { Page } from '@playwright/test';
|
||||
@@ -12,15 +15,13 @@ import type { Page } from '@playwright/test';
|
||||
let page: Page;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
await cleanupOnboardingTestIfNeeded();
|
||||
|
||||
page = await browser.newPage();
|
||||
await loginWithFreeUser(page);
|
||||
});
|
||||
|
||||
test('user should be able to finish onboarding', async () => {
|
||||
await gotoUrl(page, `/onboarding`);
|
||||
await expect(page.getByText('Welcome to Nhost!')).toBeVisible();
|
||||
expect(page.getByText('Welcome to Nhost!')).toBeVisible();
|
||||
const organizationName = faker.lorem.words(3).slice(0, 32);
|
||||
|
||||
await page.getByLabel('Organization Name').fill(organizationName);
|
||||
@@ -67,28 +68,34 @@ test('user should be able to finish onboarding', async () => {
|
||||
.getByTestId('hosted-payment-submit-button')
|
||||
.click({ force: true });
|
||||
|
||||
await expect(
|
||||
expect(
|
||||
page.getByText('Processing new organization request').first(),
|
||||
).toBeVisible();
|
||||
await page.waitForSelector(
|
||||
'div:has-text("Organization created successfully. Redirecting...")',
|
||||
);
|
||||
|
||||
await expect(page.getByText('Create Your First Project')).toBeVisible();
|
||||
expect(page.getByText('Create Your First Project')).toBeVisible();
|
||||
|
||||
const projectName = faker.lorem.words(3).slice(0, 32);
|
||||
await page.getByLabel('Project Name').fill(projectName);
|
||||
|
||||
await page.getByText('Create Project', { exact: true }).click();
|
||||
|
||||
await expect(page.getByText('Creating your project...')).toBeVisible();
|
||||
await expect(page.getByText('Project created successfully!')).toBeVisible();
|
||||
expect(page.getByText('Creating your project...')).toBeVisible();
|
||||
expect(page.getByText('Project created successfully!')).toBeVisible();
|
||||
|
||||
await expect(page.getByText('Internal info')).toBeVisible();
|
||||
expect(page.getByText('Internal info')).toBeVisible();
|
||||
|
||||
await page.waitForSelector('h3:has-text("Project Health")', {
|
||||
timeout: 180000,
|
||||
});
|
||||
|
||||
const newProjectSlug = getProjectSlugFromUrl(page.url());
|
||||
setNewProjectSlug(newProjectSlug);
|
||||
setNewProjectName(organizationName);
|
||||
const newOrgSlug = getOrgSlugFromUrl(page.url());
|
||||
setFreeUserStarterOrgSlug(newOrgSlug);
|
||||
});
|
||||
|
||||
test('should delete the new organization', async () => {
|
||||
@@ -100,12 +107,12 @@ test('should delete the new organization', async () => {
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
|
||||
await page.waitForSelector('h2:has-text("Delete Organization")');
|
||||
await expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
|
||||
expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
|
||||
|
||||
await page.getByLabel("I'm sure I want to delete this Organization").click();
|
||||
await expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
|
||||
expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
|
||||
await page.getByLabel('I understand this action cannot be undone').click();
|
||||
await expect(page.getByTestId('deleteOrgButton')).not.toBeDisabled();
|
||||
expect(page.getByTestId('deleteOrgButton')).not.toBeDisabled();
|
||||
|
||||
await page.getByTestId('deleteOrgButton').click();
|
||||
|
||||
@@ -138,7 +145,7 @@ test('should be able to upgrade an organization', async () => {
|
||||
await page.getByRole('link', { name: 'Billing' }).click();
|
||||
|
||||
await page.waitForSelector('h4:has-text("Subscription plan")');
|
||||
await expect(page.getByText('Upgrade')).toBeEnabled();
|
||||
expect(page.getByText('Upgrade')).toBeEnabled();
|
||||
await page.getByText('Upgrade').click();
|
||||
await page.waitForSelector('h2:has-text("Upgrade Organization")');
|
||||
|
||||
@@ -198,12 +205,12 @@ test('should be able to upgrade an organization', async () => {
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
|
||||
await page.waitForSelector('h2:has-text("Delete Organization")');
|
||||
await expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
|
||||
expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
|
||||
|
||||
await page.getByLabel("I'm sure I want to delete this Organization").click();
|
||||
await expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
|
||||
expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
|
||||
await page.getByLabel('I understand this action cannot be undone').click();
|
||||
await expect(page.getByTestId('deleteOrgButton')).not.toBeDisabled();
|
||||
expect(page.getByTestId('deleteOrgButton')).not.toBeDisabled();
|
||||
|
||||
await page.getByTestId('deleteOrgButton').click();
|
||||
|
||||
|
||||
@@ -4,64 +4,61 @@ import {
|
||||
TEST_PROJECT_SUBDOMAIN,
|
||||
} from '@/e2e/env';
|
||||
import { expect, test } from '@/e2e/fixtures/auth-hook';
|
||||
import { cleanupRemoteSchemaTestIfNeeded } from '@/e2e/utils';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { snakeCase } from 'snake-case';
|
||||
|
||||
const REMOTE_SCHEMA_TEST_URL = `https://${TEST_PROJECT_SUBDOMAIN}.functions.eu-central-1.staging.nhost.run/v1/${TEST_PROJECT_REMOTE_SCHEMA_NAME}`;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await cleanupRemoteSchemaTestIfNeeded();
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ authenticatedNhostPage: page }) => {
|
||||
const remoteSchemasRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/graphql/remote-schemas`;
|
||||
await page.goto(remoteSchemasRoute);
|
||||
await page.waitForURL(remoteSchemasRoute);
|
||||
});
|
||||
|
||||
test('should create and delete a remote schema from URL', async ({
|
||||
authenticatedNhostPage: page,
|
||||
}) => {
|
||||
await page.getByRole('button', { name: /add remote schema/i }).click();
|
||||
await expect(page.getByText(/create a new remote schema/i)).toBeVisible();
|
||||
|
||||
const schemaName = snakeCase(`e2e ${faker.lorem.words(2)}`);
|
||||
|
||||
await page.getByPlaceholder(/remote schema name/i).fill(schemaName);
|
||||
await page
|
||||
.getByPlaceholder(/graphql-service\.example\.com/i)
|
||||
.fill(REMOTE_SCHEMA_TEST_URL);
|
||||
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForSelector(
|
||||
'div:has-text("The remote schema has been created successfully.")',
|
||||
);
|
||||
|
||||
const detailsUrl = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/graphql/remote-schemas/${schemaName}`;
|
||||
await page.waitForURL(detailsUrl);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: schemaName, exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
const schemaLink = page.getByRole('link', {
|
||||
name: schemaName,
|
||||
exact: true,
|
||||
test.describe('Remote Schemas', () => {
|
||||
test.beforeEach(async ({ authenticatedNhostPage: page }) => {
|
||||
const remoteSchemasRoute = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/graphql/remote-schemas`;
|
||||
await page.goto(remoteSchemasRoute);
|
||||
await page.waitForURL(remoteSchemasRoute);
|
||||
});
|
||||
|
||||
await schemaLink.hover();
|
||||
await page
|
||||
.getByRole('listitem')
|
||||
.filter({ hasText: schemaName })
|
||||
.getByRole('button')
|
||||
.click();
|
||||
test('should create and delete a remote schema from URL', async ({
|
||||
authenticatedNhostPage: page,
|
||||
}) => {
|
||||
await page.getByRole('button', { name: /add remote schema/i }).click();
|
||||
await expect(page.getByText(/create a new remote schema/i)).toBeVisible();
|
||||
|
||||
await page.getByRole('menuitem', { name: /delete remote schema/i }).click();
|
||||
await page.getByRole('button', { name: /^delete$/i }).click();
|
||||
const schemaName = snakeCase(`e2e ${faker.lorem.words(2)}`);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: schemaName, exact: true }),
|
||||
).toHaveCount(0);
|
||||
await page.getByPlaceholder(/remote schema name/i).fill(schemaName);
|
||||
await page
|
||||
.getByPlaceholder(/graphql-service\.example\.com/i)
|
||||
.fill(REMOTE_SCHEMA_TEST_URL);
|
||||
|
||||
await page.getByRole('button', { name: /create/i }).click();
|
||||
|
||||
await page.waitForSelector(
|
||||
'div:has-text("The remote schema has been created successfully.")',
|
||||
);
|
||||
|
||||
const detailsUrl = `/orgs/${TEST_ORGANIZATION_SLUG}/projects/${TEST_PROJECT_SUBDOMAIN}/graphql/remote-schemas/${schemaName}`;
|
||||
await page.waitForURL(detailsUrl);
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: schemaName, exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
const schemaLink = page.getByRole('link', {
|
||||
name: schemaName,
|
||||
exact: true,
|
||||
});
|
||||
|
||||
await schemaLink.hover();
|
||||
await page
|
||||
.getByRole('listitem')
|
||||
.filter({ hasText: schemaName })
|
||||
.getByRole('button')
|
||||
.click();
|
||||
|
||||
await page.getByRole('menuitem', { name: /delete remote schema/i }).click();
|
||||
await page.getByRole('button', { name: /^delete$/i }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('link', { name: schemaName, exact: true }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import {
|
||||
TEST_ONBOARDING_USER,
|
||||
TEST_FREE_USER_EMAILS,
|
||||
TEST_ORGANIZATION_SLUG,
|
||||
TEST_PROJECT_ADMIN_SECRET,
|
||||
TEST_PROJECT_SUBDOMAIN,
|
||||
TEST_STAGING_REGION,
|
||||
TEST_STAGING_SUBDOMAIN,
|
||||
TEST_USER_PASSWORD,
|
||||
} from '@/e2e/env';
|
||||
import { expect } from '@/e2e/fixtures/auth-hook';
|
||||
import { isEmptyValue } from '@/lib/utils';
|
||||
import type { ExportMetadataResponse } from '@/utils/hasura-api/generated/schemas';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { type Page } from '@playwright/test';
|
||||
import { add, format } from 'date-fns-v4';
|
||||
@@ -132,26 +125,12 @@ export async function prepareTable({
|
||||
),
|
||||
);
|
||||
await page.getByLabel('Primary Key').click();
|
||||
|
||||
await page
|
||||
.getByRole('option', { name: columns[0].name, exact: true })
|
||||
.waitFor({ timeout: 1000 });
|
||||
await expect(
|
||||
page.getByRole('option', { name: columns[0].name, exact: true }),
|
||||
).toBeVisible();
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const primaryKey of primaryKeys) {
|
||||
await page.waitForTimeout(1000);
|
||||
await page.getByRole('option', { name: primaryKey, exact: true }).click();
|
||||
await page
|
||||
.locator(`div[data-testid="${primaryKey}"]`)
|
||||
.waitFor({ timeout: 1000 });
|
||||
}
|
||||
await Promise.all(
|
||||
primaryKeys.map(async (primaryKey) => {
|
||||
await page.getByRole('option', { name: primaryKey, exact: true }).click();
|
||||
}),
|
||||
);
|
||||
await page.getByText('Create a New Table').click();
|
||||
await page.waitForTimeout(1000);
|
||||
await expect(
|
||||
page.getByRole('option', { name: columns[0].name, exact: true }),
|
||||
).not.toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,6 +232,42 @@ export async function gotoUrl(page: Page, url: string) {
|
||||
await page.waitForURL(url, { waitUntil: 'load' });
|
||||
}
|
||||
|
||||
let newOrgSlug: string;
|
||||
|
||||
export function getNewOrgSlug() {
|
||||
return newOrgSlug;
|
||||
}
|
||||
|
||||
export function setNewOrgSlug(slug: string) {
|
||||
newOrgSlug = slug;
|
||||
}
|
||||
|
||||
let freeUserStarterOrgSlug: string;
|
||||
|
||||
export function getFreeUserStarterOrgSlug() {
|
||||
return freeUserStarterOrgSlug;
|
||||
}
|
||||
|
||||
export function setFreeUserStarterOrgSlug(slug: string) {
|
||||
freeUserStarterOrgSlug = slug;
|
||||
}
|
||||
|
||||
let newProjectSlug: string;
|
||||
|
||||
export function getNewProjectSlug() {
|
||||
return newProjectSlug;
|
||||
}
|
||||
|
||||
export function setNewProjectSlug(slug: string) {
|
||||
newProjectSlug = slug;
|
||||
}
|
||||
|
||||
export function getProjectSlugFromUrl(url: string) {
|
||||
const [, projectSlug] = url.split('/projects/');
|
||||
|
||||
return projectSlug;
|
||||
}
|
||||
|
||||
export function getOrgSlugFromUrl(url: string) {
|
||||
const orgSlug = url.split('/orgs/')[1].split('/projects/')[0];
|
||||
return orgSlug;
|
||||
@@ -263,13 +278,33 @@ export function getCardExpiration() {
|
||||
return format(now, 'MMyy');
|
||||
}
|
||||
|
||||
let newProjectName: string;
|
||||
|
||||
export function getNewProjectName() {
|
||||
return newProjectName;
|
||||
}
|
||||
|
||||
export function setNewProjectName(name: string) {
|
||||
newProjectName = name;
|
||||
}
|
||||
|
||||
function getRandomUserIndex(): number {
|
||||
return Math.floor(Math.random() * TEST_FREE_USER_EMAILS.length);
|
||||
}
|
||||
|
||||
export async function loginWithFreeUser(page: Page) {
|
||||
const userIndex = getRandomUserIndex();
|
||||
|
||||
const freeUserEmail = TEST_FREE_USER_EMAILS[userIndex];
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Selected userIndex: ${userIndex}`);
|
||||
await page.goto('/');
|
||||
await page.waitForURL('/signin');
|
||||
await page.getByRole('link', { name: /continue with email/i }).click();
|
||||
|
||||
await page.waitForURL('/signin/email');
|
||||
await page.getByLabel('Email').fill(TEST_ONBOARDING_USER);
|
||||
await page.getByLabel('Email').fill(freeUserEmail);
|
||||
await page.getByLabel('Password').fill(TEST_USER_PASSWORD);
|
||||
await page.getByRole('button', { name: /sign in/i }).click();
|
||||
|
||||
@@ -284,132 +319,3 @@ export function toPascalCase(str: string, divider = ' ') {
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join('');
|
||||
}
|
||||
|
||||
export async function cleanupOnboardingTestIfNeeded() {
|
||||
const signinUrl = `https://${TEST_STAGING_SUBDOMAIN}.auth.${TEST_STAGING_REGION}.nhost.run/v1/signin/email-password`;
|
||||
const graphqlUrl = `https://${TEST_STAGING_SUBDOMAIN}.graphql.${TEST_STAGING_REGION}.nhost.run/v1`;
|
||||
|
||||
try {
|
||||
const response = await fetch(signinUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email: TEST_ONBOARDING_USER,
|
||||
password: TEST_USER_PASSWORD,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
const userId = data.session?.user?.id;
|
||||
const accessToken = data.session?.accessToken;
|
||||
const organizationPayload = {
|
||||
query: `
|
||||
query {
|
||||
organizations(where: { members: {userID: {_eq: "${userId}"}} }) {
|
||||
id
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
||||
const authHeader = `Bearer ${accessToken}`;
|
||||
|
||||
const orgResponse = await fetch(graphqlUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: authHeader,
|
||||
},
|
||||
body: JSON.stringify(organizationPayload),
|
||||
});
|
||||
|
||||
const orgData = await orgResponse.json();
|
||||
|
||||
const organizations = orgData.data?.organizations;
|
||||
|
||||
if (organizations && organizations.length > 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Cleaning up organization');
|
||||
await Promise.all(
|
||||
organizations.map(({ id }) =>
|
||||
fetch(graphqlUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: authHeader,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: `
|
||||
mutation {
|
||||
billingDeleteOrganization(organizationID: "${id}")
|
||||
}
|
||||
`,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanupRemoteSchemaTestIfNeeded() {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://${TEST_PROJECT_SUBDOMAIN}.hasura.eu-central-1.staging.nhost.run/v1/metadata`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-hasura-admin-secret': TEST_PROJECT_ADMIN_SECRET,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: 'export_metadata',
|
||||
version: 2,
|
||||
args: {},
|
||||
}),
|
||||
},
|
||||
);
|
||||
const data = (await response.json()) as ExportMetadataResponse;
|
||||
|
||||
const remoteSchemas = data.metadata?.remote_schemas;
|
||||
|
||||
if (isEmptyValue(remoteSchemas)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const schemasToDelete = remoteSchemas!.filter((remoteSchema) =>
|
||||
/^e2e_\w+_\w+$/.test(remoteSchema.name),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
schemasToDelete.map((remoteSchema) =>
|
||||
fetch(
|
||||
`https://${TEST_PROJECT_SUBDOMAIN}.hasura.eu-central-1.staging.nhost.run/v1/metadata`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-hasura-admin-secret': TEST_PROJECT_ADMIN_SECRET,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
args: [
|
||||
{
|
||||
type: 'remove_remote_schema',
|
||||
args: {
|
||||
name: remoteSchema.name,
|
||||
},
|
||||
},
|
||||
],
|
||||
source: 'default',
|
||||
type: 'bulk',
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,15 +9,15 @@
|
||||
"analyze": "ANALYZE=true pnpm build --no-lint",
|
||||
"start": "next start",
|
||||
"lint": "next lint --max-warnings 0",
|
||||
"test": "pnpm lint && pnpm test:vitest",
|
||||
"test:vitest": "vitest --run",
|
||||
"test": "vitest --run",
|
||||
"test:watch": "vitest",
|
||||
"generate": "echo 'This needs to be fixed.'",
|
||||
"codegen": "DOTENV_CONFIG_PATH=./.env.local graphql-codegen -r dotenv/config --config graphql.config.yaml --errors-only",
|
||||
"codegen-graphite": "graphql-codegen --config graphite.graphql.config.yaml --errors-only",
|
||||
"codegen-hasura-api": "orval --config src/utils/hasura-api/orval.config.ts",
|
||||
"format": "prettier --write \"src/**/*.{js,ts,tsx,jsx,json,md}\" --plugin-search-dir=.",
|
||||
"install-browsers": "pnpm playwright install && pnpm playwright install-deps",
|
||||
"storybook": "start-storybook -p 6006 -s public",
|
||||
"build-storybook": "build-storybook",
|
||||
"e2e:tests": "pnpm playwright test --config=playwright.config.ts -x",
|
||||
"e2e": "pnpm e2e:tests --project=main",
|
||||
"e2e:local": "pnpm e2e:tests --project=local",
|
||||
@@ -110,6 +110,7 @@
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-intersection-observer": "^9.8.1",
|
||||
"react-is": "18.2.0",
|
||||
"react-loading-skeleton": "^2.2.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-merge-refs": "^3.0.2",
|
||||
"react-resizable-layout": "^0.7.2",
|
||||
@@ -134,12 +135,21 @@
|
||||
"@babel/core": "^7.24.3",
|
||||
"@eslint/js": "9.26.0",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@graphql-codegen/cli": "^6.0.0",
|
||||
"@graphql-codegen/cli": "^5.0.2",
|
||||
"@graphql-codegen/typescript": "^3.0.4",
|
||||
"@graphql-codegen/typescript-operations": "^3.0.4",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||
"@next/bundle-analyzer": "^12.3.4",
|
||||
"@playwright/test": "1.54.1",
|
||||
"@storybook/addon-actions": "^6.5.16",
|
||||
"@storybook/addon-essentials": "^6.5.16",
|
||||
"@storybook/addon-interactions": "^6.5.16",
|
||||
"@storybook/addon-links": "^6.5.16",
|
||||
"@storybook/addon-postcss": "^2.0.0",
|
||||
"@storybook/builder-webpack5": "^6.5.16",
|
||||
"@storybook/manager-webpack5": "^6.5.16",
|
||||
"@storybook/react": "^7.6.17",
|
||||
"@storybook/testing-library": "^0.2.2",
|
||||
"@tailwindcss/typography": "^0.5.12",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
@@ -149,7 +159,7 @@
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
"@types/node": "^20.14.8",
|
||||
"@types/node": "^16.18.93",
|
||||
"@types/pluralize": "^0.0.30",
|
||||
"@types/react": "18.2.73",
|
||||
"@types/react-dom": "^18.2.23",
|
||||
@@ -159,8 +169,8 @@
|
||||
"@types/validator": "^13.11.9",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@vitest/coverage-v8": "^0.32.4",
|
||||
"audit-ci": "^6.6.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"babel-loader": "^8.3.0",
|
||||
@@ -184,21 +194,24 @@
|
||||
"eslint-plugin-vue": "^9.26.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"lint-staged": "^15.2.2",
|
||||
"msw": "^2.11.4",
|
||||
"msw": "^1.3.5",
|
||||
"msw-storybook-addon": "^1.10.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"orval": "^7.11.2",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"react-date-fns-hooks": "^0.9.4",
|
||||
"require-from-string": "^2.0.2",
|
||||
"snake-case": "^3.0.4",
|
||||
"storybook-addon-next-router": "^4.0.2",
|
||||
"tailwindcss": "^3.4.12",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths-webpack-plugin": "^4.1.0",
|
||||
"vite": "^5.4.20",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^0.32.4"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -229,9 +242,6 @@
|
||||
"@lezer/highlight": "^1.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild@<=0.24.2": ">=0.25.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@ dotenv.config({ path: path.resolve(__dirname, '.env.test') });
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
maxFailures: process.env.CI ? 3 : 1,
|
||||
maxFailures: 1,
|
||||
timeout: 120 * 1000,
|
||||
expect: {
|
||||
timeout: 10000,
|
||||
},
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
retries: 2,
|
||||
workers: 1,
|
||||
reporter: 'html',
|
||||
use: {
|
||||
|
||||
10004
dashboard/pnpm-lock.yaml
generated
10004
dashboard/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -66,6 +66,7 @@ let
|
||||
"${submodule}/tsconfig.test.json"
|
||||
"${submodule}/vitest.config.ts"
|
||||
"${submodule}/vitest.global-setup.ts"
|
||||
(inDirectory "${submodule}/.storybook")
|
||||
(inDirectory "${submodule}/e2e")
|
||||
(inDirectory "${submodule}/public")
|
||||
(inDirectory "${submodule}/src")
|
||||
@@ -212,3 +213,5 @@ rec {
|
||||
}).copyTo}/bin/copy-to dir:$out
|
||||
'';
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import type { PropsWithoutRef } from 'react';
|
||||
|
||||
import type { ReadOnlyToggleProps } from './ReadOnlyToggle';
|
||||
import ReadOnlyToggle from './ReadOnlyToggle';
|
||||
|
||||
export default {
|
||||
title: 'Common Components / ReadOnlyToggle',
|
||||
component: ReadOnlyToggle,
|
||||
argTypes: {
|
||||
checked: {
|
||||
options: [null, true, false],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof ReadOnlyToggle>;
|
||||
|
||||
const Template: ComponentStory<typeof ReadOnlyToggle> = function Template(
|
||||
args: PropsWithoutRef<ReadOnlyToggleProps>,
|
||||
) {
|
||||
return <ReadOnlyToggle {...args} />;
|
||||
};
|
||||
|
||||
export const Null = Template.bind({});
|
||||
Null.args = {
|
||||
checked: null,
|
||||
};
|
||||
|
||||
export const True = Template.bind({});
|
||||
True.args = {
|
||||
checked: true,
|
||||
};
|
||||
|
||||
export const False = Template.bind({});
|
||||
False.args = {
|
||||
checked: false,
|
||||
};
|
||||
|
||||
export const CustomClasses = Template.bind({});
|
||||
CustomClasses.args = {
|
||||
checked: true,
|
||||
className: '!bg-red-500',
|
||||
slotProps: {
|
||||
label: {
|
||||
className: '!text-sm !text-white',
|
||||
},
|
||||
},
|
||||
};
|
||||
125
dashboard/src/components/ui/v2/Button/Button.stories.tsx
Normal file
125
dashboard/src/components/ui/v2/Button/Button.stories.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { PlusCircleIcon } from '@/components/ui/v2/icons/PlusCircleIcon';
|
||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import type { ButtonProps } from './Button';
|
||||
import Button from './Button';
|
||||
|
||||
export default {
|
||||
title: 'UI Library / Button',
|
||||
component: Button,
|
||||
argTypes: {
|
||||
variant: {
|
||||
options: ['contained', 'outlined', 'borderless'],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
color: {
|
||||
options: ['primary', 'secondary', 'error'],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
disabled: {
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
size: {
|
||||
options: ['small', 'medium', 'large'],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
},
|
||||
} as Meta<typeof Button>;
|
||||
|
||||
const Template: StoryFn<ButtonProps> = function TemplateFunction(
|
||||
args: ButtonProps,
|
||||
) {
|
||||
return <Button {...args} />;
|
||||
};
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
Primary.args = {
|
||||
children: 'Button',
|
||||
color: 'primary',
|
||||
};
|
||||
|
||||
export const PrimaryOutlined = Template.bind({});
|
||||
PrimaryOutlined.args = {
|
||||
children: 'Button',
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
};
|
||||
|
||||
export const PrimaryBorderless = Template.bind({});
|
||||
PrimaryBorderless.args = {
|
||||
children: 'Button',
|
||||
variant: 'borderless',
|
||||
color: 'primary',
|
||||
};
|
||||
|
||||
export const Secondary = Template.bind({});
|
||||
Secondary.args = {
|
||||
children: 'Button',
|
||||
color: 'secondary',
|
||||
};
|
||||
|
||||
export const SecondaryOutlined = Template.bind({});
|
||||
SecondaryOutlined.args = {
|
||||
children: 'Button',
|
||||
variant: 'outlined',
|
||||
color: 'secondary',
|
||||
};
|
||||
|
||||
export const SecondaryBorderless = Template.bind({});
|
||||
SecondaryBorderless.args = {
|
||||
children: 'Button',
|
||||
variant: 'borderless',
|
||||
color: 'secondary',
|
||||
};
|
||||
|
||||
export const Danger = Template.bind({});
|
||||
Danger.args = {
|
||||
children: 'Button',
|
||||
color: 'error',
|
||||
};
|
||||
|
||||
export const DangerOutlined = Template.bind({});
|
||||
DangerOutlined.args = {
|
||||
children: 'Button',
|
||||
variant: 'outlined',
|
||||
color: 'error',
|
||||
};
|
||||
|
||||
export const DangerBorderless = Template.bind({});
|
||||
DangerBorderless.args = {
|
||||
children: 'Button',
|
||||
variant: 'borderless',
|
||||
color: 'error',
|
||||
};
|
||||
|
||||
export const Small = Template.bind({});
|
||||
Small.args = {
|
||||
children: 'Button',
|
||||
variant: 'contained',
|
||||
color: 'primary',
|
||||
size: 'small',
|
||||
};
|
||||
|
||||
export const Large = Template.bind({});
|
||||
Large.args = {
|
||||
children: 'Button',
|
||||
variant: 'contained',
|
||||
color: 'primary',
|
||||
size: 'large',
|
||||
};
|
||||
|
||||
export const WithStartIcon = Template.bind({});
|
||||
WithStartIcon.args = {
|
||||
children: 'Button',
|
||||
variant: 'contained',
|
||||
color: 'primary',
|
||||
startIcon: <PlusIcon />,
|
||||
};
|
||||
|
||||
export const WithEndIcon = Template.bind({});
|
||||
WithEndIcon.args = {
|
||||
children: 'Button',
|
||||
variant: 'contained',
|
||||
color: 'primary',
|
||||
endIcon: <PlusCircleIcon />,
|
||||
};
|
||||
86
dashboard/src/components/ui/v2/Chip/Chip.stories.tsx
Normal file
86
dashboard/src/components/ui/v2/Chip/Chip.stories.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { XIcon } from '@/components/ui/v2/icons/XIcon';
|
||||
import type { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import type { ChipProps } from './Chip';
|
||||
import Chip from './Chip';
|
||||
|
||||
export default {
|
||||
title: 'UI Library / Chip',
|
||||
component: Chip,
|
||||
argTypes: {
|
||||
variant: {
|
||||
options: ['contained', 'outlined'],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
color: {
|
||||
options: ['primary', 'secondary', 'error', 'info'],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
disabled: {
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
size: {
|
||||
options: ['small', 'medium'],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof Chip>;
|
||||
|
||||
const Template: ComponentStory<typeof Chip> = function Template(
|
||||
args: ChipProps,
|
||||
) {
|
||||
return <Chip {...args} />;
|
||||
};
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
Primary.args = {
|
||||
label: 'Chip',
|
||||
color: 'primary',
|
||||
};
|
||||
|
||||
export const PrimaryOutlined = Template.bind({});
|
||||
PrimaryOutlined.args = {
|
||||
label: 'Chip',
|
||||
variant: 'outlined',
|
||||
color: 'primary',
|
||||
};
|
||||
|
||||
export const Secondary = Template.bind({});
|
||||
Secondary.args = {
|
||||
label: 'Chip',
|
||||
color: 'secondary',
|
||||
};
|
||||
|
||||
export const SecondaryOutlined = Template.bind({});
|
||||
SecondaryOutlined.args = {
|
||||
label: 'Chip',
|
||||
variant: 'outlined',
|
||||
color: 'secondary',
|
||||
};
|
||||
|
||||
export const Danger = Template.bind({});
|
||||
Danger.args = {
|
||||
label: 'Chip',
|
||||
color: 'error',
|
||||
};
|
||||
|
||||
export const DangerOutlined = Template.bind({});
|
||||
DangerOutlined.args = {
|
||||
label: 'Chip',
|
||||
variant: 'outlined',
|
||||
color: 'error',
|
||||
};
|
||||
|
||||
export const Small = Template.bind({});
|
||||
Small.args = {
|
||||
label: 'Chip',
|
||||
color: 'primary',
|
||||
size: 'small',
|
||||
};
|
||||
|
||||
export const WithDeleteIcon = Template.bind({});
|
||||
WithDeleteIcon.args = {
|
||||
label: 'Chip',
|
||||
color: 'primary',
|
||||
deleteIcon: <XIcon />,
|
||||
onDelete: () => {},
|
||||
};
|
||||
38
dashboard/src/components/ui/v2/Select/Select.stories.tsx
Normal file
38
dashboard/src/components/ui/v2/Select/Select.stories.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Option } from '@/components/ui/v2/Option';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import type { SelectProps } from './Select';
|
||||
import Select from './Select';
|
||||
|
||||
export default {
|
||||
title: 'UI Library / Select',
|
||||
component: Select,
|
||||
argTypes: {},
|
||||
} as Meta<typeof Select>;
|
||||
|
||||
const Template: StoryFn<SelectProps<any>> = function TemplateFunction(args) {
|
||||
return (
|
||||
<Select className="w-64" {...args}>
|
||||
<Option value="value1">Value 1</Option>
|
||||
<Option value="value2">Value 2</Option>
|
||||
<Option value="value3">Value 3</Option>
|
||||
<Option value="value4">Value 4</Option>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
defaultValue: 'value1',
|
||||
};
|
||||
|
||||
export const WithLabel = Template.bind({});
|
||||
WithLabel.args = {
|
||||
label: 'Label',
|
||||
};
|
||||
|
||||
export const Disabled = Template.bind({});
|
||||
Disabled.args = {
|
||||
label: 'Label',
|
||||
disabled: true,
|
||||
defaultValue: 'value1',
|
||||
};
|
||||
28
dashboard/src/components/ui/v2/Switch/Switch.stories.tsx
Normal file
28
dashboard/src/components/ui/v2/Switch/Switch.stories.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import type { SwitchProps } from './Switch';
|
||||
import Switch from './Switch';
|
||||
|
||||
export default {
|
||||
title: 'UI Library / Switch',
|
||||
component: Switch,
|
||||
argTypes: {},
|
||||
} as Meta<typeof Switch>;
|
||||
|
||||
const Template: StoryFn<SwitchProps> = function TemplateFunction(
|
||||
args: SwitchProps,
|
||||
) {
|
||||
return <Switch label="Accept Rules" {...args} />;
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {};
|
||||
|
||||
export const Checked = Template.bind({});
|
||||
Checked.args = {
|
||||
checked: true,
|
||||
};
|
||||
|
||||
export const Disabled = Template.bind({});
|
||||
Disabled.args = {
|
||||
disabled: true,
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
import { ApplicationPaused } from '@/features/orgs/projects/common/components/ApplicationPaused';
|
||||
import { ApplicationPausedBanner } from '@/features/orgs/projects/common/components/ApplicationPausedBanner';
|
||||
import { useRouter } from 'next/router';
|
||||
import { type PropsWithChildren } from 'react';
|
||||
|
||||
const baseProjectPageRoute = '/orgs/[orgSlug]/projects/[appSubdomain]/';
|
||||
const blockedPausedProjectPages = [
|
||||
'database',
|
||||
'database/browser/[dataSourceSlug]',
|
||||
'graphql',
|
||||
'graphql/remote-schemas',
|
||||
'graphql/remote-schemas/[remoteSchemaSlug]',
|
||||
'hasura',
|
||||
'users',
|
||||
'storage',
|
||||
'ai/auto-embeddings',
|
||||
'ai/assistants',
|
||||
'metrics',
|
||||
].map((page) => baseProjectPageRoute.concat(page));
|
||||
|
||||
function PausedProjectContent({ children }: PropsWithChildren) {
|
||||
const { route } = useRouter();
|
||||
|
||||
const isOnOverviewPage = route === '/orgs/[orgSlug]/projects/[appSubdomain]';
|
||||
|
||||
if (isOnOverviewPage) {
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto mt-5 flex max-w-7xl p-4 pb-0">
|
||||
<ApplicationPausedBanner
|
||||
alertClassName="flex-row"
|
||||
textContainerClassName="flex flex-col items-center justify-center text-left"
|
||||
wakeUpButtonClassName="w-fit self-center"
|
||||
/>
|
||||
</div>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// block these pages when the project is paused
|
||||
if (blockedPausedProjectPages.includes(route)) {
|
||||
return <ApplicationPaused />;
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
export default PausedProjectContent;
|
||||
@@ -1,8 +1,5 @@
|
||||
import { mockApplication } from '@/tests/mocks';
|
||||
import {
|
||||
getProjectQuery,
|
||||
getProjectStateQuery,
|
||||
} from '@/tests/msw/mocks/graphql/getProjectQuery';
|
||||
import { getProjectQuery } from '@/tests/msw/mocks/graphql/getProjectQuery';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import {
|
||||
createGraphqlMockResolver,
|
||||
@@ -32,7 +29,7 @@ function TestComponent() {
|
||||
);
|
||||
}
|
||||
|
||||
const server = setupServer(tokenQuery, getProjectStateQuery());
|
||||
const server = setupServer(tokenQuery);
|
||||
|
||||
const getUseRouterObject = (
|
||||
route: string = '/orgs/[orgSlug]/projects/[appSubdomain]',
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import { type AuthenticatedLayoutProps } from '@/components/layout/AuthenticatedLayout';
|
||||
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { ApplicationPaused } from '@/features/orgs/projects/common/components/ApplicationPaused';
|
||||
import { ApplicationPausedBanner } from '@/features/orgs/projects/common/components/ApplicationPausedBanner';
|
||||
import { ApplicationProvisioning } from '@/features/orgs/projects/common/components/ApplicationProvisioning';
|
||||
import { ApplicationRestoring } from '@/features/orgs/projects/common/components/ApplicationRestoring';
|
||||
import { ApplicationUnknown } from '@/features/orgs/projects/common/components/ApplicationUnknown';
|
||||
import { ApplicationUnpausing } from '@/features/orgs/projects/common/components/ApplicationUnpausing';
|
||||
import { useAppState } from '@/features/orgs/projects/common/hooks/useAppState';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { isEmptyValue, isNotEmptyValue } from '@/lib/utils';
|
||||
import { useProjectWithState } from '@/features/orgs/projects/hooks/useProjectWithState';
|
||||
import { isEmptyValue } from '@/lib/utils';
|
||||
import { useAuth } from '@/providers/Auth';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { getConfigServerUrl, isPlatform as isPlatformFn } from '@/utils/env';
|
||||
import { NextSeo } from 'next-seo';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import { useCallback, useEffect, useMemo, type ReactNode } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import ProjectViewWithState from './ProjectViewWithState';
|
||||
|
||||
const platFormOnlyPages = [
|
||||
'/orgs/[orgSlug]/projects/[appSubdomain]/deployments',
|
||||
@@ -48,15 +56,115 @@ function ProjectLayoutContent({
|
||||
children,
|
||||
mainContainerProps = {},
|
||||
}: ProjectLayoutContentProps) {
|
||||
const { route, push } = useRouter();
|
||||
const {
|
||||
route,
|
||||
query: { appSubdomain },
|
||||
push,
|
||||
} = useRouter();
|
||||
|
||||
const { state } = useAppState();
|
||||
const isPlatform = useIsPlatform();
|
||||
|
||||
const { project, loading, error, projectNotFound } = useProject();
|
||||
const { project, loading, error, projectNotFound } = useProjectWithState();
|
||||
const { isAuthenticated, isLoading, isSigningOut } = useAuth();
|
||||
|
||||
const isUserLoggedIn = isAuthenticated && !isLoading && !isSigningOut;
|
||||
|
||||
const isOnOverviewPage = route === '/orgs/[orgSlug]/projects/[appSubdomain]';
|
||||
|
||||
const renderPausedProjectContent = useCallback(
|
||||
(_children: ReactNode) => {
|
||||
const baseProjectPageRoute = '/orgs/[orgSlug]/projects/[appSubdomain]/';
|
||||
const blockedPausedProjectPages = [
|
||||
'database',
|
||||
'database/browser/[dataSourceSlug]',
|
||||
'database/browser/[dataSourceSlug]/[schemaSlug]/[tableSlug]',
|
||||
'graphql',
|
||||
'graphql/remote-schemas',
|
||||
'graphql/remote-schemas/[remoteSchemaSlug]',
|
||||
'hasura',
|
||||
'users',
|
||||
'storage',
|
||||
'run',
|
||||
'ai/auto-embeddings',
|
||||
'ai/assistants',
|
||||
'metrics',
|
||||
].map((page) => baseProjectPageRoute.concat(page));
|
||||
|
||||
// show an alert box on top of the overview page with a wake up button
|
||||
if (isOnOverviewPage) {
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto mt-5 flex max-w-7xl p-4 pb-0">
|
||||
<ApplicationPausedBanner
|
||||
alertClassName="flex-row"
|
||||
textContainerClassName="flex flex-col items-center justify-center text-left"
|
||||
wakeUpButtonClassName="w-fit self-center"
|
||||
/>
|
||||
</div>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// block these pages when the project is paused
|
||||
if (blockedPausedProjectPages.includes(route)) {
|
||||
return <ApplicationPaused />;
|
||||
}
|
||||
|
||||
return _children;
|
||||
},
|
||||
[route, isOnOverviewPage, children],
|
||||
);
|
||||
|
||||
// Render application state based on the current state
|
||||
const projectPageContent = useMemo(() => {
|
||||
if (!appSubdomain || state === undefined) {
|
||||
return children;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case ApplicationStatus.Empty:
|
||||
case ApplicationStatus.Provisioning:
|
||||
return <ApplicationProvisioning />;
|
||||
case ApplicationStatus.Errored:
|
||||
if (isOnOverviewPage) {
|
||||
return (
|
||||
<>
|
||||
<div className="w-full p-4">
|
||||
<Alert severity="error" className="mx-auto max-w-7xl">
|
||||
Error deploying the project most likely due to invalid
|
||||
configuration. Please review your project's configuration
|
||||
and logs for more information.
|
||||
</Alert>
|
||||
</div>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return children;
|
||||
case ApplicationStatus.Pausing:
|
||||
case ApplicationStatus.Paused:
|
||||
return renderPausedProjectContent(children);
|
||||
case ApplicationStatus.Unpausing:
|
||||
return <ApplicationUnpausing />;
|
||||
case ApplicationStatus.Restoring:
|
||||
return <ApplicationRestoring />;
|
||||
case ApplicationStatus.Updating:
|
||||
case ApplicationStatus.Live:
|
||||
case ApplicationStatus.Migrating:
|
||||
return children;
|
||||
default:
|
||||
return <ApplicationUnknown />;
|
||||
}
|
||||
}, [
|
||||
state,
|
||||
children,
|
||||
appSubdomain,
|
||||
isOnOverviewPage,
|
||||
renderPausedProjectContent,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isPlatformOnlyPage(route) ||
|
||||
@@ -75,11 +183,13 @@ function ProjectLayoutContent({
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle loading state
|
||||
if (loading) {
|
||||
return <LoadingScreen data-testid="projectLoadingIndicator" />;
|
||||
}
|
||||
|
||||
if (isNotEmptyValue(error)) {
|
||||
// Handle error state
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -101,7 +211,7 @@ function ProjectLayoutContent({
|
||||
)}
|
||||
{...mainContainerProps}
|
||||
>
|
||||
<ProjectViewWithState>{children}</ProjectViewWithState>
|
||||
{projectPageContent}
|
||||
<NextSeo title={!isPlatform ? 'Local App' : project?.name} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
import {
|
||||
getProjectQuery,
|
||||
getProjectStateQuery,
|
||||
} from '@/tests/msw/mocks/graphql/getProjectQuery';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import { queryClient, render, screen } from '@/tests/testUtils';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { vi } from 'vitest';
|
||||
import ProjectViewWithState from './ProjectViewWithState';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
useRouter: vi.fn(),
|
||||
push: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('next/router', () => ({
|
||||
useRouter: mocks.useRouter,
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
'@/features/orgs/projects/common/components/ApplicationProvisioning',
|
||||
() => ({
|
||||
ApplicationProvisioning: () => <div>Application Provisioning</div>,
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
'@/features/orgs/projects/common/components/ApplicationRestoring',
|
||||
() => ({
|
||||
ApplicationRestoring: () => (
|
||||
<div data-testid="appRestore">Application Restoring</div>
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
'@/features/orgs/projects/common/components/ApplicationUnknown',
|
||||
() => ({
|
||||
ApplicationUnknown: () => (
|
||||
<div data-testid="appUnknown">Application Unknown</div>
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
'@/features/orgs/projects/common/components/ApplicationUnpausing',
|
||||
() => ({
|
||||
ApplicationUnpausing: () => (
|
||||
<div data-testid="appUnpausing">Application Unpausing</div>
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
'@/features/orgs/projects/common/components/ApplicationPausedBanner',
|
||||
() => ({
|
||||
ApplicationPausedBanner: () => (
|
||||
<div data-testid="appBanner">Application Banner</div>
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
const getUseRouterObject = (
|
||||
route: string = '/orgs/[orgSlug]/projects/[appSubdomain]',
|
||||
) => ({
|
||||
basePath: '',
|
||||
pathname: '/orgs/xyz/projects/test-project',
|
||||
route,
|
||||
asPath: '/orgs/xyz/projects/test-project',
|
||||
isLocaleDomain: false,
|
||||
isReady: true,
|
||||
isPreview: false,
|
||||
query: {
|
||||
orgSlug: 'xyz',
|
||||
appSubdomain: 'test-project',
|
||||
},
|
||||
push: mocks.push,
|
||||
replace: vi.fn(),
|
||||
reload: vi.fn(),
|
||||
back: vi.fn(),
|
||||
prefetch: vi.fn(),
|
||||
beforePopState: vi.fn(),
|
||||
events: {
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
emit: vi.fn(),
|
||||
},
|
||||
isFallback: false,
|
||||
});
|
||||
|
||||
function TestComponent() {
|
||||
return (
|
||||
<ProjectViewWithState>
|
||||
<h1>Application content</h1>
|
||||
</ProjectViewWithState>
|
||||
);
|
||||
}
|
||||
|
||||
const server = setupServer(tokenQuery);
|
||||
|
||||
describe('ProjectViewWithState', () => {
|
||||
beforeAll(() => {
|
||||
process.env.NEXT_PUBLIC_NHOST_PLATFORM = 'true';
|
||||
process.env.NEXT_PUBLIC_ENV = 'production';
|
||||
server.listen();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear();
|
||||
mocks.useRouter.mockRestore();
|
||||
mocks.push.mockRestore();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should render the nothing when the state is empty', async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject());
|
||||
server.use(getProjectQuery);
|
||||
server.use(getProjectStateQuery([{ stateId: ApplicationStatus.Empty }]));
|
||||
render(<TestComponent />);
|
||||
expect(screen.queryByText('Application content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the application in provisioning state', async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject());
|
||||
server.use(getProjectQuery);
|
||||
server.use(
|
||||
getProjectStateQuery([{ stateId: ApplicationStatus.Provisioning }]),
|
||||
);
|
||||
render(<TestComponent />);
|
||||
expect(
|
||||
await screen.findByText('Application Provisioning'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText('Application content')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the application in pausing state', async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject());
|
||||
server.use(getProjectQuery);
|
||||
server.use(getProjectStateQuery([{ stateId: ApplicationStatus.Pausing }]));
|
||||
render(<TestComponent />);
|
||||
expect(await screen.findByText('Application content')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Application Banner')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the application Unpausing application state', async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject());
|
||||
server.use(getProjectQuery);
|
||||
server.use(
|
||||
getProjectStateQuery([{ stateId: ApplicationStatus.Unpausing }]),
|
||||
);
|
||||
render(<TestComponent />);
|
||||
expect(screen.queryByText('Application content')).not.toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText('Application Unpausing'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the application paused application state', async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject());
|
||||
server.use(getProjectQuery);
|
||||
server.use(getProjectStateQuery([{ stateId: ApplicationStatus.Paused }]));
|
||||
render(<TestComponent />);
|
||||
expect(await screen.findByText('Application content')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Application Banner')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the application when the state is updating', async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject());
|
||||
server.use(getProjectQuery);
|
||||
server.use(getProjectStateQuery([{ stateId: ApplicationStatus.Updating }]));
|
||||
render(<TestComponent />);
|
||||
|
||||
expect(await screen.findByText('Application content')).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText('Application Restoring')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Unknown')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Unpausing')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Banner')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the application when the state is live', async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject());
|
||||
server.use(getProjectQuery);
|
||||
server.use(getProjectStateQuery([{ stateId: ApplicationStatus.Live }]));
|
||||
render(<TestComponent />);
|
||||
|
||||
expect(await screen.findByText('Application content')).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText('Application Restoring')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Unknown')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Unpausing')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Banner')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the application when the state is migrating', async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject());
|
||||
server.use(getProjectQuery);
|
||||
server.use(
|
||||
getProjectStateQuery([{ stateId: ApplicationStatus.Migrating }]),
|
||||
);
|
||||
render(<TestComponent />);
|
||||
|
||||
expect(await screen.findByText('Application content')).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText('Application Restoring')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Unknown')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Unpausing')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Banner')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the application in an error state', async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject());
|
||||
server.use(getProjectQuery);
|
||||
server.use(getProjectStateQuery([{ stateId: ApplicationStatus.Errored }]));
|
||||
render(<TestComponent />);
|
||||
|
||||
expect(await screen.findByText('Application content')).toBeInTheDocument();
|
||||
|
||||
expect(await screen.findByText(/Error deploying/)).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText('Application Restoring')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Unknown')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Unpausing')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Application Banner')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the application in an error state', async () => {
|
||||
mocks.useRouter.mockImplementation(() => getUseRouterObject());
|
||||
server.use(getProjectQuery);
|
||||
server.use(
|
||||
getProjectStateQuery([{ stateId: ApplicationStatus.Restoring }]),
|
||||
);
|
||||
render(<TestComponent />);
|
||||
|
||||
expect(
|
||||
await screen.findByText('Application Restoring'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText('Application content')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,68 +0,0 @@
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { ApplicationProvisioning } from '@/features/orgs/projects/common/components/ApplicationProvisioning';
|
||||
import { ApplicationRestoring } from '@/features/orgs/projects/common/components/ApplicationRestoring';
|
||||
import { ApplicationUnknown } from '@/features/orgs/projects/common/components/ApplicationUnknown';
|
||||
import { ApplicationUnpausing } from '@/features/orgs/projects/common/components/ApplicationUnpausing';
|
||||
import { useAppState } from '@/features/orgs/projects/common/hooks/useAppState';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { useRouter } from 'next/router';
|
||||
import { type PropsWithChildren, useMemo } from 'react';
|
||||
|
||||
import PausedProjectContent from './PausedProjectContent';
|
||||
|
||||
function ProjectViewWithState({ children }: PropsWithChildren) {
|
||||
const {
|
||||
query: { appSubdomain },
|
||||
route,
|
||||
} = useRouter();
|
||||
|
||||
const { state } = useAppState();
|
||||
|
||||
const isOnOverviewPage = route === '/orgs/[orgSlug]/projects/[appSubdomain]';
|
||||
|
||||
const projectPageContent = useMemo(() => {
|
||||
if (!appSubdomain || state === undefined) {
|
||||
return children;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case ApplicationStatus.Empty:
|
||||
return null;
|
||||
case ApplicationStatus.Provisioning:
|
||||
return <ApplicationProvisioning />;
|
||||
case ApplicationStatus.Errored:
|
||||
if (isOnOverviewPage) {
|
||||
return (
|
||||
<>
|
||||
<div className="w-full p-4">
|
||||
<Alert severity="error" className="mx-auto max-w-7xl">
|
||||
Error deploying the project most likely due to invalid
|
||||
configuration. Please review your project's configuration
|
||||
and logs for more information.
|
||||
</Alert>
|
||||
</div>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return children;
|
||||
case ApplicationStatus.Pausing:
|
||||
case ApplicationStatus.Paused:
|
||||
return <PausedProjectContent>{children}</PausedProjectContent>;
|
||||
case ApplicationStatus.Unpausing:
|
||||
return <ApplicationUnpausing />;
|
||||
case ApplicationStatus.Restoring:
|
||||
return <ApplicationRestoring />;
|
||||
case ApplicationStatus.Updating:
|
||||
case ApplicationStatus.Live:
|
||||
case ApplicationStatus.Migrating:
|
||||
return children;
|
||||
default:
|
||||
return <ApplicationUnknown />;
|
||||
}
|
||||
}, [state, children, appSubdomain, isOnOverviewPage]);
|
||||
|
||||
return projectPageContent;
|
||||
}
|
||||
|
||||
export default ProjectViewWithState;
|
||||
@@ -1,87 +0,0 @@
|
||||
import {
|
||||
getNotFoundProjectStateQuery,
|
||||
getProjectStateQuery,
|
||||
} from '@/tests/msw/mocks/graphql/getProjectQuery';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import { queryClient, render, screen, waitFor } from '@/tests/testUtils';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { vi } from 'vitest';
|
||||
import useAppState from './useAppState';
|
||||
|
||||
function TestComponent() {
|
||||
const { state } = useAppState();
|
||||
|
||||
return <h1>State: {state}</h1>;
|
||||
}
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
refetch: vi.fn(),
|
||||
useProjectWithState: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/orgs/projects/hooks/useProject', async () => ({
|
||||
useProject: () => ({ refetch: mocks.refetch }),
|
||||
}));
|
||||
|
||||
const server = setupServer(tokenQuery);
|
||||
|
||||
describe('useAppState', () => {
|
||||
beforeAll(() => {
|
||||
process.env.NEXT_PUBLIC_NHOST_PLATFORM = 'true';
|
||||
process.env.NEXT_PUBLIC_ENV = 'production';
|
||||
server.listen();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear();
|
||||
mocks.refetch.mockRestore();
|
||||
mocks.useProjectWithState.mockRestore();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should refetch the project, when the project is not found', async () => {
|
||||
server.use(getNotFoundProjectStateQuery);
|
||||
render(<TestComponent />);
|
||||
expect(await screen.findByText('State: 0')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(mocks.refetch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should not refetch the project if the state is empty', async () => {
|
||||
server.use(getProjectStateQuery([{ stateId: ApplicationStatus.Empty }]));
|
||||
render(<TestComponent />);
|
||||
expect(await screen.findByText('State: 0')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(mocks.refetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should return empty state if the application state has not been filled yet', async () => {
|
||||
server.use(getProjectStateQuery([]));
|
||||
render(<TestComponent />);
|
||||
expect(await screen.findByText('State: 0')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(mocks.refetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should return the first state from the response', async () => {
|
||||
server.use(
|
||||
getProjectStateQuery([
|
||||
{ stateId: ApplicationStatus.Live },
|
||||
{ stateId: ApplicationStatus.Empty },
|
||||
]),
|
||||
);
|
||||
render(<TestComponent />);
|
||||
expect(await screen.findByText('State: 5')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(mocks.refetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,5 @@
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { useProjectWithState } from '@/features/orgs/projects/hooks/useProjectWithState';
|
||||
import { isNotEmptyValue } from '@/lib/utils';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* This hook returns the current application state. If the application state
|
||||
@@ -10,19 +7,27 @@ import { useEffect } from 'react';
|
||||
*/
|
||||
export default function useAppState(): {
|
||||
state: ApplicationStatus;
|
||||
message?: string | null;
|
||||
} {
|
||||
const { project, projectNotFound } = useProjectWithState();
|
||||
const { refetch } = useProject();
|
||||
const { project } = useProjectWithState();
|
||||
const noApplication = !project;
|
||||
|
||||
useEffect(() => {
|
||||
if (projectNotFound) {
|
||||
refetch();
|
||||
}
|
||||
}, [projectNotFound, refetch]);
|
||||
if (noApplication) {
|
||||
return { state: ApplicationStatus.Empty };
|
||||
}
|
||||
|
||||
const emptyApplicationStates = !project.appStates;
|
||||
|
||||
if (noApplication || emptyApplicationStates) {
|
||||
return { state: ApplicationStatus.Empty };
|
||||
}
|
||||
|
||||
if (project.appStates?.length === 0) {
|
||||
return { state: ApplicationStatus.Empty };
|
||||
}
|
||||
|
||||
return {
|
||||
state: isNotEmptyValue(project?.appStates?.[0])
|
||||
? project.appStates[0].stateId
|
||||
: ApplicationStatus.Empty,
|
||||
state: project.appStates[0].stateId,
|
||||
message: project.appStates[0].message,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,6 +74,9 @@ export default function useCheckProvisioning() {
|
||||
createdAt: data.app.appStates[0].createdAt,
|
||||
});
|
||||
stopPolling();
|
||||
// Will update the cache and update with the new application state
|
||||
// which will trigger the correct application component
|
||||
// under `src\components\applications\App.tsx`
|
||||
memoizedUpdateCache();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import hasuraMetadataQuery from '@/tests/msw/mocks/rest/hasuraMetadataQuery';
|
||||
import tableQuery from '@/tests/msw/mocks/rest/tableQuery';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import type { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import type { ColumnAutocompleteProps } from './ColumnAutocomplete';
|
||||
import ColumnAutocomplete from './ColumnAutocomplete';
|
||||
|
||||
export default {
|
||||
title: 'Data Browser / ColumnAutocomplete',
|
||||
component: ColumnAutocomplete,
|
||||
parameters: {
|
||||
docs: {
|
||||
source: {
|
||||
type: 'code',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof ColumnAutocomplete>;
|
||||
|
||||
const defaultParameters = {
|
||||
nextRouter: {
|
||||
path: '/[workspaceSlug]/[appSlug]/database/browser/[dataSourceSlug]/[schemaSlug]/[tableSlug]',
|
||||
asPath: '/workspace/app/database/browser/default/public/users',
|
||||
query: {
|
||||
workspaceSlug: 'workspace',
|
||||
appSlug: 'app',
|
||||
dataSourceSlug: 'default',
|
||||
schemaSlug: 'public',
|
||||
tableSlug: 'books',
|
||||
},
|
||||
},
|
||||
msw: {
|
||||
handlers: [tokenQuery, tableQuery, hasuraMetadataQuery],
|
||||
},
|
||||
};
|
||||
|
||||
const Template: ComponentStory<typeof ColumnAutocomplete> = function Template(
|
||||
args: ColumnAutocompleteProps,
|
||||
) {
|
||||
const [submittedValues, setSubmittedValues] = useState<string>('');
|
||||
|
||||
const form = useForm<{ firstReference: string; secondReference: string }>({
|
||||
defaultValues: {
|
||||
firstReference: null as any,
|
||||
secondReference: null as any,
|
||||
},
|
||||
});
|
||||
|
||||
function handleSubmit(values: {
|
||||
firstReference: string;
|
||||
secondReference: string;
|
||||
}) {
|
||||
setSubmittedValues(JSON.stringify(values, null, 2));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit} className="grid grid-flow-row gap-2">
|
||||
<ColumnAutocomplete
|
||||
{...args}
|
||||
onChange={(newValue) =>
|
||||
form.setValue('firstReference', newValue.value, {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
onInitialized={(newValue) => {
|
||||
form.setValue('firstReference', newValue.value, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ColumnAutocomplete
|
||||
{...args}
|
||||
onChange={(newValue) =>
|
||||
form.setValue('secondReference', newValue.value, {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
onInitialized={(newValue) => {
|
||||
form.setValue('secondReference', newValue.value, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button type="submit" className="justify-self-start">
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
|
||||
<Text component="pre" className="!font-mono !text-gray-700">
|
||||
{submittedValues || 'The form has not been submitted yet.'}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Basic = Template.bind({});
|
||||
Basic.args = {
|
||||
schema: 'public',
|
||||
table: 'books',
|
||||
};
|
||||
Basic.parameters = defaultParameters;
|
||||
|
||||
export const DefaultValue = Template.bind({});
|
||||
DefaultValue.args = {
|
||||
schema: 'public',
|
||||
table: 'books',
|
||||
value: 'author.id',
|
||||
};
|
||||
DefaultValue.parameters = defaultParameters;
|
||||
|
||||
export const DisabledRelationships = Template.bind({});
|
||||
DisabledRelationships.args = {
|
||||
schema: 'public',
|
||||
table: 'books',
|
||||
disableRelationships: true,
|
||||
};
|
||||
DisabledRelationships.parameters = defaultParameters;
|
||||
@@ -127,10 +127,6 @@ describe('RowPermissionsSection', () => {
|
||||
process.env.NEXT_PUBLIC_ENV = 'dev';
|
||||
server.listen();
|
||||
});
|
||||
beforeEach(() => {
|
||||
server.restoreHandlers();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import type { RuleGroup } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||
import permissionVariablesQuery from '@/tests/msw/mocks/graphql/permissionVariablesQuery';
|
||||
import hasuraMetadataQuery from '@/tests/msw/mocks/rest/hasuraMetadataQuery';
|
||||
import tableQuery from '@/tests/msw/mocks/rest/tableQuery';
|
||||
import type { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import type { RuleGroupEditorProps } from './RuleGroupEditor';
|
||||
import RuleGroupEditor from './RuleGroupEditor';
|
||||
|
||||
export default {
|
||||
title: 'Data Browser / RuleGroupEditor',
|
||||
component: RuleGroupEditor,
|
||||
parameters: {
|
||||
docs: {
|
||||
source: {
|
||||
type: 'code',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof RuleGroupEditor>;
|
||||
|
||||
const defaultParameters = {
|
||||
nextRouter: {
|
||||
path: '/[workspaceSlug]/[appSlug]/database/browser/[dataSourceSlug]/[schemaSlug]/[tableSlug]',
|
||||
asPath: '/workspace/app/database/browser/default/public/users',
|
||||
query: {
|
||||
workspaceSlug: 'workspace',
|
||||
appSlug: 'app',
|
||||
dataSourceSlug: 'default',
|
||||
schemaSlug: 'public',
|
||||
tableSlug: 'books',
|
||||
},
|
||||
},
|
||||
msw: {
|
||||
handlers: [tableQuery, hasuraMetadataQuery, permissionVariablesQuery],
|
||||
},
|
||||
};
|
||||
|
||||
const Template: ComponentStory<typeof RuleGroupEditor> = function Template(
|
||||
args: RuleGroupEditorProps,
|
||||
) {
|
||||
const [submittedValues, setSubmittedValues] = useState<string>();
|
||||
|
||||
const form = useForm<{ ruleGroupEditor: RuleGroup }>({
|
||||
defaultValues: {
|
||||
ruleGroupEditor: {
|
||||
operator: '_and',
|
||||
rules: [{ column: '', operator: '_eq', value: '' }],
|
||||
groups: [],
|
||||
},
|
||||
},
|
||||
reValidateMode: 'onSubmit',
|
||||
});
|
||||
|
||||
function handleSubmit(values: { ruleGroupEditor: RuleGroup }) {
|
||||
setSubmittedValues(JSON.stringify(values, null, 2));
|
||||
}
|
||||
|
||||
// note: Storybook passes `onRemove` as a prop, but we don't want to use it
|
||||
return (
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit} className="grid grid-flow-row gap-2">
|
||||
<RuleGroupEditor
|
||||
{...args}
|
||||
schema="public"
|
||||
table="books"
|
||||
name="ruleGroupEditor"
|
||||
/>
|
||||
|
||||
<Button type="submit" className="justify-self-start">
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
|
||||
<Text component="pre" className="!font-mono !text-gray-700">
|
||||
{submittedValues || 'The form has not been submitted yet.'}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {};
|
||||
Default.parameters = defaultParameters;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import type { ManagePermissionOptions } from './managePermission';
|
||||
import managePermission from './managePermission';
|
||||
@@ -12,7 +12,9 @@ const defaultParameters: ManagePermissionOptions = {
|
||||
};
|
||||
|
||||
const server = setupServer(
|
||||
http.post('http://localhost:1337/v1/metadata', () => HttpResponse.json({})),
|
||||
rest.post('http://localhost:1337/v1/metadata', (_req, res, ctx) =>
|
||||
res(ctx.json({})),
|
||||
),
|
||||
);
|
||||
|
||||
beforeAll(() => server.listen());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HasuraMetadata } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import prepareTrackForeignKeyRelationsMetadata from './prepareTrackForeignKeyRelationsMetadata';
|
||||
|
||||
@@ -28,8 +28,11 @@ const testMetadataResponse: { metadata: HasuraMetadata } = {
|
||||
};
|
||||
|
||||
const metadataHandlers = [
|
||||
http.post(`${APP_URL}/v1/metadata`, () =>
|
||||
HttpResponse.json(testMetadataResponse),
|
||||
rest.post(`${APP_URL}/v1/metadata`, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json<{ metadata: HasuraMetadata }>(testMetadataResponse),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -128,53 +131,56 @@ test('should only prepare a one-to-one relationship if the table does not have a
|
||||
|
||||
test('should drop existing relationships and prepare a new one-to-many relationship', async () => {
|
||||
server.use(
|
||||
http.post(`${APP_URL}/v1/metadata`, () =>
|
||||
HttpResponse.json({
|
||||
...testMetadataResponse,
|
||||
metadata: {
|
||||
...testMetadataResponse.metadata,
|
||||
sources: [
|
||||
{
|
||||
...testMetadataResponse.metadata.sources[0],
|
||||
tables: [
|
||||
{
|
||||
...testMetadataResponse.metadata.sources[0].tables[0],
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'author',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'author_id',
|
||||
rest.post(`${APP_URL}/v1/metadata`, (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json<{ metadata: HasuraMetadata }>({
|
||||
...testMetadataResponse,
|
||||
metadata: {
|
||||
...testMetadataResponse.metadata,
|
||||
sources: [
|
||||
{
|
||||
...testMetadataResponse.metadata.sources[0],
|
||||
tables: [
|
||||
{
|
||||
...testMetadataResponse.metadata.sources[0].tables[0],
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'author',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'author_id',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'authors',
|
||||
schema: 'public',
|
||||
],
|
||||
},
|
||||
configuration: {},
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'books',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'author_id',
|
||||
table: {
|
||||
name: 'books',
|
||||
schema: 'public',
|
||||
{
|
||||
table: {
|
||||
name: 'authors',
|
||||
schema: 'public',
|
||||
},
|
||||
configuration: {},
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'books',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'author_id',
|
||||
table: {
|
||||
name: 'books',
|
||||
schema: 'public',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
object_relationships: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
object_relationships: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { render, screen, TestUserEvent } from '@/tests/testUtils';
|
||||
import { vi } from 'vitest';
|
||||
import DatabasePiTRSettings from './DatabasePiTRSettings';
|
||||
|
||||
import { getOrganizations } from '@/tests/msw/mocks/graphql/getOrganizationQuery';
|
||||
import { getProjectQuery } from '@/tests/msw/mocks/graphql/getProjectQuery';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import { setupServer } from 'msw/node';
|
||||
@@ -76,7 +75,7 @@ vi.mock('@/features/orgs/components/common/TransferProjectDialog', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
const server = setupServer(tokenQuery, getOrganizations);
|
||||
const server = setupServer(tokenQuery);
|
||||
|
||||
describe('DatabasePiTRSettings', () => {
|
||||
beforeAll(() => {
|
||||
|
||||
@@ -52,11 +52,11 @@ function UpgradeNotification({ description }: Props) {
|
||||
<ArrowSquareOutIcon className="ml-1 h-4 w-4" />
|
||||
</Link>
|
||||
<OpenTransferDialogButton onClick={handleTransferDialogOpen} />
|
||||
<TransferProjectDialog
|
||||
open={transferProjectDialogOpen}
|
||||
setOpen={setTransferProjectDialogOpen}
|
||||
/>
|
||||
</Text>
|
||||
<TransferProjectDialog
|
||||
open={transferProjectDialogOpen}
|
||||
setOpen={setTransferProjectDialogOpen}
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,17 +20,6 @@ const mockServices = [
|
||||
'job-backup',
|
||||
];
|
||||
|
||||
Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', {
|
||||
value: vi.fn(() => ({
|
||||
width: 100,
|
||||
height: 40,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 40,
|
||||
right: 100,
|
||||
})),
|
||||
});
|
||||
|
||||
vi.mock('@/features/orgs/projects/hooks/useProject', async () => ({
|
||||
useProject: () => ({ project: mockApplication }),
|
||||
}));
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import { render, screen, waitFor } from '@/tests/testUtils';
|
||||
import { HttpResponse, graphql } from 'msw';
|
||||
import { graphql } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { beforeAll, expect, test, vi } from 'vitest';
|
||||
import HasuraCorsDomainSettings from './HasuraCorsDomainSettings';
|
||||
|
||||
const server = setupServer(
|
||||
tokenQuery,
|
||||
graphql.query('GetHasuraSettings', () =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
graphql.query('GetHasuraSettings', (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
config: {
|
||||
id: 'HasuraSettings',
|
||||
__typename: 'HasuraSettings',
|
||||
@@ -29,8 +29,8 @@ const server = setupServer(
|
||||
resources: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -62,9 +62,9 @@ describe('HasuraCorsDomainSettings', () => {
|
||||
|
||||
test('should enable switch by default when CORS domain is set to one or more domains', async () => {
|
||||
server.use(
|
||||
graphql.query('GetHasuraSettings', () =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
graphql.query('GetHasuraSettings', (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
config: {
|
||||
id: 'HasuraSettings',
|
||||
__typename: 'HasuraSettings',
|
||||
@@ -84,8 +84,8 @@ describe('HasuraCorsDomainSettings', () => {
|
||||
resources: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { localApplication } from '@/features/orgs/utils/local-dashboard';
|
||||
import { isEmptyValue } from '@/lib/utils';
|
||||
import { useAuth } from '@/providers/Auth';
|
||||
import { useNhostClient } from '@/providers/nhost';
|
||||
import {
|
||||
@@ -19,7 +18,6 @@ export interface UseProjectReturnType {
|
||||
loading?: boolean;
|
||||
error?: Error | null;
|
||||
refetch: (variables?: any) => Promise<any>;
|
||||
projectNotFound: boolean;
|
||||
}
|
||||
|
||||
export default function useProject(): UseProjectReturnType {
|
||||
@@ -41,14 +39,13 @@ export default function useProject(): UseProjectReturnType {
|
||||
[isPlatform, isAuthenticated, isAuthLoading, appSubdomain, isRouterReady],
|
||||
);
|
||||
|
||||
const { data, isLoading, refetch, error, isFetched } = useQuery(
|
||||
const { data, isLoading, refetch, error } = useQuery(
|
||||
['project', appSubdomain as string],
|
||||
async () => {
|
||||
const response = await nhost.graphql.request<{
|
||||
apps: ProjectFragment[];
|
||||
}>(GetProjectDocument, { subdomain: (appSubdomain as string) || '' });
|
||||
|
||||
return response?.body.data;
|
||||
return response.body;
|
||||
},
|
||||
{
|
||||
enabled: shouldFetchProject,
|
||||
@@ -57,11 +54,10 @@ export default function useProject(): UseProjectReturnType {
|
||||
|
||||
if (isPlatform) {
|
||||
return {
|
||||
project: data?.apps?.[0] || null,
|
||||
project: data?.data?.apps?.[0] || null,
|
||||
loading: isLoading && shouldFetchProject,
|
||||
error: Array.isArray(error || {}) ? error?.[0] : error,
|
||||
refetch,
|
||||
projectNotFound: isFetched && !isLoading && isEmptyValue(data?.apps),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -70,6 +66,5 @@ export default function useProject(): UseProjectReturnType {
|
||||
loading: false,
|
||||
error: null,
|
||||
refetch: () => Promise.resolve(),
|
||||
projectNotFound: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@ describe('useProjectLogs - Subscription Creation & Cleanup', () => {
|
||||
loading: false,
|
||||
error: undefined,
|
||||
refetch: vi.fn(),
|
||||
projectNotFound: false,
|
||||
});
|
||||
|
||||
// Mock subscribeToMore to return an unsubscribe function
|
||||
@@ -134,7 +133,6 @@ describe('useProjectLogs - Subscription Creation & Cleanup', () => {
|
||||
loading: true,
|
||||
error: undefined,
|
||||
refetch: vi.fn(),
|
||||
projectNotFound: false,
|
||||
});
|
||||
|
||||
renderHook(() => useProjectLogs(defaultProps));
|
||||
@@ -148,7 +146,6 @@ describe('useProjectLogs - Subscription Creation & Cleanup', () => {
|
||||
loading: false,
|
||||
error: undefined,
|
||||
refetch: vi.fn(),
|
||||
projectNotFound: false,
|
||||
});
|
||||
|
||||
renderHook(() => useProjectLogs(defaultProps));
|
||||
|
||||
@@ -20,17 +20,6 @@ const mockServices = [
|
||||
'job-backup',
|
||||
];
|
||||
|
||||
Object.defineProperty(HTMLElement.prototype, 'getBoundingClientRect', {
|
||||
value: vi.fn(() => ({
|
||||
width: 100,
|
||||
height: 40,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 40,
|
||||
right: 100,
|
||||
})),
|
||||
});
|
||||
|
||||
vi.mock('@/features/orgs/projects/hooks/useProject', async () => ({
|
||||
useProject: () => ({ project: mockApplication }),
|
||||
}));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mockApplication, mockOrganization } from '@/tests/mocks';
|
||||
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
|
||||
import { queryClient, render, screen } from '@/tests/testUtils';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { afterAll, beforeAll, vi } from 'vitest';
|
||||
import OverviewDeployments from './OverviewDeployments';
|
||||
@@ -36,9 +36,8 @@ vi.mock('next/router', () => ({
|
||||
|
||||
const server = setupServer(
|
||||
tokenQuery,
|
||||
http.get(
|
||||
'https://local.graphql.local.nhost.run/v1',
|
||||
() => new HttpResponse(null, { status: 200 }),
|
||||
rest.get('https://local.graphql.local.nhost.run/v1', (_req, res, ctx) =>
|
||||
res(ctx.status(200)),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -50,9 +49,8 @@ beforeAll(() => {
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers(
|
||||
http.get(
|
||||
'https://local.graphql.local.nhost.run/v1',
|
||||
() => new HttpResponse(null, { status: 200 }),
|
||||
rest.get('https://local.graphql.local.nhost.run/v1', (_req, res, ctx) =>
|
||||
res(ctx.status(200)),
|
||||
),
|
||||
);
|
||||
queryClient.clear();
|
||||
@@ -65,31 +63,37 @@ afterAll(() => {
|
||||
|
||||
test('should render an empty state when GitHub is not connected', async () => {
|
||||
server.use(
|
||||
http.post(
|
||||
rest.post(
|
||||
'https://local.graphql.local.nhost.run/v1',
|
||||
async ({ request }) => {
|
||||
const { operationName } = (await request.json()) as any;
|
||||
async (req, res, ctx) => {
|
||||
const { operationName } = await req.json();
|
||||
if (operationName === 'getProject') {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
apps: [{ ...mockApplication, githubRepository: null }],
|
||||
},
|
||||
});
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
apps: [{ ...mockApplication, githubRepository: null }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (operationName === 'getOrganization') {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
organizations: [{ ...mockOrganization }],
|
||||
},
|
||||
});
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
organizations: [{ ...mockOrganization }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
deployments: [],
|
||||
},
|
||||
});
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
deployments: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -103,28 +107,32 @@ test('should render an empty state when GitHub is not connected', async () => {
|
||||
});
|
||||
test('should render an empty state when GitHub is connected, but there are no deployments', async () => {
|
||||
server.use(
|
||||
http.post(
|
||||
rest.post(
|
||||
'https://local.graphql.local.nhost.run/v1',
|
||||
async ({ request }) => {
|
||||
const { operationName } = (await request.json()) as any;
|
||||
async (_req, res, ctx) => {
|
||||
const { operationName } = await _req.json();
|
||||
|
||||
if (operationName === 'getProject') {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
apps: [{ ...mockApplication }],
|
||||
},
|
||||
});
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
apps: [{ ...mockApplication }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (operationName === 'getOrganization') {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
organizations: [{ ...mockOrganization }],
|
||||
},
|
||||
});
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
organizations: [{ ...mockOrganization }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return HttpResponse.json({ data: { deployments: [] } });
|
||||
return res(ctx.json({ data: { deployments: [] } }));
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -147,46 +155,52 @@ test('should render an empty state when GitHub is connected, but there are no de
|
||||
test('should render a list of deployments', async () => {
|
||||
server.use(
|
||||
tokenQuery,
|
||||
http.post(
|
||||
rest.post(
|
||||
'https://local.graphql.local.nhost.run/v1',
|
||||
async ({ request }) => {
|
||||
const { operationName } = (await request.json()) as any;
|
||||
async (_req, res, ctx) => {
|
||||
const { operationName } = await _req.json();
|
||||
|
||||
if (operationName === 'ScheduledOrPendingDeploymentsSub') {
|
||||
return HttpResponse.json({ data: { deployments: [] } });
|
||||
return res(ctx.json({ data: { deployments: [] } }));
|
||||
}
|
||||
|
||||
if (operationName === 'getProject') {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
apps: [{ ...mockApplication }],
|
||||
},
|
||||
});
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
apps: [{ ...mockApplication }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (operationName === 'getOrganization') {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
organizations: [{ ...mockOrganization }],
|
||||
},
|
||||
});
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
organizations: [{ ...mockOrganization }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
deployments: [
|
||||
{
|
||||
id: '1',
|
||||
commitSHA: 'abc123',
|
||||
deploymentStartedAt: '2021-08-01T00:00:00.000Z',
|
||||
deploymentEndedAt: '2021-08-01T00:05:00.000Z',
|
||||
deploymentStatus: 'DEPLOYED',
|
||||
commitUserName: 'test.user',
|
||||
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
|
||||
commitMessage: 'Test commit message',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
deployments: [
|
||||
{
|
||||
id: '1',
|
||||
commitSHA: 'abc123',
|
||||
deploymentStartedAt: '2021-08-01T00:00:00.000Z',
|
||||
deploymentEndedAt: '2021-08-01T00:05:00.000Z',
|
||||
deploymentStatus: 'DEPLOYED',
|
||||
commitUserName: 'test.user',
|
||||
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
|
||||
commitMessage: 'Test commit message',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -213,61 +227,69 @@ test('should render a list of deployments', async () => {
|
||||
test('should disable redeployments if a deployment is already in progress', async () => {
|
||||
server.use(
|
||||
tokenQuery,
|
||||
http.post(
|
||||
rest.post(
|
||||
'https://local.graphql.local.nhost.run/v1',
|
||||
async ({ request }) => {
|
||||
const { operationName } = (await request.json()) as any;
|
||||
async (req, res, ctx) => {
|
||||
const { operationName } = await req.json();
|
||||
|
||||
if (operationName === 'ScheduledOrPendingDeploymentsSub') {
|
||||
return HttpResponse.json({
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
deployments: [
|
||||
{
|
||||
id: '2',
|
||||
commitSHA: 'abc234',
|
||||
deploymentStartedAt: '2021-08-02T00:00:00.000Z',
|
||||
deploymentEndedAt: null,
|
||||
deploymentStatus: 'PENDING',
|
||||
commitUserName: 'test.user',
|
||||
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
|
||||
commitMessage: 'Test commit message',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (operationName === 'getProject') {
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
apps: [{ ...mockApplication }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (operationName === 'getOrganization') {
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
organizations: [{ ...mockOrganization }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return res(
|
||||
ctx.json({
|
||||
data: {
|
||||
deployments: [
|
||||
{
|
||||
id: '2',
|
||||
commitSHA: 'abc234',
|
||||
deploymentStartedAt: '2021-08-02T00:00:00.000Z',
|
||||
deploymentEndedAt: null,
|
||||
deploymentStatus: 'PENDING',
|
||||
id: '1',
|
||||
commitSHA: 'abc123',
|
||||
deploymentStartedAt: '2021-08-01T00:00:00.000Z',
|
||||
deploymentEndedAt: '2021-08-01T00:05:00.000Z',
|
||||
deploymentStatus: 'DEPLOYED',
|
||||
commitUserName: 'test.user',
|
||||
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
|
||||
commitMessage: 'Test commit message',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (operationName === 'getProject') {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
apps: [{ ...mockApplication }],
|
||||
},
|
||||
});
|
||||
}
|
||||
if (operationName === 'getOrganization') {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
organizations: [{ ...mockOrganization }],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
deployments: [
|
||||
{
|
||||
id: '1',
|
||||
commitSHA: 'abc123',
|
||||
deploymentStartedAt: '2021-08-01T00:00:00.000Z',
|
||||
deploymentEndedAt: '2021-08-01T00:05:00.000Z',
|
||||
deploymentStatus: 'DEPLOYED',
|
||||
commitUserName: 'test.user',
|
||||
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
|
||||
commitMessage: 'Test commit message',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import { mockOrganization, mockOrganizations } from '@/tests/mocks';
|
||||
import { HttpResponse } from 'msw';
|
||||
import nhostGraphQLLink from './nhostGraphQLLink';
|
||||
|
||||
export const getOrganizations = nhostGraphQLLink.query('getOrganizations', () =>
|
||||
HttpResponse.json({
|
||||
data: { organizations: mockOrganizations },
|
||||
}),
|
||||
export const getOrganizations = nhostGraphQLLink.query(
|
||||
'getOrganizations',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
organizations: mockOrganizations,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
export const getOrganization = nhostGraphQLLink.query('getOrganization', () =>
|
||||
HttpResponse.json({
|
||||
data: { organizations: [{ ...mockOrganization }] },
|
||||
}),
|
||||
export const getOrganization = nhostGraphQLLink.query(
|
||||
'getOrganization',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
organizations: [{ ...mockOrganization }],
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { HttpResponse } from 'msw';
|
||||
import nhostGraphQLLink from './nhostGraphQLLink';
|
||||
|
||||
export const getPostgresSettings = nhostGraphQLLink.query(
|
||||
'GetPostgresSettings',
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
systemConfig: {
|
||||
postgres: {
|
||||
database: 'gnlivtcgjxctuujxpslj',
|
||||
@@ -30,15 +29,15 @@ export const getPostgresSettings = nhostGraphQLLink.query(
|
||||
__typename: 'ConfigPostgres',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
export const getPiTRNotEnabledPostgresSettings = nhostGraphQLLink.query(
|
||||
'GetPostgresSettings',
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
systemConfig: {
|
||||
postgres: {
|
||||
database: 'gnlivtcgjxctuujxpslj',
|
||||
@@ -63,6 +62,8 @@ export const getPiTRNotEnabledPostgresSettings = nhostGraphQLLink.query(
|
||||
__typename: 'ConfigPostgres',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// {"data":}
|
||||
|
||||
@@ -1,35 +1,12 @@
|
||||
import { mockApplication } from '@/tests/mocks';
|
||||
import { HttpResponse } from 'msw';
|
||||
import nhostGraphQLLink from './nhostGraphQLLink';
|
||||
|
||||
export const getProjectQuery = nhostGraphQLLink.query('getProject', () =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
apps: [{ ...mockApplication, githubRepository: null }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const getProjectStateQuery = (appStates?: any) =>
|
||||
nhostGraphQLLink.query('getProjectState', () =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
apps: [
|
||||
{
|
||||
...mockApplication,
|
||||
appStates: appStates || mockApplication.appStates,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const getNotFoundProjectStateQuery = nhostGraphQLLink.query(
|
||||
'getProjectState',
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
apps: [],
|
||||
},
|
||||
}),
|
||||
export const getProjectQuery = nhostGraphQLLink.query(
|
||||
'getProject',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
apps: [{ ...mockApplication, githubRepository: null }],
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,141 +1,144 @@
|
||||
import { HttpResponse } from 'msw';
|
||||
import nhostGraphQLLink from './nhostGraphQLLink';
|
||||
|
||||
export const getProjectsQuery = nhostGraphQLLink.query('getProjects', () =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
apps: [
|
||||
{
|
||||
id: 'pitr-usa-id',
|
||||
name: 'pitr-not-enabled-usa',
|
||||
slug: 'pitr-not-enabled-usa',
|
||||
createdAt: '2025-03-10T12:35:23.193578+00:00',
|
||||
subdomain: 'ocrnpctsphttfxkuefyx',
|
||||
region: {
|
||||
id: '1',
|
||||
name: 'us-east-1',
|
||||
__typename: 'regions',
|
||||
},
|
||||
deployments: [],
|
||||
creator: {
|
||||
id: 'creator-r-elek-id',
|
||||
email: 'robert@elek.com',
|
||||
displayName: 'Robert',
|
||||
__typename: 'users',
|
||||
},
|
||||
appStates: [
|
||||
{
|
||||
id: 'cd2b77ac-3ef1-4a76-819b-ff1caca09213',
|
||||
appId: 'pitr-usa-id',
|
||||
message:
|
||||
'failed to get dns manager: unknown region: 55985cd4-af14-4d2a-90a5-2a1253ebc1db',
|
||||
stateId: 8,
|
||||
createdAt: '2025-03-10T12:39:23.734345+00:00',
|
||||
__typename: 'appStateHistory',
|
||||
export const getProjectsQuery = nhostGraphQLLink.query(
|
||||
'getProjects',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
apps: [
|
||||
{
|
||||
id: 'pitr-usa-id',
|
||||
name: 'pitr-not-enabled-usa',
|
||||
slug: 'pitr-not-enabled-usa',
|
||||
createdAt: '2025-03-10T12:35:23.193578+00:00',
|
||||
subdomain: 'ocrnpctsphttfxkuefyx',
|
||||
region: {
|
||||
id: '1',
|
||||
name: 'us-east-1',
|
||||
__typename: 'regions',
|
||||
},
|
||||
],
|
||||
__typename: 'apps',
|
||||
},
|
||||
{
|
||||
id: 'pitr-region-TEST-eu-id',
|
||||
name: 'pitr-region-test-eu',
|
||||
slug: 'pitr-region-test-eu',
|
||||
createdAt: '2025-03-10T12:45:40.813234+00:00',
|
||||
subdomain: 'doszbxwibtopsbfgbjpg',
|
||||
region: {
|
||||
id: 'dd6f8e01-35a9-4ba6-8dc6-ed972f2db93c',
|
||||
name: 'eu-central-1',
|
||||
__typename: 'regions',
|
||||
},
|
||||
deployments: [],
|
||||
creator: {
|
||||
id: 'creator-r-elek-id',
|
||||
email: 'robert@elek.com',
|
||||
displayName: 'Robert',
|
||||
__typename: 'users',
|
||||
},
|
||||
appStates: [
|
||||
{
|
||||
id: 'c7fbf7ad-b60c-432b-86c2-5a9509054c47',
|
||||
appId: 'pitr-region-TEST-eu-id',
|
||||
message: '',
|
||||
stateId: 5,
|
||||
createdAt: '2025-03-12T11:08:59.926611+00:00',
|
||||
__typename: 'appStateHistory',
|
||||
deployments: [],
|
||||
creator: {
|
||||
id: 'creator-r-elek-id',
|
||||
email: 'robert@elek.com',
|
||||
displayName: 'Robert',
|
||||
__typename: 'users',
|
||||
},
|
||||
],
|
||||
__typename: 'apps',
|
||||
},
|
||||
{
|
||||
id: 'pitr-test-id',
|
||||
name: 'pitr-test',
|
||||
slug: 'pitr-test',
|
||||
createdAt: '2025-03-04T13:48:59.76498+00:00',
|
||||
subdomain: 'gnlivtcgjxctuujxpslj',
|
||||
region: {
|
||||
id: '1',
|
||||
name: 'us-east-1',
|
||||
__typename: 'regions',
|
||||
appStates: [
|
||||
{
|
||||
id: 'cd2b77ac-3ef1-4a76-819b-ff1caca09213',
|
||||
appId: 'pitr-usa-id',
|
||||
message:
|
||||
'failed to get dns manager: unknown region: 55985cd4-af14-4d2a-90a5-2a1253ebc1db',
|
||||
stateId: 8,
|
||||
createdAt: '2025-03-10T12:39:23.734345+00:00',
|
||||
__typename: 'appStateHistory',
|
||||
},
|
||||
],
|
||||
__typename: 'apps',
|
||||
},
|
||||
deployments: [],
|
||||
creator: {
|
||||
id: 'creator-d-elek-id',
|
||||
email: 'dbarrosop@dravetech.com',
|
||||
displayName: 'David Elek',
|
||||
__typename: 'users',
|
||||
},
|
||||
appStates: [
|
||||
{
|
||||
id: 'fc344bc6-1c59-447a-813f-e0f65754b0e0',
|
||||
appId: 'pitr-test-id',
|
||||
message:
|
||||
'failed to deploy application to kubernetes: failed to deploy application: failed to check rollout status: error running kubectl: exit status 1',
|
||||
stateId: 8,
|
||||
createdAt: '2025-03-11T15:34:41.25304+00:00',
|
||||
__typename: 'appStateHistory',
|
||||
{
|
||||
id: 'pitr-region-TEST-eu-id',
|
||||
name: 'pitr-region-test-eu',
|
||||
slug: 'pitr-region-test-eu',
|
||||
createdAt: '2025-03-10T12:45:40.813234+00:00',
|
||||
subdomain: 'doszbxwibtopsbfgbjpg',
|
||||
region: {
|
||||
id: 'dd6f8e01-35a9-4ba6-8dc6-ed972f2db93c',
|
||||
name: 'eu-central-1',
|
||||
__typename: 'regions',
|
||||
},
|
||||
],
|
||||
__typename: 'apps',
|
||||
},
|
||||
{
|
||||
id: 'pitr14-id',
|
||||
name: 'pitr14',
|
||||
slug: 'pitr14',
|
||||
createdAt: '2025-02-25T08:55:22.82937+00:00',
|
||||
subdomain: 'jqumebxpocjytrhevonb',
|
||||
region: {
|
||||
id: '1',
|
||||
name: 'us-east-1',
|
||||
__typename: 'regions',
|
||||
},
|
||||
deployments: [],
|
||||
creator: {
|
||||
id: 'creator-d-elek-id',
|
||||
email: 'david@elek.com',
|
||||
displayName: 'David Elek',
|
||||
__typename: 'users',
|
||||
},
|
||||
appStates: [
|
||||
{
|
||||
id: '04bc2db3-a948-48fb-b674-7a8a0133dd2b',
|
||||
appId: 'pitr14-id',
|
||||
message: '',
|
||||
stateId: 5,
|
||||
createdAt: '2025-03-11T20:47:03.102948+00:00',
|
||||
__typename: 'appStateHistory',
|
||||
deployments: [],
|
||||
creator: {
|
||||
id: 'creator-r-elek-id',
|
||||
email: 'robert@elek.com',
|
||||
displayName: 'Robert',
|
||||
__typename: 'users',
|
||||
},
|
||||
],
|
||||
__typename: 'apps',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
appStates: [
|
||||
{
|
||||
id: 'c7fbf7ad-b60c-432b-86c2-5a9509054c47',
|
||||
appId: 'pitr-region-TEST-eu-id',
|
||||
message: '',
|
||||
stateId: 5,
|
||||
createdAt: '2025-03-12T11:08:59.926611+00:00',
|
||||
__typename: 'appStateHistory',
|
||||
},
|
||||
],
|
||||
__typename: 'apps',
|
||||
},
|
||||
{
|
||||
id: 'pitr-test-id',
|
||||
name: 'pitr-test',
|
||||
slug: 'pitr-test',
|
||||
createdAt: '2025-03-04T13:48:59.76498+00:00',
|
||||
subdomain: 'gnlivtcgjxctuujxpslj',
|
||||
region: {
|
||||
id: '1',
|
||||
name: 'us-east-1',
|
||||
__typename: 'regions',
|
||||
},
|
||||
deployments: [],
|
||||
creator: {
|
||||
id: 'creator-d-elek-id',
|
||||
email: 'dbarrosop@dravetech.com',
|
||||
displayName: 'David Elek',
|
||||
__typename: 'users',
|
||||
},
|
||||
appStates: [
|
||||
{
|
||||
id: 'fc344bc6-1c59-447a-813f-e0f65754b0e0',
|
||||
appId: 'pitr-test-id',
|
||||
message:
|
||||
'failed to deploy application to kubernetes: failed to deploy application: failed to check rollout status: error running kubectl: exit status 1',
|
||||
stateId: 8,
|
||||
createdAt: '2025-03-11T15:34:41.25304+00:00',
|
||||
__typename: 'appStateHistory',
|
||||
},
|
||||
],
|
||||
__typename: 'apps',
|
||||
},
|
||||
{
|
||||
id: 'pitr14-id',
|
||||
name: 'pitr14',
|
||||
slug: 'pitr14',
|
||||
createdAt: '2025-02-25T08:55:22.82937+00:00',
|
||||
subdomain: 'jqumebxpocjytrhevonb',
|
||||
region: {
|
||||
id: '1',
|
||||
name: 'us-east-1',
|
||||
__typename: 'regions',
|
||||
},
|
||||
deployments: [],
|
||||
creator: {
|
||||
id: 'creator-d-elek-id',
|
||||
email: 'david@elek.com',
|
||||
displayName: 'David Elek',
|
||||
__typename: 'users',
|
||||
},
|
||||
appStates: [
|
||||
{
|
||||
id: '04bc2db3-a948-48fb-b674-7a8a0133dd2b',
|
||||
appId: 'pitr14-id',
|
||||
message: '',
|
||||
stateId: 5,
|
||||
createdAt: '2025-03-11T20:47:03.102948+00:00',
|
||||
__typename: 'appStateHistory',
|
||||
},
|
||||
],
|
||||
__typename: 'apps',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
export const getEmptyProjectsQuery = nhostGraphQLLink.query('getProjects', () =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
apps: [],
|
||||
},
|
||||
}),
|
||||
export const getEmptyProjectsQuery = nhostGraphQLLink.query(
|
||||
'getProjects',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
apps: [],
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { HttpResponse } from 'msw';
|
||||
import nhostGraphQLLink from './nhostGraphQLLink';
|
||||
|
||||
export const organizationMemberInvites = nhostGraphQLLink.query(
|
||||
'organizationMemberInvites',
|
||||
() => HttpResponse.json({ data: { organizationMemberInvites: [] } }),
|
||||
(_req, res, ctx) => res(ctx.data({ organizationMemberInvites: [] })),
|
||||
);
|
||||
|
||||
export const organizationNewRequests = nhostGraphQLLink.query(
|
||||
'organizationNewRequests',
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
organizationNewRequests: [
|
||||
{
|
||||
id: 'org-request-id-1',
|
||||
@@ -18,6 +17,6 @@ export const organizationNewRequests = nhostGraphQLLink.query(
|
||||
__typename: 'organization_new_request',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { HttpResponse } from 'msw';
|
||||
import nhostGraphQLLink from './nhostGraphQLLink';
|
||||
|
||||
const permissionVariablesQuery = nhostGraphQLLink.query(
|
||||
'GetRolesPermissions',
|
||||
async () =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.delay(250),
|
||||
ctx.data({
|
||||
config: {
|
||||
auth: {
|
||||
user: {
|
||||
@@ -32,8 +32,8 @@ const permissionVariablesQuery = nhostGraphQLLink.query(
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
export default permissionVariablesQuery;
|
||||
|
||||
@@ -1,46 +1,50 @@
|
||||
import { HttpResponse } from 'msw';
|
||||
import nhostGraphQLLink from './nhostGraphQLLink';
|
||||
|
||||
/**
|
||||
* Use this handler to simulate a query that returns only the Pro plan.
|
||||
*/
|
||||
export const getProPlanOnlyQuery = nhostGraphQLLink.query('GetPlans', () =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
plans: [
|
||||
{
|
||||
__typename: 'plans',
|
||||
id: 'dc5e805e-1bef-4d43-809e-9fdf865e211a',
|
||||
name: 'Pro',
|
||||
price: 25,
|
||||
isFree: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
export const getProPlanOnlyQuery = nhostGraphQLLink.query(
|
||||
'GetPlans',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
plans: [
|
||||
{
|
||||
__typename: 'plans',
|
||||
id: 'dc5e805e-1bef-4d43-809e-9fdf865e211a',
|
||||
name: 'Pro',
|
||||
price: 25,
|
||||
isFree: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Use this handler to simulate a query that returns all the available plans.
|
||||
*/
|
||||
export const getAllPlansQuery = nhostGraphQLLink.query('GetPlans', () =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
plans: [
|
||||
{
|
||||
__typename: 'plans',
|
||||
id: '00000000-0000-0000-0000-000000000000',
|
||||
name: 'Starter',
|
||||
price: 0,
|
||||
isFree: true,
|
||||
},
|
||||
{
|
||||
__typename: 'plans',
|
||||
id: '00000000-0000-0000-0000-000000000001',
|
||||
name: 'Pro',
|
||||
price: 25,
|
||||
isFree: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
export const getAllPlansQuery = nhostGraphQLLink.query(
|
||||
'GetPlans',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
plans: [
|
||||
{
|
||||
__typename: 'plans',
|
||||
id: '00000000-0000-0000-0000-000000000000',
|
||||
name: 'Starter',
|
||||
price: 0,
|
||||
isFree: true,
|
||||
},
|
||||
{
|
||||
__typename: 'plans',
|
||||
id: '00000000-0000-0000-0000-000000000001',
|
||||
name: 'Pro',
|
||||
price: 25,
|
||||
isFree: false,
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { HttpResponse } from 'msw';
|
||||
import nhostGraphQLLink from './nhostGraphQLLink';
|
||||
|
||||
export const prefetchNewAppQuery = nhostGraphQLLink.query(
|
||||
'PrefetchNewApp',
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
regions: [
|
||||
{
|
||||
id: 'dd6f8e01-35a9-4ba6-8dc6-ed972f2db93c',
|
||||
@@ -68,6 +67,6 @@ export const prefetchNewAppQuery = nhostGraphQLLink.query(
|
||||
__typename: 'plans',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { HttpResponse } from 'msw';
|
||||
import nhostGraphQLLink from './nhostGraphQLLink';
|
||||
|
||||
/**
|
||||
* Use this handler to simulate the initial state of the allocated resources.
|
||||
*/
|
||||
export const resourcesUnavailableQuery = nhostGraphQLLink.query(
|
||||
'GetResources',
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
config: {
|
||||
__typename: 'ConfigConfig',
|
||||
postgres: {
|
||||
@@ -23,8 +23,8 @@ export const resourcesUnavailableQuery = nhostGraphQLLink.query(
|
||||
resources: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -32,9 +32,9 @@ export const resourcesUnavailableQuery = nhostGraphQLLink.query(
|
||||
*/
|
||||
export const resourcesAvailableQuery = nhostGraphQLLink.query(
|
||||
'GetResources',
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
config: {
|
||||
__typename: 'ConfigConfig',
|
||||
postgres: {
|
||||
@@ -86,8 +86,8 @@ export const resourcesAvailableQuery = nhostGraphQLLink.query(
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -95,9 +95,9 @@ export const resourcesAvailableQuery = nhostGraphQLLink.query(
|
||||
*/
|
||||
export const resourcesUpdatedQuery = nhostGraphQLLink.query(
|
||||
'GetResources',
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
config: {
|
||||
__typename: 'ConfigConfig',
|
||||
postgres: {
|
||||
@@ -137,6 +137,6 @@ export const resourcesUpdatedQuery = nhostGraphQLLink.query(
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { HttpResponse } from 'msw';
|
||||
import nhostGraphQLLink from './nhostGraphQLLink';
|
||||
|
||||
export default nhostGraphQLLink.mutation('UpdateConfig', () =>
|
||||
HttpResponse.json({
|
||||
data: {
|
||||
export default nhostGraphQLLink.mutation('UpdateConfig', (req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
updateConfig: {
|
||||
id: 'ConfigConfig',
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,415 +1,419 @@
|
||||
import { delay, http, HttpResponse } from 'msw';
|
||||
import { rest } from 'msw';
|
||||
|
||||
const hasuraMetadataQuery = http.post(
|
||||
const hasuraMetadataQuery = rest.post(
|
||||
'https://local.hasura.local.nhost.run/v1/metadata',
|
||||
async () => {
|
||||
await delay(250);
|
||||
|
||||
return HttpResponse.json({
|
||||
metadata: {
|
||||
version: 3,
|
||||
sources: [
|
||||
{
|
||||
name: 'default',
|
||||
kind: 'postgres',
|
||||
tables: [
|
||||
{
|
||||
table: { name: 'authors', schema: 'public' },
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'books',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'author_id',
|
||||
table: { name: 'books', schema: 'public' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: { name: 'books', schema: 'public' },
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'author',
|
||||
using: { foreign_key_constraint_on: 'author_id' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
configuration: {
|
||||
connection_info: {
|
||||
database_url: { from_env: 'HASURA_GRAPHQL_DATABASE_URL' },
|
||||
isolation_level: 'read-committed',
|
||||
pool_settings: {
|
||||
connection_lifetime: 600,
|
||||
idle_timeout: 180,
|
||||
max_connections: 50,
|
||||
retries: 1,
|
||||
},
|
||||
use_prepared_statements: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
resource_version: 10,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export const hasuraRelationShipsMetadataQuery = http.post(
|
||||
'https://local.hasura.local.nhost.run/v1/metadata',
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
resource_version: 26,
|
||||
metadata: {
|
||||
version: 3,
|
||||
sources: [
|
||||
{
|
||||
name: 'default',
|
||||
kind: 'postgres',
|
||||
tables: [
|
||||
{
|
||||
table: {
|
||||
name: 'country',
|
||||
schema: 'public',
|
||||
},
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'county',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'countryId',
|
||||
table: {
|
||||
name: 'county',
|
||||
schema: 'public',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.delay(250),
|
||||
ctx.json({
|
||||
metadata: {
|
||||
version: 3,
|
||||
sources: [
|
||||
{
|
||||
name: 'default',
|
||||
kind: 'postgres',
|
||||
tables: [
|
||||
{
|
||||
table: { name: 'authors', schema: 'public' },
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'books',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'author_id',
|
||||
table: { name: 'books', schema: 'public' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'county',
|
||||
schema: 'public',
|
||||
],
|
||||
},
|
||||
object_relationships: [
|
||||
{
|
||||
{
|
||||
table: { name: 'books', schema: 'public' },
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'author',
|
||||
using: { foreign_key_constraint_on: 'author_id' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
configuration: {
|
||||
connection_info: {
|
||||
database_url: { from_env: 'HASURA_GRAPHQL_DATABASE_URL' },
|
||||
isolation_level: 'read-committed',
|
||||
pool_settings: {
|
||||
connection_lifetime: 600,
|
||||
idle_timeout: 180,
|
||||
max_connections: 50,
|
||||
retries: 1,
|
||||
},
|
||||
use_prepared_statements: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
resource_version: 10,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
export const hasuraRelationShipsMetadataQuery = rest.post(
|
||||
'https://local.hasura.local.nhost.run/v1/metadata',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.json({
|
||||
resource_version: 26,
|
||||
metadata: {
|
||||
version: 3,
|
||||
sources: [
|
||||
{
|
||||
name: 'default',
|
||||
kind: 'postgres',
|
||||
tables: [
|
||||
{
|
||||
table: {
|
||||
name: 'country',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'countryId',
|
||||
},
|
||||
schema: 'public',
|
||||
},
|
||||
],
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'town',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'countyId',
|
||||
table: {
|
||||
name: 'town',
|
||||
schema: 'public',
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'county',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'countryId',
|
||||
table: {
|
||||
name: 'county',
|
||||
schema: 'public',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'town',
|
||||
schema: 'public',
|
||||
],
|
||||
},
|
||||
object_relationships: [
|
||||
{
|
||||
{
|
||||
table: {
|
||||
name: 'county',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'countyId',
|
||||
},
|
||||
schema: 'public',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
configuration: {
|
||||
connection_info: {
|
||||
database_url: {
|
||||
from_env: 'HASURA_GRAPHQL_DATABASE_URL',
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'country',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'countryId',
|
||||
},
|
||||
},
|
||||
],
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'town',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'countyId',
|
||||
table: {
|
||||
name: 'town',
|
||||
schema: 'public',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
isolation_level: 'read-committed',
|
||||
pool_settings: {
|
||||
connection_lifetime: 600,
|
||||
idle_timeout: 180,
|
||||
max_connections: 50,
|
||||
retries: 1,
|
||||
{
|
||||
table: {
|
||||
name: 'town',
|
||||
schema: 'public',
|
||||
},
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'county',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'countyId',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
configuration: {
|
||||
connection_info: {
|
||||
database_url: {
|
||||
from_env: 'HASURA_GRAPHQL_DATABASE_URL',
|
||||
},
|
||||
isolation_level: 'read-committed',
|
||||
pool_settings: {
|
||||
connection_lifetime: 600,
|
||||
idle_timeout: 180,
|
||||
max_connections: 50,
|
||||
retries: 1,
|
||||
},
|
||||
use_prepared_statements: true,
|
||||
},
|
||||
use_prepared_statements: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
export const hasuraColumnMetadataQuery = http.post(
|
||||
export const hasuraColumnMetadataQuery = rest.post(
|
||||
'https://local.hasura.local.nhost.run/v1/metadata',
|
||||
() =>
|
||||
HttpResponse.json({
|
||||
resource_version: 389,
|
||||
metadata: {
|
||||
version: 3,
|
||||
sources: [
|
||||
{
|
||||
name: 'default',
|
||||
kind: 'postgres',
|
||||
tables: [
|
||||
{
|
||||
table: {
|
||||
name: 'actor',
|
||||
schema: 'public',
|
||||
},
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'actor_movie',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'actor_id',
|
||||
table: {
|
||||
name: 'actor_movie',
|
||||
schema: 'public',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'actor_movie',
|
||||
schema: 'public',
|
||||
},
|
||||
object_relationships: [
|
||||
{
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.json({
|
||||
resource_version: 389,
|
||||
metadata: {
|
||||
version: 3,
|
||||
sources: [
|
||||
{
|
||||
name: 'default',
|
||||
kind: 'postgres',
|
||||
tables: [
|
||||
{
|
||||
table: {
|
||||
name: 'actor',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'actor_id',
|
||||
},
|
||||
schema: 'public',
|
||||
},
|
||||
{
|
||||
name: 'movie',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'movie_id',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'director',
|
||||
schema: 'public',
|
||||
},
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'movies',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'director_id',
|
||||
table: {
|
||||
name: 'movies',
|
||||
schema: 'public',
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'actor_movie',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'actor_id',
|
||||
table: {
|
||||
name: 'actor_movie',
|
||||
schema: 'public',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'movies',
|
||||
schema: 'public',
|
||||
],
|
||||
},
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'author',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'director_id',
|
||||
},
|
||||
},
|
||||
],
|
||||
array_relationships: [
|
||||
{
|
||||
{
|
||||
table: {
|
||||
name: 'actor_movie',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'movie_id',
|
||||
table: {
|
||||
name: 'actor_movie',
|
||||
schema: 'public',
|
||||
},
|
||||
schema: 'public',
|
||||
},
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'actor',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'actor_id',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'notes',
|
||||
schema: 'public',
|
||||
{
|
||||
name: 'movie',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'movie_id',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'user',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'owner',
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'director',
|
||||
schema: 'public',
|
||||
},
|
||||
],
|
||||
insert_permissions: [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
check: {
|
||||
owner: {
|
||||
_eq: 'X-Hasura-User-Id',
|
||||
},
|
||||
},
|
||||
columns: ['id', 'note', 'owner'],
|
||||
},
|
||||
},
|
||||
],
|
||||
select_permissions: [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
columns: ['id', 'note'],
|
||||
filter: {
|
||||
owner: {
|
||||
_eq: 'X-Hasura-User-Id',
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'movies',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'director_id',
|
||||
table: {
|
||||
name: 'movies',
|
||||
schema: 'public',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
update_permissions: [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
columns: ['note'],
|
||||
filter: {
|
||||
owner: {
|
||||
_eq: 'X-Hasura-User-Id',
|
||||
},
|
||||
},
|
||||
check: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
delete_permissions: [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
filter: {
|
||||
id: {
|
||||
_eq: 'X-Hasura-User-Id',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'buckets',
|
||||
schema: 'storage',
|
||||
],
|
||||
},
|
||||
configuration: {
|
||||
column_config: {
|
||||
cache_control: {
|
||||
custom_name: 'cacheControl',
|
||||
},
|
||||
created_at: {
|
||||
custom_name: 'createdAt',
|
||||
},
|
||||
download_expiration: {
|
||||
custom_name: 'downloadExpiration',
|
||||
},
|
||||
id: {
|
||||
custom_name: 'id',
|
||||
},
|
||||
max_upload_file_size: {
|
||||
custom_name: 'maxUploadFileSize',
|
||||
},
|
||||
min_upload_file_size: {
|
||||
custom_name: 'minUploadFileSize',
|
||||
},
|
||||
presigned_urls_enabled: {
|
||||
custom_name: 'presignedUrlsEnabled',
|
||||
},
|
||||
updated_at: {
|
||||
custom_name: 'updatedAt',
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'movies',
|
||||
schema: 'public',
|
||||
},
|
||||
custom_column_names: {
|
||||
cache_control: 'cacheControl',
|
||||
created_at: 'createdAt',
|
||||
download_expiration: 'downloadExpiration',
|
||||
id: 'id',
|
||||
max_upload_file_size: 'maxUploadFileSize',
|
||||
min_upload_file_size: 'minUploadFileSize',
|
||||
presigned_urls_enabled: 'presignedUrlsEnabled',
|
||||
updated_at: 'updatedAt',
|
||||
},
|
||||
custom_name: 'buckets',
|
||||
custom_root_fields: {
|
||||
delete: 'deleteBuckets',
|
||||
delete_by_pk: 'deleteBucket',
|
||||
insert: 'insertBuckets',
|
||||
insert_one: 'insertBucket',
|
||||
select: 'buckets',
|
||||
select_aggregate: 'bucketsAggregate',
|
||||
select_by_pk: 'bucket',
|
||||
update: 'updateBuckets',
|
||||
update_by_pk: 'updateBucket',
|
||||
},
|
||||
},
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'files',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'bucket_id',
|
||||
table: {
|
||||
name: 'files',
|
||||
schema: 'storage',
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'author',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'director_id',
|
||||
},
|
||||
},
|
||||
],
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'actor_movie',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'movie_id',
|
||||
table: {
|
||||
name: 'actor_movie',
|
||||
schema: 'public',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: {
|
||||
name: 'notes',
|
||||
schema: 'public',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
configuration: {
|
||||
connection_info: {
|
||||
database_url: {
|
||||
from_env: 'HASURA_GRAPHQL_DATABASE_URL',
|
||||
object_relationships: [
|
||||
{
|
||||
name: 'user',
|
||||
using: {
|
||||
foreign_key_constraint_on: 'owner',
|
||||
},
|
||||
},
|
||||
],
|
||||
insert_permissions: [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
check: {
|
||||
owner: {
|
||||
_eq: 'X-Hasura-User-Id',
|
||||
},
|
||||
},
|
||||
columns: ['id', 'note', 'owner'],
|
||||
},
|
||||
},
|
||||
],
|
||||
select_permissions: [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
columns: ['id', 'note'],
|
||||
filter: {
|
||||
owner: {
|
||||
_eq: 'X-Hasura-User-Id',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
update_permissions: [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
columns: ['note'],
|
||||
filter: {
|
||||
owner: {
|
||||
_eq: 'X-Hasura-User-Id',
|
||||
},
|
||||
},
|
||||
check: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
delete_permissions: [
|
||||
{
|
||||
role: 'user',
|
||||
permission: {
|
||||
filter: {
|
||||
id: {
|
||||
_eq: 'X-Hasura-User-Id',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
isolation_level: 'read-committed',
|
||||
pool_settings: {
|
||||
connection_lifetime: 600,
|
||||
idle_timeout: 180,
|
||||
max_connections: 50,
|
||||
retries: 1,
|
||||
{
|
||||
table: {
|
||||
name: 'buckets',
|
||||
schema: 'storage',
|
||||
},
|
||||
configuration: {
|
||||
column_config: {
|
||||
cache_control: {
|
||||
custom_name: 'cacheControl',
|
||||
},
|
||||
created_at: {
|
||||
custom_name: 'createdAt',
|
||||
},
|
||||
download_expiration: {
|
||||
custom_name: 'downloadExpiration',
|
||||
},
|
||||
id: {
|
||||
custom_name: 'id',
|
||||
},
|
||||
max_upload_file_size: {
|
||||
custom_name: 'maxUploadFileSize',
|
||||
},
|
||||
min_upload_file_size: {
|
||||
custom_name: 'minUploadFileSize',
|
||||
},
|
||||
presigned_urls_enabled: {
|
||||
custom_name: 'presignedUrlsEnabled',
|
||||
},
|
||||
updated_at: {
|
||||
custom_name: 'updatedAt',
|
||||
},
|
||||
},
|
||||
custom_column_names: {
|
||||
cache_control: 'cacheControl',
|
||||
created_at: 'createdAt',
|
||||
download_expiration: 'downloadExpiration',
|
||||
id: 'id',
|
||||
max_upload_file_size: 'maxUploadFileSize',
|
||||
min_upload_file_size: 'minUploadFileSize',
|
||||
presigned_urls_enabled: 'presignedUrlsEnabled',
|
||||
updated_at: 'updatedAt',
|
||||
},
|
||||
custom_name: 'buckets',
|
||||
custom_root_fields: {
|
||||
delete: 'deleteBuckets',
|
||||
delete_by_pk: 'deleteBucket',
|
||||
insert: 'insertBuckets',
|
||||
insert_one: 'insertBucket',
|
||||
select: 'buckets',
|
||||
select_aggregate: 'bucketsAggregate',
|
||||
select_by_pk: 'bucket',
|
||||
update: 'updateBuckets',
|
||||
update_by_pk: 'updateBucket',
|
||||
},
|
||||
},
|
||||
array_relationships: [
|
||||
{
|
||||
name: 'files',
|
||||
using: {
|
||||
foreign_key_constraint_on: {
|
||||
column: 'bucket_id',
|
||||
table: {
|
||||
name: 'files',
|
||||
schema: 'storage',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
configuration: {
|
||||
connection_info: {
|
||||
database_url: {
|
||||
from_env: 'HASURA_GRAPHQL_DATABASE_URL',
|
||||
},
|
||||
isolation_level: 'read-committed',
|
||||
pool_settings: {
|
||||
connection_lifetime: 600,
|
||||
idle_timeout: 180,
|
||||
max_connections: 50,
|
||||
retries: 1,
|
||||
},
|
||||
use_prepared_statements: true,
|
||||
},
|
||||
use_prepared_statements: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
export default hasuraMetadataQuery;
|
||||
|
||||
@@ -1,23 +1,143 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { rest } from 'msw';
|
||||
|
||||
const tableQuery = http.post(
|
||||
const tableQuery = rest.post(
|
||||
'https://local.hasura.local.nhost.run/v2/query',
|
||||
async ({ request }) => {
|
||||
const body = (await request.json()) as any;
|
||||
if (/table_name = 'authors'/gim.exec(body?.args?.[0].args.sql) !== null) {
|
||||
return HttpResponse.json([
|
||||
async (req, res, ctx) => {
|
||||
const body = await req.json();
|
||||
if (/table_name = 'authors'/gim.exec(body.args[0].args.sql) !== null) {
|
||||
return res(
|
||||
ctx.delay(250),
|
||||
ctx.json([
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [
|
||||
['row_to_json'],
|
||||
[
|
||||
'{"table_catalog":"pqfgbylcwyuertjcrmgy","table_schema":"public","table_name":"authors","column_name":"id","ordinal_position":1,"column_default":"gen_random_uuid()","is_nullable":"NO","data_type":"uuid","character_maximum_length":null,"character_octet_length":null,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":null,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"pqfgbylcwyuertjcrmgy","udt_schema":"pg_catalog","udt_name":"uuid","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"1","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","is_primary":true,"is_unique":true,"column_comment":null}',
|
||||
],
|
||||
[
|
||||
'{"table_catalog":"pqfgbylcwyuertjcrmgy","table_schema":"public","table_name":"authors","column_name":"name","ordinal_position":2,"column_default":null,"is_nullable":"NO","data_type":"text","character_maximum_length":null,"character_octet_length":1073741824,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":null,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"pqfgbylcwyuertjcrmgy","udt_schema":"pg_catalog","udt_name":"text","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"2","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","is_primary":false,"is_unique":false,"column_comment":null}',
|
||||
],
|
||||
[
|
||||
'{"table_catalog":"pqfgbylcwyuertjcrmgy","table_schema":"public","table_name":"authors","column_name":"birth_date","ordinal_position":3,"column_default":null,"is_nullable":"NO","data_type":"timestamp without time zone","character_maximum_length":null,"character_octet_length":null,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":6,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"pqfgbylcwyuertjcrmgy","udt_schema":"pg_catalog","udt_name":"timestamp","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"3","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","is_primary":false,"is_unique":false,"column_comment":null}',
|
||||
],
|
||||
],
|
||||
},
|
||||
{ result_type: 'TuplesOk', result: [['row_to_json']] },
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [
|
||||
['row_to_json'],
|
||||
[
|
||||
'{"constraint_name":"authors_pkey","constraint_type":"p","constraint_definition":"PRIMARY KEY (id)","column_name":"id"}',
|
||||
],
|
||||
],
|
||||
},
|
||||
{ result_type: 'TuplesOk', result: [['count'], ['0']] },
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
if (/table_name = 'town'/gim.exec(body.args[0].args.sql) !== null) {
|
||||
return res(
|
||||
ctx.json([
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [
|
||||
['row_to_json'],
|
||||
[
|
||||
'{"table_catalog":"local","table_schema":"public","table_name":"town","column_name":"id","ordinal_position":1,"column_default":"gen_random_uuid()","is_nullable":"NO","data_type":"uuid","character_maximum_length":null,"character_octet_length":null,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":null,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"local","udt_schema":"pg_catalog","udt_name":"uuid","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"1","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","full_data_type":"uuid","is_primary":true,"is_unique":true,"column_comment":null}',
|
||||
],
|
||||
[
|
||||
'{"table_catalog":"local","table_schema":"public","table_name":"town","column_name":"name","ordinal_position":2,"column_default":null,"is_nullable":"NO","data_type":"text","character_maximum_length":null,"character_octet_length":1073741824,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":null,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"local","udt_schema":"pg_catalog","udt_name":"text","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"2","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","full_data_type":"text","is_primary":false,"is_unique":false,"column_comment":null}',
|
||||
],
|
||||
[
|
||||
'{"table_catalog":"local","table_schema":"public","table_name":"town","column_name":"countyId","ordinal_position":3,"column_default":null,"is_nullable":"NO","data_type":"uuid","character_maximum_length":null,"character_octet_length":null,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":null,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"local","udt_schema":"pg_catalog","udt_name":"uuid","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"3","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","full_data_type":"uuid","is_primary":false,"is_unique":false,"column_comment":null}',
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [['row_to_json']],
|
||||
},
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [
|
||||
['row_to_json'],
|
||||
[
|
||||
'{"constraint_name":"town_countyId_fkey","constraint_type":"f","constraint_definition":"FOREIGN KEY (\\"countyId\\") REFERENCES county(id) ON UPDATE RESTRICT ON DELETE RESTRICT","column_name":"countyId"}',
|
||||
],
|
||||
[
|
||||
'{"constraint_name":"town_pkey","constraint_type":"p","constraint_definition":"PRIMARY KEY (id)","column_name":"id"}',
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [['count'], ['0']],
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
if (/table_name = 'actor'/gim.exec(body.args[0].args.sql) !== null) {
|
||||
return res(
|
||||
ctx.json([
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [
|
||||
['row_to_json'],
|
||||
[
|
||||
'{"table_catalog":"klkudrtrpapfrseiidkp","table_schema":"public","table_name":"actor","column_name":"id","ordinal_position":1,"column_default":"gen_random_uuid()","is_nullable":"NO","data_type":"uuid","character_maximum_length":null,"character_octet_length":null,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":null,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"klkudrtrpapfrseiidkp","udt_schema":"pg_catalog","udt_name":"uuid","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"1","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","full_data_type":"uuid","is_primary":true,"is_unique":true,"column_comment":null}',
|
||||
],
|
||||
[
|
||||
'{"table_catalog":"klkudrtrpapfrseiidkp","table_schema":"public","table_name":"actor","column_name":"name","ordinal_position":2,"column_default":null,"is_nullable":"NO","data_type":"text","character_maximum_length":null,"character_octet_length":1073741824,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":null,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"klkudrtrpapfrseiidkp","udt_schema":"pg_catalog","udt_name":"text","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"2","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","full_data_type":"text","is_primary":false,"is_unique":false,"column_comment":null}',
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [
|
||||
['row_to_json'],
|
||||
['{"id":"1902e481-b080-4340-abe3-27b0a60973c6","name":"There"}'],
|
||||
['{"id":"a486b088-50e8-41d0-88b0-5bf9a3e7b5e7","name":"hello"}'],
|
||||
],
|
||||
},
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [
|
||||
['row_to_json'],
|
||||
[
|
||||
'{"constraint_name":"actor_pkey","constraint_type":"p","constraint_definition":"PRIMARY KEY (id)","column_name":"id"}',
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [['count'], ['2']],
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
return res(
|
||||
ctx.delay(250),
|
||||
ctx.json([
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [
|
||||
['row_to_json'],
|
||||
[
|
||||
'{"table_catalog":"pqfgbylcwyuertjcrmgy","table_schema":"public","table_name":"authors","column_name":"id","ordinal_position":1,"column_default":"gen_random_uuid()","is_nullable":"NO","data_type":"uuid","character_maximum_length":null,"character_octet_length":null,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":null,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"pqfgbylcwyuertjcrmgy","udt_schema":"pg_catalog","udt_name":"uuid","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"1","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","is_primary":true,"is_unique":true,"column_comment":null}',
|
||||
'{"table_catalog":"pqfgbylcwyuertjcrmgy","table_schema":"public","table_name":"books","column_name":"id","ordinal_position":1,"column_default":"gen_random_uuid()","is_nullable":"NO","data_type":"uuid","character_maximum_length":null,"character_octet_length":null,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":null,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"pqfgbylcwyuertjcrmgy","udt_schema":"pg_catalog","udt_name":"uuid","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"1","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","is_primary":true,"is_unique":true,"column_comment":null}',
|
||||
],
|
||||
[
|
||||
'{"table_catalog":"pqfgbylcwyuertjcrmgy","table_schema":"public","table_name":"authors","column_name":"name","ordinal_position":2,"column_default":null,"is_nullable":"NO","data_type":"text","character_maximum_length":null,"character_octet_length":1073741824,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":null,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"pqfgbylcwyuertjcrmgy","udt_schema":"pg_catalog","udt_name":"text","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"2","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","is_primary":false,"is_unique":false,"column_comment":null}',
|
||||
'{"table_catalog":"pqfgbylcwyuertjcrmgy","table_schema":"public","table_name":"books","column_name":"title","ordinal_position":2,"column_default":null,"is_nullable":"NO","data_type":"text","character_maximum_length":null,"character_octet_length":1073741824,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":null,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"pqfgbylcwyuertjcrmgy","udt_schema":"pg_catalog","udt_name":"text","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"2","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","is_primary":false,"is_unique":false,"column_comment":null}',
|
||||
],
|
||||
[
|
||||
'{"table_catalog":"pqfgbylcwyuertjcrmgy","table_schema":"public","table_name":"authors","column_name":"birth_date","ordinal_position":3,"column_default":null,"is_nullable":"NO","data_type":"timestamp without time zone","character_maximum_length":null,"character_octet_length":null,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":6,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"pqfgbylcwyuertjcrmgy","udt_schema":"pg_catalog","udt_name":"timestamp","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"3","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","is_primary":false,"is_unique":false,"column_comment":null}',
|
||||
'{"table_catalog":"pqfgbylcwyuertjcrmgy","table_schema":"public","table_name":"books","column_name":"release_date","ordinal_position":3,"column_default":null,"is_nullable":"NO","data_type":"timestamp without time zone","character_maximum_length":null,"character_octet_length":null,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":6,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"pqfgbylcwyuertjcrmgy","udt_schema":"pg_catalog","udt_name":"timestamp","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"3","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","is_primary":false,"is_unique":false,"column_comment":null}',
|
||||
],
|
||||
[
|
||||
'{"table_catalog":"pqfgbylcwyuertjcrmgy","table_schema":"public","table_name":"books","column_name":"author_id","ordinal_position":4,"column_default":null,"is_nullable":"NO","data_type":"uuid","character_maximum_length":null,"character_octet_length":null,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":null,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"pqfgbylcwyuertjcrmgy","udt_schema":"pg_catalog","udt_name":"uuid","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"4","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","is_primary":false,"is_unique":false,"column_comment":null}',
|
||||
],
|
||||
],
|
||||
},
|
||||
@@ -27,126 +147,16 @@ const tableQuery = http.post(
|
||||
result: [
|
||||
['row_to_json'],
|
||||
[
|
||||
'{"constraint_name":"authors_pkey","constraint_type":"p","constraint_definition":"PRIMARY KEY (id)","column_name":"id"}',
|
||||
'{"constraint_name":"books_author_id_fkey","constraint_type":"f","constraint_definition":"FOREIGN KEY (author_id) REFERENCES authors(id) ON UPDATE RESTRICT ON DELETE RESTRICT","column_name":"author_id"}',
|
||||
],
|
||||
[
|
||||
'{"constraint_name":"books_pkey","constraint_type":"p","constraint_definition":"PRIMARY KEY (id)","column_name":"id"}',
|
||||
],
|
||||
],
|
||||
},
|
||||
{ result_type: 'TuplesOk', result: [['count'], ['0']] },
|
||||
]);
|
||||
}
|
||||
|
||||
if (/table_name = 'town'/gim.exec(body.args[0].args.sql) !== null) {
|
||||
return HttpResponse.json([
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [
|
||||
['row_to_json'],
|
||||
[
|
||||
'{"table_catalog":"local","table_schema":"public","table_name":"town","column_name":"id","ordinal_position":1,"column_default":"gen_random_uuid()","is_nullable":"NO","data_type":"uuid","character_maximum_length":null,"character_octet_length":null,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":null,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"local","udt_schema":"pg_catalog","udt_name":"uuid","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"1","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","full_data_type":"uuid","is_primary":true,"is_unique":true,"column_comment":null}',
|
||||
],
|
||||
[
|
||||
'{"table_catalog":"local","table_schema":"public","table_name":"town","column_name":"name","ordinal_position":2,"column_default":null,"is_nullable":"NO","data_type":"text","character_maximum_length":null,"character_octet_length":1073741824,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":null,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"local","udt_schema":"pg_catalog","udt_name":"text","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"2","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","full_data_type":"text","is_primary":false,"is_unique":false,"column_comment":null}',
|
||||
],
|
||||
[
|
||||
'{"table_catalog":"local","table_schema":"public","table_name":"town","column_name":"countyId","ordinal_position":3,"column_default":null,"is_nullable":"NO","data_type":"uuid","character_maximum_length":null,"character_octet_length":null,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":null,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"local","udt_schema":"pg_catalog","udt_name":"uuid","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"3","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","full_data_type":"uuid","is_primary":false,"is_unique":false,"column_comment":null}',
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [['row_to_json']],
|
||||
},
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [
|
||||
['row_to_json'],
|
||||
[
|
||||
'{"constraint_name":"town_countyId_fkey","constraint_type":"f","constraint_definition":"FOREIGN KEY (\\"countyId\\") REFERENCES county(id) ON UPDATE RESTRICT ON DELETE RESTRICT","column_name":"countyId"}',
|
||||
],
|
||||
[
|
||||
'{"constraint_name":"town_pkey","constraint_type":"p","constraint_definition":"PRIMARY KEY (id)","column_name":"id"}',
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [['count'], ['0']],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
if (/table_name = 'actor'/gim.exec(body.args[0].args.sql) !== null) {
|
||||
return HttpResponse.json([
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [
|
||||
['row_to_json'],
|
||||
[
|
||||
'{"table_catalog":"klkudrtrpapfrseiidkp","table_schema":"public","table_name":"actor","column_name":"id","ordinal_position":1,"column_default":"gen_random_uuid()","is_nullable":"NO","data_type":"uuid","character_maximum_length":null,"character_octet_length":null,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":null,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"klkudrtrpapfrseiidkp","udt_schema":"pg_catalog","udt_name":"uuid","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"1","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","full_data_type":"uuid","is_primary":true,"is_unique":true,"column_comment":null}',
|
||||
],
|
||||
[
|
||||
'{"table_catalog":"klkudrtrpapfrseiidkp","table_schema":"public","table_name":"actor","column_name":"name","ordinal_position":2,"column_default":null,"is_nullable":"NO","data_type":"text","character_maximum_length":null,"character_octet_length":1073741824,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":null,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"klkudrtrpapfrseiidkp","udt_schema":"pg_catalog","udt_name":"text","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"2","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","full_data_type":"text","is_primary":false,"is_unique":false,"column_comment":null}',
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [
|
||||
['row_to_json'],
|
||||
['{"id":"1902e481-b080-4340-abe3-27b0a60973c6","name":"There"}'],
|
||||
['{"id":"a486b088-50e8-41d0-88b0-5bf9a3e7b5e7","name":"hello"}'],
|
||||
],
|
||||
},
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [
|
||||
['row_to_json'],
|
||||
[
|
||||
'{"constraint_name":"actor_pkey","constraint_type":"p","constraint_definition":"PRIMARY KEY (id)","column_name":"id"}',
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [['count'], ['2']],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
return HttpResponse.json([
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [
|
||||
['row_to_json'],
|
||||
[
|
||||
'{"table_catalog":"pqfgbylcwyuertjcrmgy","table_schema":"public","table_name":"books","column_name":"id","ordinal_position":1,"column_default":"gen_random_uuid()","is_nullable":"NO","data_type":"uuid","character_maximum_length":null,"character_octet_length":null,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":null,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"pqfgbylcwyuertjcrmgy","udt_schema":"pg_catalog","udt_name":"uuid","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"1","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","is_primary":true,"is_unique":true,"column_comment":null}',
|
||||
],
|
||||
[
|
||||
'{"table_catalog":"pqfgbylcwyuertjcrmgy","table_schema":"public","table_name":"books","column_name":"title","ordinal_position":2,"column_default":null,"is_nullable":"NO","data_type":"text","character_maximum_length":null,"character_octet_length":1073741824,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":null,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"pqfgbylcwyuertjcrmgy","udt_schema":"pg_catalog","udt_name":"text","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"2","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","is_primary":false,"is_unique":false,"column_comment":null}',
|
||||
],
|
||||
[
|
||||
'{"table_catalog":"pqfgbylcwyuertjcrmgy","table_schema":"public","table_name":"books","column_name":"release_date","ordinal_position":3,"column_default":null,"is_nullable":"NO","data_type":"timestamp without time zone","character_maximum_length":null,"character_octet_length":null,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":6,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"pqfgbylcwyuertjcrmgy","udt_schema":"pg_catalog","udt_name":"timestamp","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"3","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","is_primary":false,"is_unique":false,"column_comment":null}',
|
||||
],
|
||||
[
|
||||
'{"table_catalog":"pqfgbylcwyuertjcrmgy","table_schema":"public","table_name":"books","column_name":"author_id","ordinal_position":4,"column_default":null,"is_nullable":"NO","data_type":"uuid","character_maximum_length":null,"character_octet_length":null,"numeric_precision":null,"numeric_precision_radix":null,"numeric_scale":null,"datetime_precision":null,"interval_type":null,"interval_precision":null,"character_set_catalog":null,"character_set_schema":null,"character_set_name":null,"collation_catalog":null,"collation_schema":null,"collation_name":null,"domain_catalog":null,"domain_schema":null,"domain_name":null,"udt_catalog":"pqfgbylcwyuertjcrmgy","udt_schema":"pg_catalog","udt_name":"uuid","scope_catalog":null,"scope_schema":null,"scope_name":null,"maximum_cardinality":null,"dtd_identifier":"4","is_self_referencing":"NO","is_identity":"NO","identity_generation":null,"identity_start":null,"identity_increment":null,"identity_maximum":null,"identity_minimum":null,"identity_cycle":"NO","is_generated":"NEVER","generation_expression":null,"is_updatable":"YES","is_primary":false,"is_unique":false,"column_comment":null}',
|
||||
],
|
||||
],
|
||||
},
|
||||
{ result_type: 'TuplesOk', result: [['row_to_json']] },
|
||||
{
|
||||
result_type: 'TuplesOk',
|
||||
result: [
|
||||
['row_to_json'],
|
||||
[
|
||||
'{"constraint_name":"books_author_id_fkey","constraint_type":"f","constraint_definition":"FOREIGN KEY (author_id) REFERENCES authors(id) ON UPDATE RESTRICT ON DELETE RESTRICT","column_name":"author_id"}',
|
||||
],
|
||||
[
|
||||
'{"constraint_name":"books_pkey","constraint_type":"p","constraint_definition":"PRIMARY KEY (id)","column_name":"id"}',
|
||||
],
|
||||
],
|
||||
},
|
||||
{ result_type: 'TuplesOk', result: [['count'], ['0']] },
|
||||
]);
|
||||
]),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { mockSession } from '@/tests/mocks';
|
||||
import type { Session } from '@nhost/nhost-js/auth';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { rest } from 'msw';
|
||||
|
||||
const tokenQuery = http.post(
|
||||
const tokenQuery = rest.post(
|
||||
'https://local.auth.local.nhost.run/v1/token',
|
||||
() => HttpResponse.json<Session>(mockSession),
|
||||
(_req, res, ctx) => res(ctx.json<Session>(mockSession)),
|
||||
);
|
||||
|
||||
export default tokenQuery;
|
||||
|
||||
@@ -35,7 +35,6 @@ import userEvent, {
|
||||
type Options,
|
||||
type UserEvent,
|
||||
} from '@testing-library/user-event';
|
||||
import { HttpResponse } from 'msw';
|
||||
import { RouterContext } from 'next/dist/shared/lib/router-context.shared-runtime';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
@@ -155,9 +154,9 @@ const graphqlRequestHandlerFactory = (
|
||||
type: 'mutation' | 'query',
|
||||
responsePromise: any,
|
||||
) =>
|
||||
nhostGraphQLLink[type](operationName, async () => {
|
||||
nhostGraphQLLink[type](operationName, async (_req, res, ctx) => {
|
||||
const data = await responsePromise;
|
||||
return HttpResponse.json({ data });
|
||||
return res(ctx.data(data));
|
||||
});
|
||||
/* Helper function to pause responses to be able to test loading states */
|
||||
export const createGraphqlMockResolver = (
|
||||
|
||||
@@ -147,22 +147,6 @@
|
||||
"/products/auth/idtokens"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Workflows",
|
||||
"icon": "diagram-project",
|
||||
"pages": [
|
||||
"/products/auth/workflows/email-password",
|
||||
"/products/auth/workflows/oauth-providers",
|
||||
"/products/auth/workflows/passwordless-email",
|
||||
"/products/auth/workflows/passwordless-sms",
|
||||
"/products/auth/workflows/webauthn",
|
||||
"/products/auth/workflows/anonymous-users",
|
||||
"/products/auth/workflows/change-email",
|
||||
"/products/auth/workflows/change-password",
|
||||
"/products/auth/workflows/reset-password",
|
||||
"/products/auth/workflows/refresh-token"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Security",
|
||||
"icon": "shield",
|
||||
|
||||
@@ -4,9 +4,8 @@ set -euo pipefail
|
||||
|
||||
function build_openapi() {
|
||||
echo "⚒️⚒️⚒️ Building OpenAPI reference..."
|
||||
cp ../services/auth/docs/openapi.yaml reference/auth.yaml
|
||||
cp ../services/storage/controller/openapi.yaml reference/storage.yaml
|
||||
|
||||
cp ../packages/nhost-js/api/auth.yaml reference/
|
||||
cp ../packages/nhost-js/api/storage.yaml reference/
|
||||
|
||||
echo "⚒️⚒️⚒️ Generating documentation from OpenAPI specs for Auth service..."
|
||||
mintlify-openapi openapi \
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"test:broken-links": "pnpm exec mintlify broken-links"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mintlify": "^4.2.158",
|
||||
"mintlify": "^4.2.87",
|
||||
"prettier": "^3.5.3",
|
||||
"typedoc": "^0.28.4",
|
||||
"typedoc-plugin-markdown": "^4.6.3"
|
||||
|
||||
@@ -10,10 +10,10 @@ The Nhost CLI provides flexible development workflows that adapt to your needs.
|
||||
## Development Workflows
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Local Development" icon="laptop-code" href="/platform/cli/local-development">
|
||||
<Card title="Local Development" icon="laptop-code">
|
||||
Run the complete Nhost stack locally for offline development, fast iteration, and full control over your environment.
|
||||
</Card>
|
||||
<Card title="Cloud Development" icon="cloud" href="/platform/cli/cloud-development">
|
||||
<Card title="Cloud Development" icon="cloud">
|
||||
Develop against cloud infrastructure while maintaining local tools for simplified remote testing and team collaboration.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
1698
docs/pnpm-lock.yaml
generated
1698
docs/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@ Graphite, Nhost's AI service, extends the [Nhost stack](https://nhost.io) provid
|
||||
* Extend with custom data via webhooks
|
||||
* Automate workflows by exposing GraphQL queries or mutations or custom webhooks to the AI assistant
|
||||
* GraphQL API to interact with the assistants.
|
||||
* Permissions fully integrated with hasura and Auth; control who and who can not use which assistant via permissions.
|
||||
* Permissions fully integrated with hasura and hasura-auth; control who and who can not use which assistant via permissions.
|
||||
* Access to the underlying data for the assistant is limited to what the user can see.
|
||||
* Developer Assistant
|
||||
* Custom AI assistant with access to your project's information (i.e. database/graphql schema)
|
||||
|
||||
@@ -33,7 +33,7 @@ While the following permissions would require them first to elevate their access
|
||||
|
||||
# Protecting Auth data
|
||||
|
||||
Some user information needs to be changed via Auth's API rather than via graphql mutations. These endpoints are:
|
||||
Some user information needs to be changed via hasura-auth's API rather than via graphql mutations. These endpoints are:
|
||||
|
||||
- [/user/password](/reference/auth/post-user-password) for changing passwords
|
||||
- [/user/email/change](/reference/auth/post-user-email-change) for changing emails
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
## Sign-in anonymously
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor U as User
|
||||
participant A as Hasura Auth
|
||||
U->>+A: HTTP POST /signin/anonymous
|
||||
A->>A: Create anonymous user
|
||||
A->>-U: HTTP OK response
|
||||
Note left of A: Refresh token + access token
|
||||
```
|
||||
|
||||
## Deanonymisation
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor U as User
|
||||
participant A as Hasura Auth
|
||||
U-->A: Sign in anonymously
|
||||
U->>+A: HTTP POST /user/deanonymize
|
||||
A->>A: Deanonymise user
|
||||
alt Sign-in method is email+password
|
||||
Note over U,A: Same pathway as email+password sign-up
|
||||
else Sign-in method is passwordless
|
||||
Note over U,A: Same pathway as passwordless email
|
||||
end
|
||||
deactivate A
|
||||
```
|
||||
@@ -1,23 +0,0 @@
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor U as User
|
||||
participant A as Hasura Auth
|
||||
participant E as SMTP server
|
||||
participant F as Frontend
|
||||
U-->A: Sign in
|
||||
U->>+A: HTTP POST /user/email/change
|
||||
Note right of U: new email
|
||||
A->>A: Generate ticket
|
||||
A->>A: Store new email
|
||||
A-)E: Send verification to new email
|
||||
A->>-U: HTTP POST OK (no data)
|
||||
E-)U: Receive email
|
||||
U->>+A: HTTP GET /verify
|
||||
Note right of U: Follow email link
|
||||
A->>A: Change email
|
||||
A->>+F: HTTP redirect
|
||||
deactivate A
|
||||
F->>-U: HTTP OK response
|
||||
Note left of A: Refresh token + access token
|
||||
```
|
||||
@@ -1,17 +0,0 @@
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor U as User
|
||||
participant A as Hasura Auth
|
||||
opt Authenticated user
|
||||
U-->A: Sign in
|
||||
U->>+A: HTTP POST /user/password
|
||||
Note right of U: new password
|
||||
A->>-U: HTTP POST OK (no data)
|
||||
end
|
||||
opt Using ticket
|
||||
U->>+A: HTTP POST /user/password
|
||||
Note right of U: new password with ticket
|
||||
A->>-U: HTTP POST OK (no data)
|
||||
end
|
||||
```
|
||||
@@ -1,84 +0,0 @@
|
||||
## Sign up
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor U as User
|
||||
participant A as Hasura Auth
|
||||
participant E as SMTP server
|
||||
participant F as Frontend
|
||||
U->>+A: HTTP POST /signup/email-password
|
||||
A->>A: Create user
|
||||
alt No email verification
|
||||
A->>U: HTTP OK response
|
||||
Note left of A: Refresh token + access token
|
||||
else Email needs to be verified
|
||||
A->>A: Generate ticket
|
||||
A-)E: Send verification email
|
||||
A->>-U: HTTP OK response (no data)
|
||||
E-)U: Receive email
|
||||
U->>+A: HTTP GET /verify
|
||||
Note right of U: Follow email link
|
||||
A->>A: Flag user email as verified
|
||||
A->>-F: HTTP redirect with refresh token
|
||||
activate F
|
||||
F->>-U: HTTP OK response
|
||||
opt
|
||||
U->>+A: HTTP POST /token
|
||||
A->>-U: HTTP OK response
|
||||
Note left of A: Refresh token + access token
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Sign in
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor U as User
|
||||
participant A as Hasura Auth
|
||||
participant G as Authenticator/Authy
|
||||
U->>+A: HTTP POST /signin/email-password
|
||||
alt No MFA
|
||||
A->>U: HTTP OK response
|
||||
Note left of A: Refresh + access tokens
|
||||
else User activated MFA
|
||||
A->>-U: HTTP OK response
|
||||
Note left of A: MFA code
|
||||
U->>+G: Ask for a TOTP code
|
||||
G->>-U:
|
||||
U->>+A: HTTP POST /signin/mfa/totp
|
||||
Note right of U: TOTP code
|
||||
A->>-U: HTTP OK reponse
|
||||
Note left of A: Refresh token + access token
|
||||
end
|
||||
```
|
||||
|
||||
## Activate Multi-Factor Authentication
|
||||
|
||||
It is possible to add a step to authentication with email and password authentication. Once users registered, they can activate MFA TOTP:
|
||||
|
||||
1. Users generate a QR Code, that is then scanned in an authentication app such as [Authy](https://authy.com/) or [Google Authenticator](https://en.wikipedia.org/wiki/Google_Authenticator).
|
||||
2. They then send the TOTP code to Hasura Auth. MFA is now activated
|
||||
3. Next time they authenticate, Hasura Auth will first expect their email and password, but then, instead of completing authentication, Hasura Auth will expect the TOTP in order to return the refresh and the access tokens.
|
||||
|
||||
In order for users to be able to activate MFA TOTP, the `AUTH_MFA_ENABLED` environment variable must be set to `true`.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor U as User
|
||||
participant A as Hasura Auth
|
||||
participant G as Authenticator/Authy
|
||||
U-->A: Sign in
|
||||
U->>+A: HTTP GET /mfa/totp/generate
|
||||
A->>-U: HTTP OK response
|
||||
Note left of A: QR code as Data URL + TOTP secret
|
||||
U->>+G: Add account
|
||||
G->>-U: TOTP code
|
||||
U->>+A: HTTP POST /user/mfa
|
||||
Note right of U: TOTP code
|
||||
A->>A: Activate MFA
|
||||
A->>-U: HTTP OK response
|
||||
```
|
||||
@@ -1,25 +0,0 @@
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor U as User
|
||||
participant A as Hasura Auth
|
||||
participant P as Oauth Provider
|
||||
participant F as Frontend
|
||||
U->>+A: HTTP GET /signin/provider/{provider}
|
||||
A->>+P: Provider's authentication
|
||||
deactivate A
|
||||
P->>-A: HTTP GET /signin/provider/{provider}/callback
|
||||
activate A
|
||||
opt No user found
|
||||
A->>A: Create user
|
||||
end
|
||||
A->>A: Flag user email as verified
|
||||
A->>+F: HTTP redirect with refresh token
|
||||
deactivate A
|
||||
F->>-U: HTTP OK response
|
||||
opt
|
||||
U->>+A: HTTP POST /token
|
||||
A->>-U: HTTP OK response
|
||||
Note left of A: Refresh token + access token
|
||||
end
|
||||
```
|
||||
@@ -1,27 +0,0 @@
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor U as User
|
||||
participant A as Hasura Auth
|
||||
participant E as SMTP server
|
||||
participant F as Frontend
|
||||
U->>+A: HTTP POST /signin/passwordless/email
|
||||
opt No user found
|
||||
A->>A: Create user
|
||||
end
|
||||
A->>A: Generate ticket
|
||||
A-)E: Send verification email
|
||||
A->>-U: HTTP POST OK (no data)
|
||||
E-)U: Receive email
|
||||
U->>+A: HTTP GET /verify
|
||||
Note right of U: Follow email link
|
||||
A->>A: Flag user email as verified
|
||||
A->>+F: HTTP redirect with refresh token
|
||||
deactivate A
|
||||
F->>-U: HTTP OK response
|
||||
opt
|
||||
U->>+A: HTTP POST /token
|
||||
A->>-U: HTTP OK response
|
||||
Note left of A: Refresh token + access token
|
||||
end
|
||||
```
|
||||
@@ -1,24 +0,0 @@
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor U as User
|
||||
participant A as Hasura Auth
|
||||
participant S as SMS service
|
||||
U->>+A: HTTP POST /signin/passwordless/sms
|
||||
Note right of U: Phone number
|
||||
opt No user found
|
||||
A->>A: Create user
|
||||
end
|
||||
A-)+S: Send OTP code by SMS
|
||||
A->>-U: HTTP OK response (no data)
|
||||
S-)-U: Receive OTP code by SMS
|
||||
U->>+A: HTTP POST /signin/passwordless/sms/otp
|
||||
Note right of U: Phone number and OTP code
|
||||
A->>-U: HTTP OK response
|
||||
Note left of A: Refresh token + access token
|
||||
```
|
||||
|
||||
## Test phone numbers
|
||||
|
||||
Environmental variable `AUTH_SMS_TEST_PHONE_NUMBERS` can be set with a comma separated test phone numbers. When sign in
|
||||
is invoked the the SMS message with the verification code will be available in the logs. This way you can also test your SMS templates.
|
||||
@@ -1,10 +0,0 @@
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor U as User
|
||||
participant A as Hasura Auth
|
||||
U->>+A: HTTP POST /token
|
||||
A->>A: Update refresh token
|
||||
A->>-U: HTTP OK response
|
||||
Note left of A: Refresh token + access token
|
||||
```
|
||||
@@ -1,31 +0,0 @@
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor U as User
|
||||
participant A as Hasura Auth
|
||||
participant E as SMTP server
|
||||
participant F as Frontend
|
||||
U->>+A: HTTP POST /user/password/reset
|
||||
Note right of U: new email
|
||||
A->>A: Generate ticket
|
||||
A-)E: Send verification email
|
||||
A->>-U: HTTP POST OK (no data)
|
||||
E-)U: Receive email
|
||||
opt Use email redirect link
|
||||
U->>+A: HTTP GET /verify
|
||||
Note right of U: Follow email link
|
||||
A->>+F: HTTP redirect
|
||||
deactivate A
|
||||
F->>-U: HTTP OK response
|
||||
Note left of A: Refresh token + access token
|
||||
end
|
||||
opt Use ticket to customize reset flow (e.g. mobile)
|
||||
U->>+F: Deeplink w/ ticket
|
||||
Note left of F: User enters new password
|
||||
F->>+A: HTTP POST /user/password
|
||||
Note right of A: Set new password with ticket
|
||||
A->>+F: HTTP POST OK (no data)
|
||||
F->>+U: User handling
|
||||
Note right of U: User notified or logged in
|
||||
end
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user