Compare commits

..

14 Commits

Author SHA1 Message Date
David Barroso
5b53c568ad fix(ci): set config.custom_model_max_tokens (#3611) 2025-10-14 13:31:28 +02:00
David Barroso
24c5db943d feat(nhost-js): added pushChainFunction to functions and graphql clients (#3610) 2025-10-14 13:30:07 +02:00
David BM
ea87b81db6 chore(docs): add links to local development and cloud development (#3609)
Co-authored-by: robertkasza <robert.kasza@bishop-co.com>
2025-10-14 11:56:42 +02:00
robertkasza
226a22e322 fix(dashboard): Remove vite-plugin-dts (#3607) 2025-10-14 11:27:46 +02:00
David Barroso
9c58b4307a chore(storage): migrate to urfave and slog libraries (#3606) 2025-10-14 10:17:20 +02:00
robertkasza
7ecfa41790 fix(dashboard): Run audit and lint in dashboard (#3578) 2025-10-14 08:49:42 +02:00
David Barroso
2633747992 chore(cli): minor fix to download script when specifying version (#3602) 2025-10-13 15:26:04 +02:00
github-actions[bot]
3b107a386e release(services/auth): 0.42.2 (#3598)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-10-13 14:49:48 +02:00
David Barroso
b5ed48a832 chore(auth): add wget to docker image (#3601) 2025-10-13 14:45:58 +02:00
David BM
363730ab20 chore(dashboard): cleanup e2e remote schemas test before run (#3581) 2025-10-13 12:24:11 +02:00
robertkasza
9c77c4be51 fix(dashboard): fix flaky e2e tests (#3536) 2025-10-13 11:39:34 +02:00
github-actions[bot]
c7c6de5258 release(cli): 1.34.1 (#3577)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-10-13 09:52:42 +02:00
David Barroso
1e7f4df883 fix(cli): workaround os.Rename issues when src and dst are on different partitions (#3599) 2025-10-13 09:48:37 +02:00
David Barroso
0c8e5ac55f chore(docs): udpated README.md and CONTRIBUTING.md (#3587) 2025-10-13 09:04:28 +02:00
367 changed files with 6326 additions and 54709 deletions

View File

@@ -126,8 +126,10 @@ 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_FREE_USER_EMAILS: ${{ secrets.NHOST_TEST_FREE_USER_EMAILS }}
NHOST_TEST_ONBOARDING_USER: ${{ secrets.NHOST_TEST_ONBOARDING_USER }}
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

View File

@@ -52,12 +52,16 @@ 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
@@ -77,7 +81,10 @@ 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_FREE_USER_EMAILS: ${{ secrets.NHOST_TEST_FREE_USER_EMAILS }}
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 }}
jobs:
tests:

View File

@@ -24,4 +24,5 @@ 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']"

View File

@@ -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: false
DOCKER: true
secrets:
AWS_ACCOUNT_ID: ${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}
NIX_CACHE_PUB_KEY: ${{ secrets.NIX_CACHE_PUB_KEY }}

View File

@@ -0,0 +1,35 @@
---
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 }}

View File

@@ -24,28 +24,20 @@ 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:
- 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).
## Monorepo Structure
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.
This repository is a monorepo that contains multiple packages and applications. The structure is as follows:
In general, we follow the ["fork-and-pull" Git workflow](https://github.com/susam/gitpr)
- `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.
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.
For details about those projects and how to contribure, please refer to their respective `README.md` and `CONTRIBUTING.md` files.

View File

@@ -1,100 +0,0 @@
# 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 Normal file
View File

@@ -0,0 +1,16 @@
.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

View File

@@ -12,7 +12,7 @@
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://nhost.io/blog">Blog</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://twitter.com/nhost">Twitter</a>
<a href="https://x.com/nhost">X</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://nhost.io/discord">Discord</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
@@ -36,7 +36,7 @@ Nhost consists of open source software:
- Authentication: [Auth](https://github.com/nhost/nhost/tree/main/services/auth)
- Storage: [Storage](https://github.com/nhost/nhost/tree/main/services/storage)
- Serverless Functions: Node.js (JavaScript and TypeScript)
- [Nhost CLI](https://docs.nhost.io/platform/cli/local-development) for local development
- [Nhost CLI](https://github.com/nhost/nhost/tree/main/cli) for local development
## Architecture of Nhost
@@ -107,7 +107,6 @@ 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)

View File

@@ -2,5 +2,7 @@
// $schema provides code completion hints to IDEs.
"$schema": "https://github.com/IBM/audit-ci/raw/main/docs/schema.json",
"moderate": true,
"allowlist": ["vue-template-compiler", { "id": "CVE-2025-48068", "path": "next" }]
"allowlist": [
"GHSA-9965-vmph-33xx" // https://github.com/advisories/GHSA-9965-vmph-33xx Update package once have a fix
]
}

View File

@@ -54,6 +54,11 @@ 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
@@ -105,6 +110,11 @@ 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

View File

@@ -2,6 +2,19 @@
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

84
cli/CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,84 @@
# 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

View File

@@ -2,10 +2,13 @@ package software
import (
"context"
"errors"
"fmt"
"io"
"os"
"runtime"
"strings"
"syscall"
"github.com/nhost/nhost/cli/clienv"
"github.com/nhost/nhost/cli/software"
@@ -92,8 +95,8 @@ func install(cmd *cli.Command, ce *clienv.CliEnv, tmpFile string) error {
ce.Infoln("Copying to %s...", curBin)
if err := os.Rename(tmpFile, curBin); err != nil {
return fmt.Errorf("failed to rename %s to %s: %w", tmpFile, curBin, err)
if err := moveOrCopyFile(tmpFile, curBin); err != nil {
return fmt.Errorf("failed to move %s to %s: %w", tmpFile, curBin, err)
}
ce.Infoln("Setting permissions...")
@@ -104,3 +107,55 @@ 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
}

View File

@@ -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@$release"
release="cli@$version"
fi
# check version exists

View File

@@ -1,47 +0,0 @@
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',
}),
};

View File

@@ -1,69 +0,0 @@
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,
];

View File

@@ -62,20 +62,6 @@ 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 |

View File

@@ -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();
expect(page.locator('h2:has-text("Edit Table")')).toBeVisible();
expect(page.locator('div[data-testid="id"]')).toBeVisible();
await expect(page.locator('h2:has-text("Edit Table")')).toBeVisible();
await 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();
expect(page.locator('h2:has-text("Edit Table")')).toBeVisible();
expect(
await expect(page.locator('h2:has-text("Edit Table")')).toBeVisible();
await expect(
page.locator('button#identityColumnIndex :has-text("identity_column")'),
).toBeVisible();
expect(page.locator('[id="columns.3.defaultValue"]')).toBeDisabled();
expect(page.locator('[name="columns.3.isNullable"]')).toBeDisabled();
expect(page.locator('[name="columns.3.isUnique"]')).toBeDisabled();
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();
});
test('should create table with foreign key constraint', async ({
@@ -234,6 +234,46 @@ 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,
}) => {
@@ -280,40 +320,3 @@ 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();
});

View File

@@ -41,12 +41,13 @@ export const TEST_USER_PASSWORD = process.env.NHOST_TEST_USER_PASSWORD!;
export const TEST_PERSONAL_ORG_SLUG = process.env.NHOST_TEST_PERSONAL_ORG_SLUG!;
const freeUserEmails = process.env.NHOST_TEST_FREE_USER_EMAILS!;
export const TEST_FREE_USER_EMAILS: string[] = JSON.parse(freeUserEmails);
export const TEST_ONBOARDING_USER = process.env.NHOST_TEST_ONBOARDING_USER!;
/**
* 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!;

View File

@@ -1,13 +1,10 @@
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';
@@ -15,13 +12,15 @@ 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`);
expect(page.getByText('Welcome to Nhost!')).toBeVisible();
await expect(page.getByText('Welcome to Nhost!')).toBeVisible();
const organizationName = faker.lorem.words(3).slice(0, 32);
await page.getByLabel('Organization Name').fill(organizationName);
@@ -68,34 +67,28 @@ test('user should be able to finish onboarding', async () => {
.getByTestId('hosted-payment-submit-button')
.click({ force: true });
expect(
await expect(
page.getByText('Processing new organization request').first(),
).toBeVisible();
await page.waitForSelector(
'div:has-text("Organization created successfully. Redirecting...")',
);
expect(page.getByText('Create Your First Project')).toBeVisible();
await 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();
expect(page.getByText('Creating your project...')).toBeVisible();
expect(page.getByText('Project created successfully!')).toBeVisible();
await expect(page.getByText('Creating your project...')).toBeVisible();
await expect(page.getByText('Project created successfully!')).toBeVisible();
expect(page.getByText('Internal info')).toBeVisible();
await 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 () => {
@@ -107,12 +100,12 @@ test('should delete the new organization', async () => {
await page.getByRole('button', { name: 'Delete' }).click();
await page.waitForSelector('h2:has-text("Delete Organization")');
expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
await expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
await page.getByLabel("I'm sure I want to delete this Organization").click();
expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
await expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
await page.getByLabel('I understand this action cannot be undone').click();
expect(page.getByTestId('deleteOrgButton')).not.toBeDisabled();
await expect(page.getByTestId('deleteOrgButton')).not.toBeDisabled();
await page.getByTestId('deleteOrgButton').click();
@@ -145,7 +138,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")');
expect(page.getByText('Upgrade')).toBeEnabled();
await expect(page.getByText('Upgrade')).toBeEnabled();
await page.getByText('Upgrade').click();
await page.waitForSelector('h2:has-text("Upgrade Organization")');
@@ -205,12 +198,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")');
expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
await expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
await page.getByLabel("I'm sure I want to delete this Organization").click();
expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
await expect(page.getByTestId('deleteOrgButton')).toBeDisabled();
await page.getByLabel('I understand this action cannot be undone').click();
expect(page.getByTestId('deleteOrgButton')).not.toBeDisabled();
await expect(page.getByTestId('deleteOrgButton')).not.toBeDisabled();
await page.getByTestId('deleteOrgButton').click();

View File

@@ -4,61 +4,64 @@ 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.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);
});
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,
});
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);
});
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,
});
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);
});

View File

@@ -1,9 +1,16 @@
/* eslint-disable no-await-in-loop */
import {
TEST_FREE_USER_EMAILS,
TEST_ONBOARDING_USER,
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';
@@ -125,12 +132,26 @@ export async function prepareTable({
),
);
await page.getByLabel('Primary Key').click();
await Promise.all(
primaryKeys.map(async (primaryKey) => {
await page.getByRole('option', { name: primaryKey, exact: true }).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 page.getByText('Create a New Table').click();
await page.waitForTimeout(1000);
await expect(
page.getByRole('option', { name: columns[0].name, exact: true }),
).not.toBeVisible();
}
/**
@@ -232,42 +253,6 @@ 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;
@@ -278,33 +263,13 @@ 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(freeUserEmail);
await page.getByLabel('Email').fill(TEST_ONBOARDING_USER);
await page.getByLabel('Password').fill(TEST_USER_PASSWORD);
await page.getByRole('button', { name: /sign in/i }).click();
@@ -319,3 +284,132 @@ 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;
}
}

View File

@@ -9,15 +9,15 @@
"analyze": "ANALYZE=true pnpm build --no-lint",
"start": "next start",
"lint": "next lint --max-warnings 0",
"test": "vitest --run",
"test": "pnpm lint && pnpm test:vitest",
"test:vitest": "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=.",
"storybook": "start-storybook -p 6006 -s public",
"build-storybook": "build-storybook",
"install-browsers": "pnpm playwright install && pnpm playwright install-deps",
"e2e:tests": "pnpm playwright test --config=playwright.config.ts -x",
"e2e": "pnpm e2e:tests --project=main",
"e2e:local": "pnpm e2e:tests --project=local",
@@ -110,7 +110,6 @@
"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",
@@ -135,21 +134,12 @@
"@babel/core": "^7.24.3",
"@eslint/js": "9.26.0",
"@faker-js/faker": "^7.6.0",
"@graphql-codegen/cli": "^5.0.2",
"@graphql-codegen/cli": "^6.0.0",
"@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",
@@ -159,7 +149,7 @@
"@types/bcryptjs": "^2.4.6",
"@types/jest": "^29.5.12",
"@types/lodash.debounce": "^4.0.9",
"@types/node": "^16.18.93",
"@types/node": "^20.14.8",
"@types/pluralize": "^0.0.30",
"@types/react": "18.2.73",
"@types/react-dom": "^18.2.23",
@@ -169,8 +159,8 @@
"@types/validator": "^13.11.9",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react": "^4.2.1",
"@vitest/coverage-v8": "^0.32.4",
"@vitejs/plugin-react": "^4.7.0",
"@vitest/coverage-v8": "^3.2.4",
"audit-ci": "^6.6.1",
"autoprefixer": "^10.4.19",
"babel-loader": "^8.3.0",
@@ -194,24 +184,21 @@
"eslint-plugin-vue": "^9.26.0",
"jsdom": "^22.1.0",
"lint-staged": "^15.2.2",
"msw": "^1.3.5",
"msw-storybook-addon": "^1.10.0",
"msw": "^2.11.4",
"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": "^0.32.4"
"vitest": "^3.2.4"
},
"browserslist": {
"production": [
@@ -242,6 +229,9 @@
"@lezer/highlight": "^1.0.0"
}
}
},
"overrides": {
"esbuild@<=0.24.2": ">=0.25.0"
}
}
}

View File

@@ -6,14 +6,14 @@ dotenv.config({ path: path.resolve(__dirname, '.env.test') });
export default defineConfig({
testDir: './e2e',
maxFailures: 1,
maxFailures: process.env.CI ? 3 : 1,
timeout: 120 * 1000,
expect: {
timeout: 10000,
},
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: 2,
retries: process.env.CI ? 2 : 0,
workers: 1,
reporter: 'html',
use: {

10004
dashboard/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -66,7 +66,6 @@ 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")
@@ -213,5 +212,3 @@ rec {
}).copyTo}/bin/copy-to dir:$out
'';
}

View File

@@ -1,48 +0,0 @@
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',
},
},
};

View File

@@ -1,125 +0,0 @@
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 />,
};

View File

@@ -1,86 +0,0 @@
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: () => {},
};

View File

@@ -1,38 +0,0 @@
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',
};

View File

@@ -1,28 +0,0 @@
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,
};

View File

@@ -0,0 +1,49 @@
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;

View File

@@ -1,5 +1,8 @@
import { mockApplication } from '@/tests/mocks';
import { getProjectQuery } from '@/tests/msw/mocks/graphql/getProjectQuery';
import {
getProjectQuery,
getProjectStateQuery,
} from '@/tests/msw/mocks/graphql/getProjectQuery';
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
import {
createGraphqlMockResolver,
@@ -29,7 +32,7 @@ function TestComponent() {
);
}
const server = setupServer(tokenQuery);
const server = setupServer(tokenQuery, getProjectStateQuery());
const getUseRouterObject = (
route: string = '/orgs/[orgSlug]/projects/[appSubdomain]',

View File

@@ -1,25 +1,17 @@
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 { useProjectWithState } from '@/features/orgs/projects/hooks/useProjectWithState';
import { isEmptyValue } from '@/lib/utils';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { isEmptyValue, isNotEmptyValue } 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 { useCallback, useEffect, useMemo, type ReactNode } from 'react';
import { useEffect } from 'react';
import { twMerge } from 'tailwind-merge';
import ProjectViewWithState from './ProjectViewWithState';
const platFormOnlyPages = [
'/orgs/[orgSlug]/projects/[appSubdomain]/deployments',
@@ -56,115 +48,15 @@ function ProjectLayoutContent({
children,
mainContainerProps = {},
}: ProjectLayoutContentProps) {
const {
route,
query: { appSubdomain },
push,
} = useRouter();
const { route, push } = useRouter();
const { state } = useAppState();
const isPlatform = useIsPlatform();
const { project, loading, error, projectNotFound } = useProjectWithState();
const { project, loading, error, projectNotFound } = useProject();
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&apos;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) ||
@@ -183,13 +75,11 @@ function ProjectLayoutContent({
return null;
}
// Handle loading state
if (loading) {
return <LoadingScreen data-testid="projectLoadingIndicator" />;
}
// Handle error state
if (error) {
if (isNotEmptyValue(error)) {
throw error;
}
@@ -211,7 +101,7 @@ function ProjectLayoutContent({
)}
{...mainContainerProps}
>
{projectPageContent}
<ProjectViewWithState>{children}</ProjectViewWithState>
<NextSeo title={!isPlatform ? 'Local App' : project?.name} />
</Box>
);

View File

@@ -0,0 +1,245 @@
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();
});
});

View File

@@ -0,0 +1,68 @@
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&apos;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;

View File

@@ -0,0 +1,87 @@
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();
});
});
});

View File

@@ -1,5 +1,8 @@
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
@@ -7,27 +10,19 @@ import { ApplicationStatus } from '@/types/application';
*/
export default function useAppState(): {
state: ApplicationStatus;
message?: string | null;
} {
const { project } = useProjectWithState();
const noApplication = !project;
const { project, projectNotFound } = useProjectWithState();
const { refetch } = useProject();
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 };
}
useEffect(() => {
if (projectNotFound) {
refetch();
}
}, [projectNotFound, refetch]);
return {
state: project.appStates[0].stateId,
message: project.appStates[0].message,
state: isNotEmptyValue(project?.appStates?.[0])
? project.appStates[0].stateId
: ApplicationStatus.Empty,
};
}

View File

@@ -74,9 +74,6 @@ 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;
}

View File

@@ -1,126 +0,0 @@
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;

View File

@@ -127,6 +127,10 @@ describe('RowPermissionsSection', () => {
process.env.NEXT_PUBLIC_ENV = 'dev';
server.listen();
});
beforeEach(() => {
server.restoreHandlers();
server.resetHandlers();
});
afterAll(() => {
server.close();

View File

@@ -1,90 +0,0 @@
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;

View File

@@ -1,4 +1,4 @@
import { rest } from 'msw';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import type { ManagePermissionOptions } from './managePermission';
import managePermission from './managePermission';
@@ -12,9 +12,7 @@ const defaultParameters: ManagePermissionOptions = {
};
const server = setupServer(
rest.post('http://localhost:1337/v1/metadata', (_req, res, ctx) =>
res(ctx.json({})),
),
http.post('http://localhost:1337/v1/metadata', () => HttpResponse.json({})),
);
beforeAll(() => server.listen());

View File

@@ -1,5 +1,5 @@
import type { HasuraMetadata } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
import { rest } from 'msw';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import prepareTrackForeignKeyRelationsMetadata from './prepareTrackForeignKeyRelationsMetadata';
@@ -28,11 +28,8 @@ const testMetadataResponse: { metadata: HasuraMetadata } = {
};
const metadataHandlers = [
rest.post(`${APP_URL}/v1/metadata`, (_req, res, ctx) =>
res(
ctx.status(200),
ctx.json<{ metadata: HasuraMetadata }>(testMetadataResponse),
),
http.post(`${APP_URL}/v1/metadata`, () =>
HttpResponse.json(testMetadataResponse),
),
];
@@ -131,56 +128,53 @@ 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(
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',
},
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',
},
],
},
{
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: [],
},
],
},
],
},
}),
),
);

