diff --git a/cli/internal/hasura/commonmetadata/commonmetadata.go b/cli/internal/hasura/commonmetadata/commonmetadata.go index a9a1a39501a..6dc7ef45309 100644 --- a/cli/internal/hasura/commonmetadata/commonmetadata.go +++ b/cli/internal/hasura/commonmetadata/commonmetadata.go @@ -120,26 +120,20 @@ func (c *ClientCommonMetadataOps) ReplaceMetadata(metadata io.Reader) (io.Reader } func (c *ClientCommonMetadataOps) GetInconsistentMetadata() (*hasura.GetInconsistentMetadataResponse, error) { - request := hasura.RequestBody{ - Type: "get_inconsistent_metadata", - Args: map[string]string{}, - } - responseBody := new(bytes.Buffer) - response, err := c.send(request, responseBody) + inconsistentMetadata := new(hasura.GetInconsistentMetadataResponse) + responseBody, err := c.GetInconsistentMetadataRaw() if err != nil { return nil, err } - if response.StatusCode != http.StatusOK { - return nil, fmt.Errorf("%s", responseBody.String()) - } - inconsistentMetadata := new(hasura.GetInconsistentMetadataResponse) if err := json.NewDecoder(responseBody).Decode(inconsistentMetadata); err != nil { return nil, fmt.Errorf("decoding response: %w", err) } return inconsistentMetadata, nil } -func (c *ClientCommonMetadataOps) GetInconsistentMetadataReader() (io.Reader, error) { +// GetInconsistentMetadataRaw +// https://hasura.io/docs/latest/graphql/core/api-reference/metadata-api/manage-metadata.html#metadata-get-inconsistent-metadata +func (c *ClientCommonMetadataOps) GetInconsistentMetadataRaw() (io.Reader, error) { request := hasura.RequestBody{ Type: "get_inconsistent_metadata", Args: map[string]string{}, diff --git a/cli/internal/hasura/commonmetadataops.go b/cli/internal/hasura/commonmetadataops.go index e61bba7cb3c..eac5fec866f 100644 --- a/cli/internal/hasura/commonmetadataops.go +++ b/cli/internal/hasura/commonmetadataops.go @@ -6,8 +6,7 @@ import ( "github.com/hasura/graphql-engine/cli/v2/internal/httpc" ) -// general hasura metadata API requests -// these are not dependent on the connected source type +// CommonMetadataOperations represents Metadata API's which are not source type specific type CommonMetadataOperations interface { ExportMetadata() (metadata io.Reader, err error) ClearMetadata() (io.Reader, error) @@ -15,7 +14,7 @@ type CommonMetadataOperations interface { DropInconsistentMetadata() (io.Reader, error) ReplaceMetadata(metadata io.Reader) (io.Reader, error) GetInconsistentMetadata() (*GetInconsistentMetadataResponse, error) - GetInconsistentMetadataReader() (io.Reader, error) + GetInconsistentMetadataRaw() (io.Reader, error) SendCommonMetadataOperation(requestBody interface{}) (httpcResponse *httpc.Response, body io.Reader, error error) } diff --git a/cli/pkg/metadata/project_metadata.go b/cli/pkg/metadata/project_metadata.go index 082878ab68c..3574954b60b 100644 --- a/cli/pkg/metadata/project_metadata.go +++ b/cli/pkg/metadata/project_metadata.go @@ -59,6 +59,11 @@ func (p *ProjectMetadata) Reload() (io.Reader, error) { return metadataHandler.ReloadMetadata() } +// GetInconsistentMetadata objects from hge server +func (p *ProjectMetadata) GetInconsistentMetadata() (io.Reader, error) { + return cli.GetCommonMetadataOps(p.ec).GetInconsistentMetadataRaw() +} + // Diff will return the differences between metadata in the project (in JSON) and on the server func (p *ProjectMetadata) Diff() (io.Reader, error) { w := new(bytes.Buffer) diff --git a/cli/pkg/metadata/project_metadata_test.go b/cli/pkg/metadata/project_metadata_test.go index ae621701daf..e4a3366421e 100644 --- a/cli/pkg/metadata/project_metadata_test.go +++ b/cli/pkg/metadata/project_metadata_test.go @@ -1,10 +1,18 @@ package metadata import ( + "context" "fmt" "io/ioutil" + "net/http" + "path/filepath" + "strings" "testing" + "github.com/hasura/graphql-engine/cli/v2/internal/hasura" + + "github.com/hasura/graphql-engine/cli/v2/pkg/migrate" + "github.com/stretchr/testify/require" "github.com/hasura/graphql-engine/cli/v2/internal/testutil" @@ -204,3 +212,113 @@ func TestProjectMetadata_Reload(t *testing.T) { }) } } + +func TestProjectMetadata_GetInconsistentMetadata(t *testing.T) { + type before func(t *testing.T, p *ProjectMetadata, m *migrate.ProjectMigrate, hgePort, queryEndpoint string) + configV2Before := func(t *testing.T, metadata *ProjectMetadata, migrations *migrate.ProjectMigrate, hgePort, queryEndpoint string) { + // - apply all migrations + // - apply metadata + // - drop a table via run_sql API + // - reload metadata + err := migrations.Apply(migrate.ApplyOnAllDatabases()) + require.NoError(t, err) + _, err = metadata.Apply() + require.NoError(t, err) + + // remove a table from database + c := testutil.NewHttpcClient(t, hgePort, nil) + r, err := c.NewRequest( + http.MethodPost, + queryEndpoint, + hasura.RequestBody{ + Type: "run_sql", + Args: hasura.PGRunSQLInput{ + SQL: "DROP table t1;", + CheckMetadataConsistency: func() *bool { var v = false; return &v }(), + }, + }, + ) + require.NoError(t, err) + resp, err := c.Do(context.Background(), r, nil) + require.Equal(t, http.StatusOK, resp.StatusCode) + require.NoError(t, err) + + _, err = metadata.Reload() + require.NoError(t, err) + } + type fields struct { + projectDirectory string + } + tests := []struct { + name string + fields fields + before before + hasuraImage string + queryEndpoint string + wantErr bool + }{ + { + "can list inconsistent metadata config v3", + fields{ + projectDirectory: "testdata/projectv3", + }, + func(t *testing.T, metadata *ProjectMetadata, _ *migrate.ProjectMigrate, _ string, _ string) { + _, err := metadata.Apply() + require.NoError(t, err) + }, + testutil.HasuraDockerImage, + "v2/query", + false, + }, + { + "can list inconsistent metadata config v2", + fields{ + projectDirectory: "testdata/projectv2", + }, + configV2Before, + testutil.HasuraDockerImage, + "v2/query", + false, + }, + { + "can list inconsistent metadata config v2 v1.3.3", + fields{ + projectDirectory: "testdata/projectv2", + }, + configV2Before, + "hasura/graphql-engine:v1.3.3", + "v1/query", + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + port, teardown := testutil.StartHasura(t, tt.hasuraImage) + hgeEndpoint := fmt.Sprintf("%s:%s", testutil.BaseURL, port) + defer teardown() + + migrations, err := migrate.NewProjectMigrate(tt.fields.projectDirectory, migrate.WithEndpoint(hgeEndpoint), migrate.WithAdminSecret(testutil.TestAdminSecret)) + require.NoError(t, err) + metadata, err := NewProjectMetadata(tt.fields.projectDirectory, WithEndpoint(hgeEndpoint), WithAdminSecret(testutil.TestAdminSecret)) + require.NoError(t, err) + if tt.before != nil { + tt.before(t, metadata, migrations, port, tt.queryEndpoint) + } + got, err := metadata.GetInconsistentMetadata() + if tt.wantErr { + require.Error(t, err) + } + require.NoError(t, err) + gotb, err := ioutil.ReadAll(got) + require.NoError(t, err) + goldenFile := filepath.Join("testdata/get_inconsistent_metadata_test", strings.Join(strings.Split(tt.name, " "), "_")+".golden.json") + + // uncomment the following line to update test golden file + // require.NoError(t, ioutil.WriteFile(goldenFile, gotb, 0655)) + + wantb, err := ioutil.ReadFile(goldenFile) + require.NoError(t, err) + require.JSONEq(t, string(wantb), string(gotb)) + }) + } +} diff --git a/cli/pkg/metadata/testdata/get_inconsistent_metadata_test/can_list_inconsistent_metadata_config_v2.golden.json b/cli/pkg/metadata/testdata/get_inconsistent_metadata_test/can_list_inconsistent_metadata_config_v2.golden.json new file mode 100644 index 00000000000..c010c9c39d1 --- /dev/null +++ b/cli/pkg/metadata/testdata/get_inconsistent_metadata_test/can_list_inconsistent_metadata_config_v2.golden.json @@ -0,0 +1,14 @@ +{ + "is_consistent": false, + "inconsistent_objects": [ + { + "definition": { + "schema": "public", + "name": "t1" + }, + "reason": "Inconsistent object: no such table/view exists in source: \"t1\"", + "name": "table t1 in source default", + "type": "table" + } + ] +} \ No newline at end of file diff --git a/cli/pkg/metadata/testdata/get_inconsistent_metadata_test/can_list_inconsistent_metadata_config_v2_v1.3.3.golden.json b/cli/pkg/metadata/testdata/get_inconsistent_metadata_test/can_list_inconsistent_metadata_config_v2_v1.3.3.golden.json new file mode 100644 index 00000000000..ffd634d9395 --- /dev/null +++ b/cli/pkg/metadata/testdata/get_inconsistent_metadata_test/can_list_inconsistent_metadata_config_v2_v1.3.3.golden.json @@ -0,0 +1,13 @@ +{ + "is_consistent": false, + "inconsistent_objects": [ + { + "definition": { + "schema": "public", + "name": "t1" + }, + "reason": "no such table/view exists in postgres: \"t1\"", + "type": "table" + } + ] +} \ No newline at end of file diff --git a/cli/pkg/metadata/testdata/get_inconsistent_metadata_test/can_list_inconsistent_metadata_config_v3.golden.json b/cli/pkg/metadata/testdata/get_inconsistent_metadata_test/can_list_inconsistent_metadata_config_v3.golden.json new file mode 100644 index 00000000000..e4bb56c6d05 --- /dev/null +++ b/cli/pkg/metadata/testdata/get_inconsistent_metadata_test/can_list_inconsistent_metadata_config_v3.golden.json @@ -0,0 +1,41 @@ +{ + "is_consistent": false, + "inconsistent_objects": [ + { + "definition": { + "schema": "public", + "name": "t1" + }, + "reason": "Inconsistent object: no such table/view exists in source: \"t1\"", + "name": "table t1 in source default", + "type": "table" + }, + { + "definition": { + "schema": "public", + "name": "t2" + }, + "reason": "Inconsistent object: no such table/view exists in source: \"t2\"", + "name": "table t2 in source default", + "type": "table" + }, + { + "definition": { + "schema": "pub", + "name": "t4" + }, + "reason": "Inconsistent object: no such table/view exists in source: \"pub.t4\"", + "name": "table pub.t4 in source default", + "type": "table" + }, + { + "definition": { + "schema": "pub", + "name": "t3" + }, + "reason": "Inconsistent object: no such table/view exists in source: \"pub.t3\"", + "name": "table pub.t3 in source default", + "type": "table" + } + ] +} \ No newline at end of file