Compare commits
25 Commits
storage@0.
...
@nhost/das
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00ef639455 | ||
|
|
a54da9c072 | ||
|
|
63edfa2600 | ||
|
|
381baf2e51 | ||
|
|
951ce168e8 | ||
|
|
be8f4e5b1b | ||
|
|
010573cc31 | ||
|
|
629bbe7a78 | ||
|
|
166889be1b | ||
|
|
c80f6292c6 | ||
|
|
5c7a6788b4 | ||
|
|
6ae4e17ffe | ||
|
|
515fde79a3 | ||
|
|
545d0e33d9 | ||
|
|
9d1853742e | ||
|
|
c0beb07b77 | ||
|
|
3a79db6277 | ||
|
|
3378739967 | ||
|
|
e31ac82a55 | ||
|
|
bdd88161c6 | ||
|
|
17e8acb368 | ||
|
|
a2bc1fee6f | ||
|
|
ba2ac461e1 | ||
|
|
d2cc79e838 | ||
|
|
31a30cd460 |
3
.github/workflows/docs_checks.yaml
vendored
3
.github/workflows/docs_checks.yaml
vendored
@@ -27,6 +27,9 @@ on:
|
||||
|
||||
# nhost-js
|
||||
- packages/nhost-js/**
|
||||
|
||||
# cli
|
||||
- cli/**
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
1
.github/workflows/gen_ai_review.yaml
vendored
1
.github/workflows/gen_ai_review.yaml
vendored
@@ -23,4 +23,5 @@ jobs:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
config.model: ${{ vars.GEN_AI_MODEL }}
|
||||
config.model_turbo: $${{ vars.GEN_AI_MODEL_TURBO }}
|
||||
config.max_model_tokens: 200000
|
||||
ignore.glob: "['pnpm-lock.yaml','**/pnpm-lock.yaml', 'vendor/**','**/client_gen.go','**/models_gen.go','**/generated.go','**/*.gen.go']"
|
||||
|
||||
6
.github/workflows/gen_codeql-analysis.yml
vendored
6
.github/workflows/gen_codeql-analysis.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -51,4 +51,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
||||
2
.github/workflows/storage_checks.yaml
vendored
2
.github/workflows/storage_checks.yaml
vendored
@@ -21,7 +21,7 @@ on:
|
||||
- 'vendor/**'
|
||||
|
||||
# storage
|
||||
- 'storage/**'
|
||||
- 'services/storage/**'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
@@ -7,6 +7,8 @@ linters:
|
||||
settings:
|
||||
funlen:
|
||||
lines: 65
|
||||
wsl_v5:
|
||||
allow-whole-block: true
|
||||
disable:
|
||||
- canonicalheader
|
||||
- depguard
|
||||
|
||||
@@ -2,6 +2,29 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [cli@1.33.0] - 2025-10-02
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- *(cli)* Migrate from urfave/v2 to urfave/v3 (#3545)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(cli)* Disable tls on AUTH_SERVER_URL when auth uses custom port (#3549)
|
||||
- *(cli)* Fix breaking change in go-getter dependency (#3551)
|
||||
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(cli)* Update certs (#3552)
|
||||
|
||||
## [cli@1.32.2] - 2025-10-01
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(cli)* Remove hasura- prefix from auth/storage images (#3538)
|
||||
|
||||
## [cli@1.32.1] - 2025-09-29
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
@@ -12,9 +12,9 @@ It's recommended to use the Nhost CLI and the [Nhost GitHub Integration](https:/
|
||||
|
||||
- [Nhost Dashboard](https://github.com/nhost/nhost/tree/main/dashboard)
|
||||
- [Postgres Database](https://www.postgresql.org/)
|
||||
- [Hasura's GraphQL Engine](https://github.com/hasura/graphql-engine)
|
||||
- [Hasura Auth](https://github.com/nhost/hasura-auth)
|
||||
- [Hasura Storage](https://github.com/nhost/hasura-storage)
|
||||
- [GraphQL Engine](https://github.com/hasura/graphql-engine)
|
||||
- [Auth](https://github.com/nhost/nhost/main/auth)
|
||||
- [Storage](https://github.com/nhost/nhost/main/storage)
|
||||
- [Nhost Serverless Functions](https://github.com/nhost/functions)
|
||||
- [Minio S3](https://github.com/minio/minio)
|
||||
- [Mailhog](https://github.com/mailhog/MailHog)
|
||||
@@ -51,11 +51,18 @@ nhost up
|
||||
nhost up --ui nhost
|
||||
```
|
||||
|
||||
## MCP Server
|
||||
|
||||
The Nhost cli ships with an MCP server that lets you interact with your Nhost projects through AI assistants using the Model Context Protocol. It provides secure, controlled access to your GraphQL data, project configuration, and documentation—with granular permissions that let you specify exactly which queries and mutations an LLM can execute. For development, it streamlines your workflow by enabling AI-assisted schema management, metadata changes, and migrations, while providing direct access to your GraphQL schema for intelligent query building.
|
||||
|
||||
You can read more about the MCP server in the [MCP Server documentation](https://docs.nhost.io/platform/cli/mcp/overview).
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Get started with Nhost CLI (longer version)](https://docs.nhost.io/platform/overview/get-started-with-nhost-cli)
|
||||
- [Nhost CLI](https://docs.nhost.io/platform/cli)
|
||||
- [Reference](https://docs.nhost.io/reference/cli)
|
||||
- [MCP Server](https://docs.nhost.io/platform/cli/mcp/overview)
|
||||
|
||||
## Build from Source
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
"github.com/nhost/nhost/cli/nhostclient"
|
||||
"github.com/nhost/nhost/cli/nhostclient/graphql"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func sanitizeName(name string) string {
|
||||
@@ -55,28 +55,28 @@ func New(
|
||||
}
|
||||
}
|
||||
|
||||
func FromCLI(cCtx *cli.Context) *CliEnv {
|
||||
func FromCLI(cmd *cli.Command) *CliEnv {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &CliEnv{
|
||||
stdout: cCtx.App.Writer,
|
||||
stderr: cCtx.App.ErrWriter,
|
||||
stdout: cmd.Writer,
|
||||
stderr: cmd.ErrWriter,
|
||||
Path: NewPathStructure(
|
||||
cwd,
|
||||
cCtx.String(flagRootFolder),
|
||||
cCtx.String(flagDotNhostFolder),
|
||||
cCtx.String(flagNhostFolder),
|
||||
cmd.String(flagRootFolder),
|
||||
cmd.String(flagDotNhostFolder),
|
||||
cmd.String(flagNhostFolder),
|
||||
),
|
||||
authURL: cCtx.String(flagAuthURL),
|
||||
graphqlURL: cCtx.String(flagGraphqlURL),
|
||||
branch: cCtx.String(flagBranch),
|
||||
projectName: sanitizeName(cCtx.String(flagProjectName)),
|
||||
authURL: cmd.String(flagAuthURL),
|
||||
graphqlURL: cmd.String(flagGraphqlURL),
|
||||
branch: cmd.String(flagBranch),
|
||||
projectName: sanitizeName(cmd.String(flagProjectName)),
|
||||
nhclient: nil,
|
||||
nhpublicclient: nil,
|
||||
localSubdomain: cCtx.String(flagLocalSubdomain),
|
||||
localSubdomain: cmd.String(flagLocalSubdomain),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -53,42 +53,42 @@ func Flags() ([]cli.Flag, error) {
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagAuthURL,
|
||||
Usage: "Nhost auth URL",
|
||||
EnvVars: []string{"NHOST_CLI_AUTH_URL"},
|
||||
Sources: cli.EnvVars("NHOST_CLI_AUTH_URL"),
|
||||
Value: "https://otsispdzcwxyqzbfntmj.auth.eu-central-1.nhost.run/v1",
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagGraphqlURL,
|
||||
Usage: "Nhost GraphQL URL",
|
||||
EnvVars: []string{"NHOST_CLI_GRAPHQL_URL"},
|
||||
Sources: cli.EnvVars("NHOST_CLI_GRAPHQL_URL"),
|
||||
Value: "https://otsispdzcwxyqzbfntmj.graphql.eu-central-1.nhost.run/v1",
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagBranch,
|
||||
Usage: "Git branch name. If not set, it will be detected from the current git repository. This flag is used to dynamically create docker volumes for each branch. If you want to have a static volume name or if you are not using git, set this flag to a static value.", //nolint:lll
|
||||
EnvVars: []string{"BRANCH"},
|
||||
Sources: cli.EnvVars("BRANCH"),
|
||||
Value: branch,
|
||||
Hidden: false,
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagRootFolder,
|
||||
Usage: "Root folder of project\n\t",
|
||||
EnvVars: []string{"NHOST_ROOT_FOLDER"},
|
||||
Sources: cli.EnvVars("NHOST_ROOT_FOLDER"),
|
||||
Value: workingDir,
|
||||
Category: "Project structure",
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagDotNhostFolder,
|
||||
Usage: "Path to .nhost folder\n\t",
|
||||
EnvVars: []string{"NHOST_DOT_NHOST_FOLDER"},
|
||||
Sources: cli.EnvVars("NHOST_DOT_NHOST_FOLDER"),
|
||||
Value: dotNhostFolder,
|
||||
Category: "Project structure",
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagNhostFolder,
|
||||
Usage: "Path to nhost folder\n\t",
|
||||
EnvVars: []string{"NHOST_NHOST_FOLDER"},
|
||||
Sources: cli.EnvVars("NHOST_NHOST_FOLDER"),
|
||||
Value: nhostFolder,
|
||||
Category: "Project structure",
|
||||
},
|
||||
@@ -96,13 +96,13 @@ func Flags() ([]cli.Flag, error) {
|
||||
Name: flagProjectName,
|
||||
Usage: "Project name",
|
||||
Value: filepath.Base(fullWorkingDir),
|
||||
EnvVars: []string{"NHOST_PROJECT_NAME"},
|
||||
Sources: cli.EnvVars("NHOST_PROJECT_NAME"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagLocalSubdomain,
|
||||
Usage: "Local subdomain to reach the development environment",
|
||||
Value: "local",
|
||||
EnvVars: []string{"NHOST_LOCAL_SUBDOMAIN"},
|
||||
Sources: cli.EnvVars("NHOST_LOCAL_SUBDOMAIN"),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -29,3 +29,12 @@ func (ce *CliEnv) LoadSession(
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (ce *CliEnv) Credentials() (credentials.Credentials, error) {
|
||||
var creds credentials.Credentials
|
||||
if err := UnmarshalFile(ce.Path.AuthFile(), &creds, json.Unmarshal); err != nil {
|
||||
return credentials.Credentials{}, err
|
||||
}
|
||||
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/nhost/be/services/mimir/model"
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CommandApply() *cli.Command {
|
||||
@@ -22,38 +22,42 @@ func CommandApply() *cli.Command {
|
||||
Name: flagSubdomain,
|
||||
Usage: "Subdomain of the Nhost project to apply configuration to. Defaults to linked project",
|
||||
Required: true,
|
||||
EnvVars: []string{"NHOST_SUBDOMAIN"},
|
||||
Sources: cli.EnvVars("NHOST_SUBDOMAIN"),
|
||||
},
|
||||
&cli.BoolFlag{ //nolint:exhaustruct
|
||||
Name: flagYes,
|
||||
Usage: "Skip confirmation",
|
||||
EnvVars: []string{"NHOST_YES"},
|
||||
Sources: cli.EnvVars("NHOST_YES"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func commandApply(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandApply(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
|
||||
proj, err := ce.GetAppInfo(ctx, cmd.String(flagSubdomain))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get app info: %w", err)
|
||||
return cli.Exit(fmt.Sprintf("Failed to get app info: %v", err), 1)
|
||||
}
|
||||
|
||||
ce.Infoln("Validating configuration...")
|
||||
|
||||
cfg, _, err := ValidateRemote(
|
||||
cCtx.Context,
|
||||
ctx,
|
||||
ce,
|
||||
proj.GetSubdomain(),
|
||||
proj.GetID(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
return cli.Exit(err.Error(), 1)
|
||||
}
|
||||
|
||||
return Apply(cCtx.Context, ce, proj.ID, cfg, cCtx.Bool(flagYes))
|
||||
if err := Apply(ctx, ce, proj.ID, cfg, cmd.Bool(flagYes)); err != nil {
|
||||
return cli.Exit(err.Error(), 1)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Apply(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package config
|
||||
|
||||
import "github.com/urfave/cli/v2"
|
||||
import "github.com/urfave/cli/v3"
|
||||
|
||||
const flagSubdomain = "subdomain"
|
||||
|
||||
@@ -9,7 +9,7 @@ func Command() *cli.Command {
|
||||
Name: "config",
|
||||
Aliases: []string{},
|
||||
Usage: "Perform config operations",
|
||||
Subcommands: []*cli.Command{
|
||||
Commands: []*cli.Command{
|
||||
CommandDefault(),
|
||||
CommandExample(),
|
||||
CommandApply(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
@@ -8,7 +9,7 @@ import (
|
||||
"github.com/nhost/nhost/cli/project"
|
||||
"github.com/nhost/nhost/cli/project/env"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CommandDefault() *cli.Command {
|
||||
@@ -21,8 +22,8 @@ func CommandDefault() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func commandDefault(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandDefault(_ context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
if err := os.MkdirAll(ce.Path.NhostFolder(), 0o755); err != nil { //nolint:mnd
|
||||
return fmt.Errorf("failed to create nhost folder: %w", err)
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/nhost/be/services/mimir/model"
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/wI2L/jsondiff"
|
||||
)
|
||||
|
||||
@@ -31,13 +31,13 @@ func CommandEdit() *cli.Command {
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagSubdomain,
|
||||
Usage: "If specified, edit this subdomain's overlay, otherwise edit base configuation",
|
||||
EnvVars: []string{"NHOST_SUBDOMAIN"},
|
||||
Sources: cli.EnvVars("NHOST_SUBDOMAIN"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagEditor,
|
||||
Usage: "Editor to use",
|
||||
Value: "vim",
|
||||
EnvVars: []string{"EDITOR"},
|
||||
Sources: cli.EnvVars("EDITOR"),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -139,11 +139,11 @@ func GenerateJSONPatch(origfilepath, newfilepath, dst string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func edit(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func edit(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
if cCtx.String(flagSubdomain) == "" {
|
||||
if err := EditFile(cCtx.Context, cCtx.String(flagEditor), ce.Path.NhostToml()); err != nil {
|
||||
if cmd.String(flagSubdomain) == "" {
|
||||
if err := EditFile(ctx, cmd.String(flagEditor), ce.Path.NhostToml()); err != nil {
|
||||
return fmt.Errorf("failed to edit config: %w", err)
|
||||
}
|
||||
|
||||
@@ -163,17 +163,17 @@ func edit(cCtx *cli.Context) error {
|
||||
tmpfileName := filepath.Join(tmpdir, "nhost.toml")
|
||||
|
||||
if err := CopyConfig[model.ConfigConfig](
|
||||
ce.Path.NhostToml(), tmpfileName, ce.Path.Overlay(cCtx.String(flagSubdomain)),
|
||||
ce.Path.NhostToml(), tmpfileName, ce.Path.Overlay(cmd.String(flagSubdomain)),
|
||||
); err != nil {
|
||||
return fmt.Errorf("failed to copy config: %w", err)
|
||||
}
|
||||
|
||||
if err := EditFile(cCtx.Context, cCtx.String(flagEditor), tmpfileName); err != nil {
|
||||
if err := EditFile(ctx, cmd.String(flagEditor), tmpfileName); err != nil {
|
||||
return fmt.Errorf("failed to edit config: %w", err)
|
||||
}
|
||||
|
||||
if err := GenerateJSONPatch(
|
||||
ce.Path.NhostToml(), tmpfileName, ce.Path.Overlay(cCtx.String(flagSubdomain)),
|
||||
ce.Path.NhostToml(), tmpfileName, ce.Path.Overlay(cmd.String(flagSubdomain)),
|
||||
); err != nil {
|
||||
return fmt.Errorf("failed to generate json patch: %w", err)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/nhost/be/services/mimir/model"
|
||||
"github.com/nhost/be/services/mimir/schema"
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CommandExample() *cli.Command {
|
||||
@@ -22,8 +23,8 @@ func CommandExample() *cli.Command {
|
||||
|
||||
func ptr[T any](v T) *T { return &v }
|
||||
|
||||
func commandExample(cCtx *cli.Context) error { //nolint:funlen,maintidx
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandExample(_ context.Context, cmd *cli.Command) error { //nolint:funlen,maintidx
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
//nolint:mnd
|
||||
cfg := model.ConfigConfig{
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/nhost/nhost/cli/project/env"
|
||||
"github.com/nhost/nhost/cli/system"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -36,21 +36,21 @@ func CommandPull() *cli.Command {
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagSubdomain,
|
||||
Usage: "Pull this subdomain's configuration. Defaults to linked project",
|
||||
EnvVars: []string{"NHOST_SUBDOMAIN"},
|
||||
Sources: cli.EnvVars("NHOST_SUBDOMAIN"),
|
||||
},
|
||||
&cli.BoolFlag{ //nolint:exhaustruct
|
||||
Name: flagYes,
|
||||
Usage: "Skip confirmation",
|
||||
EnvVars: []string{"NHOST_YES"},
|
||||
Sources: cli.EnvVars("NHOST_YES"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func commandPull(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandPull(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
skipConfirmation := cCtx.Bool(flagYes)
|
||||
skipConfirmation := cmd.Bool(flagYes)
|
||||
|
||||
if !skipConfirmation {
|
||||
if err := verifyFile(ce, ce.Path.NhostToml()); err != nil {
|
||||
@@ -66,12 +66,12 @@ func commandPull(cCtx *cli.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
|
||||
proj, err := ce.GetAppInfo(ctx, cmd.String(flagSubdomain))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get app info: %w", err)
|
||||
}
|
||||
|
||||
_, err = Pull(cCtx.Context, ce, proj, writeSecrets)
|
||||
_, err = Pull(ctx, ce, proj, writeSecrets)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/nhost/be/services/mimir/model"
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/project/env"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CommandShow() *cli.Command {
|
||||
@@ -21,14 +22,14 @@ func CommandShow() *cli.Command {
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagSubdomain,
|
||||
Usage: "Show this subdomain's rendered configuration. Defaults to base configuration",
|
||||
EnvVars: []string{"NHOST_SUBDOMAIN"},
|
||||
Sources: cli.EnvVars("NHOST_SUBDOMAIN"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func commandShow(c *cli.Context) error {
|
||||
ce := clienv.FromCLI(c)
|
||||
func commandShow(_ context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
var secrets model.Secrets
|
||||
if err := clienv.UnmarshalFile(ce.Path.Secrets(), &secrets, env.Unmarshal); err != nil {
|
||||
@@ -38,7 +39,7 @@ func commandShow(c *cli.Context) error {
|
||||
)
|
||||
}
|
||||
|
||||
cfg, err := Validate(ce, c.String(flagSubdomain), secrets)
|
||||
cfg, err := Validate(ce, cmd.String(flagSubdomain), secrets)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/project/env"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
jsonpatch "gopkg.in/evanphx/json-patch.v5"
|
||||
)
|
||||
|
||||
@@ -27,24 +27,24 @@ func CommandValidate() *cli.Command {
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagSubdomain,
|
||||
Usage: "Validate this subdomain's configuration. Defaults to linked project",
|
||||
EnvVars: []string{"NHOST_SUBDOMAIN"},
|
||||
Sources: cli.EnvVars("NHOST_SUBDOMAIN"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func commandValidate(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandValidate(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
subdomain := cCtx.String(flagSubdomain)
|
||||
subdomain := cmd.String(flagSubdomain)
|
||||
if subdomain != "" && subdomain != "local" {
|
||||
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
|
||||
proj, err := ce.GetAppInfo(ctx, cmd.String(flagSubdomain))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get app info: %w", err)
|
||||
}
|
||||
|
||||
_, _, err = ValidateRemote(
|
||||
cCtx.Context,
|
||||
ctx,
|
||||
ce,
|
||||
proj.GetSubdomain(),
|
||||
proj.GetID(),
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/nhost/be/services/mimir/graph"
|
||||
cors "github.com/rs/cors/wrapper/gin"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -48,27 +48,27 @@ func Command() *cli.Command {
|
||||
Name: enablePlaygroundFlag,
|
||||
Usage: "enable graphql playground (under /v1)",
|
||||
Category: "server",
|
||||
EnvVars: []string{"ENABLE_PLAYGROUND"},
|
||||
Sources: cli.EnvVars("ENABLE_PLAYGROUND"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint: exhaustruct
|
||||
Name: storageLocalConfigPath,
|
||||
Usage: "Path to the local mimir config file",
|
||||
Value: "/tmp/root/nhost/nhost.toml",
|
||||
Category: "plugins",
|
||||
EnvVars: []string{"STORAGE_LOCAL_CONFIG_PATH"},
|
||||
Sources: cli.EnvVars("STORAGE_LOCAL_CONFIG_PATH"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint: exhaustruct
|
||||
Name: storageLocalSecretsPath,
|
||||
Usage: "Path to the local mimir secrets file",
|
||||
Value: "/tmp/root/.secrets",
|
||||
Category: "plugins",
|
||||
EnvVars: []string{"STORAGE_LOCAL_SECRETS_PATH"},
|
||||
Sources: cli.EnvVars("STORAGE_LOCAL_SECRETS_PATH"),
|
||||
},
|
||||
&cli.StringSliceFlag{ //nolint: exhaustruct
|
||||
Name: storageLocalRunServicesPath,
|
||||
Usage: "Path to the local mimir run services files",
|
||||
Category: "plugins",
|
||||
EnvVars: []string{"STORAGE_LOCAL_RUN_SERVICES_PATH"},
|
||||
Sources: cli.EnvVars("STORAGE_LOCAL_RUN_SERVICES_PATH"),
|
||||
},
|
||||
},
|
||||
Action: serve,
|
||||
@@ -103,14 +103,14 @@ func runServicesFiles(runServices ...string) map[string]string {
|
||||
return m
|
||||
}
|
||||
|
||||
func serve(cCtx *cli.Context) error {
|
||||
logger := getLogger(cCtx.Bool(debugFlag), cCtx.Bool(logFormatJSONFlag))
|
||||
logger.Info(cCtx.App.Name + " v" + cCtx.App.Version)
|
||||
logFlags(logger, cCtx)
|
||||
func serve(_ context.Context, cmd *cli.Command) error {
|
||||
logger := getLogger(cmd.Bool(debugFlag), cmd.Bool(logFormatJSONFlag))
|
||||
logger.Info(cmd.Root().Name + " v" + cmd.Root().Version)
|
||||
logFlags(logger, cmd)
|
||||
|
||||
configFile := cCtx.String(storageLocalConfigPath)
|
||||
secretsFile := cCtx.String(storageLocalSecretsPath)
|
||||
runServices := runServicesFiles(cCtx.StringSlice(storageLocalRunServicesPath)...)
|
||||
configFile := cmd.String(storageLocalConfigPath)
|
||||
secretsFile := cmd.String(storageLocalSecretsPath)
|
||||
runServices := runServicesFiles(cmd.StringSlice(storageLocalRunServicesPath)...)
|
||||
|
||||
st := NewLocal(configFile, secretsFile, runServices)
|
||||
|
||||
@@ -131,13 +131,13 @@ func serve(cCtx *cli.Context) error {
|
||||
resolver,
|
||||
dummyMiddleware,
|
||||
dummyMiddleware2,
|
||||
cCtx.Bool(enablePlaygroundFlag),
|
||||
cCtx.App.Version,
|
||||
cmd.Bool(enablePlaygroundFlag),
|
||||
cmd.Root().Version,
|
||||
[]graphql.FieldMiddleware{},
|
||||
gin.Recovery(),
|
||||
cors.Default(),
|
||||
)
|
||||
if err := r.Run(cCtx.String(bindFlag)); err != nil {
|
||||
if err := r.Run(cmd.String(bindFlag)); err != nil {
|
||||
return fmt.Errorf("failed to run gin: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func getLogger(debug bool, formatJSON bool) *logrus.Logger {
|
||||
@@ -29,15 +29,15 @@ func getLogger(debug bool, formatJSON bool) *logrus.Logger {
|
||||
return logger
|
||||
}
|
||||
|
||||
func logFlags(logger logrus.FieldLogger, cCtx *cli.Context) {
|
||||
func logFlags(logger logrus.FieldLogger, cmd *cli.Command) {
|
||||
fields := logrus.Fields{}
|
||||
|
||||
for _, flag := range cCtx.App.Flags {
|
||||
for _, flag := range cmd.Root().Flags {
|
||||
name := flag.Names()[0]
|
||||
fields[name] = cCtx.Generic(name)
|
||||
fields[name] = cmd.Value(name)
|
||||
}
|
||||
|
||||
for _, flag := range cCtx.Command.Flags {
|
||||
for _, flag := range cmd.Flags {
|
||||
name := flag.Names()[0]
|
||||
if strings.Contains(name, "pass") ||
|
||||
strings.Contains(name, "token") ||
|
||||
@@ -47,7 +47,7 @@ func logFlags(logger logrus.FieldLogger, cCtx *cli.Context) {
|
||||
continue
|
||||
}
|
||||
|
||||
fields[name] = cCtx.Generic(name)
|
||||
fields[name] = cmd.Value(name)
|
||||
}
|
||||
|
||||
logger.WithFields(fields).Info("started with settings")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package deployments
|
||||
|
||||
import "github.com/urfave/cli/v2"
|
||||
import "github.com/urfave/cli/v3"
|
||||
|
||||
const flagSubdomain = "subdomain"
|
||||
|
||||
@@ -9,7 +9,7 @@ func commonFlags() []cli.Flag {
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagSubdomain,
|
||||
Usage: "Project's subdomain to operate on, defaults to linked project",
|
||||
EnvVars: []string{"NHOST_SUBDOMAIN"},
|
||||
Sources: cli.EnvVars("NHOST_SUBDOMAIN"),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ func Command() *cli.Command {
|
||||
Name: "deployments",
|
||||
Aliases: []string{},
|
||||
Usage: "Manage deployments",
|
||||
Subcommands: []*cli.Command{
|
||||
Commands: []*cli.Command{
|
||||
CommandList(),
|
||||
CommandLogs(),
|
||||
CommandNew(),
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package deployments
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/nhostclient/graphql"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CommandList() *cli.Command {
|
||||
@@ -77,21 +78,21 @@ func printDeployments(ce *clienv.CliEnv, deployments []*graphql.ListDeployments_
|
||||
ce.Println("%s", clienv.Table(id, date, duration, status, user, ref, message))
|
||||
}
|
||||
|
||||
func commandList(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandList(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
|
||||
proj, err := ce.GetAppInfo(ctx, cmd.String(flagSubdomain))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get app info: %w", err)
|
||||
}
|
||||
|
||||
cl, err := ce.GetNhostClient(cCtx.Context)
|
||||
cl, err := ce.GetNhostClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get nhost client: %w", err)
|
||||
}
|
||||
|
||||
deployments, err := cl.ListDeployments(
|
||||
cCtx.Context,
|
||||
ctx,
|
||||
proj.ID,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/nhostclient"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -101,28 +101,28 @@ func showLogsFollow(
|
||||
}
|
||||
}
|
||||
|
||||
func commandLogs(cCtx *cli.Context) error {
|
||||
deploymentID := cCtx.Args().First()
|
||||
func commandLogs(ctx context.Context, cmd *cli.Command) error {
|
||||
deploymentID := cmd.Args().First()
|
||||
if deploymentID == "" {
|
||||
return errors.New("deployment_id is required") //nolint:err113
|
||||
}
|
||||
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
cl, err := ce.GetNhostClient(cCtx.Context)
|
||||
cl, err := ce.GetNhostClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get nhost client: %w", err)
|
||||
}
|
||||
|
||||
if cCtx.Bool(flagFollow) {
|
||||
ctx, cancel := context.WithTimeout(cCtx.Context, cCtx.Duration(flagTimeout))
|
||||
if cmd.Bool(flagFollow) {
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, cmd.Duration(flagTimeout))
|
||||
defer cancel()
|
||||
|
||||
if _, err := showLogsFollow(ctx, ce, cl, deploymentID); err != nil {
|
||||
if _, err := showLogsFollow(ctxWithTimeout, ce, cl, deploymentID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := showLogsSimple(cCtx.Context, ce, cl, deploymentID); err != nil {
|
||||
if err := showLogsSimple(ctx, ce, cl, deploymentID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/nhostclient/graphql"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -40,7 +40,7 @@ func CommandNew() *cli.Command {
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagRef,
|
||||
Usage: "Git reference",
|
||||
EnvVars: []string{"GITHUB_SHA"},
|
||||
Sources: cli.EnvVars("GITHUB_SHA"),
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
@@ -51,7 +51,7 @@ func CommandNew() *cli.Command {
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagUser,
|
||||
Usage: "Commit user name",
|
||||
EnvVars: []string{"GITHUB_ACTOR"},
|
||||
Sources: cli.EnvVars("GITHUB_ACTOR"),
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
@@ -67,28 +67,28 @@ func ptr[i any](v i) *i {
|
||||
return &v
|
||||
}
|
||||
|
||||
func commandNew(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandNew(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
cl, err := ce.GetNhostClient(cCtx.Context)
|
||||
cl, err := ce.GetNhostClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get nhost client: %w", err)
|
||||
}
|
||||
|
||||
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
|
||||
proj, err := ce.GetAppInfo(ctx, cmd.String(flagSubdomain))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get app info: %w", err)
|
||||
}
|
||||
|
||||
resp, err := cl.InsertDeployment(
|
||||
cCtx.Context,
|
||||
ctx,
|
||||
graphql.DeploymentsInsertInput{
|
||||
App: nil,
|
||||
AppID: ptr(proj.ID),
|
||||
CommitMessage: ptr(cCtx.String(flagMessage)),
|
||||
CommitSha: ptr(cCtx.String(flagRef)),
|
||||
CommitUserAvatarURL: ptr(cCtx.String(flagUserAvatarURL)),
|
||||
CommitUserName: ptr(cCtx.String(flagUser)),
|
||||
CommitMessage: ptr(cmd.String(flagMessage)),
|
||||
CommitSha: ptr(cmd.String(flagRef)),
|
||||
CommitUserAvatarURL: ptr(cmd.String(flagUserAvatarURL)),
|
||||
CommitUserName: ptr(cmd.String(flagUser)),
|
||||
DeploymentStatus: ptr("SCHEDULED"),
|
||||
},
|
||||
)
|
||||
@@ -98,13 +98,13 @@ func commandNew(cCtx *cli.Context) error {
|
||||
|
||||
ce.Println("Deployment created: %s", resp.InsertDeployment.ID)
|
||||
|
||||
if cCtx.Bool(flagFollow) {
|
||||
if cmd.Bool(flagFollow) {
|
||||
ce.Println("")
|
||||
|
||||
ctx, cancel := context.WithTimeout(cCtx.Context, cCtx.Duration(flagTimeout))
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, cmd.Duration(flagTimeout))
|
||||
defer cancel()
|
||||
|
||||
status, err := showLogsFollow(ctx, ce, cl, resp.InsertDeployment.ID)
|
||||
status, err := showLogsFollow(ctxWithTimeout, ce, cl, resp.InsertDeployment.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error streaming logs: %w", err)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/nhost/nhost/cli/cmd/software"
|
||||
"github.com/nhost/nhost/cli/dockercompose"
|
||||
"github.com/nhost/nhost/cli/nhostclient/graphql"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -34,19 +34,19 @@ func CommandCloud() *cli.Command {
|
||||
Name: flagHTTPPort,
|
||||
Usage: "HTTP port to listen on",
|
||||
Value: defaultHTTPPort,
|
||||
EnvVars: []string{"NHOST_HTTP_PORT"},
|
||||
Sources: cli.EnvVars("NHOST_HTTP_PORT"),
|
||||
},
|
||||
&cli.BoolFlag{ //nolint:exhaustruct
|
||||
Name: flagDisableTLS,
|
||||
Usage: "Disable TLS",
|
||||
Value: false,
|
||||
EnvVars: []string{"NHOST_DISABLE_TLS"},
|
||||
Sources: cli.EnvVars("NHOST_DISABLE_TLS"),
|
||||
},
|
||||
&cli.BoolFlag{ //nolint:exhaustruct
|
||||
Name: flagApplySeeds,
|
||||
Usage: "Apply seeds. If the .nhost folder does not exist, seeds will be applied regardless of this flag",
|
||||
Value: false,
|
||||
EnvVars: []string{"NHOST_APPLY_SEEDS"},
|
||||
Sources: cli.EnvVars("NHOST_APPLY_SEEDS"),
|
||||
},
|
||||
&cli.UintFlag{ //nolint:exhaustruct
|
||||
Name: flagsHasuraConsolePort,
|
||||
@@ -56,42 +56,42 @@ func CommandCloud() *cli.Command {
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagDashboardVersion,
|
||||
Usage: "Dashboard version to use",
|
||||
Value: "nhost/dashboard:2.33.0",
|
||||
EnvVars: []string{"NHOST_DASHBOARD_VERSION"},
|
||||
Value: "nhost/dashboard:2.38.0",
|
||||
Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagConfigserverImage,
|
||||
Hidden: true,
|
||||
Value: "",
|
||||
EnvVars: []string{"NHOST_CONFIGSERVER_IMAGE"},
|
||||
Sources: cli.EnvVars("NHOST_CONFIGSERVER_IMAGE"),
|
||||
},
|
||||
&cli.BoolFlag{ //nolint:exhaustruct
|
||||
Name: flagDownOnError,
|
||||
Usage: "Skip confirmation",
|
||||
EnvVars: []string{"NHOST_YES"},
|
||||
Sources: cli.EnvVars("NHOST_YES"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagCACertificates,
|
||||
Usage: "Mounts and everrides path to CA certificates in the containers",
|
||||
EnvVars: []string{"NHOST_CA_CERTIFICATES"},
|
||||
Sources: cli.EnvVars("NHOST_CA_CERTIFICATES"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagSubdomain,
|
||||
Usage: "Project's subdomain to operate on, defaults to linked project",
|
||||
EnvVars: []string{"NHOST_SUBDOMAIN"},
|
||||
Sources: cli.EnvVars("NHOST_SUBDOMAIN"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagPostgresURL,
|
||||
Usage: "Postgres URL",
|
||||
Required: true,
|
||||
EnvVars: []string{"NHOST_POSTGRES_URL"},
|
||||
Sources: cli.EnvVars("NHOST_POSTGRES_URL"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func commandCloud(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandCloud(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
if !clienv.PathExists(ce.Path.NhostToml()) {
|
||||
return errors.New( //nolint:err113
|
||||
@@ -105,38 +105,38 @@ func commandCloud(cCtx *cli.Context) error {
|
||||
)
|
||||
}
|
||||
|
||||
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
|
||||
proj, err := ce.GetAppInfo(ctx, cmd.String(flagSubdomain))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get app info: %w", err)
|
||||
}
|
||||
|
||||
configserverImage := cCtx.String(flagConfigserverImage)
|
||||
configserverImage := cmd.String(flagConfigserverImage)
|
||||
if configserverImage == "" {
|
||||
configserverImage = "nhost/cli:" + cCtx.App.Version
|
||||
configserverImage = "nhost/cli:" + cmd.Root().Version
|
||||
}
|
||||
|
||||
applySeeds := cCtx.Bool(flagApplySeeds)
|
||||
applySeeds := cmd.Bool(flagApplySeeds)
|
||||
|
||||
return Cloud(
|
||||
cCtx.Context,
|
||||
ctx,
|
||||
ce,
|
||||
cCtx.App.Version,
|
||||
cCtx.Uint(flagHTTPPort),
|
||||
!cCtx.Bool(flagDisableTLS),
|
||||
cmd.Root().Version,
|
||||
cmd.Uint(flagHTTPPort),
|
||||
!cmd.Bool(flagDisableTLS),
|
||||
applySeeds,
|
||||
dockercompose.ExposePorts{
|
||||
Auth: cCtx.Uint(flagAuthPort),
|
||||
Storage: cCtx.Uint(flagStoragePort),
|
||||
Graphql: cCtx.Uint(flagsHasuraPort),
|
||||
Console: cCtx.Uint(flagsHasuraConsolePort),
|
||||
Functions: cCtx.Uint(flagsFunctionsPort),
|
||||
Auth: cmd.Uint(flagAuthPort),
|
||||
Storage: cmd.Uint(flagStoragePort),
|
||||
Graphql: cmd.Uint(flagsHasuraPort),
|
||||
Console: cmd.Uint(flagsHasuraConsolePort),
|
||||
Functions: cmd.Uint(flagsFunctionsPort),
|
||||
},
|
||||
cCtx.String(flagDashboardVersion),
|
||||
cmd.String(flagDashboardVersion),
|
||||
configserverImage,
|
||||
cCtx.String(flagCACertificates),
|
||||
cCtx.Bool(flagDownOnError),
|
||||
cmd.String(flagCACertificates),
|
||||
cmd.Bool(flagDownOnError),
|
||||
proj,
|
||||
cCtx.String(flagPostgresURL),
|
||||
cmd.String(flagPostgresURL),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package dev
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/dockercompose"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CommandCompose() *cli.Command {
|
||||
@@ -17,9 +19,9 @@ func CommandCompose() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func commandCompose(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandCompose(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
dc := dockercompose.New(ce.Path.WorkingDir(), ce.Path.DockerCompose(), ce.ProjectName())
|
||||
|
||||
return dc.Wrapper(cCtx.Context, cCtx.Args().Slice()...) //nolint:wrapcheck
|
||||
return dc.Wrapper(ctx, cmd.Args().Slice()...) //nolint:wrapcheck
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package dev
|
||||
|
||||
import "github.com/urfave/cli/v2"
|
||||
import "github.com/urfave/cli/v3"
|
||||
|
||||
func Command() *cli.Command {
|
||||
return &cli.Command{ //nolint:exhaustruct
|
||||
Name: "dev",
|
||||
Aliases: []string{},
|
||||
Usage: "Operate local development environment",
|
||||
Subcommands: []*cli.Command{
|
||||
Commands: []*cli.Command{
|
||||
CommandCompose(),
|
||||
CommandHasura(),
|
||||
},
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package dev
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/dockercompose"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,12 +28,12 @@ func CommandDown() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func commandDown(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandDown(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
dc := dockercompose.New(ce.Path.WorkingDir(), ce.Path.DockerCompose(), ce.ProjectName())
|
||||
|
||||
if err := dc.Stop(cCtx.Context, cCtx.Bool(flagVolumes)); err != nil {
|
||||
if err := dc.Stop(ctx, cmd.Bool(flagVolumes)); err != nil {
|
||||
ce.Warnln("failed to stop Nhost development environment: %s", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package dev
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/nhost/be/services/mimir/model"
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/dockercompose"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CommandHasura() *cli.Command {
|
||||
@@ -21,8 +22,8 @@ func CommandHasura() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func commandHasura(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandHasura(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
cfg := &model.ConfigConfig{} //nolint:exhaustruct
|
||||
if err := clienv.UnmarshalFile(ce.Path.NhostToml(), cfg, toml.Unmarshal); err != nil {
|
||||
@@ -32,10 +33,10 @@ func commandHasura(cCtx *cli.Context) error {
|
||||
docker := dockercompose.NewDocker()
|
||||
|
||||
return docker.HasuraWrapper( //nolint:wrapcheck
|
||||
cCtx.Context,
|
||||
ctx,
|
||||
ce.LocalSubdomain(),
|
||||
ce.Path.NhostFolder(),
|
||||
*cfg.Hasura.Version,
|
||||
cCtx.Args().Slice()...,
|
||||
cmd.Args().Slice()...,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package dev
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/dockercompose"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CommandLogs() *cli.Command {
|
||||
@@ -17,12 +19,12 @@ func CommandLogs() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func commandLogs(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandLogs(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
dc := dockercompose.New(ce.Path.WorkingDir(), ce.Path.DockerCompose(), ce.ProjectName())
|
||||
|
||||
if err := dc.Logs(cCtx.Context, cCtx.Args().Slice()...); err != nil {
|
||||
if err := dc.Logs(ctx, cmd.Args().Slice()...); err != nil {
|
||||
ce.Warnln("%s", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"github.com/nhost/nhost/cli/cmd/software"
|
||||
"github.com/nhost/nhost/cli/dockercompose"
|
||||
"github.com/nhost/nhost/cli/project/env"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func deptr[T any](t *T) T { //nolint:ireturn
|
||||
@@ -63,25 +63,25 @@ func CommandUp() *cli.Command { //nolint:funlen
|
||||
Name: flagHTTPPort,
|
||||
Usage: "HTTP port to listen on",
|
||||
Value: defaultHTTPPort,
|
||||
EnvVars: []string{"NHOST_HTTP_PORT"},
|
||||
Sources: cli.EnvVars("NHOST_HTTP_PORT"),
|
||||
},
|
||||
&cli.BoolFlag{ //nolint:exhaustruct
|
||||
Name: flagDisableTLS,
|
||||
Usage: "Disable TLS",
|
||||
Value: false,
|
||||
EnvVars: []string{"NHOST_DISABLE_TLS"},
|
||||
Sources: cli.EnvVars("NHOST_DISABLE_TLS"),
|
||||
},
|
||||
&cli.UintFlag{ //nolint:exhaustruct
|
||||
Name: flagPostgresPort,
|
||||
Usage: "Postgres port to listen on",
|
||||
Value: defaultPostgresPort,
|
||||
EnvVars: []string{"NHOST_POSTGRES_PORT"},
|
||||
Sources: cli.EnvVars("NHOST_POSTGRES_PORT"),
|
||||
},
|
||||
&cli.BoolFlag{ //nolint:exhaustruct
|
||||
Name: flagApplySeeds,
|
||||
Usage: "Apply seeds. If the .nhost folder does not exist, seeds will be applied regardless of this flag",
|
||||
Value: false,
|
||||
EnvVars: []string{"NHOST_APPLY_SEEDS"},
|
||||
Sources: cli.EnvVars("NHOST_APPLY_SEEDS"),
|
||||
},
|
||||
&cli.UintFlag{ //nolint:exhaustruct
|
||||
Name: flagAuthPort,
|
||||
@@ -111,39 +111,39 @@ func CommandUp() *cli.Command { //nolint:funlen
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagDashboardVersion,
|
||||
Usage: "Dashboard version to use",
|
||||
Value: "nhost/dashboard:2.33.0",
|
||||
EnvVars: []string{"NHOST_DASHBOARD_VERSION"},
|
||||
Value: "nhost/dashboard:2.38.0",
|
||||
Sources: cli.EnvVars("NHOST_DASHBOARD_VERSION"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagConfigserverImage,
|
||||
Hidden: true,
|
||||
Value: "",
|
||||
EnvVars: []string{"NHOST_CONFIGSERVER_IMAGE"},
|
||||
Sources: cli.EnvVars("NHOST_CONFIGSERVER_IMAGE"),
|
||||
},
|
||||
&cli.StringSliceFlag{ //nolint:exhaustruct
|
||||
Name: flagRunService,
|
||||
Usage: "Run service to add to the development environment. Can be passed multiple times. Comma-separated values are also accepted. Format: /path/to/run-service.toml[:overlay_name]", //nolint:lll
|
||||
EnvVars: []string{"NHOST_RUN_SERVICE"},
|
||||
Sources: cli.EnvVars("NHOST_RUN_SERVICE"),
|
||||
},
|
||||
&cli.BoolFlag{ //nolint:exhaustruct
|
||||
Name: flagDownOnError,
|
||||
Usage: "Skip confirmation",
|
||||
EnvVars: []string{"NHOST_YES"},
|
||||
Sources: cli.EnvVars("NHOST_YES"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagCACertificates,
|
||||
Usage: "Mounts and everrides path to CA certificates in the containers",
|
||||
EnvVars: []string{"NHOST_CA_CERTIFICATES"},
|
||||
Sources: cli.EnvVars("NHOST_CA_CERTIFICATES"),
|
||||
},
|
||||
},
|
||||
Subcommands: []*cli.Command{
|
||||
Commands: []*cli.Command{
|
||||
CommandCloud(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func commandUp(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandUp(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
// projname to be root directory
|
||||
|
||||
@@ -159,33 +159,33 @@ func commandUp(cCtx *cli.Context) error {
|
||||
)
|
||||
}
|
||||
|
||||
configserverImage := cCtx.String(flagConfigserverImage)
|
||||
configserverImage := cmd.String(flagConfigserverImage)
|
||||
if configserverImage == "" {
|
||||
configserverImage = "nhost/cli:" + cCtx.App.Version
|
||||
configserverImage = "nhost/cli:" + cmd.Root().Version
|
||||
}
|
||||
|
||||
applySeeds := cCtx.Bool(flagApplySeeds) || !clienv.PathExists(ce.Path.DotNhostFolder())
|
||||
applySeeds := cmd.Bool(flagApplySeeds) || !clienv.PathExists(ce.Path.DotNhostFolder())
|
||||
|
||||
return Up(
|
||||
cCtx.Context,
|
||||
ctx,
|
||||
ce,
|
||||
cCtx.App.Version,
|
||||
cCtx.Uint(flagHTTPPort),
|
||||
!cCtx.Bool(flagDisableTLS),
|
||||
cCtx.Uint(flagPostgresPort),
|
||||
cmd.Root().Version,
|
||||
cmd.Uint(flagHTTPPort),
|
||||
!cmd.Bool(flagDisableTLS),
|
||||
cmd.Uint(flagPostgresPort),
|
||||
applySeeds,
|
||||
dockercompose.ExposePorts{
|
||||
Auth: cCtx.Uint(flagAuthPort),
|
||||
Storage: cCtx.Uint(flagStoragePort),
|
||||
Graphql: cCtx.Uint(flagsHasuraPort),
|
||||
Console: cCtx.Uint(flagsHasuraConsolePort),
|
||||
Functions: cCtx.Uint(flagsFunctionsPort),
|
||||
Auth: cmd.Uint(flagAuthPort),
|
||||
Storage: cmd.Uint(flagStoragePort),
|
||||
Graphql: cmd.Uint(flagsHasuraPort),
|
||||
Console: cmd.Uint(flagsHasuraConsolePort),
|
||||
Functions: cmd.Uint(flagsFunctionsPort),
|
||||
},
|
||||
cCtx.String(flagDashboardVersion),
|
||||
cmd.String(flagDashboardVersion),
|
||||
configserverImage,
|
||||
cCtx.String(flagCACertificates),
|
||||
cCtx.StringSlice(flagRunService),
|
||||
cCtx.Bool(flagDownOnError),
|
||||
cmd.String(flagCACertificates),
|
||||
cmd.StringSlice(flagRunService),
|
||||
cmd.Bool(flagDownOnError),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"os/exec"
|
||||
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -35,13 +35,13 @@ func CommandConfigure() *cli.Command {
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagDockerConfig,
|
||||
Usage: "Path to docker config file",
|
||||
EnvVars: []string{"DOCKER_CONFIG"},
|
||||
Sources: cli.EnvVars("DOCKER_CONFIG"),
|
||||
Value: home + "/.docker/config.json",
|
||||
},
|
||||
&cli.BoolFlag{ //nolint:exhaustruct
|
||||
Name: flagNoInteractive,
|
||||
Usage: "Do not prompt for confirmation",
|
||||
EnvVars: []string{"NO_INTERACTIVE"},
|
||||
Sources: cli.EnvVars("NO_INTERACTIVE"),
|
||||
Value: false,
|
||||
},
|
||||
},
|
||||
@@ -140,21 +140,21 @@ func configureDocker(dockerConfig string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func actionConfigure(c *cli.Context) error {
|
||||
ce := clienv.FromCLI(c)
|
||||
func actionConfigure(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
if err := writeScript(c.Context, ce); err != nil {
|
||||
if err := writeScript(ctx, ce); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Bool(flagNoInteractive) {
|
||||
return configureDocker(c.String(flagDockerConfig))
|
||||
if cmd.Bool(flagNoInteractive) {
|
||||
return configureDocker(cmd.String(flagDockerConfig))
|
||||
}
|
||||
|
||||
//nolint:lll
|
||||
ce.PromptMessage(
|
||||
"I am about to configure docker to authenticate with Nhost's registry. This will modify your docker config file on %s. Should I continue? [y/N] ",
|
||||
c.String(flagDockerConfig),
|
||||
cmd.String(flagDockerConfig),
|
||||
)
|
||||
|
||||
v, err := ce.PromptInput(false)
|
||||
@@ -163,7 +163,7 @@ func actionConfigure(c *cli.Context) error {
|
||||
}
|
||||
|
||||
if v == "y" || v == "Y" {
|
||||
return configureDocker(c.String(flagDockerConfig))
|
||||
return configureDocker(cmd.String(flagDockerConfig))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package dockercredentials
|
||||
|
||||
import "github.com/urfave/cli/v2"
|
||||
import "github.com/urfave/cli/v3"
|
||||
|
||||
func Command() *cli.Command {
|
||||
return &cli.Command{ //nolint:exhaustruct
|
||||
Name: "docker-credentials",
|
||||
Aliases: []string{},
|
||||
Usage: "Perform docker-credentials operations",
|
||||
Subcommands: []*cli.Command{
|
||||
Commands: []*cli.Command{
|
||||
CommandGet(),
|
||||
CommandErase(),
|
||||
CommandStore(),
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package dockercredentials
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli/v2"
|
||||
"context"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CommandErase() *cli.Command {
|
||||
@@ -14,7 +16,7 @@ func CommandErase() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func actionErase(c *cli.Context) error {
|
||||
_, _ = c.App.Writer.Write([]byte("Please, use the nhost CLI to logout\n"))
|
||||
func actionErase(_ context.Context, cmd *cli.Command) error {
|
||||
_, _ = cmd.Root().Writer.Write([]byte("Please, use the nhost CLI to logout\n"))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -26,14 +26,14 @@ func CommandGet() *cli.Command {
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagAuthURL,
|
||||
Usage: "Nhost auth URL",
|
||||
EnvVars: []string{"NHOST_CLI_AUTH_URL"},
|
||||
Sources: cli.EnvVars("NHOST_CLI_AUTH_URL"),
|
||||
Value: "https://otsispdzcwxyqzbfntmj.auth.eu-central-1.nhost.run/v1",
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagGraphqlURL,
|
||||
Usage: "Nhost GraphQL URL",
|
||||
EnvVars: []string{"NHOST_CLI_GRAPHQL_URL"},
|
||||
Sources: cli.EnvVars("NHOST_CLI_GRAPHQL_URL"),
|
||||
Value: "https://otsispdzcwxyqzbfntmj.graphql.eu-central-1.nhost.run/v1",
|
||||
Hidden: true,
|
||||
},
|
||||
@@ -69,15 +69,15 @@ type response struct {
|
||||
Secret string `json:"Secret"`
|
||||
}
|
||||
|
||||
func actionGet(c *cli.Context) error {
|
||||
scanner := bufio.NewScanner(c.App.Reader)
|
||||
func actionGet(ctx context.Context, cmd *cli.Command) error {
|
||||
scanner := bufio.NewScanner(cmd.Root().Reader)
|
||||
|
||||
var input string
|
||||
for scanner.Scan() {
|
||||
input += scanner.Text()
|
||||
}
|
||||
|
||||
token, err := getToken(c.Context, c.String(flagAuthURL), c.String(flagGraphqlURL))
|
||||
token, err := getToken(ctx, cmd.String(flagAuthURL), cmd.String(flagGraphqlURL))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func actionGet(c *cli.Context) error {
|
||||
return fmt.Errorf("failed to marshal response: %w", err)
|
||||
}
|
||||
|
||||
if _, err = c.App.Writer.Write(b); err != nil {
|
||||
if _, err = cmd.Root().Writer.Write(b); err != nil {
|
||||
return fmt.Errorf("failed to write response: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package dockercredentials
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli/v2"
|
||||
"context"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CommandStore() *cli.Command {
|
||||
@@ -14,7 +16,7 @@ func CommandStore() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func actionStore(c *cli.Context) error {
|
||||
_, _ = c.App.Writer.Write([]byte("Please, use the nhost CLI to login\n"))
|
||||
func actionStore(_ context.Context, cmd *cli.Command) error {
|
||||
_, _ = cmd.Root().Writer.Write([]byte("Please, use the nhost CLI to login\n"))
|
||||
return nil
|
||||
}
|
||||
|
||||
92
cli/cmd/mcp/config/config.go
Normal file
92
cli/cmd/mcp/config/config.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/nhost/nhost/cli/mcp/config"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
flagConfirm = "confirm"
|
||||
)
|
||||
|
||||
func Command() *cli.Command {
|
||||
return &cli.Command{ //nolint:exhaustruct
|
||||
Name: "config",
|
||||
Usage: "Generate and save configuration file",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{ //nolint:exhaustruct
|
||||
Name: flagConfirm,
|
||||
Usage: "Skip confirmation prompt",
|
||||
Value: false,
|
||||
Sources: cli.EnvVars("CONFIRM"),
|
||||
},
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "dump",
|
||||
Usage: "Dump the configuration to stdout for verification",
|
||||
Flags: []cli.Flag{},
|
||||
Action: actionDump,
|
||||
},
|
||||
},
|
||||
Action: action,
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:forbidigo
|
||||
func action(_ context.Context, cmd *cli.Command) error {
|
||||
cfg, err := config.RunWizard()
|
||||
if err != nil {
|
||||
return cli.Exit(fmt.Sprintf("failed to run wizard: %s", err), 1)
|
||||
}
|
||||
|
||||
tomlData, err := toml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return cli.Exit(fmt.Sprintf("failed to marshal config: %s", err), 1)
|
||||
}
|
||||
|
||||
fmt.Println("Configuration Preview:")
|
||||
fmt.Println("---------------------")
|
||||
fmt.Println(string(tomlData))
|
||||
fmt.Println()
|
||||
|
||||
filePath := config.GetConfigPath(cmd)
|
||||
fmt.Printf("Save configuration to %s?\n", filePath)
|
||||
fmt.Print("Proceed? (y/N): ")
|
||||
|
||||
var confirm string
|
||||
if _, err := fmt.Scanln(&confirm); err != nil {
|
||||
return cli.Exit(fmt.Sprintf("failed to read input: %s", err), 1)
|
||||
}
|
||||
|
||||
if confirm != "y" && confirm != "Y" {
|
||||
fmt.Println("Operation cancelled.")
|
||||
return nil
|
||||
}
|
||||
|
||||
dir := filepath.Dir(filePath)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil { //nolint:mnd
|
||||
return fmt.Errorf("failed to create config directory: %w", err)
|
||||
}
|
||||
|
||||
data, err := toml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filePath, data, 0o600); err != nil { //nolint:mnd
|
||||
return fmt.Errorf("failed to write config file: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("\nConfiguration saved successfully!")
|
||||
fmt.Println("Note: Review the documentation for additional configuration options,")
|
||||
fmt.Println(" especially for fine-tuning LLM access permissions.")
|
||||
|
||||
return nil
|
||||
}
|
||||
35
cli/cmd/mcp/config/dump.go
Normal file
35
cli/cmd/mcp/config/dump.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/nhost/nhost/cli/mcp/config"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func actionDump(_ context.Context, cmd *cli.Command) error {
|
||||
configPath := config.GetConfigPath(cmd)
|
||||
if configPath == "" {
|
||||
return cli.Exit("config file path is required", 1)
|
||||
}
|
||||
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
fmt.Println("Please, run `mcp-nhost config` to configure the service.") //nolint:forbidigo
|
||||
return cli.Exit("failed to load config file "+err.Error(), 1)
|
||||
}
|
||||
|
||||
b, err := toml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return cli.Exit("failed to marshal config file "+err.Error(), 1)
|
||||
}
|
||||
|
||||
fmt.Println("Configuration Preview:") //nolint:forbidigo
|
||||
fmt.Println("---------------------") //nolint:forbidigo
|
||||
fmt.Println(string(b)) //nolint:forbidigo
|
||||
fmt.Println() //nolint:forbidigo
|
||||
|
||||
return nil
|
||||
}
|
||||
117
cli/cmd/mcp/gen/gen.go
Normal file
117
cli/cmd/mcp/gen/gen.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package gen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/nhost/nhost/cli/mcp/graphql"
|
||||
"github.com/nhost/nhost/cli/mcp/nhost/auth"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
flagNhostAuthURL = "nhost-auth-url"
|
||||
flagNhostGraphqlURL = "nhost-graphql-url"
|
||||
flagNhostPAT = "nhost-pat"
|
||||
flagWithMutations = "with-mutations"
|
||||
)
|
||||
|
||||
func Command() *cli.Command {
|
||||
return &cli.Command{ //nolint:exhaustruct
|
||||
Name: "gen",
|
||||
Usage: "Generate GraphQL schema for Nhost Cloud",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagNhostAuthURL,
|
||||
Usage: "Nhost auth URL",
|
||||
Hidden: true,
|
||||
Value: "https://otsispdzcwxyqzbfntmj.auth.eu-central-1.nhost.run/v1",
|
||||
Sources: cli.EnvVars("NHOST_AUTH_URL"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagNhostGraphqlURL,
|
||||
Usage: "Nhost GraphQL URL",
|
||||
Hidden: true,
|
||||
Value: "https://otsispdzcwxyqzbfntmj.graphql.eu-central-1.nhost.run/v1",
|
||||
Sources: cli.EnvVars("NHOST_GRAPHQL_URL"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagNhostPAT,
|
||||
Usage: "Personal Access Token",
|
||||
Required: true,
|
||||
Sources: cli.EnvVars("NHOST_PAT"),
|
||||
},
|
||||
&cli.BoolFlag{ //nolint:exhaustruct
|
||||
Name: flagWithMutations,
|
||||
Usage: "Include mutations in the generated schema",
|
||||
Value: false,
|
||||
Sources: cli.EnvVars("WITH_MUTATIONS"),
|
||||
},
|
||||
},
|
||||
Action: action,
|
||||
}
|
||||
}
|
||||
|
||||
func action(ctx context.Context, cmd *cli.Command) error {
|
||||
interceptor, err := auth.WithPAT(
|
||||
cmd.String(flagNhostAuthURL), cmd.String(flagNhostPAT),
|
||||
)
|
||||
if err != nil {
|
||||
return cli.Exit(err.Error(), 1)
|
||||
}
|
||||
|
||||
var introspection graphql.ResponseIntrospection
|
||||
if err := graphql.Query(
|
||||
ctx,
|
||||
cmd.String(flagNhostGraphqlURL),
|
||||
graphql.IntrospectionQuery,
|
||||
nil,
|
||||
&introspection,
|
||||
nil,
|
||||
nil,
|
||||
interceptor,
|
||||
); err != nil {
|
||||
return cli.Exit(err.Error(), 1)
|
||||
}
|
||||
|
||||
filter := graphql.Filter{
|
||||
AllowQueries: []graphql.Queries{
|
||||
{
|
||||
Name: "organizations",
|
||||
DisableNesting: true,
|
||||
},
|
||||
{
|
||||
Name: "organization",
|
||||
DisableNesting: true,
|
||||
},
|
||||
{
|
||||
Name: "app",
|
||||
DisableNesting: true,
|
||||
},
|
||||
{
|
||||
Name: "apps",
|
||||
DisableNesting: true,
|
||||
},
|
||||
{
|
||||
Name: "config",
|
||||
DisableNesting: false,
|
||||
},
|
||||
},
|
||||
AllowMutations: []graphql.Queries{},
|
||||
}
|
||||
|
||||
if cmd.Bool(flagWithMutations) {
|
||||
filter.AllowMutations = []graphql.Queries{
|
||||
{
|
||||
Name: "updateConfig",
|
||||
DisableNesting: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
schema := graphql.ParseSchema(introspection, filter)
|
||||
|
||||
fmt.Print(schema) //nolint:forbidigo
|
||||
|
||||
return nil
|
||||
}
|
||||
33
cli/cmd/mcp/mcp.go
Normal file
33
cli/cmd/mcp/mcp.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"github.com/nhost/nhost/cli/cmd/mcp/config"
|
||||
"github.com/nhost/nhost/cli/cmd/mcp/gen"
|
||||
"github.com/nhost/nhost/cli/cmd/mcp/start"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
flagConfigFile = "config-file"
|
||||
)
|
||||
|
||||
func Command() *cli.Command {
|
||||
return &cli.Command{ //nolint:exhaustruct
|
||||
Name: "mcp",
|
||||
Aliases: []string{},
|
||||
Usage: "Model Context Protocol (MCP) related commands",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagConfigFile,
|
||||
Usage: "Configuration file path. Defaults to $NHOST_DOT_NHOST_FOLDER/nhost-mcp.toml",
|
||||
Value: "",
|
||||
Sources: cli.EnvVars("NHOST_MCP_CONFIG_FILE"),
|
||||
},
|
||||
},
|
||||
Commands: []*cli.Command{
|
||||
config.Command(),
|
||||
start.Command(),
|
||||
gen.Command(),
|
||||
},
|
||||
}
|
||||
}
|
||||
401
cli/cmd/mcp/mcp_test.go
Normal file
401
cli/cmd/mcp/mcp_test.go
Normal file
@@ -0,0 +1,401 @@
|
||||
package mcp_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/mark3labs/mcp-go/client"
|
||||
"github.com/mark3labs/mcp-go/client/transport"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
nhostmcp "github.com/nhost/nhost/cli/cmd/mcp"
|
||||
"github.com/nhost/nhost/cli/cmd/mcp/start"
|
||||
"github.com/nhost/nhost/cli/cmd/user"
|
||||
"github.com/nhost/nhost/cli/mcp/resources"
|
||||
"github.com/nhost/nhost/cli/mcp/tools/cloud"
|
||||
"github.com/nhost/nhost/cli/mcp/tools/docs"
|
||||
"github.com/nhost/nhost/cli/mcp/tools/project"
|
||||
"github.com/nhost/nhost/cli/mcp/tools/schemas"
|
||||
)
|
||||
|
||||
func ptr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
func TestStart(t *testing.T) { //nolint:cyclop,maintidx,paralleltest
|
||||
loginCmd := user.CommandLogin()
|
||||
mcpCmd := nhostmcp.Command()
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
mcpCmd.Writer = buf
|
||||
|
||||
go func() {
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
|
||||
if err := loginCmd.Run(
|
||||
context.Background(),
|
||||
[]string{
|
||||
"main",
|
||||
"--pat=user-pat",
|
||||
},
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := mcpCmd.Run(
|
||||
context.Background(),
|
||||
[]string{
|
||||
"main",
|
||||
"start",
|
||||
"--bind=:9000",
|
||||
"--config-file=testdata/sample.toml",
|
||||
},
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
transportClient, err := transport.NewSSE("http://localhost:9000/sse")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create transport client: %v", err)
|
||||
}
|
||||
|
||||
mcpClient := client.NewClient(transportClient)
|
||||
|
||||
if err := mcpClient.Start(t.Context()); err != nil {
|
||||
t.Fatalf("failed to start mcp client: %v", err)
|
||||
}
|
||||
defer mcpClient.Close()
|
||||
|
||||
initRequest := mcp.InitializeRequest{} //nolint:exhaustruct
|
||||
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
|
||||
initRequest.Params.ClientInfo = mcp.Implementation{
|
||||
Name: "example-client",
|
||||
Version: "1.0.0",
|
||||
}
|
||||
|
||||
res, err := mcpClient.Initialize(
|
||||
context.Background(),
|
||||
initRequest,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to initialize mcp client: %v", err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(
|
||||
res,
|
||||
//nolint:tagalign
|
||||
&mcp.InitializeResult{
|
||||
ProtocolVersion: "2025-06-18",
|
||||
Capabilities: mcp.ServerCapabilities{
|
||||
Elicitation: nil,
|
||||
Experimental: nil,
|
||||
Logging: nil,
|
||||
Prompts: nil,
|
||||
Resources: &struct {
|
||||
Subscribe bool "json:\"subscribe,omitempty\""
|
||||
ListChanged bool "json:\"listChanged,omitempty\""
|
||||
}{
|
||||
Subscribe: false,
|
||||
ListChanged: false,
|
||||
},
|
||||
Sampling: nil,
|
||||
Tools: &struct {
|
||||
ListChanged bool "json:\"listChanged,omitempty\""
|
||||
}{
|
||||
ListChanged: true,
|
||||
},
|
||||
},
|
||||
ServerInfo: mcp.Implementation{
|
||||
Name: "mcp",
|
||||
Version: "",
|
||||
},
|
||||
Instructions: start.ServerInstructions + `
|
||||
|
||||
Configured projects:
|
||||
- local (local): Local development project running via the Nhost CLI
|
||||
- asdasdasdasdasd (eu-central-1): Staging project for my awesome app
|
||||
- qweqweqweqweqwe (us-east-1): Production project for my awesome app
|
||||
|
||||
The following resources are available:
|
||||
|
||||
- schema://nhost-cloud: Schema to interact with the Nhost Cloud. Projects are equivalent
|
||||
to apps in the schema. IDs are typically uuids.
|
||||
- schema://graphql-management: GraphQL's management schema for an Nhost project.
|
||||
This tool is useful to properly understand how manage hasura metadata, migrations,
|
||||
permissions, remote schemas, etc.
|
||||
- schema://nhost.toml: Cuelang schema for the nhost.toml configuration file. Run nhost
|
||||
config validate after making changes to your nhost.toml file to ensure it is valid.
|
||||
`,
|
||||
Result: mcp.Result{
|
||||
Meta: nil,
|
||||
},
|
||||
},
|
||||
); diff != "" {
|
||||
t.Errorf("ServerInfo mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
tools, err := mcpClient.ListTools(
|
||||
context.Background(),
|
||||
mcp.ListToolsRequest{}, //nolint:exhaustruct
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to list tools: %v", err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(
|
||||
tools,
|
||||
//nolint:exhaustruct,lll
|
||||
&mcp.ListToolsResult{
|
||||
Tools: []mcp.Tool{
|
||||
{
|
||||
Name: "cloud-graphql-query",
|
||||
Description: cloud.ToolGraphqlQueryInstructions,
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"query": map[string]any{
|
||||
"description": "graphql query to perform",
|
||||
"type": "string",
|
||||
},
|
||||
"variables": map[string]any{
|
||||
"description": "variables to use in the query",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Required: []string{"query"},
|
||||
},
|
||||
Annotations: mcp.ToolAnnotation{
|
||||
Title: "Perform GraphQL Query on Nhost Cloud Platform",
|
||||
ReadOnlyHint: ptr(false),
|
||||
DestructiveHint: ptr(true),
|
||||
IdempotentHint: ptr(false),
|
||||
OpenWorldHint: ptr(true),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "get-schema",
|
||||
Description: schemas.ToolGetGraphqlSchemaInstructions,
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"role": map[string]any{
|
||||
"description": string("role to use when executing queries. Keep in mind the schema depends on the role so if you retrieved the schema for a different role previously retrieve it for this role beforehand as it might differ"),
|
||||
"type": string("string"),
|
||||
},
|
||||
"subdomain": map[string]any{
|
||||
"description": string("Project to get the GraphQL schema for. Required when service is `project`"),
|
||||
"enum": []any{string("local"), string("asdasdasdasdasd"), string("qweqweqweqweqwe")},
|
||||
"type": string("string"),
|
||||
},
|
||||
"mutations": map[string]any{
|
||||
"description": string("list of mutations to fetch"),
|
||||
"type": string("array"),
|
||||
},
|
||||
"queries": map[string]any{
|
||||
"description": string("list of queries to fetch"),
|
||||
"type": string("array"),
|
||||
},
|
||||
"summary": map[string]any{
|
||||
"default": bool(true),
|
||||
"description": string("only return a summary of the schema"),
|
||||
"type": string("boolean"),
|
||||
},
|
||||
},
|
||||
Required: []string{"role", "subdomain"},
|
||||
},
|
||||
Annotations: mcp.ToolAnnotation{
|
||||
Title: "Get GraphQL/API schema for various services",
|
||||
ReadOnlyHint: ptr(true),
|
||||
DestructiveHint: ptr(false),
|
||||
IdempotentHint: ptr(true),
|
||||
OpenWorldHint: ptr(true),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "graphql-query",
|
||||
Description: project.ToolGraphqlQueryInstructions,
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"query": map[string]any{
|
||||
"description": "graphql query to perform",
|
||||
"type": "string",
|
||||
},
|
||||
"subdomain": map[string]any{
|
||||
"description": "Project to perform the GraphQL query against",
|
||||
"type": "string",
|
||||
"enum": []any{
|
||||
string("local"),
|
||||
string("asdasdasdasdasd"),
|
||||
string("qweqweqweqweqwe"),
|
||||
},
|
||||
},
|
||||
"role": map[string]any{
|
||||
"description": "role to use when executing queries. Keep in mind the schema depends on the role so if you retrieved the schema for a different role previously retrieve it for this role beforehand as it might differ",
|
||||
"type": "string",
|
||||
},
|
||||
"userId": map[string]any{
|
||||
"description": string("Overrides X-Hasura-User-Id in the GraphQL query/mutation. Credentials must allow it (i.e. admin secret must be in use)"),
|
||||
"type": string("string"),
|
||||
},
|
||||
"variables": map[string]any{
|
||||
"description": "variables to use in the query",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Required: []string{"query", "subdomain", "role"},
|
||||
},
|
||||
Annotations: mcp.ToolAnnotation{
|
||||
Title: "Perform GraphQL Query on Nhost Project running on Nhost Cloud",
|
||||
ReadOnlyHint: ptr(false),
|
||||
DestructiveHint: ptr(true),
|
||||
IdempotentHint: ptr(false),
|
||||
OpenWorldHint: ptr(true),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "manage-graphql",
|
||||
Description: project.ToolManageGraphqlInstructions,
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"body": map[string]any{
|
||||
"description": "The body for the HTTP request",
|
||||
"type": "string",
|
||||
},
|
||||
"path": map[string]any{
|
||||
"description": "The path for the HTTP request",
|
||||
"type": "string",
|
||||
},
|
||||
"subdomain": map[string]any{
|
||||
"description": "Project to perform the GraphQL management operation against",
|
||||
"type": "string",
|
||||
"enum": []any{
|
||||
string("local"),
|
||||
string("asdasdasdasdasd"),
|
||||
string("qweqweqweqweqwe"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"subdomain", "path", "body"},
|
||||
},
|
||||
Annotations: mcp.ToolAnnotation{
|
||||
Title: "Manage GraphQL's Metadata on an Nhost Development Project",
|
||||
ReadOnlyHint: ptr(false),
|
||||
DestructiveHint: ptr(true),
|
||||
IdempotentHint: ptr(true),
|
||||
OpenWorldHint: ptr(true),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "search",
|
||||
Description: docs.ToolSearchInstructions,
|
||||
InputSchema: mcp.ToolInputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]any{
|
||||
"query": map[string]any{
|
||||
"description": string("The search query"),
|
||||
"type": string("string"),
|
||||
},
|
||||
},
|
||||
Required: []string{"query"},
|
||||
},
|
||||
Annotations: mcp.ToolAnnotation{
|
||||
Title: "Search Nhost Docs",
|
||||
ReadOnlyHint: ptr(true),
|
||||
IdempotentHint: ptr(true),
|
||||
DestructiveHint: ptr(false),
|
||||
OpenWorldHint: ptr(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cmpopts.SortSlices(func(a, b mcp.Tool) bool {
|
||||
return a.Name < b.Name
|
||||
}),
|
||||
); diff != "" {
|
||||
t.Errorf("ListToolsResult mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
resourceList, err := mcpClient.ListResources(
|
||||
context.Background(),
|
||||
mcp.ListResourcesRequest{}, //nolint:exhaustruct
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to list resources: %v", err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(
|
||||
resourceList,
|
||||
//nolint:exhaustruct
|
||||
&mcp.ListResourcesResult{
|
||||
Resources: []mcp.Resource{
|
||||
{
|
||||
Annotated: mcp.Annotated{
|
||||
Annotations: &mcp.Annotations{
|
||||
Audience: []mcp.Role{"agent"},
|
||||
Priority: 9,
|
||||
},
|
||||
},
|
||||
URI: "schema://graphql-management",
|
||||
Name: "graphql-management",
|
||||
Description: resources.GraphqlManagementDescription,
|
||||
MIMEType: "text/plain",
|
||||
},
|
||||
|
||||
{
|
||||
Annotated: mcp.Annotated{
|
||||
Annotations: &mcp.Annotations{
|
||||
Audience: []mcp.Role{"agent"},
|
||||
Priority: 9,
|
||||
},
|
||||
},
|
||||
URI: "schema://nhost-cloud",
|
||||
Name: "nhost-cloud",
|
||||
Description: resources.CloudDescription,
|
||||
MIMEType: "text/plain",
|
||||
},
|
||||
{
|
||||
Annotated: mcp.Annotated{
|
||||
Annotations: &mcp.Annotations{
|
||||
Audience: []mcp.Role{"agent"},
|
||||
Priority: 9,
|
||||
},
|
||||
},
|
||||
URI: "schema://nhost.toml",
|
||||
Name: "nhost.toml",
|
||||
Description: resources.NhostTomlResourceDescription,
|
||||
MIMEType: "text/plain",
|
||||
},
|
||||
},
|
||||
},
|
||||
); diff != "" {
|
||||
t.Errorf("ListResourcesResult mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
if res.Capabilities.Prompts != nil {
|
||||
prompts, err := mcpClient.ListPrompts(
|
||||
context.Background(),
|
||||
mcp.ListPromptsRequest{}, //nolint:exhaustruct
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to list prompts: %v", err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(
|
||||
prompts,
|
||||
//nolint:exhaustruct
|
||||
&mcp.ListPromptsResult{
|
||||
Prompts: []mcp.Prompt{},
|
||||
},
|
||||
); diff != "" {
|
||||
t.Errorf("ListPromptsResult mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
208
cli/cmd/mcp/start/start.go
Normal file
208
cli/cmd/mcp/start/start.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package start
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/mcp/config"
|
||||
"github.com/nhost/nhost/cli/mcp/nhost/auth"
|
||||
"github.com/nhost/nhost/cli/mcp/resources"
|
||||
"github.com/nhost/nhost/cli/mcp/tools/cloud"
|
||||
"github.com/nhost/nhost/cli/mcp/tools/docs"
|
||||
"github.com/nhost/nhost/cli/mcp/tools/project"
|
||||
"github.com/nhost/nhost/cli/mcp/tools/schemas"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
flagNhostAuthURL = "nhost-auth-url"
|
||||
flagNhostGraphqlURL = "nhost-graphql-url"
|
||||
flagBind = "bind"
|
||||
)
|
||||
|
||||
const (
|
||||
// this seems to be largely ignored by clients, or at least by cursor.
|
||||
// we also need to look into roots and resources as those might be helpful.
|
||||
ServerInstructions = `
|
||||
This is an MCP server to interact with the Nhost Cloud and with Nhost projects.
|
||||
|
||||
Important notes to anyone using this MCP server. Do not use this MCP server without
|
||||
following these instructions:
|
||||
|
||||
1. Make sure you are clear on which environment the user wants to operate against.
|
||||
2. Before attempting to call any tool, always make sure you list resources, roots, and
|
||||
resource templates to understand what is available.
|
||||
3. Apps and projects are the same and while users may talk about projects in Nhost's GraphQL
|
||||
api those are referred as apps.
|
||||
4. If you have an error querying the GraphQL API, please check the schema again. The schema may
|
||||
have changed and the query you are using may be invalid.
|
||||
5. Always follow the instructions provided by each tool. If you need to deviate from these
|
||||
instructions, please, confirm with the user before doing so.
|
||||
`
|
||||
)
|
||||
|
||||
func Command() *cli.Command {
|
||||
return &cli.Command{ //nolint:exhaustruct
|
||||
Name: "start",
|
||||
Usage: "Starts the MCP server",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagNhostAuthURL,
|
||||
Usage: "Nhost auth URL",
|
||||
Hidden: true,
|
||||
Value: "https://otsispdzcwxyqzbfntmj.auth.eu-central-1.nhost.run/v1",
|
||||
Category: "Cloud Platform",
|
||||
Sources: cli.EnvVars("NHOST_AUTH_URL"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagNhostGraphqlURL,
|
||||
Usage: "Nhost GraphQL URL",
|
||||
Hidden: true,
|
||||
Value: "https://otsispdzcwxyqzbfntmj.graphql.eu-central-1.nhost.run/v1",
|
||||
Category: "Cloud Platform",
|
||||
Sources: cli.EnvVars("NHOST_GRAPHQL_URL"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagBind,
|
||||
Usage: "Bind address in the form $host:$port. If omitted use stdio",
|
||||
Required: false,
|
||||
Category: "General",
|
||||
Sources: cli.EnvVars("BIND"),
|
||||
},
|
||||
},
|
||||
Action: action,
|
||||
}
|
||||
}
|
||||
|
||||
func action(ctx context.Context, cmd *cli.Command) error {
|
||||
cfg, err := getConfig(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ServerInstructions := ServerInstructions
|
||||
ServerInstructions += "\n\n"
|
||||
ServerInstructions += cfg.Projects.Instructions()
|
||||
ServerInstructions += "\n"
|
||||
ServerInstructions += resources.Instructions()
|
||||
|
||||
mcpServer := server.NewMCPServer(
|
||||
cmd.Root().Name,
|
||||
cmd.Root().Version,
|
||||
server.WithInstructions(ServerInstructions),
|
||||
)
|
||||
|
||||
if err := resources.Register(cfg, mcpServer); err != nil {
|
||||
return cli.Exit(fmt.Sprintf("failed to register resources: %s", err), 1)
|
||||
}
|
||||
|
||||
if cfg.Cloud != nil {
|
||||
if err := registerCloud(
|
||||
cmd,
|
||||
mcpServer,
|
||||
cfg,
|
||||
cmd.String(flagNhostAuthURL),
|
||||
cmd.String(flagNhostGraphqlURL),
|
||||
); err != nil {
|
||||
return cli.Exit(fmt.Sprintf("failed to register cloud tools: %s", err), 1)
|
||||
}
|
||||
}
|
||||
|
||||
if len(cfg.Projects) > 0 {
|
||||
if err := registerProjectTool(mcpServer, cfg); err != nil {
|
||||
return cli.Exit(fmt.Sprintf("failed to register project tools: %s", err), 1)
|
||||
}
|
||||
}
|
||||
|
||||
resources := schemas.NewTool(cfg)
|
||||
resources.Register(mcpServer)
|
||||
|
||||
d, err := docs.NewTool(ctx)
|
||||
if err != nil {
|
||||
return cli.Exit(fmt.Sprintf("failed to initialize docs tools: %s", err), 1)
|
||||
}
|
||||
|
||||
d.Register(mcpServer)
|
||||
|
||||
return start(mcpServer, cmd.String(flagBind))
|
||||
}
|
||||
|
||||
func getConfig(cmd *cli.Command) (*config.Config, error) {
|
||||
configPath := config.GetConfigPath(cmd)
|
||||
if configPath == "" {
|
||||
return nil, cli.Exit("config file path is required", 1)
|
||||
}
|
||||
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
fmt.Println("Please, run `mcp-nhost config` to configure the service.") //nolint:forbidigo
|
||||
return nil, cli.Exit("failed to load config file "+err.Error(), 1)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func registerCloud(
|
||||
cmd *cli.Command,
|
||||
mcpServer *server.MCPServer,
|
||||
cfg *config.Config,
|
||||
authURL string,
|
||||
graphqlURL string,
|
||||
) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
creds, err := ce.Credentials()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load credentials: %w", err)
|
||||
}
|
||||
|
||||
interceptor, err := auth.WithPAT(
|
||||
authURL,
|
||||
creds.PersonalAccessToken,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create PAT interceptor: %w", err)
|
||||
}
|
||||
|
||||
cloudTool := cloud.NewTool(
|
||||
graphqlURL, cfg.Cloud.EnableMutations, interceptor,
|
||||
)
|
||||
|
||||
if err := cloudTool.Register(mcpServer); err != nil {
|
||||
return fmt.Errorf("failed to register tools: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerProjectTool(
|
||||
mcpServer *server.MCPServer,
|
||||
cfg *config.Config,
|
||||
) error {
|
||||
projectTool := project.NewTool(cfg)
|
||||
if err := projectTool.Register(mcpServer); err != nil {
|
||||
return fmt.Errorf("failed to register tool: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func start(
|
||||
mcpServer *server.MCPServer,
|
||||
bind string,
|
||||
) error {
|
||||
if bind != "" {
|
||||
sseServer := server.NewSSEServer(mcpServer, server.WithBaseURL(bind))
|
||||
if err := sseServer.Start(bind); err != nil {
|
||||
return cli.Exit(fmt.Sprintf("failed to serve tcp: %v", err), 1)
|
||||
}
|
||||
} else {
|
||||
if err := server.ServeStdio(mcpServer); err != nil {
|
||||
return cli.Exit(fmt.Sprintf("failed to serve stdio: %v", err), 1)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
29
cli/cmd/mcp/testdata/sample.toml
vendored
Normal file
29
cli/cmd/mcp/testdata/sample.toml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
[cloud]
|
||||
enable_mutations = true
|
||||
|
||||
[[projects]]
|
||||
subdomain = 'local'
|
||||
region = 'local'
|
||||
description = 'Local development project running via the Nhost CLI'
|
||||
admin_secret = 'nhost-admin-secret'
|
||||
manage_metadata = true
|
||||
allow_queries = ['*']
|
||||
allow_mutations = ['*']
|
||||
|
||||
[[projects]]
|
||||
subdomain = 'asdasdasdasdasd'
|
||||
region = 'eu-central-1'
|
||||
description = 'Staging project for my awesome app'
|
||||
manage_metadata = false
|
||||
admin_secret = 'your-admin-secret-1'
|
||||
allow_queries = ['*']
|
||||
allow_mutations = ['*']
|
||||
|
||||
[[projects]]
|
||||
subdomain = 'qweqweqweqweqwe'
|
||||
region = 'us-east-1'
|
||||
description = 'Production project for my awesome app'
|
||||
manage_metadata = false
|
||||
pat = 'pat-for-qweqweqweqweqwe'
|
||||
allow_queries = ['getComments']
|
||||
allow_mutations = ['insertComment', 'updateComment', 'deleteComment']
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/hashicorp/go-getter"
|
||||
"github.com/hashicorp/go-getter/v2"
|
||||
"github.com/nhost/be/services/mimir/model"
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/cmd/config"
|
||||
"github.com/nhost/nhost/cli/dockercompose"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
@@ -75,14 +75,14 @@ func CommandInit() *cli.Command {
|
||||
Name: flagRemote,
|
||||
Usage: "Initialize pulling configuration, migrations and metadata from the linked project",
|
||||
Value: false,
|
||||
EnvVars: []string{"NHOST_REMOTE"},
|
||||
Sources: cli.EnvVars("NHOST_REMOTE"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func commandInit(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandInit(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
if clienv.PathExists(ce.Path.NhostFolder()) {
|
||||
return errors.New("nhost folder already exists") //nolint:err113
|
||||
@@ -98,12 +98,12 @@ func commandInit(cCtx *cli.Context) error {
|
||||
return fmt.Errorf("failed to initialize configuration: %w", err)
|
||||
}
|
||||
|
||||
if cCtx.Bool(flagRemote) {
|
||||
if err := InitRemote(cCtx.Context, ce); err != nil {
|
||||
if cmd.Bool(flagRemote) {
|
||||
if err := InitRemote(ctx, ce); err != nil {
|
||||
return fmt.Errorf("failed to initialize remote project: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := initInit(cCtx.Context, ce.Path); err != nil {
|
||||
if err := initInit(ctx, ce.Path); err != nil {
|
||||
return fmt.Errorf("failed to initialize project: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -129,17 +129,12 @@ func initInit(
|
||||
return err
|
||||
}
|
||||
|
||||
getclient := &getter.Client{ //nolint:exhaustruct
|
||||
Ctx: ctx,
|
||||
Src: "github.com/nhost/hasura-auth/email-templates",
|
||||
Dst: "nhost/emails",
|
||||
Mode: getter.ClientModeAny,
|
||||
Detectors: []getter.Detector{
|
||||
&getter.GitHubDetector{},
|
||||
},
|
||||
}
|
||||
|
||||
if err := getclient.Get(); err != nil {
|
||||
getclient := &getter.Client{} //nolint:exhaustruct
|
||||
if _, err := getclient.Get(ctx, &getter.Request{ //nolint:exhaustruct
|
||||
Src: "git::https://github.com/nhost/hasura-auth.git//email-templates",
|
||||
Dst: "nhost/emails",
|
||||
DisableSymlinks: true,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to download email templates: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package project
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CommandLink() *cli.Command {
|
||||
@@ -18,14 +19,14 @@ func CommandLink() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func commandLink(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandLink(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
if err := os.MkdirAll(ce.Path.DotNhostFolder(), 0o755); err != nil { //nolint:mnd
|
||||
return fmt.Errorf("failed to create .nhost folder: %w", err)
|
||||
}
|
||||
|
||||
_, err := ce.Link(cCtx.Context)
|
||||
_, err := ce.Link(ctx)
|
||||
|
||||
return err //nolint:wrapcheck
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CommandList() *cli.Command {
|
||||
@@ -18,9 +18,9 @@ func CommandList() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func commandList(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
return List(cCtx.Context, ce)
|
||||
func commandList(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
return List(ctx, ce)
|
||||
}
|
||||
|
||||
func List(ctx context.Context, ce *clienv.CliEnv) error {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package run
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/nhost/be/services/mimir/model"
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/nhostclient/graphql"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CommandConfigDeploy() *cli.Command {
|
||||
@@ -23,13 +24,13 @@ func CommandConfigDeploy() *cli.Command {
|
||||
Usage: "Service configuration file",
|
||||
Value: "nhost-run-service.toml",
|
||||
Required: true,
|
||||
EnvVars: []string{"NHOST_RUN_SERVICE_CONFIG"},
|
||||
Sources: cli.EnvVars("NHOST_RUN_SERVICE_CONFIG"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagServiceID,
|
||||
Usage: "Service ID to update. Applies overlay of the same name",
|
||||
Required: true,
|
||||
EnvVars: []string{"NHOST_RUN_SERVICE_ID"},
|
||||
Sources: cli.EnvVars("NHOST_RUN_SERVICE_ID"),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -49,23 +50,23 @@ func transform[T, V any](t *T) (*V, error) {
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
func commandConfigDeploy(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandConfigDeploy(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
cl, err := ce.GetNhostClient(cCtx.Context)
|
||||
cl, err := ce.GetNhostClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get nhost client: %w", err)
|
||||
}
|
||||
|
||||
secrets, appID, err := getRemoteSecrets(cCtx.Context, cl, cCtx.String(flagServiceID))
|
||||
secrets, appID, err := getRemoteSecrets(ctx, cl, cmd.String(flagServiceID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := Validate(
|
||||
ce,
|
||||
cCtx.String(flagConfig),
|
||||
cCtx.String(flagServiceID),
|
||||
cmd.String(flagConfig),
|
||||
cmd.String(flagServiceID),
|
||||
secrets,
|
||||
true,
|
||||
)
|
||||
@@ -81,9 +82,9 @@ func commandConfigDeploy(cCtx *cli.Context) error {
|
||||
}
|
||||
|
||||
if _, err := cl.ReplaceRunServiceConfig(
|
||||
cCtx.Context,
|
||||
ctx,
|
||||
appID,
|
||||
cCtx.String(flagServiceID),
|
||||
cmd.String(flagServiceID),
|
||||
*replaceConfig,
|
||||
); err != nil {
|
||||
return fmt.Errorf("failed to replace service config: %w", err)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package run
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -8,7 +9,7 @@ import (
|
||||
"github.com/nhost/be/services/mimir/model"
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/cmd/config"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const flagEditor = "editor"
|
||||
@@ -25,31 +26,31 @@ func CommandConfigEdit() *cli.Command {
|
||||
Usage: "Service configuration file",
|
||||
Value: "nhost-run-service.toml",
|
||||
Required: true,
|
||||
EnvVars: []string{"NHOST_RUN_SERVICE_CONFIG"},
|
||||
Sources: cli.EnvVars("NHOST_RUN_SERVICE_CONFIG"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagEditor,
|
||||
Usage: "Editor to use",
|
||||
Value: "vim",
|
||||
EnvVars: []string{"EDITOR"},
|
||||
Sources: cli.EnvVars("EDITOR"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagOverlayName,
|
||||
Usage: "If specified, apply this overlay",
|
||||
EnvVars: []string{"NHOST_RUN_SERVICE_ID", "NHOST_SERVICE_OVERLAY_NAME"},
|
||||
Sources: cli.EnvVars("NHOST_RUN_SERVICE_ID", "NHOST_SERVICE_OVERLAY_NAME"),
|
||||
},
|
||||
},
|
||||
Action: commandConfigEdit,
|
||||
}
|
||||
}
|
||||
|
||||
func commandConfigEdit(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandConfigEdit(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
overlayName := cCtx.String(flagOverlayName)
|
||||
overlayName := cmd.String(flagOverlayName)
|
||||
if overlayName == "" {
|
||||
if err := config.EditFile(
|
||||
cCtx.Context, cCtx.String(flagEditor), cCtx.String(flagConfig),
|
||||
ctx, cmd.String(flagEditor), cmd.String(flagConfig),
|
||||
); err != nil {
|
||||
return fmt.Errorf("failed to edit config: %w", err)
|
||||
}
|
||||
@@ -58,7 +59,7 @@ func commandConfigEdit(cCtx *cli.Context) error {
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(ce.Path.RunServiceOverlaysFolder(
|
||||
cCtx.String(flagConfig),
|
||||
cmd.String(flagConfig),
|
||||
), 0o755); err != nil { //nolint:mnd
|
||||
return fmt.Errorf("failed to create json patches directory: %w", err)
|
||||
}
|
||||
@@ -72,21 +73,21 @@ func commandConfigEdit(cCtx *cli.Context) error {
|
||||
tmpfileName := filepath.Join(tmpdir, "nhost.toml")
|
||||
|
||||
if err := config.CopyConfig[model.ConfigRunServiceConfig](
|
||||
cCtx.String(flagConfig),
|
||||
cmd.String(flagConfig),
|
||||
tmpfileName,
|
||||
ce.Path.RunServiceOverlay(cCtx.String(flagConfig), overlayName),
|
||||
ce.Path.RunServiceOverlay(cmd.String(flagConfig), overlayName),
|
||||
); err != nil {
|
||||
return fmt.Errorf("failed to copy config: %w", err)
|
||||
}
|
||||
|
||||
if err := config.EditFile(cCtx.Context, cCtx.String(flagEditor), tmpfileName); err != nil {
|
||||
if err := config.EditFile(ctx, cmd.String(flagEditor), tmpfileName); err != nil {
|
||||
return fmt.Errorf("failed to edit config: %w", err)
|
||||
}
|
||||
|
||||
if err := config.GenerateJSONPatch(
|
||||
cCtx.String(flagConfig),
|
||||
cmd.String(flagConfig),
|
||||
tmpfileName,
|
||||
ce.Path.RunServiceOverlay(cCtx.String(flagConfig), overlayName),
|
||||
ce.Path.RunServiceOverlay(cmd.String(flagConfig), overlayName),
|
||||
); err != nil {
|
||||
return fmt.Errorf("failed to generate json patch: %w", err)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package run
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/nhost/be/services/mimir/model"
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const flagImage = "image"
|
||||
@@ -22,29 +23,29 @@ func CommandConfigEditImage() *cli.Command {
|
||||
Aliases: []string{},
|
||||
Usage: "Service configuration file",
|
||||
Value: "nhost-run-service.toml",
|
||||
EnvVars: []string{"NHOST_RUN_SERVICE_CONFIG"},
|
||||
Sources: cli.EnvVars("NHOST_RUN_SERVICE_CONFIG"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagImage,
|
||||
Aliases: []string{},
|
||||
Usage: "Image to use",
|
||||
Required: true,
|
||||
EnvVars: []string{"NHOST_RUN_SERVICE_IMAGE"},
|
||||
Sources: cli.EnvVars("NHOST_RUN_SERVICE_IMAGE"),
|
||||
},
|
||||
},
|
||||
Action: commandConfigEditImage,
|
||||
}
|
||||
}
|
||||
|
||||
func commandConfigEditImage(cCtx *cli.Context) error {
|
||||
func commandConfigEditImage(_ context.Context, cmd *cli.Command) error {
|
||||
var cfg model.ConfigRunServiceConfig
|
||||
if err := clienv.UnmarshalFile(cCtx.String(flagConfig), &cfg, toml.Unmarshal); err != nil {
|
||||
if err := clienv.UnmarshalFile(cmd.String(flagConfig), &cfg, toml.Unmarshal); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
cfg.Image.Image = cCtx.String(flagImage)
|
||||
cfg.Image.Image = cmd.String(flagImage)
|
||||
|
||||
if err := clienv.MarshalFile(cfg, cCtx.String(flagConfig), toml.Marshal); err != nil {
|
||||
if err := clienv.MarshalFile(cfg, cmd.String(flagConfig), toml.Marshal); err != nil {
|
||||
return fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package run
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/nhost/be/services/mimir/model"
|
||||
"github.com/nhost/be/services/mimir/schema"
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func ptr[T any](v T) *T {
|
||||
@@ -24,8 +25,8 @@ func CommandConfigExample() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func commandConfigExample(cCtx *cli.Context) error { //nolint:funlen
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandConfigExample(_ context.Context, cmd *cli.Command) error { //nolint:funlen
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
//nolint:mnd
|
||||
cfg := &model.ConfigRunServiceConfig{
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package run
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/nhost/be/services/mimir/model"
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const flagServiceID = "service-id"
|
||||
@@ -23,36 +24,36 @@ func CommandConfigPull() *cli.Command {
|
||||
Aliases: []string{},
|
||||
Usage: "Service configuration file",
|
||||
Value: "nhost-run-service.toml",
|
||||
EnvVars: []string{"NHOST_RUN_SERVICE_CONFIG"},
|
||||
Sources: cli.EnvVars("NHOST_RUN_SERVICE_CONFIG"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagServiceID,
|
||||
Usage: "Service ID to update",
|
||||
Required: true,
|
||||
EnvVars: []string{"NHOST_RUN_SERVICE_ID"},
|
||||
Sources: cli.EnvVars("NHOST_RUN_SERVICE_ID"),
|
||||
},
|
||||
},
|
||||
Action: commandConfigPull,
|
||||
}
|
||||
}
|
||||
|
||||
func commandConfigPull(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandConfigPull(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
cl, err := ce.GetNhostClient(cCtx.Context)
|
||||
cl, err := ce.GetNhostClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get nhost client: %w", err)
|
||||
}
|
||||
|
||||
appID, err := getAppIDFromServiceID(cCtx.Context, cl, cCtx.String(flagServiceID))
|
||||
appID, err := getAppIDFromServiceID(ctx, cl, cmd.String(flagServiceID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := cl.GetRunServiceConfigRawJSON(
|
||||
cCtx.Context,
|
||||
ctx,
|
||||
appID,
|
||||
cCtx.String(flagServiceID),
|
||||
cmd.String(flagServiceID),
|
||||
false,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -64,7 +65,7 @@ func commandConfigPull(cCtx *cli.Context) error {
|
||||
return fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
if err := clienv.MarshalFile(v, cCtx.String(flagConfig), toml.Marshal); err != nil {
|
||||
if err := clienv.MarshalFile(v, cmd.String(flagConfig), toml.Marshal); err != nil {
|
||||
return fmt.Errorf("failed to save config to file: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package run
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/nhost/be/services/mimir/model"
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/project/env"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CommandConfigShow() *cli.Command {
|
||||
@@ -23,19 +24,19 @@ func CommandConfigShow() *cli.Command {
|
||||
Aliases: []string{},
|
||||
Usage: "Service configuration file",
|
||||
Value: "nhost-run-service.toml",
|
||||
EnvVars: []string{"NHOST_RUN_SERVICE_CONFIG"},
|
||||
Sources: cli.EnvVars("NHOST_RUN_SERVICE_CONFIG"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagOverlayName,
|
||||
Usage: "If specified, apply this overlay",
|
||||
EnvVars: []string{"NHOST_RUN_SERVICE_ID", "NHOST_SERVICE_OVERLAY_NAME"},
|
||||
Sources: cli.EnvVars("NHOST_RUN_SERVICE_ID", "NHOST_SERVICE_OVERLAY_NAME"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func commandConfigShow(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandConfigShow(_ context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
var secrets model.Secrets
|
||||
if err := clienv.UnmarshalFile(ce.Path.Secrets(), &secrets, env.Unmarshal); err != nil {
|
||||
@@ -47,8 +48,8 @@ func commandConfigShow(cCtx *cli.Context) error {
|
||||
|
||||
cfg, err := Validate(
|
||||
ce,
|
||||
cCtx.String(flagConfig),
|
||||
cCtx.String(flagOverlayName),
|
||||
cmd.String(flagConfig),
|
||||
cmd.String(flagOverlayName),
|
||||
secrets,
|
||||
false,
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/nhost/nhost/cli/nhostclient/graphql"
|
||||
"github.com/nhost/nhost/cli/project/env"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -35,17 +35,17 @@ func CommandConfigValidate() *cli.Command {
|
||||
Aliases: []string{},
|
||||
Usage: "Service configuration file",
|
||||
Value: "nhost-run-service.toml",
|
||||
EnvVars: []string{"NHOST_RUN_SERVICE_CONFIG"},
|
||||
Sources: cli.EnvVars("NHOST_RUN_SERVICE_CONFIG"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagOverlayName,
|
||||
Usage: "If specified, apply this overlay",
|
||||
EnvVars: []string{"NHOST_SERVICE_OVERLAY_NAME"},
|
||||
Sources: cli.EnvVars("NHOST_SERVICE_OVERLAY_NAME"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagServiceID,
|
||||
Usage: "If specified, apply this overlay and remote secrets for this service",
|
||||
EnvVars: []string{"NHOST_RUN_SERVICE_ID"},
|
||||
Sources: cli.EnvVars("NHOST_RUN_SERVICE_ID"),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -157,35 +157,35 @@ func getRemoteSecrets(
|
||||
return respToSecrets(secretsResp.GetAppSecrets()), appID, nil
|
||||
}
|
||||
|
||||
func commandConfigValidate(cCtx *cli.Context) error {
|
||||
func commandConfigValidate(ctx context.Context, cmd *cli.Command) error {
|
||||
var (
|
||||
overlayName string
|
||||
serviceID string
|
||||
)
|
||||
|
||||
switch {
|
||||
case cCtx.String(flagServiceID) != "" && cCtx.String(flagOverlayName) != "":
|
||||
case cmd.String(flagServiceID) != "" && cmd.String(flagOverlayName) != "":
|
||||
return errors.New("cannot specify both service id and overlay name") //nolint:err113
|
||||
case cCtx.String(flagServiceID) != "":
|
||||
serviceID = cCtx.String(flagServiceID)
|
||||
case cmd.String(flagServiceID) != "":
|
||||
serviceID = cmd.String(flagServiceID)
|
||||
overlayName = serviceID
|
||||
case cCtx.String(flagOverlayName) != "":
|
||||
overlayName = cCtx.String(flagOverlayName)
|
||||
case cmd.String(flagOverlayName) != "":
|
||||
overlayName = cmd.String(flagOverlayName)
|
||||
}
|
||||
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
var secrets model.Secrets
|
||||
|
||||
ce.Infoln("Getting secrets...")
|
||||
|
||||
if serviceID != "" {
|
||||
cl, err := ce.GetNhostClient(cCtx.Context)
|
||||
cl, err := ce.GetNhostClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get nhost client: %w", err)
|
||||
}
|
||||
|
||||
secrets, _, err = getRemoteSecrets(cCtx.Context, cl, serviceID)
|
||||
secrets, _, err = getRemoteSecrets(ctx, cl, serviceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -202,7 +202,7 @@ func commandConfigValidate(cCtx *cli.Context) error {
|
||||
|
||||
if _, err := Validate(
|
||||
ce,
|
||||
cCtx.String(flagConfig),
|
||||
cmd.String(flagConfig),
|
||||
overlayName,
|
||||
secrets,
|
||||
false,
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package run
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/nhost/be/services/mimir/model"
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/project/env"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -28,17 +29,17 @@ func CommandEnv() *cli.Command {
|
||||
Aliases: []string{},
|
||||
Usage: "Service configuration file",
|
||||
Value: "nhost-run-service.toml",
|
||||
EnvVars: []string{"NHOST_RUN_SERVICE_CONFIG"},
|
||||
Sources: cli.EnvVars("NHOST_RUN_SERVICE_CONFIG"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagOverlayName,
|
||||
Usage: "If specified, apply this overlay",
|
||||
EnvVars: []string{"NHOST_RUN_SERVICE_ID", "NHOST_SERVICE_OVERLAY_NAME"},
|
||||
Sources: cli.EnvVars("NHOST_RUN_SERVICE_ID", "NHOST_SERVICE_OVERLAY_NAME"),
|
||||
},
|
||||
&cli.BoolFlag{ //nolint:exhaustruct
|
||||
Name: flagDevPrependExport,
|
||||
Usage: "Prepend 'export' to each line",
|
||||
EnvVars: []string{"NHOST_RuN_SERVICE_ENV_PREPEND_EXPORT"},
|
||||
Sources: cli.EnvVars("NHOST_RuN_SERVICE_ENV_PREPEND_EXPORT"),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -49,8 +50,8 @@ func escape(s string) string {
|
||||
return re.ReplaceAllString(s, "\\$0")
|
||||
}
|
||||
|
||||
func commandConfigDev(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandConfigDev(_ context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
var secrets model.Secrets
|
||||
if err := clienv.UnmarshalFile(ce.Path.Secrets(), &secrets, env.Unmarshal); err != nil {
|
||||
@@ -62,8 +63,8 @@ func commandConfigDev(cCtx *cli.Context) error {
|
||||
|
||||
cfg, err := Validate(
|
||||
ce,
|
||||
cCtx.String(flagConfig),
|
||||
cCtx.String(flagOverlayName),
|
||||
cmd.String(flagConfig),
|
||||
cmd.String(flagOverlayName),
|
||||
secrets,
|
||||
false,
|
||||
)
|
||||
@@ -73,7 +74,7 @@ func commandConfigDev(cCtx *cli.Context) error {
|
||||
|
||||
for _, v := range cfg.GetEnvironment() {
|
||||
value := escape(v.Value)
|
||||
if cCtx.Bool(flagDevPrependExport) {
|
||||
if cmd.Bool(flagDevPrependExport) {
|
||||
ce.Println("export %s=\"%s\"", v.Name, value)
|
||||
} else {
|
||||
ce.Println("%s=\"%s\"", v.Name, value)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package run
|
||||
|
||||
import "github.com/urfave/cli/v2"
|
||||
import "github.com/urfave/cli/v3"
|
||||
|
||||
func Command() *cli.Command {
|
||||
return &cli.Command{ //nolint:exhaustruct
|
||||
Name: "run",
|
||||
Aliases: []string{},
|
||||
Usage: "Perform operations on Nhost Run",
|
||||
Subcommands: []*cli.Command{
|
||||
Commands: []*cli.Command{
|
||||
CommandConfigShow(),
|
||||
CommandConfigDeploy(),
|
||||
CommandConfigEdit(),
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package secrets //nolint:dupl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CommandCreate() *cli.Command {
|
||||
@@ -19,28 +20,28 @@ func CommandCreate() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func commandCreate(cCtx *cli.Context) error {
|
||||
if cCtx.NArg() != 2 { //nolint:mnd
|
||||
func commandCreate(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.NArg() != 2 { //nolint:mnd
|
||||
return errors.New("invalid number of arguments") //nolint:err113
|
||||
}
|
||||
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
|
||||
proj, err := ce.GetAppInfo(ctx, cmd.String(flagSubdomain))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get app info: %w", err)
|
||||
}
|
||||
|
||||
cl, err := ce.GetNhostClient(cCtx.Context)
|
||||
cl, err := ce.GetNhostClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get nhost client: %w", err)
|
||||
}
|
||||
|
||||
if _, err := cl.CreateSecret(
|
||||
cCtx.Context,
|
||||
ctx,
|
||||
proj.ID,
|
||||
cCtx.Args().Get(0),
|
||||
cCtx.Args().Get(1),
|
||||
cmd.Args().Get(0),
|
||||
cmd.Args().Get(1),
|
||||
); err != nil {
|
||||
return fmt.Errorf("failed to create secret: %w", err)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CommandDelete() *cli.Command {
|
||||
@@ -19,27 +20,27 @@ func CommandDelete() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func commandDelete(cCtx *cli.Context) error {
|
||||
if cCtx.NArg() != 1 {
|
||||
func commandDelete(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.NArg() != 1 {
|
||||
return errors.New("invalid number of arguments") //nolint:err113
|
||||
}
|
||||
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
|
||||
proj, err := ce.GetAppInfo(ctx, cmd.String(flagSubdomain))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get app info: %w", err)
|
||||
}
|
||||
|
||||
cl, err := ce.GetNhostClient(cCtx.Context)
|
||||
cl, err := ce.GetNhostClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get nhost client: %w", err)
|
||||
}
|
||||
|
||||
if _, err := cl.DeleteSecret(
|
||||
cCtx.Context,
|
||||
ctx,
|
||||
proj.ID,
|
||||
cCtx.Args().Get(0),
|
||||
cmd.Args().Get(0),
|
||||
); err != nil {
|
||||
return fmt.Errorf("failed to delete secret: %w", err)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package secrets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CommandList() *cli.Command {
|
||||
@@ -17,21 +18,21 @@ func CommandList() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func commandList(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandList(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
|
||||
proj, err := ce.GetAppInfo(ctx, cmd.String(flagSubdomain))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get app info: %w", err)
|
||||
}
|
||||
|
||||
cl, err := ce.GetNhostClient(cCtx.Context)
|
||||
cl, err := ce.GetNhostClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get nhost client: %w", err)
|
||||
}
|
||||
|
||||
secrets, err := cl.GetSecrets(
|
||||
cCtx.Context,
|
||||
ctx,
|
||||
proj.ID,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package secrets
|
||||
|
||||
import "github.com/urfave/cli/v2"
|
||||
import "github.com/urfave/cli/v3"
|
||||
|
||||
const flagSubdomain = "subdomain"
|
||||
|
||||
@@ -9,7 +9,7 @@ func commonFlags() []cli.Flag {
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagSubdomain,
|
||||
Usage: "Project's subdomain to operate on, defaults to linked project",
|
||||
EnvVars: []string{"NHOST_SUBDOMAIN"},
|
||||
Sources: cli.EnvVars("NHOST_SUBDOMAIN"),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ func Command() *cli.Command {
|
||||
Name: "secrets",
|
||||
Aliases: []string{},
|
||||
Usage: "Manage secrets",
|
||||
Subcommands: []*cli.Command{
|
||||
Commands: []*cli.Command{
|
||||
CommandCreate(),
|
||||
CommandDelete(),
|
||||
CommandList(),
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package secrets //nolint:dupl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CommandUpdate() *cli.Command {
|
||||
@@ -19,28 +20,28 @@ func CommandUpdate() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func commandUpdate(cCtx *cli.Context) error {
|
||||
if cCtx.NArg() != 2 { //nolint:mnd
|
||||
func commandUpdate(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.NArg() != 2 { //nolint:mnd
|
||||
return errors.New("invalid number of arguments") //nolint:err113
|
||||
}
|
||||
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
proj, err := ce.GetAppInfo(cCtx.Context, cCtx.String(flagSubdomain))
|
||||
proj, err := ce.GetAppInfo(ctx, cmd.String(flagSubdomain))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get app info: %w", err)
|
||||
}
|
||||
|
||||
cl, err := ce.GetNhostClient(cCtx.Context)
|
||||
cl, err := ce.GetNhostClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get nhost client: %w", err)
|
||||
}
|
||||
|
||||
if _, err := cl.UpdateSecret(
|
||||
cCtx.Context,
|
||||
ctx,
|
||||
proj.ID,
|
||||
cCtx.Args().Get(0),
|
||||
cCtx.Args().Get(1),
|
||||
cmd.Args().Get(0),
|
||||
cmd.Args().Get(1),
|
||||
); err != nil {
|
||||
return fmt.Errorf("failed to update secret: %w", err)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package software
|
||||
|
||||
import "github.com/urfave/cli/v2"
|
||||
import "github.com/urfave/cli/v3"
|
||||
|
||||
const (
|
||||
devVersion = "dev"
|
||||
@@ -11,7 +11,7 @@ func Command() *cli.Command {
|
||||
Name: "sw",
|
||||
Aliases: []string{},
|
||||
Usage: "Perform software management operations",
|
||||
Subcommands: []*cli.Command{
|
||||
Commands: []*cli.Command{
|
||||
CommandUninstall(),
|
||||
CommandUpgrade(),
|
||||
CommandVersion(),
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package software
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -22,29 +23,29 @@ func CommandUninstall() *cli.Command {
|
||||
&cli.BoolFlag{ //nolint:exhaustruct
|
||||
Name: forceFlag,
|
||||
Usage: "Force uninstall without confirmation",
|
||||
EnvVars: []string{"NHOST_FORCE_UNINSTALL"},
|
||||
Sources: cli.EnvVars("NHOST_FORCE_UNINSTALL"),
|
||||
DefaultText: "false",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func commandUninstall(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandUninstall(_ context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
path, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find installed CLI: %w", err)
|
||||
}
|
||||
|
||||
if cCtx.App.Version == devVersion || cCtx.App.Version == "" {
|
||||
if cmd.Root().Version == devVersion || cmd.Root().Version == "" {
|
||||
// we fake it in dev mode
|
||||
path = "/tmp/nhost"
|
||||
}
|
||||
|
||||
ce.Infoln("Found Nhost cli in %s", path)
|
||||
|
||||
if !cCtx.Bool(forceFlag) {
|
||||
if !cmd.Bool(forceFlag) {
|
||||
ce.PromptMessage("Are you sure you want to uninstall Nhost CLI? [y/N] ")
|
||||
|
||||
resp, err := ce.PromptInput(false)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package software
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
@@ -8,7 +9,7 @@ import (
|
||||
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/software"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CommandUpgrade() *cli.Command {
|
||||
@@ -20,12 +21,12 @@ func CommandUpgrade() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func commandUpgrade(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandUpgrade(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
mgr := software.NewManager()
|
||||
|
||||
releases, err := mgr.GetReleases(cCtx.Context, cCtx.App.Version)
|
||||
releases, err := mgr.GetReleases(ctx, cmd.Root().Version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get releases: %w", err)
|
||||
}
|
||||
@@ -36,7 +37,7 @@ func commandUpgrade(cCtx *cli.Context) error {
|
||||
}
|
||||
|
||||
latest := releases[0]
|
||||
if latest.TagName == cCtx.App.Version {
|
||||
if latest.TagName == cmd.Root().Version {
|
||||
ce.Infoln("You have the latest version. Hurray!")
|
||||
return nil
|
||||
}
|
||||
@@ -71,20 +72,20 @@ func commandUpgrade(cCtx *cli.Context) error {
|
||||
defer os.Remove(tmpFile.Name())
|
||||
defer tmpFile.Close()
|
||||
|
||||
if err := mgr.DownloadAsset(cCtx.Context, url, tmpFile); err != nil {
|
||||
if err := mgr.DownloadAsset(ctx, url, tmpFile); err != nil {
|
||||
return fmt.Errorf("failed to download asset: %w", err)
|
||||
}
|
||||
|
||||
return install(cCtx, ce, tmpFile.Name())
|
||||
return install(cmd, ce, tmpFile.Name())
|
||||
}
|
||||
|
||||
func install(cCtx *cli.Context, ce *clienv.CliEnv, tmpFile string) error {
|
||||
func install(cmd *cli.Command, ce *clienv.CliEnv, tmpFile string) error {
|
||||
curBin, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find installed CLI: %w", err)
|
||||
}
|
||||
|
||||
if cCtx.App.Version == devVersion || cCtx.App.Version == "" {
|
||||
if cmd.Root().Version == devVersion || cmd.Root().Version == "" {
|
||||
// we are in dev mode, we fake curBin for testing
|
||||
curBin = "/tmp/nhost"
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/nhost/nhost/cli/nhostclient/graphql"
|
||||
"github.com/nhost/nhost/cli/project/env"
|
||||
"github.com/nhost/nhost/cli/software"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func CommandVersion() *cli.Command {
|
||||
@@ -111,11 +111,11 @@ func CheckVersions(
|
||||
|
||||
checkServiceVersion(
|
||||
ce, graphql.SoftwareTypeEnumAuth, *cfg.GetAuth().GetVersion(), swv,
|
||||
"https://github.com/nhost/hasura-auth/releases",
|
||||
"https://github.com/nhost/nhost/releases",
|
||||
)
|
||||
checkServiceVersion(
|
||||
ce, graphql.SoftwareTypeEnumStorage, *cfg.GetStorage().GetVersion(), swv,
|
||||
"https://github.com/nhost/hasura-storage/releases",
|
||||
"https://github.com/nhost/nhost/releases",
|
||||
)
|
||||
checkServiceVersion(
|
||||
ce, graphql.SoftwareTypeEnumPostgreSQL, *cfg.GetPostgres().GetVersion(), swv,
|
||||
@@ -132,8 +132,8 @@ func CheckVersions(
|
||||
return checkCLIVersion(ctx, ce, appVersion)
|
||||
}
|
||||
|
||||
func commandVersion(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandVersion(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
var (
|
||||
cfg *model.ConfigConfig
|
||||
@@ -157,5 +157,5 @@ func commandVersion(cCtx *cli.Context) error {
|
||||
ce.Warnln("🟡 No Nhost project found")
|
||||
}
|
||||
|
||||
return CheckVersions(cCtx.Context, ce, cfg, cCtx.App.Version)
|
||||
return CheckVersions(ctx, ce, cfg, cmd.Root().Version)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,26 +23,26 @@ func CommandLogin() *cli.Command {
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagPAT,
|
||||
Usage: "Use this Personal Access Token instead of generating a new one with your email/password",
|
||||
EnvVars: []string{"NHOST_PAT"},
|
||||
Sources: cli.EnvVars("NHOST_PAT"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagEmail,
|
||||
Usage: "Email address",
|
||||
EnvVars: []string{"NHOST_EMAIL"},
|
||||
Sources: cli.EnvVars("NHOST_EMAIL"),
|
||||
},
|
||||
&cli.StringFlag{ //nolint:exhaustruct
|
||||
Name: flagPassword,
|
||||
Usage: "Password",
|
||||
EnvVars: []string{"NHOST_PASSWORD"},
|
||||
Sources: cli.EnvVars("NHOST_PASSWORD"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func commandLogin(cCtx *cli.Context) error {
|
||||
ce := clienv.FromCLI(cCtx)
|
||||
func commandLogin(ctx context.Context, cmd *cli.Command) error {
|
||||
ce := clienv.FromCLI(cmd)
|
||||
_, err := ce.Login(
|
||||
cCtx.Context, cCtx.String(flagPAT), cCtx.String(flagEmail), cCtx.String(flagPassword),
|
||||
ctx, cmd.String(flagPAT), cmd.String(flagEmail), cmd.String(flagPassword),
|
||||
)
|
||||
|
||||
return err //nolint:wrapcheck
|
||||
|
||||
@@ -41,7 +41,7 @@ func auth( //nolint:funlen
|
||||
envars, err := appconfig.HasuraAuthEnv(
|
||||
cfg,
|
||||
"http://graphql:8080/v1/graphql",
|
||||
URL(subdomain, "auth", httpPort, useTLS)+"/v1",
|
||||
URL(subdomain, "auth", httpPort, useTLS && exposePort == 0)+"/v1",
|
||||
"postgres://nhost_hasura@postgres:5432/local",
|
||||
"postgres://nhost_auth_admin@postgres:5432/local",
|
||||
&model.ConfigSmtp{
|
||||
@@ -67,7 +67,7 @@ func auth( //nolint:funlen
|
||||
}
|
||||
|
||||
svc := &Service{
|
||||
Image: "nhost/hasura-auth:" + *cfg.Auth.Version,
|
||||
Image: "nhost/auth:" + *cfg.Auth.Version,
|
||||
DependsOn: map[string]DependsOn{
|
||||
"graphql": {
|
||||
Condition: "service_healthy",
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
func expectedAuth() *Service {
|
||||
//nolint:lll
|
||||
return &Service{
|
||||
Image: "nhost/hasura-auth:0.31.0",
|
||||
Image: "nhost/auth:0.31.0",
|
||||
Command: nil,
|
||||
DependsOn: map[string]DependsOn{
|
||||
"graphql": {Condition: "service_healthy"},
|
||||
@@ -132,7 +132,7 @@ func expectedAuth() *Service {
|
||||
"AUTH_RATE_LIMIT_SMS_INTERVAL": "5m",
|
||||
"AUTH_REFRESH_TOKEN_EXPIRES_IN": "99",
|
||||
"AUTH_REQUIRE_ELEVATED_CLAIM": "required",
|
||||
"AUTH_SERVER_URL": "http://dev.auth.local.nhost.run:1336/v1",
|
||||
"AUTH_SERVER_URL": "https://dev.auth.local.nhost.run:1336/v1",
|
||||
"AUTH_SMS_PASSWORDLESS_ENABLED": "true",
|
||||
"AUTH_SMS_PROVIDER": "twilio",
|
||||
"AUTH_SMS_TWILIO_ACCOUNT_SID": "smsAccountSid",
|
||||
@@ -186,7 +186,7 @@ func expectedAuth() *Service {
|
||||
"traefik.http.routers.auth.entrypoints": "web",
|
||||
"traefik.http.routers.auth.rule": "(HostRegexp(`^.+\\.auth\\.local\\.nhost\\.run$`) || Host(`local.auth.nhost.run`))",
|
||||
"traefik.http.routers.auth.service": "auth",
|
||||
"traefik.http.routers.auth.tls": "false",
|
||||
"traefik.http.routers.auth.tls": "true",
|
||||
"traefik.http.services.auth.loadbalancer.server.port": "4000",
|
||||
},
|
||||
Ports: nil,
|
||||
@@ -216,7 +216,7 @@ func TestAuth(t *testing.T) {
|
||||
{
|
||||
name: "default",
|
||||
cfg: getConfig,
|
||||
useTlS: false,
|
||||
useTlS: true,
|
||||
exposePort: 0,
|
||||
expected: expectedAuth,
|
||||
},
|
||||
@@ -227,11 +227,11 @@ func TestAuth(t *testing.T) {
|
||||
cfg.Auth.Version = ptr("0.21.3")
|
||||
return cfg
|
||||
},
|
||||
useTlS: false,
|
||||
useTlS: true,
|
||||
exposePort: 0,
|
||||
expected: func() *Service {
|
||||
svc := expectedAuth()
|
||||
svc.Image = "nhost/hasura-auth:0.21.3"
|
||||
svc.Image = "nhost/auth:0.21.3"
|
||||
svc.Labels["traefik.http.middlewares.replace-auth.replacepathregex.regex"] = "/v1(/|$$)(.*)"
|
||||
svc.Labels["traefik.http.middlewares.replace-auth.replacepathregex.replacement"] = "/$$2"
|
||||
svc.Labels["traefik.http.routers.auth.middlewares"] = "replace-auth"
|
||||
@@ -243,7 +243,7 @@ func TestAuth(t *testing.T) {
|
||||
{
|
||||
name: "custom port",
|
||||
cfg: getConfig,
|
||||
useTlS: false,
|
||||
useTlS: true,
|
||||
exposePort: 8080,
|
||||
expected: func() *Service {
|
||||
svc := expectedAuth()
|
||||
|
||||
@@ -49,7 +49,7 @@ func storage( //nolint:funlen
|
||||
}
|
||||
|
||||
return &Service{
|
||||
Image: "nhost/hasura-storage:" + *cfg.GetStorage().GetVersion(),
|
||||
Image: "nhost/storage:" + *cfg.GetStorage().GetVersion(),
|
||||
DependsOn: map[string]DependsOn{
|
||||
"minio": {
|
||||
Condition: "service_started",
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
func expectedStorage() *Service {
|
||||
return &Service{
|
||||
Image: "nhost/hasura-storage:0.2.5",
|
||||
Image: "nhost/storage:0.2.5",
|
||||
DependsOn: map[string]DependsOn{
|
||||
"graphql": {Condition: "service_healthy"},
|
||||
"minio": {Condition: "service_started"},
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
name = 'GREET'
|
||||
value = 'Sayonara'
|
||||
|
||||
[[global.environment]]
|
||||
name = 'NODE_ENV'
|
||||
value = 'production'
|
||||
|
||||
[hasura]
|
||||
version = 'v2.46.0-ce'
|
||||
adminSecret = '{{ secrets.HASURA_GRAPHQL_ADMIN_SECRET }}'
|
||||
@@ -63,11 +67,6 @@ default = "00000000-0000-0000-0000-000000000000"
|
||||
expiresIn = 2592000
|
||||
|
||||
[auth.method]
|
||||
[auth.method.anonymous]
|
||||
enabled = false
|
||||
|
||||
[auth.method.emailPasswordless]
|
||||
enabled = false
|
||||
|
||||
[auth.method.emailPassword]
|
||||
hibpEnabled = false
|
||||
@@ -139,46 +138,11 @@ version = '14.18-20250728-1'
|
||||
[postgres.resources.storage]
|
||||
capacity = 1
|
||||
|
||||
[postgres.settings]
|
||||
maxConnections = 100
|
||||
sharedBuffers = '256MB'
|
||||
effectiveCacheSize = '768MB'
|
||||
maintenanceWorkMem = '64MB'
|
||||
checkpointCompletionTarget = 0.9
|
||||
walBuffers = '-1'
|
||||
defaultStatisticsTarget = 100
|
||||
randomPageCost = 1.1
|
||||
effectiveIOConcurrency = 200
|
||||
workMem = '1310kB'
|
||||
hugePages = 'off'
|
||||
minWalSize = '80MB'
|
||||
maxWalSize = '1GB'
|
||||
maxWorkerProcesses = 8
|
||||
maxParallelWorkersPerGather = 2
|
||||
maxParallelWorkers = 8
|
||||
maxParallelMaintenanceWorkers = 2
|
||||
|
||||
[provider]
|
||||
|
||||
[storage]
|
||||
version = '0.7.1'
|
||||
|
||||
[ai]
|
||||
version = '0.8.0'
|
||||
webhookSecret = '{{ secrets.GRAPHITE_WEBHOOK_SECRET }}'
|
||||
|
||||
[ai.resources]
|
||||
[ai.resources.compute]
|
||||
cpu = 125
|
||||
memory = 256
|
||||
|
||||
[ai.openai]
|
||||
organization = ''
|
||||
apiKey = '{{ secrets.OPENAI_API_KEY }}'
|
||||
|
||||
[ai.autoEmbeddings]
|
||||
synchPeriodMinutes = 5
|
||||
|
||||
[observability]
|
||||
[observability.grafana]
|
||||
adminPassword = '{{ secrets.GRAFANA_ADMIN_PASSWORD }}'
|
||||
|
||||
6
cli/gen_nhost_schema.sh
Executable file
6
cli/gen_nhost_schema.sh
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
# This script generates the GraphQL schema for Nhost and saves it to a file.
|
||||
# This is only needed if the filter in cmd/gen/gen.go is changed.
|
||||
|
||||
go run main.go mcp gen > tools/cloud/schema.graphql
|
||||
go run main.go mcp gen --with-mutations > tools/cloud/schema-with-mutations.graphql
|
||||
61
cli/main.go
61
cli/main.go
@@ -1,24 +1,25 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/Yamashou/gqlgenc/clientv2"
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/cmd/config"
|
||||
"github.com/nhost/nhost/cli/cmd/configserver"
|
||||
"github.com/nhost/nhost/cli/cmd/deployments"
|
||||
"github.com/nhost/nhost/cli/cmd/dev"
|
||||
"github.com/nhost/nhost/cli/cmd/dockercredentials"
|
||||
"github.com/nhost/nhost/cli/cmd/mcp"
|
||||
"github.com/nhost/nhost/cli/cmd/project"
|
||||
"github.com/nhost/nhost/cli/cmd/run"
|
||||
"github.com/nhost/nhost/cli/cmd/secrets"
|
||||
"github.com/nhost/nhost/cli/cmd/software"
|
||||
"github.com/nhost/nhost/cli/cmd/user"
|
||||
"github.com/urfave/cli/v2"
|
||||
docs "github.com/urfave/cli-docs/v3"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var Version string
|
||||
@@ -29,11 +30,11 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app := &cli.App{ //nolint: exhaustruct
|
||||
Name: "nhost",
|
||||
EnableBashCompletion: true,
|
||||
Version: Version,
|
||||
Description: "Nhost CLI tool",
|
||||
app := &cli.Command{ //nolint: exhaustruct
|
||||
Name: "nhost",
|
||||
EnableShellCompletion: true,
|
||||
Version: Version,
|
||||
Description: "Nhost CLI tool",
|
||||
Commands: []*cli.Command{
|
||||
config.Command(),
|
||||
configserver.Command(),
|
||||
@@ -43,6 +44,7 @@ func main() {
|
||||
dev.CommandDown(),
|
||||
dev.CommandLogs(),
|
||||
dockercredentials.Command(),
|
||||
mcp.Command(),
|
||||
project.CommandInit(),
|
||||
project.CommandList(),
|
||||
project.CommandLink(),
|
||||
@@ -50,18 +52,7 @@ func main() {
|
||||
secrets.Command(),
|
||||
software.Command(),
|
||||
user.CommandLogin(),
|
||||
{
|
||||
Name: "docs",
|
||||
Hidden: true,
|
||||
Action: func(ctx *cli.Context) error {
|
||||
s, err := ctx.App.ToMarkdown()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate docs: %w", err)
|
||||
}
|
||||
fmt.Println(s) //nolint:forbidigo
|
||||
return nil
|
||||
},
|
||||
},
|
||||
markdownDocs(),
|
||||
},
|
||||
Metadata: map[string]any{
|
||||
"Author": "Nhost",
|
||||
@@ -70,13 +61,29 @@ func main() {
|
||||
Flags: flags,
|
||||
}
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
var graphqlErr *clientv2.ErrorResponse
|
||||
|
||||
if errors.As(err, &graphqlErr) {
|
||||
log.Fatal(graphqlErr.GqlErrors)
|
||||
}
|
||||
|
||||
if err := app.Run(context.Background(), os.Args); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func markdownDocs() *cli.Command {
|
||||
return &cli.Command{ //nolint:exhaustruct
|
||||
Name: "docs",
|
||||
Usage: "Generate markdown documentation for the CLI",
|
||||
Action: func(_ context.Context, cmd *cli.Command) error {
|
||||
md, err := docs.ToMarkdown(cmd.Root())
|
||||
if err != nil {
|
||||
return cli.Exit("failed to generate markdown documentation: "+err.Error(), 1)
|
||||
}
|
||||
|
||||
fmt.Println("---") //nolint:forbidigo
|
||||
fmt.Println("title: Nhost CLI Reference") //nolint:forbidigo
|
||||
fmt.Println("icon: terminal") //nolint:forbidigo
|
||||
fmt.Println("---") //nolint:forbidigo
|
||||
fmt.Println() //nolint:forbidigo
|
||||
fmt.Println(md) //nolint:forbidigo
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
201
cli/mcp/config/config.go
Normal file
201
cli/mcp/config/config.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/nhost/nhost/cli/clienv"
|
||||
"github.com/nhost/nhost/cli/mcp/nhost/auth"
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultLocalConfigServerURL = "https://local.dashboard.local.nhost.run/v1/configserver/graphql"
|
||||
DefaultLocalGraphqlURL = "https://local.graphql.local.nhost.run/v1"
|
||||
)
|
||||
|
||||
var ErrProjectNotConfigured = errors.New("project not configured")
|
||||
|
||||
type Config struct {
|
||||
// If configured allows managing the cloud. For instance, this allows you to configure
|
||||
// projects, list projects, organizations, and so on.
|
||||
Cloud *Cloud `json:"cloud,omitempty" toml:"cloud"`
|
||||
|
||||
// Projects is a list of projects that you want to allow access to. This grants access
|
||||
// to the GraphQL schema allowing it to inspect it and run allowed queries and mutations.
|
||||
Projects ProjectList `json:"projects" toml:"projects"`
|
||||
}
|
||||
|
||||
type Cloud struct {
|
||||
// If enabled you can run mutations against the Nhost Cloud to manipulate project's configurations
|
||||
// amongst other things. Queries are always allowed if this section is configured.
|
||||
EnableMutations bool `json:"enable_mutations" toml:"enable_mutations"`
|
||||
}
|
||||
|
||||
type ProjectList []Project
|
||||
|
||||
func (pl ProjectList) Get(subdomain string) (*Project, error) {
|
||||
for _, p := range pl {
|
||||
if p.Subdomain == subdomain {
|
||||
return &p, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: %s", ErrProjectNotConfigured, subdomain)
|
||||
}
|
||||
|
||||
func (pl ProjectList) Subdomains() []string {
|
||||
subdomains := make([]string, 0, len(pl))
|
||||
|
||||
for _, p := range pl {
|
||||
subdomains = append(subdomains, p.Subdomain)
|
||||
}
|
||||
|
||||
return subdomains
|
||||
}
|
||||
|
||||
func (pl ProjectList) Instructions() string {
|
||||
if len(pl) == 0 {
|
||||
return "No projects configured. Please, run `nhost mcp config` to configure your projects."
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Configured projects:\n")
|
||||
|
||||
for _, p := range pl {
|
||||
sb.WriteString(fmt.Sprintf("- %s (%s): %s\n", p.Subdomain, p.Region, p.Description))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
// Project's subdomain
|
||||
Subdomain string `json:"subdomain" toml:"subdomain"`
|
||||
|
||||
// Project's region
|
||||
Region string `json:"region" toml:"region"`
|
||||
|
||||
// Project's description
|
||||
Description string `json:"description,omitempty" toml:"description,omitempty"`
|
||||
|
||||
// Admin secret to operate against the project.
|
||||
// Either admin secret or PAT is required.
|
||||
AdminSecret *string `json:"admin_secret,omitempty" toml:"admin_secret,omitempty"`
|
||||
|
||||
// PAT to operate against the project. Note this PAT must belong to this project.
|
||||
// Either admin secret or PAT is required.
|
||||
PAT *string `json:"pat,omitempty" toml:"pat,omitempty"`
|
||||
|
||||
// If enabled, allows managing the project's metadata (tables, relationships,
|
||||
// permissions, etc).
|
||||
ManageMetadata bool `json:"manage_metadata,omitempty" toml:"manage_metadata,omitempty"`
|
||||
|
||||
// List of queries that are allowed to be executed against the project.
|
||||
// If empty, no queries are allowed. Use [*] to allow all queries.
|
||||
AllowQueries []string `json:"allow_queries" toml:"allow_queries"`
|
||||
|
||||
// List of mutations that are allowed to be executed against the project.
|
||||
// If empty, no mutations are allowed. Use [*] to allow all mutations.
|
||||
// Note that this is only used if the project is configured to allow mutations.
|
||||
AllowMutations []string `json:"allow_mutations" toml:"allow_mutations"`
|
||||
|
||||
// GraphQL URL to use when running against the project. Defaults to constructed URL with
|
||||
// the subdomain and region.
|
||||
GraphqlURL string `json:"graphql_url,omitzero" toml:"graphql_url,omitzero"`
|
||||
|
||||
// Auth URL to use when running against the project. Defaults to constructed URL with
|
||||
// the subdomain and region.
|
||||
AuthURL string `json:"auth_url,omitzero" toml:"auth_url,omitzero"`
|
||||
|
||||
// Hasura's base URL. Defaults to constructed URL with the subdomain and region.
|
||||
HasuraURL string `json:"hasura_url,omitzero" toml:"hasura_url,omitzero"`
|
||||
}
|
||||
|
||||
func (p *Project) GetAuthURL() string {
|
||||
if p.AuthURL != "" {
|
||||
return p.AuthURL
|
||||
}
|
||||
|
||||
return fmt.Sprintf("https://%s.auth.%s.nhost.run/v1", p.Subdomain, p.Region)
|
||||
}
|
||||
|
||||
func (p *Project) GetGraphqlURL() string {
|
||||
if p.GraphqlURL != "" {
|
||||
return p.GraphqlURL
|
||||
}
|
||||
|
||||
return fmt.Sprintf("https://%s.graphql.%s.nhost.run/v1", p.Subdomain, p.Region)
|
||||
}
|
||||
|
||||
func (p *Project) GetHasuraURL() string {
|
||||
if p.HasuraURL != "" {
|
||||
return p.HasuraURL
|
||||
}
|
||||
|
||||
return fmt.Sprintf("https://%s.hasura.%s.nhost.run", p.Subdomain, p.Region)
|
||||
}
|
||||
|
||||
func (p *Project) GetAuthInterceptor() (func(ctx context.Context, req *http.Request) error, error) {
|
||||
if p.AdminSecret != nil {
|
||||
return auth.WithAdminSecret(*p.AdminSecret), nil
|
||||
} else if p.PAT != nil {
|
||||
interceptor, err := auth.WithPAT(p.GetAuthURL(), *p.PAT)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create PAT interceptor: %w", err)
|
||||
}
|
||||
|
||||
return interceptor, nil
|
||||
}
|
||||
|
||||
return func(_ context.Context, _ *http.Request) error {
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetConfigPath(cmd *cli.Command) string {
|
||||
configPath := cmd.String("config-file")
|
||||
if configPath != "" {
|
||||
return configPath
|
||||
}
|
||||
|
||||
ce := clienv.FromCLI(cmd)
|
||||
|
||||
return filepath.Join(ce.Path.DotNhostFolder(), "mcp-nhost.toml")
|
||||
}
|
||||
|
||||
func Load(path string) (*Config, error) {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
interpolated := interpolateEnv(string(content), os.Getenv)
|
||||
|
||||
decoder := toml.NewDecoder(strings.NewReader(interpolated))
|
||||
decoder.DisallowUnknownFields()
|
||||
|
||||
var config Config
|
||||
if err := decoder.Decode(&config); err != nil {
|
||||
var (
|
||||
decodeErr *toml.DecodeError
|
||||
strictErr *toml.StrictMissingError
|
||||
)
|
||||
|
||||
if errors.As(err, &decodeErr) {
|
||||
return nil, errors.New("\n" + decodeErr.String()) //nolint:err113
|
||||
} else if errors.As(err, &strictErr) {
|
||||
return nil, errors.New("\n" + strictErr.String()) //nolint:err113
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to unmarshal config file: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
60
cli/mcp/config/interpolate.go
Normal file
60
cli/mcp/config/interpolate.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package config
|
||||
|
||||
import "strings"
|
||||
|
||||
// interpolateEnv replaces environment variables in the format $VAR.
|
||||
// Supports escaping $ with $$ or \$.
|
||||
func interpolateEnv(s string, getenv func(string) string) string { //nolint:cyclop
|
||||
var result strings.Builder
|
||||
result.Grow(len(s))
|
||||
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch {
|
||||
case s[i] == '\\' && i+1 < len(s) && s[i+1] == '$':
|
||||
// Handle \$ escape sequence
|
||||
result.WriteByte('$')
|
||||
|
||||
i++ // skip the $
|
||||
case s[i] == '$' && i+1 < len(s) && s[i+1] == '$':
|
||||
// Handle $$ escape sequence
|
||||
result.WriteByte('$')
|
||||
|
||||
i++ // skip the second $
|
||||
case s[i] == '$':
|
||||
// Start of variable substitution
|
||||
i++
|
||||
if i >= len(s) {
|
||||
result.WriteByte('$')
|
||||
break
|
||||
}
|
||||
|
||||
// Extract variable name
|
||||
start := i
|
||||
for i < len(s) && (isAlphaNumUnderscore(s[i])) {
|
||||
i++
|
||||
}
|
||||
|
||||
if i == start {
|
||||
// No valid variable name found
|
||||
result.WriteByte('$')
|
||||
|
||||
i--
|
||||
} else {
|
||||
varName := s[start:i]
|
||||
if value := getenv(varName); value != "" {
|
||||
result.WriteString(value)
|
||||
}
|
||||
|
||||
i-- // Back up one because the loop will increment
|
||||
}
|
||||
default:
|
||||
result.WriteByte(s[i])
|
||||
}
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func isAlphaNumUnderscore(c byte) bool {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'
|
||||
}
|
||||
256
cli/mcp/config/interpolate_test.go
Normal file
256
cli/mcp/config/interpolate_test.go
Normal file
@@ -0,0 +1,256 @@
|
||||
package config //nolint:testpackage
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestInterpolateEnv(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
envVars map[string]string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "simple variable substitution",
|
||||
input: "admin_secret = \"$SECRET\"",
|
||||
envVars: map[string]string{"SECRET": "mysecret"},
|
||||
expected: "admin_secret = \"mysecret\"",
|
||||
},
|
||||
{
|
||||
name: "multiple variables",
|
||||
input: "$VAR1 and $VAR2",
|
||||
envVars: map[string]string{"VAR1": "hello", "VAR2": "world"},
|
||||
expected: "hello and world",
|
||||
},
|
||||
{
|
||||
name: "variable with underscores",
|
||||
input: "$MY_VAR_123",
|
||||
envVars: map[string]string{"MY_VAR_123": "value"},
|
||||
expected: "value",
|
||||
},
|
||||
{
|
||||
name: "escaped with $$",
|
||||
input: "price = $$100",
|
||||
envVars: map[string]string{},
|
||||
expected: "price = $100",
|
||||
},
|
||||
{
|
||||
name: "escaped with backslash",
|
||||
input: "price = \\$100",
|
||||
envVars: map[string]string{},
|
||||
expected: "price = $100",
|
||||
},
|
||||
{
|
||||
name: "mix of escaped and variable",
|
||||
input: "$$SECRET is $SECRET",
|
||||
envVars: map[string]string{"SECRET": "hidden"},
|
||||
expected: "$SECRET is hidden",
|
||||
},
|
||||
{
|
||||
name: "undefined variable",
|
||||
input: "value = $UNDEFINED",
|
||||
envVars: map[string]string{},
|
||||
expected: "value = ",
|
||||
},
|
||||
{
|
||||
name: "variable at end",
|
||||
input: "end$VAR",
|
||||
envVars: map[string]string{"VAR": "value"},
|
||||
expected: "endvalue",
|
||||
},
|
||||
{
|
||||
name: "dollar sign alone at end",
|
||||
input: "end$",
|
||||
envVars: map[string]string{},
|
||||
expected: "end$",
|
||||
},
|
||||
{
|
||||
name: "dollar sign with non-alphanum",
|
||||
input: "$ hello",
|
||||
envVars: map[string]string{},
|
||||
expected: "$ hello",
|
||||
},
|
||||
{
|
||||
name: "no variables",
|
||||
input: "plain text without variables",
|
||||
envVars: map[string]string{},
|
||||
expected: "plain text without variables",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
envVars: map[string]string{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "multiple escapes in a row",
|
||||
input: "$$$$",
|
||||
envVars: map[string]string{},
|
||||
expected: "$$",
|
||||
},
|
||||
{
|
||||
name: "variable surrounded by text",
|
||||
input: "prefix$VAR suffix",
|
||||
envVars: map[string]string{"VAR": "middle"},
|
||||
expected: "prefixmiddle suffix",
|
||||
},
|
||||
{
|
||||
name: "backslash escape followed by variable",
|
||||
input: "\\$100 costs $PRICE",
|
||||
envVars: map[string]string{"PRICE": "$50"},
|
||||
expected: "$100 costs $50",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create isolated getenv function
|
||||
getenv := func(key string) string {
|
||||
return tt.envVars[key]
|
||||
}
|
||||
|
||||
result := interpolateEnv(tt.input, getenv)
|
||||
if result != tt.expected {
|
||||
t.Errorf("interpolateEnv() = %q, want %q", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterpolateEnvRealWorld(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
envVars := map[string]string{
|
||||
"ADMIN_SECRET": "super-secret-key",
|
||||
"SUBDOMAIN": "myapp",
|
||||
}
|
||||
getenv := func(key string) string {
|
||||
return envVars[key]
|
||||
}
|
||||
|
||||
input := `[[projects]]
|
||||
subdomain = "local"
|
||||
region = "local"
|
||||
admin_secret = "$ADMIN_SECRET"
|
||||
|
||||
[[projects]]
|
||||
subdomain = "$SUBDOMAIN"
|
||||
admin_secret = "$ADMIN_SECRET"
|
||||
# Price is $$100
|
||||
`
|
||||
|
||||
expected := `[[projects]]
|
||||
subdomain = "local"
|
||||
region = "local"
|
||||
admin_secret = "super-secret-key"
|
||||
|
||||
[[projects]]
|
||||
subdomain = "myapp"
|
||||
admin_secret = "super-secret-key"
|
||||
# Price is $100
|
||||
`
|
||||
|
||||
result := interpolateEnv(input, getenv)
|
||||
if result != expected {
|
||||
t.Errorf("interpolateEnv() = %q, want %q", result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAlphaNumUnderscore(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
char byte
|
||||
expected bool
|
||||
}{
|
||||
{'a', true},
|
||||
{'z', true},
|
||||
{'A', true},
|
||||
{'Z', true},
|
||||
{'0', true},
|
||||
{'9', true},
|
||||
{'_', true},
|
||||
{'-', false},
|
||||
{'.', false},
|
||||
{'$', false},
|
||||
{' ', false},
|
||||
{'/', false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.char), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := isAlphaNumUnderscore(tt.char)
|
||||
if result != tt.expected {
|
||||
t.Errorf("isAlphaNumUnderscore(%q) = %v, want %v", tt.char, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ptr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
func TestLoadWithInterpolation(t *testing.T) {
|
||||
// Create a temporary config file
|
||||
content := `[[projects]]
|
||||
admin_secret = "$TEST_ADMIN_SECRET"
|
||||
|
||||
[[projects]]
|
||||
subdomain = "myapp"
|
||||
region = "us-east-1"
|
||||
admin_secret = "$TEST_PROJECT_SECRET"
|
||||
allow_queries = ["*"]
|
||||
`
|
||||
|
||||
tmpfile, err := os.CreateTemp(t.TempDir(), "config-*.toml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
if _, err := tmpfile.WriteString(content); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Set environment variables
|
||||
t.Setenv("TEST_ADMIN_SECRET", "local-secret")
|
||||
t.Setenv("TEST_PROJECT_SECRET", "project-secret")
|
||||
|
||||
// Load config
|
||||
cfg, err := Load(tmpfile.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(cfg, &Config{
|
||||
Cloud: nil,
|
||||
Projects: ProjectList{
|
||||
{ //nolint:exhaustruct
|
||||
AdminSecret: ptr("local-secret"),
|
||||
},
|
||||
{ //nolint:exhaustruct
|
||||
Subdomain: "myapp",
|
||||
Region: "us-east-1",
|
||||
AdminSecret: ptr("project-secret"),
|
||||
AllowQueries: []string{"*"},
|
||||
},
|
||||
},
|
||||
}); diff != "" {
|
||||
t.Errorf("diff = %s", diff)
|
||||
}
|
||||
}
|
||||
207
cli/mcp/config/wizard.go
Normal file
207
cli/mcp/config/wizard.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//nolint:forbidigo
|
||||
func RunWizard() (*Config, error) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Println("Welcome to the Nhost MCP Configuration Wizard!")
|
||||
fmt.Println("==============================================")
|
||||
fmt.Println()
|
||||
|
||||
cloudConfig := wizardCloud(reader)
|
||||
|
||||
fmt.Println()
|
||||
|
||||
localConfig := wizardLocal(reader)
|
||||
|
||||
fmt.Println()
|
||||
|
||||
projects := wizardProject(reader)
|
||||
|
||||
if localConfig != nil {
|
||||
projects = append(projects, *localConfig)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
|
||||
return &Config{
|
||||
Cloud: cloudConfig,
|
||||
Projects: projects,
|
||||
}, nil
|
||||
}
|
||||
|
||||
//nolint:forbidigo
|
||||
func wizardCloud(reader *bufio.Reader) *Cloud {
|
||||
fmt.Println("1. Nhost Cloud Access")
|
||||
fmt.Println(" This allows LLMs to manage your Nhost projects and organizations.")
|
||||
fmt.Println(" You can view and configure projects as you would in the dashboard.")
|
||||
|
||||
if promptYesNo(reader, "Enable Nhost Cloud access?") {
|
||||
fmt.Println(" Note: If you haven't already, run `nhost login` to authenticate.")
|
||||
|
||||
return &Cloud{
|
||||
EnableMutations: true,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint:forbidigo
|
||||
func wizardLocal(reader *bufio.Reader) *Project {
|
||||
fmt.Println("2. Local Development Access")
|
||||
fmt.Println(" This allows LLMs to interact with your local Nhost environment,")
|
||||
fmt.Println(" including project configuration and GraphQL API access.")
|
||||
fmt.Println(" This gives LLMs context to generate code to interact with your Nhost project.")
|
||||
|
||||
if promptYesNo(reader, "Enable local development access?") {
|
||||
adminSecret := promptString(reader, "Enter Admin Secret (default: nhost-admin-secret):")
|
||||
if adminSecret == "" {
|
||||
adminSecret = "nhost-admin-secret" //nolint:gosec
|
||||
}
|
||||
|
||||
return &Project{
|
||||
Subdomain: "local",
|
||||
Region: "local",
|
||||
Description: "Local development project running via the Nhost CLI",
|
||||
AdminSecret: &adminSecret,
|
||||
PAT: nil,
|
||||
ManageMetadata: true,
|
||||
AllowQueries: []string{"*"},
|
||||
AllowMutations: []string{"*"},
|
||||
AuthURL: "",
|
||||
GraphqlURL: "",
|
||||
HasuraURL: "",
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint:forbidigo
|
||||
func wizardProject(reader *bufio.Reader) []Project {
|
||||
projects := make([]Project, 0)
|
||||
|
||||
fmt.Println("3. Project-Specific Access")
|
||||
fmt.Println(" Configure LLM access to your projects' GraphQL APIs.")
|
||||
fmt.Println(
|
||||
" This allows using agents to query and analyze your data and even to add new data",
|
||||
)
|
||||
fmt.Println(
|
||||
" You can control which queries and mutations are allowed per project. See the docs",
|
||||
)
|
||||
fmt.Println(" for more details on how to configure this.")
|
||||
|
||||
if promptYesNo(reader, "Configure project access?") {
|
||||
for {
|
||||
project := Project{
|
||||
Description: "",
|
||||
Subdomain: "",
|
||||
Region: "",
|
||||
AdminSecret: nil,
|
||||
PAT: nil,
|
||||
ManageMetadata: false,
|
||||
AllowQueries: []string{"*"},
|
||||
AllowMutations: []string{"*"},
|
||||
GraphqlURL: "",
|
||||
AuthURL: "",
|
||||
HasuraURL: "",
|
||||
}
|
||||
|
||||
project.Subdomain = promptString(reader, "Project subdomain:")
|
||||
project.Region = promptString(reader, "Project region:")
|
||||
project.Description = promptString(
|
||||
reader,
|
||||
"Project description to provide additional information to LLMs:",
|
||||
)
|
||||
project.ManageMetadata = promptYesNo(
|
||||
reader,
|
||||
"Allow managing metadata (tables, relationships, permissions, etc)?",
|
||||
)
|
||||
|
||||
authType := promptChoice(
|
||||
reader,
|
||||
"Select authentication method:",
|
||||
[]string{"Admin Secret", "PAT"},
|
||||
)
|
||||
if authType == "Admin Secret" {
|
||||
adminSecret := promptString(reader, "Project Admin Secret:")
|
||||
project.AdminSecret = &adminSecret
|
||||
} else {
|
||||
pat := promptString(reader, "Project PAT:")
|
||||
project.PAT = &pat
|
||||
}
|
||||
|
||||
projects = append(projects, project)
|
||||
|
||||
if !promptYesNo(reader, "Add another project?") {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return projects
|
||||
}
|
||||
|
||||
//nolint:forbidigo
|
||||
func promptString(reader *bufio.Reader, prompt string) string {
|
||||
fmt.Print(prompt + " ")
|
||||
|
||||
input, _ := reader.ReadString('\n')
|
||||
|
||||
return strings.TrimSpace(input)
|
||||
}
|
||||
|
||||
//nolint:forbidigo
|
||||
func promptYesNo(reader *bufio.Reader, prompt string) bool {
|
||||
for {
|
||||
fmt.Printf("%s (y/n) ", prompt)
|
||||
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.ToLower(strings.TrimSpace(input))
|
||||
|
||||
if input == "y" || input == "yes" {
|
||||
return true
|
||||
}
|
||||
|
||||
if input == "n" || input == "no" {
|
||||
return false
|
||||
}
|
||||
|
||||
fmt.Println("Please answer with 'y' or 'n'")
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:forbidigo
|
||||
func promptChoice(reader *bufio.Reader, prompt string, options []string) string {
|
||||
for {
|
||||
fmt.Printf("%s\n", prompt)
|
||||
|
||||
for i, opt := range options {
|
||||
fmt.Printf("%d) %s\n", i+1, opt)
|
||||
}
|
||||
|
||||
fmt.Print("Enter number: ")
|
||||
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
|
||||
if num := strings.TrimSpace(input); num != "" {
|
||||
switch num {
|
||||
case "1":
|
||||
return options[0]
|
||||
case "2":
|
||||
return options[1]
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Please select a valid option")
|
||||
}
|
||||
}
|
||||
96
cli/mcp/graphql/introspection_query.go
Normal file
96
cli/mcp/graphql/introspection_query.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package graphql
|
||||
|
||||
const IntrospectionQuery = `
|
||||
query IntrospectionQuery {
|
||||
__schema {
|
||||
queryType {
|
||||
...FullType
|
||||
}
|
||||
mutationType {
|
||||
...FullType
|
||||
}
|
||||
types {
|
||||
...FullType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment FullType on __Type {
|
||||
kind
|
||||
name
|
||||
description
|
||||
fields(includeDeprecated: true) {
|
||||
name
|
||||
description
|
||||
args {
|
||||
...InputValue
|
||||
}
|
||||
type {
|
||||
...TypeRef
|
||||
}
|
||||
}
|
||||
inputFields {
|
||||
...InputValue
|
||||
}
|
||||
interfaces {
|
||||
...TypeRef
|
||||
}
|
||||
enumValues(includeDeprecated: true) {
|
||||
name
|
||||
description
|
||||
}
|
||||
possibleTypes {
|
||||
...TypeRef
|
||||
}
|
||||
}
|
||||
|
||||
fragment InputValue on __InputValue {
|
||||
name
|
||||
description
|
||||
type {
|
||||
...TypeRef
|
||||
}
|
||||
defaultValue
|
||||
}
|
||||
|
||||
fragment TypeRef on __Type {
|
||||
kind
|
||||
name
|
||||
ofType {
|
||||
kind
|
||||
name
|
||||
ofType {
|
||||
kind
|
||||
name
|
||||
ofType {
|
||||
kind
|
||||
name
|
||||
ofType {
|
||||
kind
|
||||
name
|
||||
ofType {
|
||||
kind
|
||||
name
|
||||
ofType {
|
||||
kind
|
||||
name
|
||||
ofType {
|
||||
kind
|
||||
name
|
||||
ofType {
|
||||
kind
|
||||
name
|
||||
ofType {
|
||||
kind
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
456
cli/mcp/graphql/parse.go
Normal file
456
cli/mcp/graphql/parse.go
Normal file
@@ -0,0 +1,456 @@
|
||||
package graphql
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Filter struct {
|
||||
AllowQueries []Queries
|
||||
AllowMutations []Queries
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
Name string
|
||||
DisableNesting bool
|
||||
}
|
||||
|
||||
// getTypeName returns the GraphQL type name with modifiers (non-null, list).
|
||||
func getTypeName(t Type) string {
|
||||
if t.Kind == KindNonNull {
|
||||
return getTypeName(*t.OfType) + "!"
|
||||
}
|
||||
|
||||
if t.Kind == KindList {
|
||||
return "[" + getTypeName(*t.OfType) + "]"
|
||||
}
|
||||
|
||||
return *t.Name
|
||||
}
|
||||
|
||||
// ParseSchema converts an introspection query result into a GraphQL SDL string.
|
||||
func ParseSchema(response ResponseIntrospection, filter Filter) string { //nolint:cyclop
|
||||
availableTypes := make(map[string]Type)
|
||||
|
||||
// Process all types in the schema
|
||||
for _, t := range response.Data.Schema.Types {
|
||||
gatherAllTypes(t, availableTypes)
|
||||
}
|
||||
|
||||
neededQueries := make(map[string]Field)
|
||||
neededTypes := make(map[string]Type)
|
||||
|
||||
for _, query := range response.Data.Schema.QueryType.Fields {
|
||||
if filter.AllowQueries == nil {
|
||||
neededQueries[query.Name] = query
|
||||
collectNeededTypesFromQuery(query, neededTypes, availableTypes, true)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for _, q := range filter.AllowQueries {
|
||||
if query.Name == q.Name {
|
||||
neededQueries[query.Name] = query
|
||||
collectNeededTypesFromQuery(query, neededTypes, availableTypes, !q.DisableNesting)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
neededMutations := make(map[string]Field)
|
||||
if response.Data.Schema.MutationType == nil {
|
||||
return render(neededQueries, neededMutations, neededTypes)
|
||||
}
|
||||
|
||||
for _, mutation := range response.Data.Schema.MutationType.Fields {
|
||||
if filter.AllowMutations == nil {
|
||||
neededMutations[mutation.Name] = mutation
|
||||
collectNeededTypesFromQuery(mutation, neededTypes, availableTypes, true)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for _, q := range filter.AllowMutations {
|
||||
if mutation.Name == q.Name {
|
||||
neededMutations[mutation.Name] = mutation
|
||||
collectNeededTypesFromQuery(
|
||||
mutation,
|
||||
neededTypes,
|
||||
availableTypes,
|
||||
!q.DisableNesting,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return render(neededQueries, neededMutations, neededTypes)
|
||||
}
|
||||
|
||||
func SummarizeSchema(response ResponseIntrospection) string {
|
||||
summary := map[string][]string{
|
||||
"query": make([]string, len(response.Data.Schema.QueryType.Fields)),
|
||||
}
|
||||
|
||||
for i, query := range response.Data.Schema.QueryType.Fields {
|
||||
summary["query"][i] = query.Name
|
||||
}
|
||||
|
||||
if response.Data.Schema.MutationType != nil {
|
||||
summary["mutation"] = make([]string, len(response.Data.Schema.MutationType.Fields))
|
||||
for _, mutation := range response.Data.Schema.MutationType.Fields {
|
||||
summary["mutation"] = append(summary["mutation"], mutation.Name)
|
||||
}
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(summary, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Sprintf("failed to marshal summary: %v", err)
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func filterNestedArgs(
|
||||
args []InputValue, neededTypes map[string]Type,
|
||||
) []InputValue {
|
||||
filtered := make([]InputValue, 0, len(args))
|
||||
for _, arg := range args {
|
||||
if arg.Type.Kind == KindInputObject || arg.Type.Kind == KindObject {
|
||||
k := fmt.Sprintf("%s:%s", arg.Type.Kind, *arg.Type.Name)
|
||||
if _, ok := neededTypes[k]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
filtered = append(filtered, arg)
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
func filterNestedFields(
|
||||
fields []Field, neededTypes map[string]Type,
|
||||
) []Field {
|
||||
filtered := make([]Field, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
if field.Type.Kind == KindInputObject || field.Type.Kind == KindObject {
|
||||
k := fmt.Sprintf("%s:%s", field.Type.Kind, *field.Type.Name)
|
||||
if _, ok := neededTypes[k]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
filtered = append(filtered, field)
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
func filterInputNestedFields(
|
||||
fields []InputValue, neededTypes map[string]Type,
|
||||
) []InputValue {
|
||||
filtered := make([]InputValue, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
if field.Type.Kind == KindInputObject || field.Type.Kind == KindObject {
|
||||
k := fmt.Sprintf("%s:%s", field.Type.Kind, *field.Type.Name)
|
||||
if _, ok := neededTypes[k]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
filtered = append(filtered, field)
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
func gatherAllTypes(t Type, availableTypes map[string]Type) {
|
||||
key := fmt.Sprintf("%s:%s", t.Kind, *t.Name)
|
||||
availableTypes[key] = t
|
||||
}
|
||||
|
||||
// collectNeededTypesFromQuery recursively collects all types needed for a given field.
|
||||
func collectNeededTypesFromQuery(
|
||||
field Field,
|
||||
neededTypes map[string]Type,
|
||||
availableType map[string]Type,
|
||||
enableNesting bool,
|
||||
) {
|
||||
collectType(field.Type, neededTypes, availableType, enableNesting, false)
|
||||
|
||||
for _, arg := range field.Args {
|
||||
collectType(arg.Type, neededTypes, availableType, enableNesting, false)
|
||||
}
|
||||
}
|
||||
|
||||
func collectType(
|
||||
t Type,
|
||||
neededTypes map[string]Type,
|
||||
availableTypes map[string]Type,
|
||||
enableNesting bool,
|
||||
nested bool,
|
||||
) {
|
||||
if t.Kind == KindNonNull || t.Kind == KindList {
|
||||
collectType(*t.OfType, neededTypes, availableTypes, enableNesting, nested)
|
||||
return
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s:%s", t.Kind, *t.Name)
|
||||
|
||||
availableType, ok := availableTypes[key]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("type %s not found in available types", key))
|
||||
}
|
||||
|
||||
if _, exists := neededTypes[key]; exists {
|
||||
return
|
||||
}
|
||||
|
||||
switch t.Kind {
|
||||
case KindObject, KindInputObject:
|
||||
if !enableNesting && nested {
|
||||
return
|
||||
}
|
||||
|
||||
neededTypes[key] = availableType
|
||||
|
||||
collectTypeObject(availableType, neededTypes, availableTypes, enableNesting)
|
||||
|
||||
case KindList, KindNonNull:
|
||||
case KindScalar, KindEnum:
|
||||
neededTypes[key] = availableType
|
||||
|
||||
collectTypeSimple(*t.Name, neededTypes, availableTypes, enableNesting)
|
||||
}
|
||||
}
|
||||
|
||||
func collectTypeObject(
|
||||
availableType Type,
|
||||
neededTypes map[string]Type,
|
||||
availableTypes map[string]Type,
|
||||
enableNesting bool,
|
||||
) {
|
||||
for _, field := range availableType.Fields {
|
||||
collectType(field.Type, neededTypes, availableTypes, enableNesting, true)
|
||||
}
|
||||
|
||||
for _, inputField := range availableType.InputFields {
|
||||
collectType(inputField.Type, neededTypes, availableTypes, enableNesting, true)
|
||||
}
|
||||
|
||||
for _, iface := range availableType.Interfaces {
|
||||
collectType(iface, neededTypes, availableTypes, enableNesting, true)
|
||||
}
|
||||
|
||||
for _, possibleType := range availableType.PossibleTypes {
|
||||
collectType(possibleType, neededTypes, availableTypes, enableNesting, true)
|
||||
}
|
||||
|
||||
if availableType.OfType != nil {
|
||||
collectType(*availableType.OfType, neededTypes, availableTypes, enableNesting, true)
|
||||
}
|
||||
}
|
||||
|
||||
func collectTypeSimple(
|
||||
name string,
|
||||
neededTypes map[string]Type,
|
||||
availableTypes map[string]Type,
|
||||
enableNesting bool,
|
||||
) {
|
||||
keyComparisonExp := string(KindInputObject) + ":" + name + "_comparison_exp"
|
||||
if _, exists := neededTypes[keyComparisonExp]; !exists {
|
||||
availableComparisonExpType, ok := availableTypes[keyComparisonExp]
|
||||
if ok {
|
||||
collectType(
|
||||
availableComparisonExpType, neededTypes, availableTypes, enableNesting, true,
|
||||
)
|
||||
neededTypes[keyComparisonExp] = availableComparisonExpType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type typeInfo struct {
|
||||
kind string
|
||||
name string
|
||||
typ Type
|
||||
}
|
||||
|
||||
func getSortedTypes(neededTypes map[string]Type) []typeInfo {
|
||||
// Sort types by kind and name
|
||||
sortedTypes := make([]typeInfo, 0, len(neededTypes))
|
||||
for key, t := range neededTypes {
|
||||
parts := strings.Split(key, ":")
|
||||
sortedTypes = append(sortedTypes, typeInfo{
|
||||
kind: parts[0],
|
||||
name: parts[1],
|
||||
typ: t,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(sortedTypes, func(i, j int) bool {
|
||||
if sortedTypes[i].kind != sortedTypes[j].kind {
|
||||
return sortedTypes[i].kind < sortedTypes[j].kind
|
||||
}
|
||||
|
||||
return sortedTypes[i].name < sortedTypes[j].name
|
||||
})
|
||||
|
||||
return sortedTypes
|
||||
}
|
||||
|
||||
func render(
|
||||
neededQueries map[string]Field,
|
||||
neededMutations map[string]Field,
|
||||
neededTypes map[string]Type,
|
||||
) string {
|
||||
// render in graphql's SDL format
|
||||
var sdl strings.Builder
|
||||
|
||||
sortedTypes := getSortedTypes(neededTypes)
|
||||
|
||||
for _, t := range sortedTypes {
|
||||
if t.kind == string(KindScalar) {
|
||||
sdl.WriteString("scalar " + t.name + "\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
for _, t := range sortedTypes {
|
||||
if t.kind == string(KindObject) {
|
||||
renderType(&sdl, t, neededTypes)
|
||||
}
|
||||
}
|
||||
|
||||
// Render input objects
|
||||
for _, t := range sortedTypes {
|
||||
if t.kind == string(KindInputObject) {
|
||||
sdl.WriteString("input " + t.name + " {\n ")
|
||||
sdl.WriteString(renderInputFields(t.typ.InputFields, neededTypes))
|
||||
sdl.WriteString("\n}\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Render queries
|
||||
if len(neededQueries) > 0 {
|
||||
sdl.WriteString("type Query {\n ")
|
||||
renderQuery(&sdl, neededQueries, neededTypes)
|
||||
sdl.WriteString("\n}\n\n")
|
||||
}
|
||||
|
||||
// Render mutations
|
||||
if len(neededMutations) > 0 {
|
||||
sdl.WriteString("type Mutation {\n ")
|
||||
renderQuery(&sdl, neededMutations, neededTypes)
|
||||
sdl.WriteString("\n}\n\n")
|
||||
}
|
||||
|
||||
return sdl.String()
|
||||
}
|
||||
|
||||
func renderArgs(
|
||||
args []InputValue, neededTypes map[string]Type,
|
||||
) string {
|
||||
if len(args) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
argStrings := make([]string, 0, len(args))
|
||||
|
||||
args = filterNestedArgs(args, neededTypes)
|
||||
for _, arg := range args {
|
||||
argStr := arg.Name + ": " + getTypeName(arg.Type)
|
||||
if arg.DefaultValue != nil {
|
||||
argStr += " = " + *arg.DefaultValue
|
||||
}
|
||||
|
||||
argStrings = append(argStrings, argStr)
|
||||
}
|
||||
|
||||
return "(" + strings.Join(argStrings, ", ") + ")"
|
||||
}
|
||||
|
||||
func renderFields(
|
||||
fields []Field, neededTypes map[string]Type,
|
||||
) string {
|
||||
if len(fields) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
fieldStrings := make([]string, 0, len(fields))
|
||||
|
||||
fields = filterNestedFields(fields, neededTypes)
|
||||
for _, field := range fields {
|
||||
fieldStr := field.Name
|
||||
if len(field.Args) > 0 {
|
||||
fieldStr += renderArgs(field.Args, neededTypes)
|
||||
}
|
||||
|
||||
fieldStr += ": " + getTypeName(field.Type)
|
||||
if field.Description != nil {
|
||||
fieldStr = `"""` + *field.Description + `"""` + "\n" + fieldStr
|
||||
}
|
||||
|
||||
fieldStrings = append(fieldStrings, fieldStr)
|
||||
}
|
||||
|
||||
return strings.Join(fieldStrings, "\n ")
|
||||
}
|
||||
|
||||
func renderInputFields(
|
||||
fields []InputValue, neededTypes map[string]Type,
|
||||
) string {
|
||||
if len(fields) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
fieldStrings := make([]string, 0, len(fields))
|
||||
|
||||
fields = filterInputNestedFields(fields, neededTypes)
|
||||
for _, field := range fields {
|
||||
fieldStr := field.Name + ": " + getTypeName(field.Type)
|
||||
if field.DefaultValue != nil {
|
||||
fieldStr += " = " + *field.DefaultValue
|
||||
}
|
||||
|
||||
if field.Description != nil {
|
||||
fieldStr = `"""` + *field.Description + `"""` + "\n " + fieldStr
|
||||
}
|
||||
|
||||
fieldStrings = append(fieldStrings, fieldStr)
|
||||
}
|
||||
|
||||
return strings.Join(fieldStrings, "\n ")
|
||||
}
|
||||
|
||||
func renderType(sdl *strings.Builder, t typeInfo, neededTypes map[string]Type) {
|
||||
sdl.WriteString("type " + t.name)
|
||||
|
||||
if len(t.typ.Interfaces) > 0 {
|
||||
var ifaces []string
|
||||
for _, iface := range t.typ.Interfaces {
|
||||
ifaces = append(ifaces, *iface.Name)
|
||||
}
|
||||
|
||||
sdl.WriteString(" implements " + strings.Join(ifaces, " & "))
|
||||
}
|
||||
|
||||
sdl.WriteString(" {\n ")
|
||||
sdl.WriteString(renderFields(t.typ.Fields, neededTypes))
|
||||
sdl.WriteString("\n}\n\n")
|
||||
}
|
||||
|
||||
func renderQuery(
|
||||
sdl *strings.Builder,
|
||||
queries map[string]Field,
|
||||
neededTypes map[string]Type,
|
||||
) {
|
||||
toRender := make([]Field, 0, len(queries))
|
||||
for _, q := range queries {
|
||||
toRender = append(toRender, q)
|
||||
}
|
||||
|
||||
sort.Slice(toRender, func(i, j int) bool {
|
||||
return toRender[i].Name < toRender[j].Name
|
||||
})
|
||||
sdl.WriteString(renderFields(toRender, neededTypes))
|
||||
}
|
||||
108
cli/mcp/graphql/parse_test.go
Normal file
108
cli/mcp/graphql/parse_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package graphql_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/nhost/nhost/cli/mcp/graphql"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b, err := os.ReadFile("testdata/schema.json")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read file: %v", err)
|
||||
}
|
||||
|
||||
var response graphql.ResponseIntrospection
|
||||
if err := json.Unmarshal(b, &response); err != nil {
|
||||
t.Fatalf("failed to unmarshal json: %v", err)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
filter graphql.Filter
|
||||
}{
|
||||
{
|
||||
name: "without_filter",
|
||||
filter: graphql.Filter{
|
||||
AllowQueries: nil,
|
||||
AllowMutations: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with_filter",
|
||||
filter: graphql.Filter{
|
||||
AllowQueries: []graphql.Queries{
|
||||
{
|
||||
Name: "app",
|
||||
DisableNesting: false,
|
||||
},
|
||||
{
|
||||
Name: "apps",
|
||||
DisableNesting: false,
|
||||
},
|
||||
},
|
||||
AllowMutations: []graphql.Queries{
|
||||
{
|
||||
Name: "updateApp",
|
||||
DisableNesting: false,
|
||||
},
|
||||
{
|
||||
Name: "updateConfig",
|
||||
DisableNesting: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with_filter_and_disable_nesting",
|
||||
filter: graphql.Filter{
|
||||
AllowQueries: []graphql.Queries{
|
||||
{
|
||||
Name: "app",
|
||||
DisableNesting: true,
|
||||
},
|
||||
{
|
||||
Name: "apps",
|
||||
DisableNesting: true,
|
||||
},
|
||||
},
|
||||
AllowMutations: []graphql.Queries{
|
||||
{
|
||||
Name: "updateApp",
|
||||
DisableNesting: true,
|
||||
},
|
||||
{
|
||||
Name: "updateConfig",
|
||||
DisableNesting: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := graphql.ParseSchema(response, tc.filter)
|
||||
|
||||
// if err := os.WriteFile("testdata/"+tc.name+".graphql", []byte(got), 0o644); err != nil {
|
||||
// t.Fatalf("failed to write file: %v", err)
|
||||
// }
|
||||
|
||||
b, err := os.ReadFile("testdata/" + tc.name + ".graphql")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read file: %v", err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(string(b), got); diff != "" {
|
||||
t.Errorf("ParseSchema() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
165
cli/mcp/graphql/query.go
Normal file
165
cli/mcp/graphql/query.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package graphql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"github.com/vektah/gqlparser/v2/ast"
|
||||
"github.com/vektah/gqlparser/v2/parser"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrQueryingGraphqlEndpoint = errors.New("error querying graphql endpoint")
|
||||
ErrGraphqlContainErrors = errors.New("graphql response contains errors")
|
||||
ErrQueryNotAllowed = errors.New("query not allowed")
|
||||
)
|
||||
|
||||
func checkAllowedOperation(
|
||||
selectionSet ast.SelectionSet,
|
||||
allowed []string,
|
||||
) error {
|
||||
if slices.Contains(allowed, "*") {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, v := range selectionSet {
|
||||
if v, ok := v.(*ast.Field); ok {
|
||||
if len(v.SelectionSet) > 0 && !slices.Contains(allowed, v.Name) {
|
||||
return fmt.Errorf("%w: %s", ErrQueryNotAllowed, v.Name)
|
||||
}
|
||||
|
||||
if err := checkAllowedOperation(v.SelectionSet, allowed); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CheckAllowedGraphqlQuery( //nolint:cyclop
|
||||
allowedQueries []string,
|
||||
allowedMutations []string,
|
||||
queryString string,
|
||||
) error {
|
||||
if allowedQueries == nil && allowedMutations == nil {
|
||||
// nil means nothing allowed
|
||||
return fmt.Errorf("%w: %s", ErrQueryNotAllowed, queryString)
|
||||
}
|
||||
|
||||
if len(allowedQueries) == 0 && len(allowedMutations) == 0 {
|
||||
// no queries or mutations allowed
|
||||
return fmt.Errorf("%w: %s", ErrQueryNotAllowed, queryString)
|
||||
}
|
||||
|
||||
query, err := parser.ParseQuery(&ast.Source{
|
||||
Name: "schema.graphql",
|
||||
Input: queryString,
|
||||
BuiltIn: false,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse query: %w", err)
|
||||
}
|
||||
|
||||
for _, operation := range query.Operations {
|
||||
if operation.Operation == ast.Subscription {
|
||||
return fmt.Errorf("%w: %s", ErrQueryNotAllowed, queryString)
|
||||
}
|
||||
|
||||
var (
|
||||
selectionSet ast.SelectionSet
|
||||
allowed []string
|
||||
)
|
||||
|
||||
if operation.Operation == ast.Query {
|
||||
selectionSet = operation.SelectionSet
|
||||
allowed = allowedQueries
|
||||
}
|
||||
|
||||
if operation.Operation == ast.Mutation {
|
||||
selectionSet = operation.SelectionSet
|
||||
allowed = allowedMutations
|
||||
}
|
||||
|
||||
if err := checkAllowedOperation(selectionSet, allowed); err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrQueryNotAllowed, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Query[T any]( //nolint:cyclop
|
||||
ctx context.Context,
|
||||
graphqlURL string,
|
||||
query string,
|
||||
variables map[string]any,
|
||||
response *Response[T],
|
||||
allowedQueries []string,
|
||||
allowedMutations []string,
|
||||
requestInterceptor ...func(ctx context.Context, req *http.Request) error,
|
||||
) error {
|
||||
if err := CheckAllowedGraphqlQuery(allowedQueries, allowedMutations, query); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(map[string]any{
|
||||
"query": query,
|
||||
"variables": variables,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal request body: %w", err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequestWithContext(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
graphqlURL,
|
||||
bytes.NewBuffer(requestBody),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
for _, interceptor := range requestInterceptor {
|
||||
if err := interceptor(ctx, request); err != nil {
|
||||
return fmt.Errorf("failed to intercept request: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
client := &http.Client{} //nolint:exhaustruct
|
||||
|
||||
resp, err := client.Do(request)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("%w: %s\n%s", ErrQueryingGraphqlEndpoint, resp.Status, b)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, response); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal response body: %w", err)
|
||||
}
|
||||
|
||||
if len(response.Errors) > 0 {
|
||||
return fmt.Errorf("%w: %s", ErrGraphqlContainErrors, body)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
155
cli/mcp/graphql/query_test.go
Normal file
155
cli/mcp/graphql/query_test.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package graphql_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/nhost/nhost/cli/mcp/graphql"
|
||||
)
|
||||
|
||||
func TestCheckAllowedGraphqlQuery(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
query string
|
||||
allowedQueries []string
|
||||
allowedMutations []string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "nil, nil",
|
||||
query: `query { user(id: 1) { name } }`,
|
||||
allowedQueries: nil,
|
||||
allowedMutations: nil,
|
||||
expectedError: graphql.ErrQueryNotAllowed,
|
||||
},
|
||||
{
|
||||
name: "nil,",
|
||||
query: `query { user(id: 1) { name } }`,
|
||||
allowedQueries: nil,
|
||||
allowedMutations: []string{"user"},
|
||||
expectedError: graphql.ErrQueryNotAllowed,
|
||||
},
|
||||
{
|
||||
name: ",nil",
|
||||
query: `mutation { user(id: 1) { name } }`,
|
||||
allowedQueries: []string{"user"},
|
||||
allowedMutations: nil,
|
||||
expectedError: graphql.ErrQueryNotAllowed,
|
||||
},
|
||||
{
|
||||
name: "no query allowed",
|
||||
query: `query { user(id: 1) { name } }`,
|
||||
allowedQueries: []string{},
|
||||
allowedMutations: []string{},
|
||||
expectedError: graphql.ErrQueryNotAllowed,
|
||||
},
|
||||
{
|
||||
name: "no mutation allowed",
|
||||
query: `mutation { user(id: 1) { name } }`,
|
||||
allowedQueries: []string{},
|
||||
allowedMutations: []string{},
|
||||
expectedError: graphql.ErrQueryNotAllowed,
|
||||
},
|
||||
{
|
||||
name: "query allowed",
|
||||
query: `query { user(id: 1) { name } }`,
|
||||
allowedQueries: []string{"user"},
|
||||
allowedMutations: []string{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "query not allowed",
|
||||
query: `query { projects(id: 1) { name } }`,
|
||||
allowedQueries: []string{"user"},
|
||||
allowedMutations: []string{},
|
||||
expectedError: graphql.ErrQueryNotAllowed,
|
||||
},
|
||||
{
|
||||
name: "mutation allowed",
|
||||
query: `mutation { user(id: 1) { name } }`,
|
||||
allowedQueries: []string{},
|
||||
allowedMutations: []string{"user"},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "mutation not allowed",
|
||||
query: `mutation { projects(id: 1) { name } }`,
|
||||
allowedQueries: []string{},
|
||||
allowedMutations: []string{"user"},
|
||||
expectedError: graphql.ErrQueryNotAllowed,
|
||||
},
|
||||
{
|
||||
name: "multiple query allowed",
|
||||
query: `query { user(id: 1) { name } projects(id: 1) { name } }`,
|
||||
allowedQueries: []string{"user", "projects"},
|
||||
allowedMutations: []string{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "multiple query not allowed",
|
||||
query: `query { user(id: 1) { name } projects(id: 1) { name } }`,
|
||||
allowedQueries: []string{"user"},
|
||||
allowedMutations: []string{},
|
||||
expectedError: graphql.ErrQueryNotAllowed,
|
||||
},
|
||||
{
|
||||
name: "multiple mutation allowed",
|
||||
query: `mutation { user(id: 1) { name } projects(id: 1) { name } }`,
|
||||
allowedQueries: []string{},
|
||||
allowedMutations: []string{"user", "projects"},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "multiple mutation not allowed",
|
||||
query: `mutation { user(id: 1) { name } projects(id: 1) { name } }`,
|
||||
allowedQueries: []string{},
|
||||
allowedMutations: []string{"user"},
|
||||
expectedError: graphql.ErrQueryNotAllowed,
|
||||
},
|
||||
{
|
||||
name: "nested query allowed",
|
||||
query: `query { user(id: 1) { name projects(id: 1) { name } } }`,
|
||||
allowedQueries: []string{"user", "projects"},
|
||||
allowedMutations: []string{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "nested query not allowed",
|
||||
query: `query { user(id: 1) { name projects(id: 1) { name } } }`,
|
||||
allowedQueries: []string{"user"},
|
||||
allowedMutations: []string{},
|
||||
expectedError: graphql.ErrQueryNotAllowed,
|
||||
},
|
||||
{
|
||||
name: "nested mutation allowed",
|
||||
query: `mutation { user(id: 1) { name projects(id: 1) { name } } }`,
|
||||
allowedQueries: []string{},
|
||||
allowedMutations: []string{"user", "projects"},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "nested mutation not allowed",
|
||||
query: `mutation { user(id: 1) { name projects(id: 1) { name } } }`,
|
||||
allowedQueries: []string{"user"},
|
||||
allowedMutations: []string{},
|
||||
expectedError: graphql.ErrQueryNotAllowed,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := graphql.CheckAllowedGraphqlQuery(
|
||||
tc.allowedQueries,
|
||||
tc.allowedMutations,
|
||||
tc.query,
|
||||
)
|
||||
if !errors.Is(err, tc.expectedError) {
|
||||
t.Errorf("expected error %v, got %v", tc.expectedError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
196156
cli/mcp/graphql/testdata/schema.json
vendored
Normal file
196156
cli/mcp/graphql/testdata/schema.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
6220
cli/mcp/graphql/testdata/with_filter.graphql
vendored
Normal file
6220
cli/mcp/graphql/testdata/with_filter.graphql
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1455
cli/mcp/graphql/testdata/with_filter_and_disable_nesting.graphql
vendored
Normal file
1455
cli/mcp/graphql/testdata/with_filter_and_disable_nesting.graphql
vendored
Normal file
File diff suppressed because it is too large
Load Diff
14517
cli/mcp/graphql/testdata/without_filter.graphql
vendored
Normal file
14517
cli/mcp/graphql/testdata/without_filter.graphql
vendored
Normal file
File diff suppressed because it is too large
Load Diff
102
cli/mcp/graphql/types.go
Normal file
102
cli/mcp/graphql/types.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package graphql
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Kind string
|
||||
|
||||
func (k *Kind) UnmarshalJSON(data []byte) error {
|
||||
var kind string
|
||||
if err := json.Unmarshal(data, &kind); err != nil {
|
||||
return err //nolint:wrapcheck
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case string(KindObject),
|
||||
string(KindNonNull),
|
||||
string(KindList),
|
||||
string(KindScalar),
|
||||
string(KindEnum),
|
||||
string(KindInputObject):
|
||||
*k = Kind(kind)
|
||||
default:
|
||||
return fmt.Errorf("invalid kind: %s", kind) //nolint:err113
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
KindObject Kind = "OBJECT"
|
||||
KindNonNull Kind = "NON_NULL"
|
||||
KindList Kind = "LIST"
|
||||
KindScalar Kind = "SCALAR"
|
||||
KindEnum Kind = "ENUM"
|
||||
KindInputObject Kind = "INPUT_OBJECT"
|
||||
)
|
||||
|
||||
type ResponseIntrospection = Response[IntrospectionResponse]
|
||||
|
||||
type Response[T any] struct {
|
||||
Data T `json:"data"`
|
||||
Errors []Errors `json:"errors"`
|
||||
}
|
||||
|
||||
type Extensions struct {
|
||||
Path string `json:"path"`
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
type Errors struct {
|
||||
Message string `json:"message"`
|
||||
Extensions Extensions `json:"extensions"`
|
||||
}
|
||||
|
||||
type IntrospectionResponse struct {
|
||||
Schema Schema `json:"__schema"`
|
||||
}
|
||||
|
||||
// Schema represents the GraphQL schema.
|
||||
type Schema struct {
|
||||
QueryType Type `json:"queryType"`
|
||||
MutationType *Type `json:"mutationType"`
|
||||
Types []Type `json:"types"`
|
||||
}
|
||||
|
||||
// Type represents a GraphQL type (__Type).
|
||||
type Type struct {
|
||||
Kind Kind `json:"kind"`
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
Fields []Field `json:"fields"`
|
||||
InputFields []InputValue `json:"inputFields"`
|
||||
Interfaces []Type `json:"interfaces"`
|
||||
EnumValues []EnumValue `json:"enumValues"`
|
||||
PossibleTypes []Type `json:"possibleTypes"`
|
||||
// For TypeRef fragment
|
||||
OfType *Type `json:"ofType"`
|
||||
}
|
||||
|
||||
// Field represents a field in a GraphQL type.
|
||||
type Field struct {
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
Args []InputValue `json:"args"`
|
||||
Type Type `json:"type"`
|
||||
}
|
||||
|
||||
// InputValue represents an input value in a GraphQL schema.
|
||||
type InputValue struct {
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
Type Type `json:"type"`
|
||||
DefaultValue *string `json:"defaultValue"`
|
||||
}
|
||||
|
||||
// EnumValue represents an enum value in a GraphQL schema.
|
||||
type EnumValue struct {
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
}
|
||||
3937
cli/mcp/nhost/auth/auth.gen.go
Normal file
3937
cli/mcp/nhost/auth/auth.gen.go
Normal file
File diff suppressed because it is too large
Load Diff
2
cli/mcp/nhost/auth/auth.go
Normal file
2
cli/mcp/nhost/auth/auth.go
Normal file
@@ -0,0 +1,2 @@
|
||||
//go:generate oapi-codegen -generate types,client -response-type-suffix R -package auth -o auth.gen.go openapi.yaml
|
||||
package auth
|
||||
82
cli/mcp/nhost/auth/interceptors.go
Normal file
82
cli/mcp/nhost/auth/interceptors.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrSigninIn = errors.New("error during sign in")
|
||||
|
||||
func WithPAT(
|
||||
url string,
|
||||
pat string,
|
||||
) (func(ctx context.Context, req *http.Request) error, error) {
|
||||
authc, err := NewClientWithResponses(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't not create auth client: %w", err)
|
||||
}
|
||||
|
||||
session := &struct {
|
||||
AccessToken string
|
||||
ExpiresAt time.Time
|
||||
}{
|
||||
AccessToken: "",
|
||||
ExpiresAt: time.Time{},
|
||||
}
|
||||
|
||||
return func(ctx context.Context, req *http.Request) error {
|
||||
if time.Now().Add(time.Minute).After(session.ExpiresAt) {
|
||||
resp, err := authc.PostSigninPatWithResponse(
|
||||
ctx,
|
||||
SignInPATRequest{
|
||||
PersonalAccessToken: pat,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sign in with PAT: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return fmt.Errorf("%w: %s\n%s", ErrSigninIn, resp.Status(), resp.Body)
|
||||
}
|
||||
|
||||
session.AccessToken = resp.JSON200.Session.AccessToken
|
||||
session.ExpiresAt = time.Now().Add(
|
||||
time.Second * time.Duration(resp.JSON200.Session.AccessTokenExpiresIn))
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", "Bearer "+session.AccessToken)
|
||||
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func WithAdminSecret(
|
||||
adminSecret string,
|
||||
) func(ctx context.Context, req *http.Request) error {
|
||||
return func(_ context.Context, req *http.Request) error {
|
||||
req.Header.Add("X-Hasura-Admin-Secret", adminSecret)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithRole(
|
||||
role string,
|
||||
) func(ctx context.Context, req *http.Request) error {
|
||||
return func(_ context.Context, req *http.Request) error {
|
||||
req.Header.Add("X-Hasura-Role", role)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithUserID(
|
||||
userID string,
|
||||
) func(ctx context.Context, req *http.Request) error {
|
||||
return func(_ context.Context, req *http.Request) error {
|
||||
req.Header.Add("X-Hasura-User-Id", userID)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
1202
cli/mcp/nhost/auth/openapi.yaml
Normal file
1202
cli/mcp/nhost/auth/openapi.yaml
Normal file
File diff suppressed because it is too large
Load Diff
1446
cli/mcp/nhost/graphql/graphql.gen.go
Normal file
1446
cli/mcp/nhost/graphql/graphql.gen.go
Normal file
File diff suppressed because it is too large
Load Diff
9
cli/mcp/nhost/graphql/graphql.go
Normal file
9
cli/mcp/nhost/graphql/graphql.go
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:generate oapi-codegen -generate types,client -response-type-suffix R -package graphql -o graphql.gen.go openapi.yaml
|
||||
package graphql
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed openapi.yaml
|
||||
var Schema string
|
||||
1079
cli/mcp/nhost/graphql/openapi.yaml
Normal file
1079
cli/mcp/nhost/graphql/openapi.yaml
Normal file
File diff suppressed because it is too large
Load Diff
69
cli/mcp/resources/cloud.go
Normal file
69
cli/mcp/resources/cloud.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/nhost/nhost/cli/mcp/config"
|
||||
)
|
||||
|
||||
//go:embed cloud_schema.graphql
|
||||
var schemaGraphql string
|
||||
|
||||
//go:embed cloud_schema-with-mutations.graphql
|
||||
var schemaGraphqlWithMutations string
|
||||
|
||||
const (
|
||||
CloudResourceURI = "schema://nhost-cloud"
|
||||
CloudDescription = `Schema to interact with the Nhost Cloud. Projects are equivalent
|
||||
to apps in the schema. IDs are typically uuids.`
|
||||
)
|
||||
|
||||
type Cloud struct {
|
||||
schema string
|
||||
}
|
||||
|
||||
func NewCloud(cfg *config.Config) *Cloud {
|
||||
schema := schemaGraphql
|
||||
if cfg.Cloud.EnableMutations {
|
||||
schema = schemaGraphqlWithMutations
|
||||
}
|
||||
|
||||
return &Cloud{
|
||||
schema: schema,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Cloud) Register(server *server.MCPServer) {
|
||||
server.AddResource(
|
||||
mcp.Resource{
|
||||
URI: CloudResourceURI,
|
||||
Name: "nhost-cloud",
|
||||
Annotated: mcp.Annotated{
|
||||
Annotations: &mcp.Annotations{
|
||||
Audience: []mcp.Role{"agent"},
|
||||
Priority: 9.0, //nolint:mnd
|
||||
},
|
||||
},
|
||||
Description: CloudDescription,
|
||||
MIMEType: "text/plain",
|
||||
Meta: nil,
|
||||
},
|
||||
t.handle,
|
||||
)
|
||||
}
|
||||
|
||||
func (t *Cloud) handle(
|
||||
_ context.Context, request mcp.ReadResourceRequest,
|
||||
) ([]mcp.ResourceContents, error) {
|
||||
return []mcp.ResourceContents{
|
||||
mcp.TextResourceContents{
|
||||
URI: request.Params.URI,
|
||||
MIMEType: "text/plain",
|
||||
Text: t.schema,
|
||||
Meta: nil,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
1365
cli/mcp/resources/cloud_schema-with-mutations.graphql
Normal file
1365
cli/mcp/resources/cloud_schema-with-mutations.graphql
Normal file
File diff suppressed because it is too large
Load Diff
829
cli/mcp/resources/cloud_schema.graphql
Normal file
829
cli/mcp/resources/cloud_schema.graphql
Normal file
@@ -0,0 +1,829 @@
|
||||
scalar Boolean
|
||||
|
||||
scalar ConfigEmail
|
||||
|
||||
scalar ConfigHasuraAPIs
|
||||
|
||||
scalar ConfigInt32
|
||||
|
||||
scalar ConfigLocale
|
||||
|
||||
scalar ConfigPort
|
||||
|
||||
scalar ConfigUint
|
||||
|
||||
scalar ConfigUint32
|
||||
|
||||
scalar ConfigUint8
|
||||
|
||||
scalar ConfigUrl
|
||||
|
||||
scalar ConfigUserRole
|
||||
|
||||
scalar Float
|
||||
|
||||
scalar Int
|
||||
|
||||
scalar String
|
||||
|
||||
scalar jsonb
|
||||
|
||||
scalar timestamptz
|
||||
|
||||
scalar uuid
|
||||
|
||||
type ConfigAI {
|
||||
autoEmbeddings: ConfigAIAutoEmbeddings
|
||||
openai: ConfigAIOpenai!
|
||||
resources: ConfigAIResources!
|
||||
version: String
|
||||
webhookSecret: String!
|
||||
}
|
||||
|
||||
type ConfigAIAutoEmbeddings {
|
||||
synchPeriodMinutes: ConfigUint32
|
||||
}
|
||||
|
||||
type ConfigAIOpenai {
|
||||
apiKey: String!
|
||||
organization: String
|
||||
}
|
||||
|
||||
type ConfigAIResources {
|
||||
compute: ConfigComputeResources!
|
||||
}
|
||||
|
||||
type ConfigAuth {
|
||||
elevatedPrivileges: ConfigAuthElevatedPrivileges
|
||||
method: ConfigAuthMethod
|
||||
misc: ConfigAuthMisc
|
||||
rateLimit: ConfigAuthRateLimit
|
||||
redirections: ConfigAuthRedirections
|
||||
resources: ConfigResources
|
||||
session: ConfigAuthSession
|
||||
signUp: ConfigAuthSignUp
|
||||
totp: ConfigAuthTotp
|
||||
user: ConfigAuthUser
|
||||
version: String
|
||||
}
|
||||
|
||||
type ConfigAuthElevatedPrivileges {
|
||||
mode: String
|
||||
}
|
||||
|
||||
type ConfigAuthMethod {
|
||||
anonymous: ConfigAuthMethodAnonymous
|
||||
emailPassword: ConfigAuthMethodEmailPassword
|
||||
emailPasswordless: ConfigAuthMethodEmailPasswordless
|
||||
oauth: ConfigAuthMethodOauth
|
||||
otp: ConfigAuthMethodOtp
|
||||
smsPasswordless: ConfigAuthMethodSmsPasswordless
|
||||
webauthn: ConfigAuthMethodWebauthn
|
||||
}
|
||||
|
||||
type ConfigAuthMethodAnonymous {
|
||||
enabled: Boolean
|
||||
}
|
||||
|
||||
type ConfigAuthMethodEmailPassword {
|
||||
emailVerificationRequired: Boolean
|
||||
hibpEnabled: Boolean
|
||||
passwordMinLength: ConfigUint8
|
||||
}
|
||||
|
||||
type ConfigAuthMethodEmailPasswordless {
|
||||
enabled: Boolean
|
||||
}
|
||||
|
||||
type ConfigAuthMethodOauth {
|
||||
apple: ConfigAuthMethodOauthApple
|
||||
azuread: ConfigAuthMethodOauthAzuread
|
||||
bitbucket: ConfigStandardOauthProvider
|
||||
discord: ConfigStandardOauthProviderWithScope
|
||||
facebook: ConfigStandardOauthProviderWithScope
|
||||
github: ConfigStandardOauthProviderWithScope
|
||||
gitlab: ConfigStandardOauthProviderWithScope
|
||||
google: ConfigStandardOauthProviderWithScope
|
||||
linkedin: ConfigStandardOauthProviderWithScope
|
||||
spotify: ConfigStandardOauthProviderWithScope
|
||||
strava: ConfigStandardOauthProviderWithScope
|
||||
twitch: ConfigStandardOauthProviderWithScope
|
||||
twitter: ConfigAuthMethodOauthTwitter
|
||||
windowslive: ConfigStandardOauthProviderWithScope
|
||||
workos: ConfigAuthMethodOauthWorkos
|
||||
}
|
||||
|
||||
type ConfigAuthMethodOauthApple {
|
||||
audience: String
|
||||
clientId: String
|
||||
enabled: Boolean
|
||||
keyId: String
|
||||
privateKey: String
|
||||
scope: [String!]
|
||||
teamId: String
|
||||
}
|
||||
|
||||
type ConfigAuthMethodOauthAzuread {
|
||||
clientId: String
|
||||
clientSecret: String
|
||||
enabled: Boolean
|
||||
tenant: String
|
||||
}
|
||||
|
||||
type ConfigAuthMethodOauthTwitter {
|
||||
consumerKey: String
|
||||
consumerSecret: String
|
||||
enabled: Boolean
|
||||
}
|
||||
|
||||
type ConfigAuthMethodOauthWorkos {
|
||||
clientId: String
|
||||
clientSecret: String
|
||||
connection: String
|
||||
enabled: Boolean
|
||||
organization: String
|
||||
}
|
||||
|
||||
type ConfigAuthMethodOtp {
|
||||
email: ConfigAuthMethodOtpEmail
|
||||
}
|
||||
|
||||
type ConfigAuthMethodOtpEmail {
|
||||
enabled: Boolean
|
||||
}
|
||||
|
||||
type ConfigAuthMethodSmsPasswordless {
|
||||
enabled: Boolean
|
||||
}
|
||||
|
||||
type ConfigAuthMethodWebauthn {
|
||||
attestation: ConfigAuthMethodWebauthnAttestation
|
||||
enabled: Boolean
|
||||
relyingParty: ConfigAuthMethodWebauthnRelyingParty
|
||||
}
|
||||
|
||||
type ConfigAuthMethodWebauthnAttestation {
|
||||
timeout: ConfigUint32
|
||||
}
|
||||
|
||||
type ConfigAuthMethodWebauthnRelyingParty {
|
||||
id: String
|
||||
name: String
|
||||
origins: [ConfigUrl!]
|
||||
}
|
||||
|
||||
type ConfigAuthMisc {
|
||||
concealErrors: Boolean
|
||||
}
|
||||
|
||||
type ConfigAuthRateLimit {
|
||||
bruteForce: ConfigRateLimit
|
||||
emails: ConfigRateLimit
|
||||
global: ConfigRateLimit
|
||||
signups: ConfigRateLimit
|
||||
sms: ConfigRateLimit
|
||||
}
|
||||
|
||||
type ConfigAuthRedirections {
|
||||
allowedUrls: [String!]
|
||||
clientUrl: ConfigUrl
|
||||
}
|
||||
|
||||
type ConfigAuthSession {
|
||||
accessToken: ConfigAuthSessionAccessToken
|
||||
refreshToken: ConfigAuthSessionRefreshToken
|
||||
}
|
||||
|
||||
type ConfigAuthSessionAccessToken {
|
||||
customClaims: [ConfigAuthsessionaccessTokenCustomClaims!]
|
||||
expiresIn: ConfigUint32
|
||||
}
|
||||
|
||||
type ConfigAuthSessionRefreshToken {
|
||||
expiresIn: ConfigUint32
|
||||
}
|
||||
|
||||
type ConfigAuthSignUp {
|
||||
disableNewUsers: Boolean
|
||||
enabled: Boolean
|
||||
turnstile: ConfigAuthSignUpTurnstile
|
||||
}
|
||||
|
||||
type ConfigAuthSignUpTurnstile {
|
||||
secretKey: String!
|
||||
}
|
||||
|
||||
type ConfigAuthTotp {
|
||||
enabled: Boolean
|
||||
issuer: String
|
||||
}
|
||||
|
||||
type ConfigAuthUser {
|
||||
email: ConfigAuthUserEmail
|
||||
emailDomains: ConfigAuthUserEmailDomains
|
||||
gravatar: ConfigAuthUserGravatar
|
||||
locale: ConfigAuthUserLocale
|
||||
roles: ConfigAuthUserRoles
|
||||
}
|
||||
|
||||
type ConfigAuthUserEmail {
|
||||
allowed: [ConfigEmail!]
|
||||
blocked: [ConfigEmail!]
|
||||
}
|
||||
|
||||
type ConfigAuthUserEmailDomains {
|
||||
allowed: [String!]
|
||||
blocked: [String!]
|
||||
}
|
||||
|
||||
type ConfigAuthUserGravatar {
|
||||
default: String
|
||||
enabled: Boolean
|
||||
rating: String
|
||||
}
|
||||
|
||||
type ConfigAuthUserLocale {
|
||||
allowed: [ConfigLocale!]
|
||||
default: ConfigLocale
|
||||
}
|
||||
|
||||
type ConfigAuthUserRoles {
|
||||
allowed: [ConfigUserRole!]
|
||||
default: ConfigUserRole
|
||||
}
|
||||
|
||||
type ConfigAuthsessionaccessTokenCustomClaims {
|
||||
key: String!
|
||||
value: String!
|
||||
}
|
||||
|
||||
type ConfigAutoscaler {
|
||||
maxReplicas: ConfigUint8!
|
||||
}
|
||||
|
||||
type ConfigClaimMap {
|
||||
claim: String!
|
||||
default: String
|
||||
path: String
|
||||
value: String
|
||||
}
|
||||
|
||||
type ConfigComputeResources {
|
||||
cpu: ConfigUint32!
|
||||
memory: ConfigUint32!
|
||||
}
|
||||
|
||||
type ConfigConfig {
|
||||
ai: ConfigAI
|
||||
auth: ConfigAuth
|
||||
functions: ConfigFunctions
|
||||
global: ConfigGlobal
|
||||
graphql: ConfigGraphql
|
||||
hasura: ConfigHasura!
|
||||
observability: ConfigObservability!
|
||||
postgres: ConfigPostgres!
|
||||
provider: ConfigProvider
|
||||
storage: ConfigStorage
|
||||
}
|
||||
|
||||
type ConfigFunctions {
|
||||
node: ConfigFunctionsNode
|
||||
rateLimit: ConfigRateLimit
|
||||
resources: ConfigFunctionsResources
|
||||
}
|
||||
|
||||
type ConfigFunctionsNode {
|
||||
version: Int
|
||||
}
|
||||
|
||||
type ConfigFunctionsResources {
|
||||
networking: ConfigNetworking
|
||||
}
|
||||
|
||||
type ConfigGlobal {
|
||||
environment: [ConfigGlobalEnvironmentVariable!]
|
||||
}
|
||||
|
||||
type ConfigGlobalEnvironmentVariable {
|
||||
name: String!
|
||||
value: String!
|
||||
}
|
||||
|
||||
type ConfigGrafana {
|
||||
adminPassword: String!
|
||||
alerting: ConfigGrafanaAlerting
|
||||
contacts: ConfigGrafanaContacts
|
||||
smtp: ConfigGrafanaSmtp
|
||||
}
|
||||
|
||||
type ConfigGrafanaAlerting {
|
||||
enabled: Boolean
|
||||
}
|
||||
|
||||
type ConfigGrafanaContacts {
|
||||
discord: [ConfigGrafanacontactsDiscord!]
|
||||
emails: [String!]
|
||||
pagerduty: [ConfigGrafanacontactsPagerduty!]
|
||||
slack: [ConfigGrafanacontactsSlack!]
|
||||
webhook: [ConfigGrafanacontactsWebhook!]
|
||||
}
|
||||
|
||||
type ConfigGrafanaSmtp {
|
||||
host: String!
|
||||
password: String!
|
||||
port: ConfigPort!
|
||||
sender: String!
|
||||
user: String!
|
||||
}
|
||||
|
||||
type ConfigGrafanacontactsDiscord {
|
||||
avatarUrl: String!
|
||||
url: String!
|
||||
}
|
||||
|
||||
type ConfigGrafanacontactsPagerduty {
|
||||
class: String!
|
||||
component: String!
|
||||
group: String!
|
||||
integrationKey: String!
|
||||
severity: String!
|
||||
}
|
||||
|
||||
type ConfigGrafanacontactsSlack {
|
||||
endpointURL: String!
|
||||
iconEmoji: String!
|
||||
iconURL: String!
|
||||
mentionChannel: String!
|
||||
mentionGroups: [String!]!
|
||||
mentionUsers: [String!]!
|
||||
recipient: String!
|
||||
token: String!
|
||||
url: String!
|
||||
username: String!
|
||||
}
|
||||
|
||||
type ConfigGrafanacontactsWebhook {
|
||||
authorizationCredentials: String!
|
||||
authorizationScheme: String!
|
||||
httpMethod: String!
|
||||
maxAlerts: Int!
|
||||
password: String!
|
||||
url: String!
|
||||
username: String!
|
||||
}
|
||||
|
||||
type ConfigGraphql {
|
||||
security: ConfigGraphqlSecurity
|
||||
}
|
||||
|
||||
type ConfigGraphqlSecurity {
|
||||
forbidAminSecret: Boolean
|
||||
maxDepthQueries: ConfigUint
|
||||
}
|
||||
|
||||
type ConfigHasura {
|
||||
adminSecret: String!
|
||||
authHook: ConfigHasuraAuthHook
|
||||
events: ConfigHasuraEvents
|
||||
jwtSecrets: [ConfigJWTSecret!]
|
||||
logs: ConfigHasuraLogs
|
||||
rateLimit: ConfigRateLimit
|
||||
resources: ConfigResources
|
||||
settings: ConfigHasuraSettings
|
||||
version: String
|
||||
webhookSecret: String!
|
||||
}
|
||||
|
||||
type ConfigHasuraAuthHook {
|
||||
mode: String
|
||||
sendRequestBody: Boolean
|
||||
url: String!
|
||||
}
|
||||
|
||||
type ConfigHasuraEvents {
|
||||
httpPoolSize: ConfigUint32
|
||||
}
|
||||
|
||||
type ConfigHasuraLogs {
|
||||
level: String
|
||||
}
|
||||
|
||||
type ConfigHasuraSettings {
|
||||
corsDomain: [ConfigUrl!]
|
||||
devMode: Boolean
|
||||
enableAllowList: Boolean
|
||||
enableConsole: Boolean
|
||||
enableRemoteSchemaPermissions: Boolean
|
||||
enabledAPIs: [ConfigHasuraAPIs!]
|
||||
inferFunctionPermissions: Boolean
|
||||
liveQueriesMultiplexedRefetchInterval: ConfigUint32
|
||||
stringifyNumericTypes: Boolean
|
||||
}
|
||||
|
||||
type ConfigIngress {
|
||||
fqdn: [String!]
|
||||
tls: ConfigIngressTls
|
||||
}
|
||||
|
||||
type ConfigIngressTls {
|
||||
clientCA: String
|
||||
}
|
||||
|
||||
type ConfigJWTSecret {
|
||||
allowed_skew: ConfigUint32
|
||||
audience: String
|
||||
claims_format: String
|
||||
claims_map: [ConfigClaimMap!]
|
||||
claims_namespace: String
|
||||
claims_namespace_path: String
|
||||
header: String
|
||||
issuer: String
|
||||
jwk_url: ConfigUrl
|
||||
key: String
|
||||
kid: String
|
||||
signingKey: String
|
||||
type: String
|
||||
}
|
||||
|
||||
type ConfigNetworking {
|
||||
ingresses: [ConfigIngress!]
|
||||
}
|
||||
|
||||
type ConfigObservability {
|
||||
grafana: ConfigGrafana!
|
||||
}
|
||||
|
||||
type ConfigPostgres {
|
||||
pitr: ConfigPostgresPitr
|
||||
resources: ConfigPostgresResources!
|
||||
settings: ConfigPostgresSettings
|
||||
version: String
|
||||
}
|
||||
|
||||
type ConfigPostgresPitr {
|
||||
retention: ConfigUint8
|
||||
}
|
||||
|
||||
type ConfigPostgresResources {
|
||||
compute: ConfigResourcesCompute
|
||||
enablePublicAccess: Boolean
|
||||
replicas: Int
|
||||
storage: ConfigPostgresResourcesStorage!
|
||||
}
|
||||
|
||||
type ConfigPostgresResourcesStorage {
|
||||
capacity: ConfigUint32!
|
||||
}
|
||||
|
||||
type ConfigPostgresSettings {
|
||||
archiveTimeout: ConfigInt32
|
||||
checkpointCompletionTarget: Float
|
||||
defaultStatisticsTarget: ConfigInt32
|
||||
effectiveCacheSize: String
|
||||
effectiveIOConcurrency: ConfigInt32
|
||||
hugePages: String
|
||||
jit: String
|
||||
maintenanceWorkMem: String
|
||||
maxConnections: ConfigInt32
|
||||
maxParallelMaintenanceWorkers: ConfigInt32
|
||||
maxParallelWorkers: ConfigInt32
|
||||
maxParallelWorkersPerGather: ConfigInt32
|
||||
maxReplicationSlots: ConfigInt32
|
||||
maxWalSenders: ConfigInt32
|
||||
maxWalSize: String
|
||||
maxWorkerProcesses: ConfigInt32
|
||||
minWalSize: String
|
||||
randomPageCost: Float
|
||||
sharedBuffers: String
|
||||
walBuffers: String
|
||||
walLevel: String
|
||||
workMem: String
|
||||
}
|
||||
|
||||
type ConfigProvider {
|
||||
sms: ConfigSms
|
||||
smtp: ConfigSmtp
|
||||
}
|
||||
|
||||
type ConfigRateLimit {
|
||||
interval: String!
|
||||
limit: ConfigUint32!
|
||||
}
|
||||
|
||||
type ConfigResources {
|
||||
autoscaler: ConfigAutoscaler
|
||||
compute: ConfigResourcesCompute
|
||||
networking: ConfigNetworking
|
||||
replicas: ConfigUint8
|
||||
}
|
||||
|
||||
type ConfigResourcesCompute {
|
||||
cpu: ConfigUint32!
|
||||
memory: ConfigUint32!
|
||||
}
|
||||
|
||||
type ConfigSms {
|
||||
accountSid: String!
|
||||
authToken: String!
|
||||
messagingServiceId: String!
|
||||
provider: String
|
||||
}
|
||||
|
||||
type ConfigSmtp {
|
||||
host: String!
|
||||
method: String!
|
||||
password: String!
|
||||
port: ConfigPort!
|
||||
secure: Boolean!
|
||||
sender: String!
|
||||
user: String!
|
||||
}
|
||||
|
||||
type ConfigStandardOauthProvider {
|
||||
clientId: String
|
||||
clientSecret: String
|
||||
enabled: Boolean
|
||||
}
|
||||
|
||||
type ConfigStandardOauthProviderWithScope {
|
||||
audience: String
|
||||
clientId: String
|
||||
clientSecret: String
|
||||
enabled: Boolean
|
||||
scope: [String!]
|
||||
}
|
||||
|
||||
type ConfigStorage {
|
||||
antivirus: ConfigStorageAntivirus
|
||||
rateLimit: ConfigRateLimit
|
||||
resources: ConfigResources
|
||||
version: String
|
||||
}
|
||||
|
||||
type ConfigStorageAntivirus {
|
||||
server: String
|
||||
}
|
||||
|
||||
type apps {
|
||||
appSecrets: [ConfigEnvironmentVariable!]!
|
||||
"""An array relationship"""
|
||||
appStates(distinct_on: [appStateHistory_select_column!], limit: Int, offset: Int, order_by: [appStateHistory_order_by!]): [appStateHistory!]!
|
||||
automaticDeploys: Boolean!
|
||||
"""An array relationship"""
|
||||
backups(distinct_on: [backups_select_column!], limit: Int, offset: Int, order_by: [backups_order_by!]): [backups!]!
|
||||
config(resolve: Boolean!): ConfigConfig
|
||||
createdAt: timestamptz!
|
||||
creatorUserId: uuid
|
||||
"""An array relationship"""
|
||||
deployments(distinct_on: [deployments_select_column!], limit: Int, offset: Int, order_by: [deployments_order_by!]): [deployments!]!
|
||||
desiredState: Int!
|
||||
"""An array relationship"""
|
||||
featureFlags(distinct_on: [featureFlags_select_column!], limit: Int, offset: Int, order_by: [featureFlags_order_by!]): [featureFlags!]!
|
||||
githubRepositoryId: uuid
|
||||
id: uuid!
|
||||
isLocked: Boolean
|
||||
isLockedReason: String
|
||||
metadataFunctions(path: String): jsonb!
|
||||
name: String!
|
||||
nhostBaseFolder: String!
|
||||
"""An object relationship"""
|
||||
organization: organizations
|
||||
organizationID: uuid
|
||||
"""An object relationship"""
|
||||
region: regions!
|
||||
repositoryProductionBranch: String!
|
||||
"""An array relationship"""
|
||||
runServices(distinct_on: [run_service_select_column!], limit: Int, offset: Int, order_by: [run_service_order_by!]): [run_service!]!
|
||||
"""An aggregate relationship"""
|
||||
runServices_aggregate(distinct_on: [run_service_select_column!], limit: Int, offset: Int, order_by: [run_service_order_by!]): run_service_aggregate!
|
||||
slug: String!
|
||||
subdomain: String!
|
||||
updatedAt: timestamptz!
|
||||
workspaceId: uuid
|
||||
}
|
||||
|
||||
type organizations {
|
||||
"""An array relationship"""
|
||||
allowedPrivateRegions(distinct_on: [regions_allowed_organization_select_column!], limit: Int, offset: Int, order_by: [regions_allowed_organization_order_by!]): [regions_allowed_organization!]!
|
||||
"""An array relationship"""
|
||||
apps(distinct_on: [apps_select_column!], limit: Int, offset: Int, order_by: [apps_order_by!], where: apps_bool_exp): [apps!]!
|
||||
createdAt: timestamptz!
|
||||
current_threshold: organization_costs_thresholds_enum!
|
||||
id: uuid!
|
||||
"""An array relationship"""
|
||||
invites(distinct_on: [organization_member_invites_select_column!], limit: Int, offset: Int, order_by: [organization_member_invites_order_by!]): [organization_member_invites!]!
|
||||
"""An array relationship"""
|
||||
members(distinct_on: [organization_members_select_column!], limit: Int, offset: Int, order_by: [organization_members_order_by!]): [organization_members!]!
|
||||
name: String!
|
||||
"""An object relationship"""
|
||||
plan: plans!
|
||||
planID: uuid!
|
||||
slug: String!
|
||||
status: organization_status_enum!
|
||||
threshold: Int!
|
||||
updatedAt: timestamptz!
|
||||
}
|
||||
|
||||
input Boolean_comparison_exp {
|
||||
_eq: Boolean
|
||||
_gt: Boolean
|
||||
_gte: Boolean
|
||||
_in: [Boolean!]
|
||||
_is_null: Boolean
|
||||
_lt: Boolean
|
||||
_lte: Boolean
|
||||
_neq: Boolean
|
||||
_nin: [Boolean!]
|
||||
}
|
||||
|
||||
input Int_comparison_exp {
|
||||
_eq: Int
|
||||
_gt: Int
|
||||
_gte: Int
|
||||
_in: [Int!]
|
||||
_is_null: Boolean
|
||||
_lt: Int
|
||||
_lte: Int
|
||||
_neq: Int
|
||||
_nin: [Int!]
|
||||
}
|
||||
|
||||
input String_comparison_exp {
|
||||
_eq: String
|
||||
_gt: String
|
||||
_gte: String
|
||||
"""does the column match the given case-insensitive pattern"""
|
||||
_ilike: String
|
||||
_in: [String!]
|
||||
"""does the column match the given POSIX regular expression, case insensitive"""
|
||||
_iregex: String
|
||||
_is_null: Boolean
|
||||
"""does the column match the given pattern"""
|
||||
_like: String
|
||||
_lt: String
|
||||
_lte: String
|
||||
_neq: String
|
||||
"""does the column NOT match the given case-insensitive pattern"""
|
||||
_nilike: String
|
||||
_nin: [String!]
|
||||
"""does the column NOT match the given POSIX regular expression, case insensitive"""
|
||||
_niregex: String
|
||||
"""does the column NOT match the given pattern"""
|
||||
_nlike: String
|
||||
"""does the column NOT match the given POSIX regular expression, case sensitive"""
|
||||
_nregex: String
|
||||
"""does the column NOT match the given SQL regular expression"""
|
||||
_nsimilar: String
|
||||
"""does the column match the given POSIX regular expression, case sensitive"""
|
||||
_regex: String
|
||||
"""does the column match the given SQL regular expression"""
|
||||
_similar: String
|
||||
}
|
||||
|
||||
input apps_bool_exp {
|
||||
_and: [apps_bool_exp!]
|
||||
_not: apps_bool_exp
|
||||
_or: [apps_bool_exp!]
|
||||
automaticDeploys: Boolean_comparison_exp
|
||||
createdAt: timestamptz_comparison_exp
|
||||
creatorUserId: uuid_comparison_exp
|
||||
desiredState: Int_comparison_exp
|
||||
githubRepositoryId: uuid_comparison_exp
|
||||
id: uuid_comparison_exp
|
||||
isLocked: Boolean_comparison_exp
|
||||
isLockedReason: String_comparison_exp
|
||||
metadataFunctions: jsonb_comparison_exp
|
||||
name: String_comparison_exp
|
||||
nhostBaseFolder: String_comparison_exp
|
||||
organization: organizations_bool_exp
|
||||
organizationID: uuid_comparison_exp
|
||||
repositoryProductionBranch: String_comparison_exp
|
||||
slug: String_comparison_exp
|
||||
subdomain: String_comparison_exp
|
||||
updatedAt: timestamptz_comparison_exp
|
||||
workspaceId: uuid_comparison_exp
|
||||
}
|
||||
|
||||
input apps_order_by {
|
||||
automaticDeploys: order_by
|
||||
createdAt: order_by
|
||||
creatorUserId: order_by
|
||||
desiredState: order_by
|
||||
githubRepositoryId: order_by
|
||||
id: order_by
|
||||
isLocked: order_by
|
||||
isLockedReason: order_by
|
||||
metadataFunctions: order_by
|
||||
name: order_by
|
||||
nhostBaseFolder: order_by
|
||||
organization: organizations_order_by
|
||||
organizationID: order_by
|
||||
repositoryProductionBranch: order_by
|
||||
slug: order_by
|
||||
subdomain: order_by
|
||||
updatedAt: order_by
|
||||
workspaceId: order_by
|
||||
}
|
||||
|
||||
input jsonb_comparison_exp {
|
||||
"""is the column contained in the given json value"""
|
||||
_contained_in: jsonb
|
||||
"""does the column contain the given json value at the top level"""
|
||||
_contains: jsonb
|
||||
_eq: jsonb
|
||||
_gt: jsonb
|
||||
_gte: jsonb
|
||||
"""does the string exist as a top-level key in the column"""
|
||||
_has_key: String
|
||||
"""do all of these strings exist as top-level keys in the column"""
|
||||
_has_keys_all: [String!]
|
||||
"""do any of these strings exist as top-level keys in the column"""
|
||||
_has_keys_any: [String!]
|
||||
_in: [jsonb!]
|
||||
_is_null: Boolean
|
||||
_lt: jsonb
|
||||
_lte: jsonb
|
||||
_neq: jsonb
|
||||
_nin: [jsonb!]
|
||||
}
|
||||
|
||||
input organization_costs_thresholds_enum_comparison_exp {
|
||||
_eq: organization_costs_thresholds_enum
|
||||
_in: [organization_costs_thresholds_enum!]
|
||||
_is_null: Boolean
|
||||
_neq: organization_costs_thresholds_enum
|
||||
_nin: [organization_costs_thresholds_enum!]
|
||||
}
|
||||
|
||||
input organization_status_enum_comparison_exp {
|
||||
_eq: organization_status_enum
|
||||
_in: [organization_status_enum!]
|
||||
_is_null: Boolean
|
||||
_neq: organization_status_enum
|
||||
_nin: [organization_status_enum!]
|
||||
}
|
||||
|
||||
input organizations_bool_exp {
|
||||
_and: [organizations_bool_exp!]
|
||||
_not: organizations_bool_exp
|
||||
_or: [organizations_bool_exp!]
|
||||
apps: apps_bool_exp
|
||||
createdAt: timestamptz_comparison_exp
|
||||
current_threshold: organization_costs_thresholds_enum_comparison_exp
|
||||
id: uuid_comparison_exp
|
||||
name: String_comparison_exp
|
||||
planID: uuid_comparison_exp
|
||||
slug: String_comparison_exp
|
||||
status: organization_status_enum_comparison_exp
|
||||
threshold: Int_comparison_exp
|
||||
updatedAt: timestamptz_comparison_exp
|
||||
}
|
||||
|
||||
input organizations_order_by {
|
||||
createdAt: order_by
|
||||
current_threshold: order_by
|
||||
id: order_by
|
||||
name: order_by
|
||||
planID: order_by
|
||||
slug: order_by
|
||||
status: order_by
|
||||
threshold: order_by
|
||||
updatedAt: order_by
|
||||
}
|
||||
|
||||
input timestamptz_comparison_exp {
|
||||
_eq: timestamptz
|
||||
_gt: timestamptz
|
||||
_gte: timestamptz
|
||||
_in: [timestamptz!]
|
||||
_is_null: Boolean
|
||||
_lt: timestamptz
|
||||
_lte: timestamptz
|
||||
_neq: timestamptz
|
||||
_nin: [timestamptz!]
|
||||
}
|
||||
|
||||
input uuid_comparison_exp {
|
||||
_eq: uuid
|
||||
_gt: uuid
|
||||
_gte: uuid
|
||||
_in: [uuid!]
|
||||
_is_null: Boolean
|
||||
_lt: uuid
|
||||
_lte: uuid
|
||||
_neq: uuid
|
||||
_nin: [uuid!]
|
||||
}
|
||||
|
||||
type Query {
|
||||
"""fetch data from the table: "apps" using primary key columns"""
|
||||
app(id: uuid!): apps
|
||||
"""An array relationship"""
|
||||
apps(distinct_on: [apps_select_column!], limit: Int, offset: Int, order_by: [apps_order_by!], where: apps_bool_exp): [apps!]!
|
||||
config(appID: uuid!, resolve: Boolean!): ConfigConfig
|
||||
"""fetch data from the table: "organizations" using primary key columns"""
|
||||
organization(id: uuid!): organizations
|
||||
"""An array relationship"""
|
||||
organizations(distinct_on: [organizations_select_column!], limit: Int, offset: Int, order_by: [organizations_order_by!], where: organizations_bool_exp): [organizations!]!
|
||||
}
|
||||
|
||||
54
cli/mcp/resources/graphql_management_schema.go
Normal file
54
cli/mcp/resources/graphql_management_schema.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/nhost/nhost/cli/mcp/nhost/graphql"
|
||||
)
|
||||
|
||||
const (
|
||||
GraphqlManagementResourceURI = "schema://graphql-management"
|
||||
GraphqlManagementDescription = `GraphQL's management schema for an Nhost project.
|
||||
This tool is useful to properly understand how manage hasura metadata, migrations,
|
||||
permissions, remote schemas, etc.`
|
||||
)
|
||||
|
||||
type GraphqlManagement struct{}
|
||||
|
||||
func NewGraphqlManagement() *GraphqlManagement {
|
||||
return &GraphqlManagement{}
|
||||
}
|
||||
|
||||
func (t *GraphqlManagement) Register(server *server.MCPServer) {
|
||||
server.AddResource(
|
||||
mcp.Resource{
|
||||
URI: GraphqlManagementResourceURI,
|
||||
Name: "graphql-management",
|
||||
Annotated: mcp.Annotated{
|
||||
Annotations: &mcp.Annotations{
|
||||
Audience: []mcp.Role{"agent"},
|
||||
Priority: 9.0, //nolint:mnd
|
||||
},
|
||||
},
|
||||
Description: GraphqlManagementDescription,
|
||||
MIMEType: "text/plain",
|
||||
Meta: nil,
|
||||
},
|
||||
t.handle,
|
||||
)
|
||||
}
|
||||
|
||||
func (t *GraphqlManagement) handle(
|
||||
_ context.Context, request mcp.ReadResourceRequest,
|
||||
) ([]mcp.ResourceContents, error) {
|
||||
return []mcp.ResourceContents{
|
||||
mcp.TextResourceContents{
|
||||
URI: request.Params.URI,
|
||||
MIMEType: "text/plain",
|
||||
Text: graphql.Schema,
|
||||
Meta: nil,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
57
cli/mcp/resources/nhost_toml.go
Normal file
57
cli/mcp/resources/nhost_toml.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
)
|
||||
|
||||
//go:embed nhost_toml_schema.cue
|
||||
var schemaNhostToml string
|
||||
|
||||
const (
|
||||
NhostTomlResourceURI = "schema://nhost.toml"
|
||||
NhostTomlResourceDescription = `Cuelang schema for the nhost.toml configuration file. Run nhost
|
||||
config validate after making changes to your nhost.toml file to ensure it is valid.`
|
||||
)
|
||||
|
||||
type NhostToml struct{}
|
||||
|
||||
func NewNhostToml() *NhostToml {
|
||||
return &NhostToml{}
|
||||
}
|
||||
|
||||
func (t *NhostToml) Register(server *server.MCPServer) {
|
||||
server.AddResource(
|
||||
mcp.Resource{
|
||||
URI: NhostTomlResourceURI,
|
||||
Name: "nhost.toml",
|
||||
Annotated: mcp.Annotated{
|
||||
Annotations: &mcp.Annotations{
|
||||
Audience: []mcp.Role{"agent"},
|
||||
Priority: 9.0, //nolint:mnd
|
||||
},
|
||||
},
|
||||
Description: NhostTomlResourceDescription,
|
||||
MIMEType: "text/plain",
|
||||
Meta: nil,
|
||||
},
|
||||
t.handle,
|
||||
)
|
||||
}
|
||||
|
||||
//go:generate cp ../../../vendor/github.com/nhost/be/services/mimir/schema/schema.cue nhost_toml_schema.cue
|
||||
func (t *NhostToml) handle(
|
||||
_ context.Context, request mcp.ReadResourceRequest,
|
||||
) ([]mcp.ResourceContents, error) {
|
||||
return []mcp.ResourceContents{
|
||||
mcp.TextResourceContents{
|
||||
URI: request.Params.URI,
|
||||
MIMEType: "text/plain",
|
||||
Text: schemaNhostToml,
|
||||
Meta: nil,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
809
cli/mcp/resources/nhost_toml_schema.cue
Normal file
809
cli/mcp/resources/nhost_toml_schema.cue
Normal file
@@ -0,0 +1,809 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"list"
|
||||
"math"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// main entrypoint to the configuration
|
||||
#Config: {
|
||||
// Global configuration that applies to all services
|
||||
global: #Global
|
||||
|
||||
// Configuration for hasura
|
||||
hasura: #Hasura
|
||||
|
||||
// Advanced configuration for GraphQL
|
||||
graphql?: #Graphql
|
||||
|
||||
// Configuration for functions service
|
||||
functions: #Functions
|
||||
|
||||
// Configuration for auth service
|
||||
auth: #Auth
|
||||
|
||||
// Configuration for postgres service
|
||||
postgres: #Postgres
|
||||
|
||||
// Configuration for third party providers like SMTP, SMS, etc.
|
||||
provider: #Provider
|
||||
|
||||
// Configuration for storage service
|
||||
storage: #Storage
|
||||
|
||||
// Configuration for graphite service
|
||||
ai?: #AI
|
||||
|
||||
// Configuration for observability service
|
||||
observability: #Observability
|
||||
|
||||
_totalResourcesCPU: (
|
||||
hasura.resources.replicas*hasura.resources.compute.cpu +
|
||||
auth.resources.replicas*auth.resources.compute.cpu +
|
||||
storage.resources.replicas*storage.resources.compute.cpu +
|
||||
postgres.resources.compute.cpu) @cuegraph(skip)
|
||||
|
||||
_totalResourcesMemory: (
|
||||
hasura.resources.replicas*hasura.resources.compute.memory +
|
||||
auth.resources.replicas*auth.resources.compute.memory +
|
||||
storage.resources.replicas*storage.resources.compute.memory +
|
||||
postgres.resources.compute.memory) @cuegraph(skip)
|
||||
|
||||
_validateResourcesTotalCpuMemoryRatioMustBe1For2: (
|
||||
_totalResourcesCPU*2.048 & _totalResourcesMemory*1.0) @cuegraph(skip)
|
||||
|
||||
_validateResourcesTotalCpuMin1000: (
|
||||
hasura.resources.compute.cpu+
|
||||
auth.resources.compute.cpu+
|
||||
storage.resources.compute.cpu+
|
||||
postgres.resources.compute.cpu) >= 1000 & true @cuegraph(skip)
|
||||
|
||||
_validateAllResourcesAreSetOrNot: (
|
||||
((hasura.resources.compute != _|_) == (auth.resources.compute != _|_)) &&
|
||||
((auth.resources.compute != _|_) == (storage.resources.compute != _|_)) &&
|
||||
((storage.resources.compute != _|_) == (postgres.resources.compute != _|_))) & true @cuegraph(skip)
|
||||
|
||||
_validateNetworkingMustBeNullOrNotSet: !storage.resources.networking | storage.resources.networking == null @cuegraph(skip)
|
||||
|
||||
_isProviderSMTPSet: provider.smtp != _|_ @cuegraph(skip)
|
||||
_isAuthRateLimitEmailsDefault: auth.rateLimit.emails.limit == 10 && auth.rateLimit.emails.interval == "1h" @cuegraph(skip)
|
||||
_validateAuthRateLimitEmailsIsDefaultOrSMTPSettingsSet: (_isProviderSMTPSet | _isAuthRateLimitEmailsDefault) & true @cuegraph(skip)
|
||||
}
|
||||
|
||||
// Global configuration that applies to all services
|
||||
#Global: {
|
||||
// User-defined environment variables that are spread over all services
|
||||
environment: [...#GlobalEnvironmentVariable] | *[]
|
||||
}
|
||||
|
||||
#GlobalEnvironmentVariable: {
|
||||
// Name of the environment variable
|
||||
name: =~"(?i)^[a-z_]{1,}[a-z0-9_]*" & !~"(?i)^NHOST_" & !~"(?i)^HASURA_"
|
||||
// Value of the environment variable
|
||||
value: string
|
||||
}
|
||||
|
||||
#Graphql: {
|
||||
security: #GraphqlSecurity
|
||||
}
|
||||
|
||||
#GraphqlSecurity: {
|
||||
forbidAminSecret: bool | *false
|
||||
maxDepthQueries: uint | *0 // 0 disables the check
|
||||
}
|
||||
|
||||
#Networking: {
|
||||
ingresses: [#Ingress] | *[]
|
||||
}
|
||||
|
||||
#Ingress: {
|
||||
fqdn: [string & net.FQDN & strings.MinRunes(1) & strings.MaxRunes(63)]
|
||||
|
||||
tls?: {
|
||||
clientCA?: string
|
||||
}
|
||||
}
|
||||
|
||||
#Autoscaler: {
|
||||
maxReplicas: uint8 & >=2 & <=100
|
||||
}
|
||||
|
||||
// Resource configuration for a service
|
||||
#Resources: {
|
||||
compute?: #ResourcesCompute
|
||||
|
||||
// Number of replicas for a service
|
||||
replicas: uint8 & >=1 & <=10 | *1
|
||||
autoscaler?: #Autoscaler
|
||||
|
||||
_validateReplicasMustBeSmallerThanMaxReplicas: (replicas <= autoscaler.maxReplicas) & true @cuegraph(skip)
|
||||
|
||||
_validateMultipleReplicasNeedsCompute: (
|
||||
replicas == 1 && autoscaler == _|_ |
|
||||
compute != _|_) & true @cuegraph(skip)
|
||||
_validateMultipleReplicasRatioMustBe1For2: (
|
||||
replicas == 1 && autoscaler == _|_ |
|
||||
(compute.cpu*2.048 == compute.memory)) & true @cuegraph(skip)
|
||||
|
||||
networking?: #Networking | null
|
||||
}
|
||||
|
||||
#ResourcesCompute: {
|
||||
// milicpus, 1000 milicpus = 1 cpu
|
||||
cpu: uint32 & >=250 & <=30000
|
||||
// MiB: 128MiB to 30GiB
|
||||
memory: uint32 & >=128 & <=62464
|
||||
|
||||
// validate CPU steps of 250 milicpus
|
||||
_validateCPUSteps250: (mod(cpu, 250) == 0) & true @cuegraph(skip)
|
||||
|
||||
// validate memory steps of 128 MiB
|
||||
_validateMemorySteps128: (mod(memory, 128) == 0) & true @cuegraph(skip)
|
||||
}
|
||||
|
||||
// Configuration for hasura service
|
||||
#Hasura: {
|
||||
// Version of hasura, you can see available versions in the URL below:
|
||||
// https://hub.docker.com/r/hasura/graphql-engine/tags
|
||||
version: string | *"v2.46.0-ce"
|
||||
|
||||
// JWT Secrets configuration
|
||||
jwtSecrets: [#JWTSecret]
|
||||
|
||||
// Admin secret
|
||||
adminSecret: string
|
||||
|
||||
// Webhook secret
|
||||
webhookSecret: string
|
||||
|
||||
// Configuration for hasura services
|
||||
// Reference: https://hasura.io/docs/latest/deployment/graphql-engine-flags/reference/
|
||||
settings: {
|
||||
// HASURA_GRAPHQL_CORS_DOMAIN
|
||||
corsDomain: [...#Url] | *["*"]
|
||||
// HASURA_GRAPHQL_DEV_MODE
|
||||
devMode: bool | *true
|
||||
// HASURA_GRAPHQL_ENABLE_ALLOWLIST
|
||||
enableAllowList: bool | *false
|
||||
// HASURA_GRAPHQL_ENABLE_CONSOLE
|
||||
enableConsole: bool | *true
|
||||
// HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS
|
||||
enableRemoteSchemaPermissions: bool | *false
|
||||
// HASURA_GRAPHQL_ENABLED_APIS
|
||||
enabledAPIs: [...#HasuraAPIs] | *["metadata", "graphql", "pgdump", "config"]
|
||||
|
||||
// HASURA_GRAPHQL_INFER_FUNCTION_PERMISSIONS
|
||||
inferFunctionPermissions: bool | *true
|
||||
|
||||
// HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_REFETCH_INTERVAL
|
||||
liveQueriesMultiplexedRefetchInterval: uint32 | *1000
|
||||
|
||||
// HASURA_GRAPHQL_STRINGIFY_NUMERIC_TYPES
|
||||
stringifyNumericTypes: bool | *false
|
||||
}
|
||||
|
||||
authHook?: {
|
||||
// HASURA_GRAPHQL_AUTH_HOOK
|
||||
url: string
|
||||
|
||||
// HASURA_GRAPHQL_AUTH_HOOK_MODE
|
||||
mode: "GET" | *"POST"
|
||||
|
||||
// HASURA_GRAPHQL_AUTH_HOOK_SEND_REQUEST_BODY
|
||||
sendRequestBody: bool | *true
|
||||
}
|
||||
|
||||
logs: {
|
||||
// HASURA_GRAPHQL_LOG_LEVEL
|
||||
level: "debug" | "info" | "error" | *"warn"
|
||||
}
|
||||
|
||||
events: {
|
||||
// HASURA_GRAPHQL_EVENTS_HTTP_POOL_SIZE
|
||||
httpPoolSize: uint32 & >=1 & <=100 | *100
|
||||
}
|
||||
|
||||
// Resources for the service
|
||||
resources?: #Resources
|
||||
|
||||
rateLimit?: #RateLimit
|
||||
}
|
||||
|
||||
// APIs for hasura
|
||||
#HasuraAPIs: "metadata" | "graphql" | "pgdump" | "config"
|
||||
|
||||
// Configuration for storage service
|
||||
#Storage: {
|
||||
// Version of storage service, you can see available versions in the URL below:
|
||||
// https://hub.docker.com/r/nhost/hasura-storage/tags
|
||||
//
|
||||
// Releases:
|
||||
//
|
||||
// https://github.com/nhost/hasura-storage/releases
|
||||
version: string | *"0.7.2"
|
||||
|
||||
// Networking (custom domains at the moment) are not allowed as we need to do further
|
||||
// configurations in the CDN. We will enable it again in the future.
|
||||
resources?: #Resources & {networking?: null}
|
||||
|
||||
antivirus?: {
|
||||
server: "tcp://run-clamav:3310"
|
||||
}
|
||||
|
||||
rateLimit?: #RateLimit
|
||||
}
|
||||
|
||||
// Configuration for functions service
|
||||
#Functions: {
|
||||
node: {
|
||||
version: 20 | *22
|
||||
}
|
||||
|
||||
resources?: {
|
||||
networking?: #Networking
|
||||
}
|
||||
|
||||
rateLimit?: #RateLimit
|
||||
}
|
||||
|
||||
// Configuration for postgres service
|
||||
#Postgres: {
|
||||
// Version of postgres, you can see available versions in the URL below:
|
||||
// https://hub.docker.com/r/nhost/postgres/tags
|
||||
version: string | *"14.18-20250728-1"
|
||||
|
||||
// Resources for the service
|
||||
resources: {
|
||||
compute?: #ResourcesCompute
|
||||
storage: {
|
||||
capacity: uint32 & >=1 & <=1000 // GiB
|
||||
}
|
||||
|
||||
replicas?: 1
|
||||
|
||||
enablePublicAccess?: bool | *false
|
||||
}
|
||||
|
||||
settings?: {
|
||||
jit: "off" | "on" | *"on"
|
||||
maxConnections: int32 | *100
|
||||
sharedBuffers: string | *"128MB"
|
||||
effectiveCacheSize: string | *"4GB"
|
||||
maintenanceWorkMem: string | *"64MB"
|
||||
checkpointCompletionTarget: number | *0.9
|
||||
walBuffers: string | *"-1"
|
||||
defaultStatisticsTarget: int32 | *100
|
||||
randomPageCost: number | *4.0
|
||||
effectiveIOConcurrency: int32 | *1
|
||||
workMem: string | *"4MB"
|
||||
hugePages: string | *"try"
|
||||
minWalSize: string | *"80MB"
|
||||
maxWalSize: string | *"1GB"
|
||||
maxWorkerProcesses: int32 | *8
|
||||
maxParallelWorkersPerGather: int32 | *2
|
||||
maxParallelWorkers: int32 | *8
|
||||
maxParallelMaintenanceWorkers: int32 | *2
|
||||
walLevel: string | *"replica"
|
||||
maxWalSenders: int32 | *10
|
||||
maxReplicationSlots: int32 | *10
|
||||
archiveTimeout: int32 & >=300 & <=1073741823 | *300
|
||||
trackIoTiming: "on" | *"off"
|
||||
|
||||
// if pitr is on we need walLevel to set to replica or logical
|
||||
_validateWalLevelIsLogicalOrReplicaIfPitrIsEnabled: ( pitr == _|_ | walLevel == "replica" | walLevel == "logical") & true @cuegraph(skip)
|
||||
}
|
||||
|
||||
pitr?: {
|
||||
retention: uint8 & 7
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration for auth service
|
||||
// You can find more information about the configuration here:
|
||||
// https://github.com/nhost/hasura-auth/blob/main/docs/environment-variables.md
|
||||
#Auth: {
|
||||
// Version of auth, you can see available versions in the URL below:
|
||||
// https://hub.docker.com/r/nhost/hasura-auth/tags
|
||||
//
|
||||
// Releases:
|
||||
//
|
||||
// https://github.com/nhost/hasura-auth/releases
|
||||
version: string | *"0.38.1"
|
||||
|
||||
// Resources for the service
|
||||
resources?: #Resources
|
||||
|
||||
elevatedPrivileges: {
|
||||
mode: "recommended" | "required" | *"disabled"
|
||||
}
|
||||
|
||||
redirections: {
|
||||
// AUTH_CLIENT_URL
|
||||
clientUrl: #Url | *"http://localhost:3000"
|
||||
// AUTH_ACCESS_CONTROL_ALLOWED_REDIRECT_URLS
|
||||
allowedUrls: [...string]
|
||||
}
|
||||
|
||||
signUp: {
|
||||
// Inverse of AUTH_DISABLE_SIGNUP
|
||||
enabled: bool | *true
|
||||
|
||||
// AUTH_DISABLE_NEW_USERS
|
||||
disableNewUsers: bool | *false
|
||||
|
||||
turnstile?: {
|
||||
secretKey: string
|
||||
}
|
||||
}
|
||||
|
||||
user: {
|
||||
roles: {
|
||||
// AUTH_USER_DEFAULT_ROLE
|
||||
default: #UserRole | *"user"
|
||||
// AUTH_USER_DEFAULT_ALLOWED_ROLES
|
||||
allowed: [...#UserRole] | *[default, "me"]
|
||||
}
|
||||
locale: {
|
||||
// AUTH_LOCALE_DEFAULT
|
||||
default: #Locale | *"en"
|
||||
// AUTH_LOCALE_ALLOWED_LOCALES
|
||||
allowed: [...#Locale] | *[default]
|
||||
}
|
||||
|
||||
gravatar: {
|
||||
// AUTH_GRAVATAR_ENABLED
|
||||
enabled: bool | *true
|
||||
// AUTH_GRAVATAR_DEFAULT
|
||||
default: "404" | "mp" | "identicon" | "monsterid" | "wavatar" | "retro" | "robohash" | *"blank"
|
||||
// AUTH_GRAVATAR_RATING
|
||||
rating: "pg" | "r" | "x" | *"g"
|
||||
}
|
||||
email: {
|
||||
// AUTH_ACCESS_CONTROL_ALLOWED_EMAILS
|
||||
allowed: [...#Email]
|
||||
// AUTH_ACCESS_CONTROL_BLOCKED_EMAILS
|
||||
blocked: [...#Email]
|
||||
|
||||
}
|
||||
emailDomains: {
|
||||
// AUTH_ACCESS_CONTROL_ALLOWED_EMAIL_DOMAINS
|
||||
allowed: [...string & net.FQDN]
|
||||
// AUTH_ACCESS_CONTROL_BLOCKED_EMAIL_DOMAINS
|
||||
blocked: [...string & net.FQDN]
|
||||
}
|
||||
}
|
||||
|
||||
session: {
|
||||
accessToken: {
|
||||
// AUTH_ACCESS_TOKEN_EXPIRES_IN
|
||||
expiresIn: uint32 | *900
|
||||
// AUTH_JWT_CUSTOM_CLAIMS
|
||||
customClaims: [...{
|
||||
key: =~"[a-zA-Z_]{1,}[a-zA-Z0-9_]*"
|
||||
value: string
|
||||
default?: string
|
||||
}] | *[]
|
||||
}
|
||||
|
||||
refreshToken: {
|
||||
// AUTH_REFRESH_TOKEN_EXPIRES_IN
|
||||
expiresIn: uint32 | *2592000
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
method: {
|
||||
anonymous: {
|
||||
enabled: bool | *false
|
||||
}
|
||||
|
||||
emailPasswordless: {
|
||||
enabled: bool | *false
|
||||
}
|
||||
|
||||
otp: {
|
||||
email: {
|
||||
enabled: bool | *false
|
||||
}
|
||||
}
|
||||
|
||||
emailPassword: {
|
||||
// Disabling email+password sign in is not implmented yet
|
||||
// enabled: bool | *true
|
||||
hibpEnabled: bool | *false
|
||||
emailVerificationRequired: bool | *true
|
||||
passwordMinLength: uint8 & >=3 | *9
|
||||
}
|
||||
|
||||
smsPasswordless: {
|
||||
enabled: bool | *false
|
||||
}
|
||||
|
||||
oauth: {
|
||||
apple: {
|
||||
enabled: bool | *false
|
||||
if enabled {
|
||||
clientId: string
|
||||
keyId: string
|
||||
teamId: string
|
||||
privateKey: string
|
||||
}
|
||||
if !enabled {
|
||||
clientId?: string
|
||||
keyId?: string
|
||||
teamId?: string
|
||||
privateKey?: string
|
||||
}
|
||||
audience?: string
|
||||
scope?: [...string]
|
||||
}
|
||||
azuread: {
|
||||
#StandardOauthProvider
|
||||
tenant: string | *"common"
|
||||
}
|
||||
bitbucket: #StandardOauthProvider
|
||||
discord: #StandardOauthProviderWithScope
|
||||
entraid: {
|
||||
#StandardOauthProvider
|
||||
tenant: string | *"common"
|
||||
}
|
||||
facebook: #StandardOauthProviderWithScope
|
||||
github: #StandardOauthProviderWithScope
|
||||
gitlab: #StandardOauthProviderWithScope
|
||||
google: #StandardOauthProviderWithScope
|
||||
linkedin: #StandardOauthProviderWithScope
|
||||
spotify: #StandardOauthProviderWithScope
|
||||
strava: #StandardOauthProviderWithScope
|
||||
twitch: #StandardOauthProviderWithScope
|
||||
twitter: {
|
||||
enabled: bool | *false
|
||||
if enabled {
|
||||
consumerKey: string
|
||||
consumerSecret: string
|
||||
}
|
||||
if !enabled {
|
||||
consumerKey?: string
|
||||
consumerSecret?: string
|
||||
}
|
||||
}
|
||||
windowslive: #StandardOauthProviderWithScope
|
||||
workos: {
|
||||
#StandardOauthProvider
|
||||
connection?: string
|
||||
organization?: string
|
||||
}
|
||||
}
|
||||
|
||||
webauthn: {
|
||||
enabled: bool | *false
|
||||
relyingParty?: {
|
||||
id: string | *""
|
||||
name?: string
|
||||
origins?: [...#Url] | *[redirections.clientUrl]
|
||||
}
|
||||
attestation: {
|
||||
timeout: uint32 | *60000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totp: {
|
||||
enabled: bool | *false
|
||||
if enabled {
|
||||
issuer: string
|
||||
}
|
||||
if !enabled {
|
||||
issuer?: string
|
||||
}
|
||||
}
|
||||
|
||||
misc: {
|
||||
concealErrors: bool | *false
|
||||
}
|
||||
|
||||
rateLimit: #AuthRateLimit
|
||||
}
|
||||
|
||||
#RateLimit: {
|
||||
limit: uint32
|
||||
interval: string & time.Duration
|
||||
}
|
||||
|
||||
#AuthRateLimit: {
|
||||
emails: #RateLimit | *{limit: 10, interval: "1h"}
|
||||
sms: #RateLimit | *{limit: 10, interval: "1h"}
|
||||
bruteForce: #RateLimit | *{limit: 10, interval: "5m"}
|
||||
signups: #RateLimit | *{limit: 10, interval: "5m"}
|
||||
global: #RateLimit | *{limit: 100, interval: "1m"}
|
||||
}
|
||||
|
||||
#StandardOauthProvider: {
|
||||
enabled: bool | *false
|
||||
if enabled {
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
}
|
||||
if !enabled {
|
||||
clientId?: string
|
||||
clientSecret?: string
|
||||
}
|
||||
}
|
||||
|
||||
#StandardOauthProviderWithScope: {
|
||||
enabled: bool | *false
|
||||
if enabled {
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
}
|
||||
if !enabled {
|
||||
clientId?: string
|
||||
clientSecret?: string
|
||||
}
|
||||
audience?: string
|
||||
scope?: [...string]
|
||||
}
|
||||
|
||||
#Provider: {
|
||||
smtp?: #Smtp
|
||||
sms?: #Sms
|
||||
}
|
||||
|
||||
#Smtp: {
|
||||
user: string
|
||||
password: string
|
||||
sender: string
|
||||
host: string & net.FQDN | net.IP
|
||||
port: #Port
|
||||
secure: bool
|
||||
method: "LOGIN" | "CRAM-MD5" | "PLAIN"
|
||||
}
|
||||
|
||||
#Sms: {
|
||||
provider: "twilio"
|
||||
accountSid: string
|
||||
authToken: string
|
||||
messagingServiceId: string
|
||||
}
|
||||
|
||||
#UserRole: string
|
||||
#Url: string
|
||||
#Port: uint16
|
||||
#Email: =~"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
|
||||
#Locale: string & strings.MinRunes(2) & strings.MaxRunes(2)
|
||||
|
||||
// See https://hasura.io/docs/latest/auth/authentication/jwt/
|
||||
#JWTSecret:
|
||||
({
|
||||
type: "HS384" | "HS512" | *"HS256"
|
||||
key: string
|
||||
} |
|
||||
{
|
||||
type: "RS256" | "RS384" | "RS512"
|
||||
key: string
|
||||
signingKey?: string
|
||||
kid?: string
|
||||
} |
|
||||
{
|
||||
jwk_url: #Url | *null
|
||||
}) &
|
||||
{
|
||||
claims_format?: "stringified_json" | *"json"
|
||||
audience?: string
|
||||
issuer?: string
|
||||
allowed_skew?: uint32
|
||||
header?: string
|
||||
} & {
|
||||
claims_map?: [...#ClaimMap]
|
||||
|
||||
} &
|
||||
({
|
||||
claims_namespace: string | *"https://hasura.io/jwt/claims"
|
||||
} |
|
||||
{
|
||||
claims_namespace_path: string
|
||||
} | *{})
|
||||
|
||||
#ClaimMap: {
|
||||
claim: string
|
||||
{
|
||||
value: string
|
||||
} | {
|
||||
path: string
|
||||
default?: string
|
||||
}
|
||||
} & {}
|
||||
|
||||
#SystemConfig: {
|
||||
auth: {
|
||||
email: {
|
||||
templates: {
|
||||
s3Key?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
graphql: {
|
||||
// manually enable graphi on a per-service basis
|
||||
// by default it follows the plan
|
||||
featureAdvancedGraphql: bool | *false
|
||||
}
|
||||
|
||||
postgres: {
|
||||
enabled: bool | *true
|
||||
majorVersion: "14" | "15" | "16" | "17" | *"14"
|
||||
if enabled {
|
||||
database: string
|
||||
}
|
||||
if !enabled {
|
||||
database?: string
|
||||
}
|
||||
connectionString: {
|
||||
backup: string
|
||||
hasura: string
|
||||
auth: string
|
||||
storage: string
|
||||
}
|
||||
|
||||
disk?: {
|
||||
iops: uint32 | *3000
|
||||
tput: uint32 | *125
|
||||
}
|
||||
}
|
||||
|
||||
persistentVolumesEncrypted: bool | *false
|
||||
}
|
||||
|
||||
#AI: {
|
||||
version: string | *"0.8.0"
|
||||
resources: {
|
||||
compute: #ComputeResources
|
||||
}
|
||||
|
||||
openai: {
|
||||
organization?: string
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
autoEmbeddings: {
|
||||
synchPeriodMinutes: uint32 | *5
|
||||
}
|
||||
|
||||
webhookSecret: string
|
||||
}
|
||||
|
||||
#Observability: {
|
||||
grafana: #Grafana
|
||||
}
|
||||
|
||||
#Grafana: {
|
||||
adminPassword: string
|
||||
|
||||
smtp?: {
|
||||
host: string & net.FQDN | net.IP
|
||||
port: #Port
|
||||
sender: string
|
||||
user: string
|
||||
password: string
|
||||
}
|
||||
|
||||
alerting: {
|
||||
enabled: bool | *false
|
||||
}
|
||||
|
||||
contacts: {
|
||||
emails?: [...string]
|
||||
pagerduty?: [...{
|
||||
integrationKey: string
|
||||
severity: string
|
||||
class: string
|
||||
component: string
|
||||
group: string
|
||||
}]
|
||||
discord?: [...{
|
||||
url: string
|
||||
avatarUrl: string
|
||||
}]
|
||||
slack?: [...{
|
||||
recipient: string
|
||||
token: string
|
||||
username: string
|
||||
iconEmoji: string
|
||||
iconURL: string
|
||||
mentionUsers: [...string]
|
||||
mentionGroups: [...string]
|
||||
mentionChannel: string
|
||||
url: string
|
||||
endpointURL: string
|
||||
}]
|
||||
webhook?: [...{
|
||||
url: string
|
||||
httpMethod: string
|
||||
username: string
|
||||
password: string
|
||||
authorizationScheme: string
|
||||
authorizationCredentials: string
|
||||
maxAlerts: int
|
||||
}]
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#RunServicePort: {
|
||||
port: #Port
|
||||
type: "http" | "grpc" | "tcp" | "udp"
|
||||
publish: bool | *false
|
||||
ingresses: [#Ingress] | *[]
|
||||
_publish_supported_only_over_http: (
|
||||
publish == false || type == "http" || type == "grpc" ) & true @cuegraph(skip)
|
||||
|
||||
rateLimit?: #RateLimit
|
||||
}
|
||||
|
||||
#RunServiceName: =~"^[a-z]([-a-z0-9]*[a-z0-9])?$" & strings.MinRunes(1) & strings.MaxRunes(30)
|
||||
|
||||
// Resource configuration for a service
|
||||
#ComputeResources: {
|
||||
// milicpus, 1000 milicpus = 1 cpu
|
||||
cpu: uint32 & >=62 & <=14000
|
||||
// MiB: 128MiB to 30GiB
|
||||
memory: uint32 & >=128 & <=28720
|
||||
|
||||
// validate memory steps of 128 MiB
|
||||
_validateMemorySteps128: (mod(memory, 128) == 0) & true @cuegraph(skip)
|
||||
}
|
||||
|
||||
// Resource configuration for a service
|
||||
#RunServiceResources: {
|
||||
compute: #ComputeResources
|
||||
|
||||
storage: [...{
|
||||
name: #RunServiceName // name of the volume, changing it will cause data loss
|
||||
capacity: uint32 & >=1 & <=1000 // GiB
|
||||
path: string
|
||||
}] | *[]
|
||||
_storage_name_must_be_unique: list.UniqueItems([for s in storage {s.name}]) & true @cuegraph(skip)
|
||||
_storage_path_must_be_unique: list.UniqueItems([for s in storage {s.path}]) & true @cuegraph(skip)
|
||||
|
||||
// Number of replicas for a service
|
||||
replicas: uint8 & <=10
|
||||
|
||||
autoscaler?: #Autoscaler
|
||||
|
||||
_validateReplicasMustBeSmallerThanMaxReplicas: (replicas <= autoscaler.maxReplicas) & true @cuegraph(skip)
|
||||
|
||||
_replcas_cant_be_greater_than_1_when_using_storage: (len(storage) == 0 | (len(storage) > 0 & replicas <= 1 && autoscaler == _|_)) & true @cuegraph(skip)
|
||||
|
||||
_validate_cpu_memory_ratio_must_be_1_for_2: (math.Abs(compute.memory-compute.cpu*2.048) <= 1.024) & true @cuegraph(skip)
|
||||
}
|
||||
|
||||
#RunServiceImage: {
|
||||
image: string
|
||||
// content of "auths", i.e., { "auths": $THIS }
|
||||
pullCredentials?: string
|
||||
}
|
||||
|
||||
#HealthCheck: {
|
||||
port: #Port
|
||||
initialDelaySeconds: int | *30
|
||||
probePeriodSeconds: int | *60
|
||||
}
|
||||
|
||||
#EnvironmentVariable: {
|
||||
// Name of the environment variable
|
||||
name: =~"(?i)^[a-z_]{1,}[a-z0-9_]*"
|
||||
// Value of the environment variable
|
||||
value: string
|
||||
}
|
||||
|
||||
#RunServiceConfig: {
|
||||
name: #RunServiceName
|
||||
image: #RunServiceImage
|
||||
command: [...string]
|
||||
environment: [...#EnvironmentVariable] | *[]
|
||||
ports?: [...#RunServicePort] | *[]
|
||||
resources: #RunServiceResources
|
||||
healthCheck?: #HealthCheck
|
||||
}
|
||||
40
cli/mcp/resources/resources.go
Normal file
40
cli/mcp/resources/resources.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/nhost/nhost/cli/mcp/config"
|
||||
)
|
||||
|
||||
func Instructions() string {
|
||||
return "The following resources are available:\n\n" +
|
||||
fmt.Sprintf("- %s: %s\n", CloudResourceURI, CloudDescription) +
|
||||
fmt.Sprintf("- %s: %s\n", GraphqlManagementResourceURI, GraphqlManagementDescription) +
|
||||
fmt.Sprintf("- %s: %s\n", NhostTomlResourceURI, NhostTomlResourceDescription)
|
||||
}
|
||||
|
||||
func Register(cfg *config.Config, server *server.MCPServer) error {
|
||||
nt := NewNhostToml()
|
||||
nt.Register(server)
|
||||
|
||||
if cfg.Cloud != nil {
|
||||
ct := NewCloud(cfg)
|
||||
ct.Register(server)
|
||||
}
|
||||
|
||||
enableGraphlManagement := false
|
||||
for _, project := range cfg.Projects {
|
||||
if project.ManageMetadata {
|
||||
enableGraphlManagement = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if enableGraphlManagement {
|
||||
gmt := NewGraphqlManagement()
|
||||
gmt.Register(server)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user