View File

@@ -3,6 +3,7 @@ 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';
@@ -75,7 +76,7 @@ vi.mock('@/features/orgs/components/common/TransferProjectDialog', async () => {
};
});
const server = setupServer(tokenQuery);
const server = setupServer(tokenQuery, getOrganizations);
describe('DatabasePiTRSettings', () => {
beforeAll(() => {

View File

@@ -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>
);
}

View File

@@ -20,6 +20,17 @@ 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 }),
}));

View File

@@ -1,15 +1,15 @@
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
import { render, screen, waitFor } from '@/tests/testUtils';
import { graphql } from 'msw';
import { HttpResponse, 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', (_req, res, ctx) =>
res(
ctx.data({
graphql.query('GetHasuraSettings', () =>
HttpResponse.json({
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', (_req, res, ctx) =>
res(
ctx.data({
graphql.query('GetHasuraSettings', () =>
HttpResponse.json({
data: {
config: {
id: 'HasuraSettings',
__typename: 'HasuraSettings',
@@ -84,8 +84,8 @@ describe('HasuraCorsDomainSettings', () => {
resources: [],
},
},
}),
),
},
}),
),
);

View File

@@ -1,5 +1,6 @@
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 {
@@ -18,6 +19,7 @@ export interface UseProjectReturnType {
loading?: boolean;
error?: Error | null;
refetch: (variables?: any) => Promise<any>;
projectNotFound: boolean;
}
export default function useProject(): UseProjectReturnType {
@@ -39,13 +41,14 @@ export default function useProject(): UseProjectReturnType {
[isPlatform, isAuthenticated, isAuthLoading, appSubdomain, isRouterReady],
);
const { data, isLoading, refetch, error } = useQuery(
const { data, isLoading, refetch, error, isFetched } = useQuery(
['project', appSubdomain as string],
async () => {
const response = await nhost.graphql.request<{
apps: ProjectFragment[];
}>(GetProjectDocument, { subdomain: (appSubdomain as string) || '' });
return response.body;
return response?.body.data;
},
{
enabled: shouldFetchProject,
@@ -54,10 +57,11 @@ export default function useProject(): UseProjectReturnType {
if (isPlatform) {
return {
project: data?.data?.apps?.[0] || null,
project: data?.apps?.[0] || null,
loading: isLoading && shouldFetchProject,
error: Array.isArray(error || {}) ? error?.[0] : error,
refetch,
projectNotFound: isFetched && !isLoading && isEmptyValue(data?.apps),
};
}
@@ -66,5 +70,6 @@ export default function useProject(): UseProjectReturnType {
loading: false,
error: null,
refetch: () => Promise.resolve(),
projectNotFound: false,
};
}

View File

@@ -82,6 +82,7 @@ describe('useProjectLogs - Subscription Creation & Cleanup', () => {
loading: false,
error: undefined,
refetch: vi.fn(),
projectNotFound: false,
});
// Mock subscribeToMore to return an unsubscribe function
@@ -133,6 +134,7 @@ describe('useProjectLogs - Subscription Creation & Cleanup', () => {
loading: true,
error: undefined,
refetch: vi.fn(),
projectNotFound: false,
});
renderHook(() => useProjectLogs(defaultProps));
@@ -146,6 +148,7 @@ describe('useProjectLogs - Subscription Creation & Cleanup', () => {
loading: false,
error: undefined,
refetch: vi.fn(),
projectNotFound: false,
});
renderHook(() => useProjectLogs(defaultProps));

View File

@@ -20,6 +20,17 @@ 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 }),
}));

View File

@@ -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 { rest } from 'msw';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { afterAll, beforeAll, vi } from 'vitest';
import OverviewDeployments from './OverviewDeployments';
@@ -36,8 +36,9 @@ vi.mock('next/router', () => ({
const server = setupServer(
tokenQuery,
rest.get('https://local.graphql.local.nhost.run/v1', (_req, res, ctx) =>
res(ctx.status(200)),
http.get(
'https://local.graphql.local.nhost.run/v1',
() => new HttpResponse(null, { status: 200 }),
),
);
@@ -49,8 +50,9 @@ beforeAll(() => {
afterEach(() => {
server.resetHandlers(
rest.get('https://local.graphql.local.nhost.run/v1', (_req, res, ctx) =>
res(ctx.status(200)),
http.get(
'https://local.graphql.local.nhost.run/v1',
() => new HttpResponse(null, { status: 200 }),
),
);
queryClient.clear();
@@ -63,37 +65,31 @@ afterAll(() => {
test('should render an empty state when GitHub is not connected', async () => {
server.use(
rest.post(
http.post(
'https://local.graphql.local.nhost.run/v1',
async (req, res, ctx) => {
const { operationName } = await req.json();
async ({ request }) => {
const { operationName } = (await request.json()) as any;
if (operationName === 'getProject') {
return res(
ctx.json({
data: {
apps: [{ ...mockApplication, githubRepository: null }],
},
}),
);
return HttpResponse.json({
data: {
apps: [{ ...mockApplication, githubRepository: null }],
},
});
}
if (operationName === 'getOrganization') {
return res(
ctx.json({
data: {
organizations: [{ ...mockOrganization }],
},
}),
);
return HttpResponse.json({
data: {
organizations: [{ ...mockOrganization }],
},
});
}
return res(
ctx.json({
data: {
deployments: [],
},
}),
);
return HttpResponse.json({
data: {
deployments: [],
},
});
},
),
);
@@ -107,32 +103,28 @@ 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(
rest.post(
http.post(
'https://local.graphql.local.nhost.run/v1',
async (_req, res, ctx) => {
const { operationName } = await _req.json();
async ({ request }) => {
const { operationName } = (await request.json()) as any;
if (operationName === 'getProject') {
return res(
ctx.json({
data: {
apps: [{ ...mockApplication }],
},
}),
);
return HttpResponse.json({
data: {
apps: [{ ...mockApplication }],
},
});
}
if (operationName === 'getOrganization') {
return res(
ctx.json({
data: {
organizations: [{ ...mockOrganization }],
},
}),
);
return HttpResponse.json({
data: {
organizations: [{ ...mockOrganization }],
},
});
}
return res(ctx.json({ data: { deployments: [] } }));
return HttpResponse.json({ data: { deployments: [] } });
},
),
);
@@ -155,52 +147,46 @@ 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,
rest.post(
http.post(
'https://local.graphql.local.nhost.run/v1',
async (_req, res, ctx) => {
const { operationName } = await _req.json();
async ({ request }) => {
const { operationName } = (await request.json()) as any;
if (operationName === 'ScheduledOrPendingDeploymentsSub') {
return res(ctx.json({ data: { deployments: [] } }));
return HttpResponse.json({ data: { deployments: [] } });
}
if (operationName === 'getProject') {
return res(
ctx.json({
data: {
apps: [{ ...mockApplication }],
},
}),
);
return HttpResponse.json({
data: {
apps: [{ ...mockApplication }],
},
});
}
if (operationName === 'getOrganization') {
return res(
ctx.json({
data: {
organizations: [{ ...mockOrganization }],
},
}),
);
return HttpResponse.json({
data: {
organizations: [{ ...mockOrganization }],
},
});
}
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',
},
],
},
}),
);
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',
},
],
},
});
},
),
);
@@ -227,69 +213,61 @@ test('should render a list of deployments', async () => {
test('should disable redeployments if a deployment is already in progress', async () => {
server.use(
tokenQuery,
rest.post(
http.post(
'https://local.graphql.local.nhost.run/v1',
async (req, res, ctx) => {
const { operationName } = await req.json();
async ({ request }) => {
const { operationName } = (await request.json()) as any;
if (operationName === 'ScheduledOrPendingDeploymentsSub') {
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({
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',
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 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',
},
],
},
});
},
),
);

View File

@@ -1,22 +1,15 @@
import { mockOrganization, mockOrganizations } from '@/tests/mocks';
import { HttpResponse } from 'msw';
import nhostGraphQLLink from './nhostGraphQLLink';
export const getOrganizations = nhostGraphQLLink.query(
'getOrganizations',
(_req, res, ctx) =>
res(
ctx.data({
organizations: mockOrganizations,
}),
),
export const getOrganizations = nhostGraphQLLink.query('getOrganizations', () =>
HttpResponse.json({
data: { organizations: mockOrganizations },
}),
);
export const getOrganization = nhostGraphQLLink.query(
'getOrganization',
(_req, res, ctx) =>
res(
ctx.data({
organizations: [{ ...mockOrganization }],
}),
),
export const getOrganization = nhostGraphQLLink.query('getOrganization', () =>
HttpResponse.json({
data: { organizations: [{ ...mockOrganization }] },
}),
);

View File

@@ -1,10 +1,11 @@
import { HttpResponse } from 'msw';
import nhostGraphQLLink from './nhostGraphQLLink';
export const getPostgresSettings = nhostGraphQLLink.query(
'GetPostgresSettings',
(_req, res, ctx) =>
res(
ctx.data({
() =>
HttpResponse.json({
data: {
systemConfig: {
postgres: {
database: 'gnlivtcgjxctuujxpslj',
@@ -29,15 +30,15 @@ export const getPostgresSettings = nhostGraphQLLink.query(
__typename: 'ConfigPostgres',
},
},
}),
),
},
}),
);
export const getPiTRNotEnabledPostgresSettings = nhostGraphQLLink.query(
'GetPostgresSettings',
(_req, res, ctx) =>
res(
ctx.data({
() =>
HttpResponse.json({
data: {
systemConfig: {
postgres: {
database: 'gnlivtcgjxctuujxpslj',
@@ -62,8 +63,6 @@ export const getPiTRNotEnabledPostgresSettings = nhostGraphQLLink.query(
__typename: 'ConfigPostgres',
},
},
}),
),
},
}),
);
// {"data":}

View File

@@ -1,12 +1,35 @@
import { mockApplication } from '@/tests/mocks';
import { HttpResponse } from 'msw';
import nhostGraphQLLink from './nhostGraphQLLink';
export const getProjectQuery = nhostGraphQLLink.query(
'getProject',
(_req, res, ctx) =>
res(
ctx.data({
apps: [{ ...mockApplication, githubRepository: null }],
}),
),
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: [],
},
}),
);

View File

@@ -1,144 +1,141 @@
import { HttpResponse } from 'msw';
import nhostGraphQLLink from './nhostGraphQLLink';
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',
},
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',
},
],
__typename: 'apps',
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',
},
{
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',
},
],
__typename: 'apps',
deployments: [],
creator: {
id: 'creator-r-elek-id',
email: 'robert@elek.com',
displayName: 'Robert',
__typename: 'users',
},
{
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',
},
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',
],
__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',
},
{
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',
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',
},
],
__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',
(_req, res, ctx) =>
res(
ctx.data({
apps: [],
}),
),
export const getEmptyProjectsQuery = nhostGraphQLLink.query('getProjects', () =>
HttpResponse.json({
data: {
apps: [],
},
}),
);

View File

@@ -1,15 +1,16 @@
import { HttpResponse } from 'msw';
import nhostGraphQLLink from './nhostGraphQLLink';
export const organizationMemberInvites = nhostGraphQLLink.query(
'organizationMemberInvites',
(_req, res, ctx) => res(ctx.data({ organizationMemberInvites: [] })),
() => HttpResponse.json({ data: { organizationMemberInvites: [] } }),
);
export const organizationNewRequests = nhostGraphQLLink.query(
'organizationNewRequests',
(_req, res, ctx) =>
res(
ctx.data({
() =>
HttpResponse.json({
data: {
organizationNewRequests: [
{
id: 'org-request-id-1',
@@ -17,6 +18,6 @@ export const organizationNewRequests = nhostGraphQLLink.query(
__typename: 'organization_new_request',
},
],
}),
),
},
}),
);

View File

@@ -1,11 +1,11 @@
import { HttpResponse } from 'msw';
import nhostGraphQLLink from './nhostGraphQLLink';
const permissionVariablesQuery = nhostGraphQLLink.query(
'GetRolesPermissions',
(_req, res, ctx) =>
res(
ctx.delay(250),
ctx.data({
async () =>
HttpResponse.json({
data: {
config: {
auth: {
user: {
@@ -32,8 +32,8 @@ const permissionVariablesQuery = nhostGraphQLLink.query(
},
},
},
}),
),
},
}),
);
export default permissionVariablesQuery;

View File

@@ -1,50 +1,46 @@
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',
(_req, res, ctx) =>
res(
ctx.data({
plans: [
{
__typename: 'plans',
id: 'dc5e805e-1bef-4d43-809e-9fdf865e211a',
name: 'Pro',
price: 25,
isFree: false,
},
],
}),
),
export const getProPlanOnlyQuery = nhostGraphQLLink.query('GetPlans', () =>
HttpResponse.json({
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',
(_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,
},
],
}),
),
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,
},
],
},
}),
);

View File

@@ -1,10 +1,11 @@
import { HttpResponse } from 'msw';
import nhostGraphQLLink from './nhostGraphQLLink';
export const prefetchNewAppQuery = nhostGraphQLLink.query(
'PrefetchNewApp',
(_req, res, ctx) =>
res(
ctx.data({
() =>
HttpResponse.json({
data: {
regions: [
{
id: 'dd6f8e01-35a9-4ba6-8dc6-ed972f2db93c',
@@ -67,6 +68,6 @@ export const prefetchNewAppQuery = nhostGraphQLLink.query(
__typename: 'plans',
},
],
}),
),
},
}),
);

View File

@@ -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',
(_req, res, ctx) =>
res(
ctx.data({
() =>
HttpResponse.json({
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',
(_req, res, ctx) =>
res(
ctx.data({
() =>
HttpResponse.json({
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',
(_req, res, ctx) =>
res(
ctx.data({
() =>
HttpResponse.json({
data: {
config: {
__typename: 'ConfigConfig',
postgres: {
@@ -137,6 +137,6 @@ export const resourcesUpdatedQuery = nhostGraphQLLink.query(
},
},
},
}),
),
},
}),
);

View File

@@ -1,11 +1,12 @@
import { HttpResponse } from 'msw';
import nhostGraphQLLink from './nhostGraphQLLink';
export default nhostGraphQLLink.mutation('UpdateConfig', (req, res, ctx) =>
res(
ctx.data({
export default nhostGraphQLLink.mutation('UpdateConfig', () =>
HttpResponse.json({
data: {
updateConfig: {
id: 'ConfigConfig',
},
}),
),
},
}),
);

View File

@@ -1,419 +1,415 @@
import { rest } from 'msw';
import { delay, http, HttpResponse } from 'msw';
const hasuraMetadataQuery = rest.post(
const hasuraMetadataQuery = http.post(
'https://local.hasura.local.nhost.run/v1/metadata',
(_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' },
},
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,
],
},
{
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,
}),
),
},
],
},
resource_version: 10,
});
},
);
export const hasuraRelationShipsMetadataQuery = rest.post(
export const hasuraRelationShipsMetadataQuery = http.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',
schema: 'public',
},
array_relationships: [
{
name: 'county',
using: {
foreign_key_constraint_on: {
column: 'countryId',
table: {
name: 'county',
schema: 'public',
},
},
},
},
],
() =>
HttpResponse.json({
resource_version: 26,
metadata: {
version: 3,
sources: [
{
name: 'default',
kind: 'postgres',
tables: [
{
table: {
name: 'country',
schema: 'public',
},
{
table: {
array_relationships: [
{
name: 'county',
schema: 'public',
},
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',
},
using: {
foreign_key_constraint_on: {
column: 'countryId',
table: {
name: 'county',
schema: 'public',
},
},
},
],
},
{
table: {
name: 'town',
schema: 'public',
},
object_relationships: [
{
name: 'county',
using: {
foreign_key_constraint_on: 'countyId',
],
},
{
table: {
name: 'county',
schema: 'public',
},
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',
},
},
},
],
},
],
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,
],
},
{
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,
},
},
],
},
}),
),
},
],
},
}),
);
export const hasuraColumnMetadataQuery = rest.post(
export const hasuraColumnMetadataQuery = http.post(
'https://local.hasura.local.nhost.run/v1/metadata',
(_req, res, ctx) =>
res(
ctx.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',
},
},
},
},
],
() =>
HttpResponse.json({
resource_version: 389,
metadata: {
version: 3,
sources: [
{
name: 'default',
kind: 'postgres',
tables: [
{
table: {
name: 'actor',
schema: 'public',
},
{
table: {
object_relationships: [
{
name: 'actor_movie',
schema: 'public',
},
object_relationships: [
{
name: 'actor',
using: {
foreign_key_constraint_on: 'actor_id',
},
},
{
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',
},
using: {
foreign_key_constraint_on: {
column: 'actor_id',
table: {
name: 'actor_movie',
schema: 'public',
},
},
},
],
},
],
},
{
table: {
name: 'actor_movie',
schema: 'public',
},
{
table: {
object_relationships: [
{
name: 'actor',
using: {
foreign_key_constraint_on: 'actor_id',
},
},
{
name: 'movie',
using: {
foreign_key_constraint_on: 'movie_id',
},
},
],
},
{
table: {
name: 'director',
schema: 'public',
},
array_relationships: [
{
name: 'movies',
schema: 'public',
},
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',
},
using: {
foreign_key_constraint_on: {
column: 'director_id',
table: {
name: 'movies',
schema: 'public',
},
},
},
],
},
],
},
{
table: {
name: 'movies',
schema: 'public',
},
{
table: {
name: 'notes',
schema: 'public',
object_relationships: [
{
name: 'author',
using: {
foreign_key_constraint_on: 'director_id',
},
},
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',
},
],
array_relationships: [
{
name: 'actor_movie',
using: {
foreign_key_constraint_on: {
column: 'movie_id',
table: {
name: 'actor_movie',
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: 'notes',
schema: 'public',
},
{
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',
object_relationships: [
{
name: 'user',
using: {
foreign_key_constraint_on: 'owner',
},
},
array_relationships: [
{
name: 'files',
using: {
foreign_key_constraint_on: {
column: 'bucket_id',
table: {
name: 'files',
schema: 'storage',
},
],
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',
},
},
},
],
},
],
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,
],
update_permissions: [
{
role: 'user',
permission: {
columns: ['note'],
filter: {
owner: {
_eq: 'X-Hasura-User-Id',
},
},
check: null,
},
},
use_prepared_statements: true,
],
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',
},
},
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,
},
},
],
},
}),
),
},
],
},
}),
);
export default hasuraMetadataQuery;

View File

@@ -1,143 +1,23 @@
import { rest } from 'msw';
import { http, HttpResponse } from 'msw';
const tableQuery = rest.post(
const tableQuery = http.post(
'https://local.hasura.local.nhost.run/v2/query',
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([
async ({ request }) => {
const body = (await request.json()) as any;
if (/table_name = 'authors'/gim.exec(body?.args?.[0].args.sql) !== null) {
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":"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":"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":"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":"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}',
'{"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}',
],
],
},
@@ -147,16 +27,126 @@ const tableQuery = rest.post(
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"}',
'{"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 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']] },
]);
},
);

View File

@@ -1,10 +1,10 @@
import { mockSession } from '@/tests/mocks';
import type { Session } from '@nhost/nhost-js/auth';
import { rest } from 'msw';
import { http, HttpResponse } from 'msw';
const tokenQuery = rest.post(
const tokenQuery = http.post(
'https://local.auth.local.nhost.run/v1/token',
(_req, res, ctx) => res(ctx.json<Session>(mockSession)),
() => HttpResponse.json<Session>(mockSession),
);
export default tokenQuery;

View File

@@ -35,6 +35,7 @@ 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';
@@ -154,9 +155,9 @@ const graphqlRequestHandlerFactory = (
type: 'mutation' | 'query',
responsePromise: any,
) =>
nhostGraphQLLink[type](operationName, async (_req, res, ctx) => {
nhostGraphQLLink[type](operationName, async () => {
const data = await responsePromise;
return res(ctx.data(data));
return HttpResponse.json({ data });
});
/* Helper function to pause responses to be able to test loading states */
export const createGraphqlMockResolver = (

View File

@@ -147,6 +147,22 @@
"/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",

View File

@@ -9,7 +9,7 @@
"test:broken-links": "pnpm exec mintlify broken-links"
},
"devDependencies": {
"mintlify": "^4.2.87",
"mintlify": "^4.2.158",
"prettier": "^3.5.3",
"typedoc": "^0.28.4",
"typedoc-plugin-markdown": "^4.6.3"

View File

@@ -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">
<Card title="Local Development" icon="laptop-code" href="/platform/cli/local-development">
Run the complete Nhost stack locally for offline development, fast iteration, and full control over your environment.
</Card>
<Card title="Cloud Development" icon="cloud">
<Card title="Cloud Development" icon="cloud" href="/platform/cli/cloud-development">
Develop against cloud infrastructure while maintaining local tools for simplified remote testing and team collaboration.
</Card>
</CardGroup>

1696
docs/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,3 @@
# Anonymous Users
## Sign-in anonymously
```mermaid

View File

@@ -1,5 +1,3 @@
# Change email
```mermaid
sequenceDiagram
autonumber

View File

@@ -1,5 +1,3 @@
# Change password
```mermaid
sequenceDiagram
autonumber

View File

@@ -1,5 +1,3 @@
# Sign up and sign in users with email and password
## Sign up
```mermaid

View File

@@ -1,5 +1,3 @@
# Oauth social providers
```mermaid
sequenceDiagram
autonumber

View File

@@ -1,5 +1,3 @@
# Passwordless with emails (magic links)
```mermaid
sequenceDiagram
autonumber

View File

@@ -1,5 +1,3 @@
# Passwordless with SMS
```mermaid
sequenceDiagram
autonumber
@@ -23,4 +21,4 @@ sequenceDiagram
## 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.
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.

View File

@@ -1,5 +1,3 @@
# Refresh tokens
```mermaid
sequenceDiagram
autonumber

View File

@@ -1,5 +1,3 @@
# Reset password
```mermaid
sequenceDiagram
autonumber

View File

@@ -1,5 +1,3 @@
# Security Keys with WebAuthn
Auth implements the WebAuthn protocol to sign in with security keys, also referred as authenticators in the WebAuthn protocol.
A user needs first to sign up with another method, for instance email+password, passwordless email or Oauth, then to add their security key to their account.

View File

@@ -590,11 +590,13 @@ This method may return different T based on the response code:
pushChainFunction(chainFunction: ChainFunction): void;
```
Add a middleware function to the fetch chain
##### Parameters
| Parameter | Type |
| --------------- | ------------------------------------------ |
| `chainFunction` | [`ChainFunction`](./fetch#chainfunction) |
| Parameter | Type | Description |
| --------------- | ------------------------------------------ | ------------------------------ |
| `chainFunction` | [`ChainFunction`](./fetch#chainfunction) | The middleware function to add |
##### Returns

View File

@@ -207,6 +207,24 @@ For a more generic request, use the `fetch` method instead.
Promise with the function response and metadata
#### pushChainFunction()
```ts
pushChainFunction(chainFunction: ChainFunction): void;
```
Add a middleware function to the fetch chain
##### Parameters
| Parameter | Type | Description |
| --------------- | ------------------------------------------ | ------------------------------ |
| `chainFunction` | [`ChainFunction`](./fetch#chainfunction) | The middleware function to add |
##### Returns
`void`
# Functions
## createAPIClient()

View File

@@ -289,6 +289,24 @@ URL for the GraphQL endpoint.
### Methods
#### pushChainFunction()
```ts
pushChainFunction(chainFunction: ChainFunction): void;
```
Add a middleware function to the fetch chain
##### Parameters
| Parameter | Type | Description |
| --------------- | ------------------------------------------ | ------------------------------ |
| `chainFunction` | [`ChainFunction`](./fetch#chainfunction) | The middleware function to add |
##### Returns
`void`
#### request()
##### Call Signature

View File

@@ -399,11 +399,13 @@ This method may return different T based on the response code:
pushChainFunction(chainFunction: ChainFunction): void;
```
Add a middleware function to the fetch chain
##### Parameters
| Parameter | Type |
| --------------- | ------------------------------------------ |
| `chainFunction` | [`ChainFunction`](./fetch#chainfunction) |
| Parameter | Type | Description |
| --------------- | ------------------------------------------ | ------------------------------ |
| `chainFunction` | [`ChainFunction`](./fetch#chainfunction) | The middleware function to add |
##### Returns

6
envrc.sample Normal file
View File

@@ -0,0 +1,6 @@
watch_file ../../flake.nix ../../nix/*.nix project.nix ./.envrc.custom
use flake .\#$(basename $PWD)
if [[ -f .envrc.custom ]]; then
. ./.envrc.custom
fi

View File

@@ -61,7 +61,7 @@
};
nixopsf = import ./nixops/project.nix {
inherit self pkgs nix-filter nixops-lib;
inherit self pkgs nix2containerPkgs nix-filter nixops-lib;
};
storagef = import ./services/storage/project.nix {
@@ -185,6 +185,7 @@
mintlify-openapi = mintlify-openapif.package;
nhost-js = nhost-jsf.package;
nixops = nixopsf.package;
nixops-docker-image = nixopsf.dockerImage;
storage = storagef.package;
storage-docker-image = storagef.dockerImage;
clamav-docker-image = storagef.clamav-docker-image;

10
go.mod
View File

@@ -37,9 +37,6 @@ require (
github.com/pquerna/otp v1.5.0
github.com/rs/cors/wrapper/gin v0.0.0-20240830163046-1084d89a1692
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.10
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/twilio/twilio-go v1.28.3
github.com/urfave/cli-docs/v3 v3.0.0-alpha6
@@ -100,7 +97,6 @@ require (
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
@@ -125,7 +121,6 @@ require (
github.com/hashicorp/go-safetemp v1.0.0 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
@@ -165,15 +160,11 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/cors v1.11.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/sosodev/duration v1.3.1 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/speakeasy-api/jsonpath v0.6.2 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
@@ -189,7 +180,6 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/net v0.44.0 // indirect

22
go.sum
View File

@@ -116,7 +116,6 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -157,8 +156,6 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
@@ -263,8 +260,6 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
@@ -403,8 +398,6 @@ github.com/rs/cors/wrapper/gin v0.0.0-20240830163046-1084d89a1692 h1:lwzJgPw5Y6p
github.com/rs/cors/wrapper/gin v0.0.0-20240830163046-1084d89a1692/go.mod h1:742Ialb8SOs5yB2PqRDzFcyND3280PoaS5/wcKQUQKE=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
@@ -414,22 +407,11 @@ github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnB
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/speakeasy-api/jsonpath v0.6.2 h1:Mys71yd6u8kuowNCR0gCVPlVAHCmKtoGXYoAtcEbqXQ=
github.com/speakeasy-api/jsonpath v0.6.2/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -444,8 +426,6 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -502,8 +482,6 @@ go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mx
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=

View File

@@ -104,9 +104,11 @@ let
dir=$(realpath --relative-to="$PWD" "$absdir")
echo " Copying node_modules for $dir"
cp -r ${node_modules}/$dir/node_modules $dir/node_modules
done
pnpm audit-ci
echo " Running pnpm audit-ci for $dir"
pnpm audit-ci --directory $dir
done
${preCheck}

View File

@@ -1,7 +1,8 @@
{ self, pkgs, nix-filter, nixops-lib }:
{ self, pkgs, nix2containerPkgs, nix-filter, nixops-lib }:
let
name = "nixops";
version = "0.0.0-dev";
created = "1970-01-01T00:00:00Z";
submodule = "${name}";
src = nix-filter.lib.filter {
@@ -34,18 +35,65 @@ let
gqlgenc
oapi-codegen
nhost-cli
gofumpt
golines
skopeo
postgresql_14_18-client
postgresql_15_13-client
postgresql_16_9-client
postgresql_17_5-client
postgresql_14_18
postgresql_15_13
postgresql_16_9
postgresql_17_5
sqlc
vacuum-go
bun
clang
pkg-config
];
nativeBuildInputs = [ ];
user = "user";
group = "user";
uid = "1000";
gid = "1000";
l = pkgs.lib // builtins;
mkUser = pkgs.runCommand "mkUser" { } ''
mkdir -p $out/etc/pam.d
echo "${user}:x:${uid}:${gid}::" > $out/etc/passwd
echo "${user}:!x:::::::" > $out/etc/shadow
echo "${group}:x:${gid}:" > $out/etc/group
echo "${group}:x::" > $out/etc/gshadow
cat > $out/etc/pam.d/other <<EOF
account sufficient pam_unix.so
auth sufficient pam_rootok.so
password requisite pam_unix.so nullok sha512
session required pam_unix.so
EOF
touch $out/etc/login.defs
mkdir -p $out/home/${user}
'';
tmpFolder = (pkgs.writeTextFile {
name = "tmp-file";
text = ''
dummy file to generate tmpdir
'';
destination = "/tmp/tmp-file";
});
nixConfig = pkgs.writeTextFile {
name = "nix-config";
text = ''
sandbox = false
sandbox-fallback = false
experimental-features = nix-command flakes
trusted-users = root ${user}
'';
destination = "/etc/nix/nix.conf";
};
in
{
check = nixops-lib.nix.check { inherit src; };
@@ -66,5 +114,78 @@ in
cp -r ${src} $out/
'';
};
}
dockerImage = pkgs.runCommand "image-as-dir" { } ''
${(nix2containerPkgs.nix2container.buildImage {
inherit name created;
tag = version;
maxLayers = 100;
initializeNixDatabase = true;
nixUid = l.toInt uid;
nixGid = l.toInt gid;
copyToRoot = [
(pkgs.buildEnv {
name = "image";
paths = [
(pkgs.buildEnv {
name = "root";
paths = with pkgs; [
coreutils
nix
bash
gnugrep
gnumake
];
pathsToLink = "/bin";
})
];
})
nixConfig
tmpFolder
mkUser
];
perms = [
{
path = mkUser;
regex = "/home/${user}";
mode = "0744";
uid = l.toInt uid;
gid = l.toInt gid;
uname = user;
gname = group;
}
{
path = tmpFolder;
regex = "/tmp";
mode = "0777";
uid = l.toInt uid;
gid = l.toInt gid;
uname = user;
gname = group;
}
];
config = {
User = "user";
WorkingDir = "/home/user";
Env = [
"NIX_PAGER=cat"
"USER=nobody"
"HOME=/home/user"
"TMPDIR=/tmp"
"SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
];
};
layers = [
(nix2containerPkgs.nix2container.buildLayer {
deps = buildInputs;
})
];
}).copyTo}/bin/copy-to dir:$out
'';
}

View File

@@ -28,7 +28,6 @@
"turbo": "2.3.3",
"typescript": "5.8.3",
"vite": "^5.4.20",
"vite-plugin-dts": "^3.9.1",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^0.32.4"
},

View File

@@ -0,0 +1,77 @@
# Developer Guide
## Requirements
We use nix to manage the development environment, the build process and for running tests.
### With Nix (Recommended)
Run `nix develop \#nhost-js` 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
make dev-env-up
pnpm install
pnpm test
```
### Formatting
Format code before committing:
```bash
pnpm format
```
### Code Generation
Generate TypeScript clients from OpenAPI specs:
```bash
pnpm generate
```
This runs `./gen.sh` which generates code from:
- `services/auth/docs/openapi.yaml` - Auth service API
- `services/storage/controller/openapi.yaml` - Storage service API
## Building
### Build for Distribution
```bash
pnpm build
```
This produces:
- TypeScript type definitions
- ESM bundles (`.es.js`)
- CommonJS bundles (`.cjs.js`)
- UMD bundles for browser usage
Output is placed in the `dist/` directory.
## Development Notes
### Code Generation
The code generation script (`gen.sh`) reads OpenAPI specifications from the auth and storage services and generates TypeScript clients. Always regenerate after API changes.
### Dependencies
This package has minimal runtime dependencies to keep bundle size small. Only `tslib` is included as a production dependency.

View File

@@ -103,7 +103,7 @@
"devDependencies": {
"@graphql-typed-document-node/core": "^3.2.0",
"@jest/globals": "^29.7.0",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-node-resolve": "^16.0.2",
"@types/jest": "^29.5.14",
"@types/node": "^22.15.17",
"@types/rollup-plugin-peer-deps-external": "^2.2.5",
@@ -118,5 +118,10 @@
"sideEffects": false,
"dependencies": {
"tslib": "^2.8.1"
},
"pnpm": {
"overrides": {
"rollup@<2.79.2": ">=2.79.2"
}
}
}

View File

@@ -4,6 +4,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
rollup@<2.79.2: '>=2.79.2'
importers:
.:
@@ -19,8 +22,8 @@ importers:
specifier: ^29.7.0
version: 29.7.0
'@rollup/plugin-node-resolve':
specifier: ^16.0.1
version: 16.0.1(rollup@0.63.5)
specifier: ^16.0.2
version: 16.0.2(rollup@4.52.4)
'@types/jest':
specifier: ^29.5.14
version: 29.5.14
@@ -44,7 +47,7 @@ importers:
version: 3.6.2
rollup-plugin-peer-deps-external:
specifier: ^2.2.4
version: 2.2.4(rollup@0.63.5)
version: 2.2.4(rollup@4.52.4)
terser:
specifier: ^5.39.0
version: 5.39.0
@@ -318,24 +321,134 @@ packages:
'@jridgewell/trace-mapping@0.3.30':
resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==}
'@rollup/plugin-node-resolve@16.0.1':
resolution: {integrity: sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==}
'@rollup/plugin-node-resolve@16.0.2':
resolution: {integrity: sha512-tCtHJ2BlhSoK4cCs25NMXfV7EALKr0jyasmqVCq3y9cBrKdmJhtsy1iTz36Xhk/O+pDJbzawxF4K6ZblqCnITQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^2.78.0||^3.0.0||^4.0.0
rollup: '>=2.79.2'
peerDependenciesMeta:
rollup:
optional: true
'@rollup/pluginutils@5.2.0':
resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==}
'@rollup/pluginutils@5.3.0':
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
rollup: '>=2.79.2'
peerDependenciesMeta:
rollup:
optional: true
'@rollup/rollup-android-arm-eabi@4.52.4':
resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.52.4':
resolution: {integrity: sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.52.4':
resolution: {integrity: sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.52.4':
resolution: {integrity: sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.52.4':
resolution: {integrity: sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.52.4':
resolution: {integrity: sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.52.4':
resolution: {integrity: sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm-musleabihf@4.52.4':
resolution: {integrity: sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm64-gnu@4.52.4':
resolution: {integrity: sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-arm64-musl@4.52.4':
resolution: {integrity: sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-loong64-gnu@4.52.4':
resolution: {integrity: sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==}
cpu: [loong64]
os: [linux]
'@rollup/rollup-linux-ppc64-gnu@4.52.4':
resolution: {integrity: sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-riscv64-gnu@4.52.4':
resolution: {integrity: sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-riscv64-musl@4.52.4':
resolution: {integrity: sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-s390x-gnu@4.52.4':
resolution: {integrity: sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==}
cpu: [s390x]
os: [linux]
'@rollup/rollup-linux-x64-gnu@4.52.4':
resolution: {integrity: sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==}
cpu: [x64]
os: [linux]
'@rollup/rollup-linux-x64-musl@4.52.4':
resolution: {integrity: sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==}
cpu: [x64]
os: [linux]
'@rollup/rollup-openharmony-arm64@4.52.4':
resolution: {integrity: sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==}
cpu: [arm64]
os: [openharmony]
'@rollup/rollup-win32-arm64-msvc@4.52.4':
resolution: {integrity: sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.52.4':
resolution: {integrity: sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-gnu@4.52.4':
resolution: {integrity: sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==}
cpu: [x64]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.52.4':
resolution: {integrity: sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==}
cpu: [x64]
os: [win32]
'@sinclair/typebox@0.27.8':
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
@@ -357,9 +470,6 @@ packages:
'@types/babel__traverse@7.28.0':
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
'@types/estree@0.0.39':
resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@@ -1096,10 +1206,11 @@ packages:
rollup-plugin-peer-deps-external@2.2.4:
resolution: {integrity: sha512-AWdukIM1+k5JDdAqV/Cxd+nejvno2FVLVeZ74NKggm3Q5s9cbbcOgUPGdbxPi4BXu7xGaZ8HG12F+thImYu/0g==}
peerDependencies:
rollup: '*'
rollup: '>=2.79.2'
rollup@0.63.5:
resolution: {integrity: sha512-dFf8LpUNzIj3oE0vCvobX6rqOzHzLBoblyFp+3znPbjiSmSvOoK2kMKx+Fv9jYduG1rvcCfCveSgEaQHjWRF6g==}
rollup@4.52.4:
resolution: {integrity: sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
semver@6.3.1:
@@ -1689,23 +1800,89 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@rollup/plugin-node-resolve@16.0.1(rollup@0.63.5)':
'@rollup/plugin-node-resolve@16.0.2(rollup@4.52.4)':
dependencies:
'@rollup/pluginutils': 5.2.0(rollup@0.63.5)
'@rollup/pluginutils': 5.3.0(rollup@4.52.4)
'@types/resolve': 1.20.2
deepmerge: 4.3.1
is-module: 1.0.0
resolve: 1.22.10
optionalDependencies:
rollup: 0.63.5
rollup: 4.52.4
'@rollup/pluginutils@5.2.0(rollup@0.63.5)':
'@rollup/pluginutils@5.3.0(rollup@4.52.4)':
dependencies:
'@types/estree': 1.0.8
estree-walker: 2.0.2
picomatch: 4.0.3
optionalDependencies:
rollup: 0.63.5
rollup: 4.52.4
'@rollup/rollup-android-arm-eabi@4.52.4':
optional: true
'@rollup/rollup-android-arm64@4.52.4':
optional: true
'@rollup/rollup-darwin-arm64@4.52.4':
optional: true
'@rollup/rollup-darwin-x64@4.52.4':
optional: true
'@rollup/rollup-freebsd-arm64@4.52.4':
optional: true
'@rollup/rollup-freebsd-x64@4.52.4':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.52.4':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.52.4':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.52.4':
optional: true
'@rollup/rollup-linux-arm64-musl@4.52.4':
optional: true
'@rollup/rollup-linux-loong64-gnu@4.52.4':
optional: true
'@rollup/rollup-linux-ppc64-gnu@4.52.4':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.52.4':
optional: true
'@rollup/rollup-linux-riscv64-musl@4.52.4':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.52.4':
optional: true
'@rollup/rollup-linux-x64-gnu@4.52.4':
optional: true
'@rollup/rollup-linux-x64-musl@4.52.4':
optional: true
'@rollup/rollup-openharmony-arm64@4.52.4':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.52.4':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.52.4':
optional: true
'@rollup/rollup-win32-x64-gnu@4.52.4':
optional: true
'@rollup/rollup-win32-x64-msvc@4.52.4':
optional: true
'@sinclair/typebox@0.27.8': {}
@@ -1738,8 +1915,6 @@ snapshots:
dependencies:
'@babel/types': 7.28.2
'@types/estree@0.0.39': {}
'@types/estree@1.0.8': {}
'@types/graceful-fs@4.1.9':
@@ -1769,7 +1944,7 @@ snapshots:
'@types/rollup-plugin-peer-deps-external@2.2.5':
dependencies:
rollup: 0.63.5
rollup: 4.52.4
'@types/stack-utils@2.0.3': {}
@@ -2614,14 +2789,37 @@ snapshots:
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
rollup-plugin-peer-deps-external@2.2.4(rollup@0.63.5):
rollup-plugin-peer-deps-external@2.2.4(rollup@4.52.4):
dependencies:
rollup: 0.63.5
rollup: 4.52.4
rollup@0.63.5:
rollup@4.52.4:
dependencies:
'@types/estree': 0.0.39
'@types/node': 22.15.17
'@types/estree': 1.0.8
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.52.4
'@rollup/rollup-android-arm64': 4.52.4
'@rollup/rollup-darwin-arm64': 4.52.4
'@rollup/rollup-darwin-x64': 4.52.4
'@rollup/rollup-freebsd-arm64': 4.52.4
'@rollup/rollup-freebsd-x64': 4.52.4
'@rollup/rollup-linux-arm-gnueabihf': 4.52.4
'@rollup/rollup-linux-arm-musleabihf': 4.52.4
'@rollup/rollup-linux-arm64-gnu': 4.52.4
'@rollup/rollup-linux-arm64-musl': 4.52.4
'@rollup/rollup-linux-loong64-gnu': 4.52.4
'@rollup/rollup-linux-ppc64-gnu': 4.52.4
'@rollup/rollup-linux-riscv64-gnu': 4.52.4
'@rollup/rollup-linux-riscv64-musl': 4.52.4
'@rollup/rollup-linux-s390x-gnu': 4.52.4
'@rollup/rollup-linux-x64-gnu': 4.52.4
'@rollup/rollup-linux-x64-musl': 4.52.4
'@rollup/rollup-openharmony-arm64': 4.52.4
'@rollup/rollup-win32-arm64-msvc': 4.52.4
'@rollup/rollup-win32-ia32-msvc': 4.52.4
'@rollup/rollup-win32-x64-gnu': 4.52.4
'@rollup/rollup-win32-x64-msvc': 4.52.4
fsevents: 2.3.3
semver@6.3.1: {}

View File

@@ -1609,6 +1609,10 @@ export interface VerifyTicketParams {
export interface Client {
baseURL: string;
/** Add a middleware function to the fetch chain
* @param chainFunction - The middleware function to add
*/
pushChainFunction(chainFunction: ChainFunction): void;
/**
Summary: Get public keys for JWT verification in JWK Set format

View File

@@ -18,6 +18,11 @@ import {
export interface Client {
baseURL: string;
/** Add a middleware function to the fetch chain
* @param chainFunction - The middleware function to add
*/
pushChainFunction(chainFunction: ChainFunction): void;
/**
* Execute a request to a serverless function
* The response body will be automatically parsed based on the content type into the following types:
@@ -27,7 +32,8 @@ export interface Client {
*
* @param path - The path to the serverless function
* @param options - Additional fetch options to apply to the request
* @returns Promise with the function response and metadata. */
* @returns Promise with the function response and metadata.
*/
fetch<T = unknown>(
path: string,
options?: RequestInit,
@@ -69,7 +75,12 @@ export const createAPIClient = (
baseURL: string,
chainFunctions: ChainFunction[] = [],
): Client => {
const enhancedFetch = createEnhancedFetch(chainFunctions);
let enhancedFetch = createEnhancedFetch(chainFunctions);
const pushChainFunction = (chainFunction: ChainFunction) => {
chainFunctions.push(chainFunction);
enhancedFetch = createEnhancedFetch(chainFunctions);
};
/**
* Executes a request to a serverless function and processes the response
@@ -148,5 +159,6 @@ export const createAPIClient = (
baseURL,
fetch,
post,
pushChainFunction,
} as Client;
};

View File

@@ -91,6 +91,11 @@ export interface Client {
* URL for the GraphQL endpoint.
*/
url: string;
/** Add a middleware function to the fetch chain
* @param chainFunction - The middleware function to add
*/
pushChainFunction(chainFunction: ChainFunction): void;
}
/**
@@ -108,7 +113,12 @@ export const createAPIClient = (
url: string,
chainFunctions: ChainFunction[] = [],
): Client => {
const enhancedFetch = createEnhancedFetch(chainFunctions);
let enhancedFetch = createEnhancedFetch(chainFunctions);
const pushChainFunction = (chainFunction: ChainFunction) => {
chainFunctions.push(chainFunction);
enhancedFetch = createEnhancedFetch(chainFunctions);
};
const executeOperation = async <
TResponseData = unknown,
@@ -183,5 +193,6 @@ export const createAPIClient = (
return {
request,
url,
pushChainFunction,
} as Client;
};

View File

@@ -461,6 +461,10 @@ export interface GetFileMetadataHeadersParams {
export interface Client {
baseURL: string;
/** Add a middleware function to the fetch chain
* @param chainFunction - The middleware function to add
*/
pushChainFunction(chainFunction: ChainFunction): void;
/**
Summary: Upload files

600
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More