diff --git a/src/typegate/src/runtimes/typegate.ts b/src/typegate/src/runtimes/typegate.ts index 8d1a08703..76fd4638b 100644 --- a/src/typegate/src/runtimes/typegate.ts +++ b/src/typegate/src/runtimes/typegate.ts @@ -114,7 +114,8 @@ export class TypeGateRuntime extends Runtime { return this.execRawPrismaQuery; case "queryPrismaModel": return this.queryPrismaModel; - + case "ping": + return (_) => true; default: if (name != null) { throw new Error(`materializer '${name}' not implemented`); diff --git a/src/typegate/src/typegraphs/typegate.json b/src/typegate/src/typegraphs/typegate.json index 1c09fcd00..16d4e7716 100644 --- a/src/typegate/src/typegraphs/typegate.json +++ b/src/typegate/src/typegraphs/typegate.json @@ -15,7 +15,8 @@ "execRawPrismaCreate": 64, "execRawPrismaUpdate": 65, "execRawPrismaDelete": 66, - "queryPrismaModel": 67 + "queryPrismaModel": 67, + "ping": 71 }, "id": [], "required": [ @@ -30,7 +31,8 @@ "execRawPrismaCreate", "execRawPrismaUpdate", "execRawPrismaDelete", - "queryPrismaModel" + "queryPrismaModel", + "ping" ], "policies": { "typegraphs": [ @@ -68,6 +70,9 @@ ], "queryPrismaModel": [ 0 + ], + "ping": [ + 0 ] } }, @@ -794,6 +799,16 @@ "rowCount": [], "data": [] } + }, + { + "type": "function", + "title": "root_ping_fn", + "input": 2, + "output": 25, + "runtimeConfig": null, + "materializer": 14, + "rate_weight": null, + "rate_calls": false } ], "materializers": [ @@ -931,6 +946,15 @@ "idempotent": true }, "data": {} + }, + { + "name": "ping", + "runtime": 1, + "effect": { + "effect": "read", + "idempotent": true + }, + "data": {} } ], "runtimes": [ diff --git a/src/typegate/src/typegraphs/typegate.py b/src/typegate/src/typegraphs/typegate.py index a47547314..7c74df1bb 100644 --- a/src/typegate/src/typegraphs/typegate.py +++ b/src/typegate/src/typegraphs/typegate.py @@ -146,6 +146,11 @@ def typegate(g: Graph): raise Exception(arg_info_by_path_id.value) arg_info_by_path_mat = Materializer(arg_info_by_path_id.value, effect=fx.read()) + ping_mat_id = runtimes.register_typegate_materializer(store, TypegateOperation.PING) + if isinstance(ping_mat_id, Err): + raise Exception(ping_mat_id.value) + ping_mat = Materializer(ping_mat_id.value, effect=fx.read()) + serialized = t.gen(t.string(), serialized_typegraph_mat) typegraph = t.struct( @@ -419,4 +424,9 @@ def typegate(g: Graph): raw_prisma_delete_mat, ), queryPrismaModel=query_prisma_model, + ping=t.func( + t.struct({}), + t.boolean(), # always True + ping_mat, + ), ) diff --git a/src/typegraph/core/src/runtimes/mod.rs b/src/typegraph/core/src/runtimes/mod.rs index 73606efb2..3a555841e 100644 --- a/src/typegraph/core/src/runtimes/mod.rs +++ b/src/typegraph/core/src/runtimes/mod.rs @@ -654,6 +654,7 @@ impl crate::wit::runtimes::Guest for crate::Lib { WitOp::RawPrismaUpdate => (WitEffect::Update(false), Op::RawPrismaQuery), WitOp::RawPrismaDelete => (WitEffect::Delete(true), Op::RawPrismaQuery), WitOp::QueryPrismaModel => (WitEffect::Read, Op::QueryPrismaModel), + WitOp::Ping => (WitEffect::Read, Op::Ping), }; Ok(Store::register_materializer(Materializer::typegate( diff --git a/src/typegraph/core/src/runtimes/typegate.rs b/src/typegraph/core/src/runtimes/typegate.rs index 61d351882..37c811083 100644 --- a/src/typegraph/core/src/runtimes/typegate.rs +++ b/src/typegraph/core/src/runtimes/typegate.rs @@ -21,6 +21,7 @@ pub enum TypegateOperation { FindPrismaModels, RawPrismaQuery, QueryPrismaModel, + Ping, } impl MaterializerConverter for TypegateOperation { @@ -44,6 +45,7 @@ impl MaterializerConverter for TypegateOperation { Self::FindPrismaModels => "findPrismaModels", Self::RawPrismaQuery => "execRawPrismaQuery", Self::QueryPrismaModel => "queryPrismaModel", + Self::Ping => "ping", } .to_string(), runtime, diff --git a/src/typegraph/core/src/utils/mod.rs b/src/typegraph/core/src/utils/mod.rs index adf261340..fe424aaeb 100644 --- a/src/typegraph/core/src/utils/mod.rs +++ b/src/typegraph/core/src/utils/mod.rs @@ -114,8 +114,8 @@ impl crate::wit::utils::Guest for crate::Lib { .build(named_provider(&service_name)?) } - fn gql_deploy_query(params: QueryDeployParams) -> Result { - let query = r" + fn gql_deploy_query(params: QueryDeployParams) -> String { + let query = " mutation InsertTypegraph($tg: String!, $secrets: String!, $targetVersion: String!) { addTypegraph(fromString: $tg, secrets: $secrets, targetVersion: $targetVersion) { name @@ -128,8 +128,8 @@ impl crate::wit::utils::Guest for crate::Lib { let mut secrets_map = IndexMap::new(); if let Some(secrets) = params.secrets { - for item in secrets { - secrets_map.insert(item.0, item.1); + for (secret, value) in secrets { + secrets_map.insert(secret, value); } } @@ -143,11 +143,11 @@ impl crate::wit::utils::Guest for crate::Lib { }), }); - Ok(req_body.to_string()) + req_body.to_string() } - fn gql_remove_query(names: Vec) -> Result { - let query = r" + fn gql_remove_query(names: Vec) -> String { + let query = " mutation($names: [String!]!) { removeTypegraphs(names: $names) } @@ -156,9 +156,20 @@ impl crate::wit::utils::Guest for crate::Lib { "query": query, "variables": json!({ "names": names, - }), + }), + }); + + req_body.to_string() + } + + fn gql_ping_query() -> String { + let query = "query { ping }"; + let req_body = json!({ + "query": query, + "variables": json!({}), }); - Ok(req_body.to_string()) + + req_body.to_string() } fn metagen_exec(config: FdkConfig) -> Result> { diff --git a/src/typegraph/core/wit/typegraph.wit b/src/typegraph/core/wit/typegraph.wit index c99c057dc..ed45a6c18 100644 --- a/src/typegraph/core/wit/typegraph.wit +++ b/src/typegraph/core/wit/typegraph.wit @@ -472,6 +472,7 @@ interface runtimes { raw-prisma-update, raw-prisma-delete, query-prisma-model, + ping, } register-typegate-materializer: func(operation: typegate-operation) -> result; @@ -638,8 +639,9 @@ interface utils { secrets: option>>, } - gql-deploy-query: func(params: query-deploy-params) -> result; - gql-remove-query: func(tg-name: list) -> result; + gql-deploy-query: func(params: query-deploy-params) -> string; + gql-remove-query: func(tg-name: list) -> string; + gql-ping-query: func() -> string; record fdk-config { workspace-path: string, diff --git a/src/typegraph/deno/src/tg_deploy.ts b/src/typegraph/deno/src/tg_deploy.ts index 498ef7a61..fe3213268 100644 --- a/src/typegraph/deno/src/tg_deploy.ts +++ b/src/typegraph/deno/src/tg_deploy.ts @@ -85,6 +85,10 @@ export async function tgDeploy( if (typegate.auth) { headers.append("Authorization", typegate.auth.asHeaderValue()); } + const url = new URL("/typegate", typegate.url); + + // Make sure we have the correct credentials before doing anything + await pingTypegate(url, headers); if (refArtifacts.length > 0) { // upload the artifacts @@ -103,7 +107,7 @@ export async function tgDeploy( // deploy the typegraph const response = (await execRequest( - new URL("/typegate", typegate.url), + url, { method: "POST", headers, @@ -165,3 +169,18 @@ export async function tgRemove( return { typegate: response }; } + +export async function pingTypegate( + url: URL, + headers: Headers, +) { + await execRequest( + url, + { + method: "POST", + headers, + body: wit_utils.gqlPingQuery(), + }, + "Failed to access typegate", + ); +} diff --git a/src/typegraph/python/typegraph/graph/tg_deploy.py b/src/typegraph/python/typegraph/graph/tg_deploy.py index af5d01e31..84946de10 100644 --- a/src/typegraph/python/typegraph/graph/tg_deploy.py +++ b/src/typegraph/python/typegraph/graph/tg_deploy.py @@ -8,12 +8,11 @@ from platform import python_version from typegraph.gen.exports.utils import QueryDeployParams -from typegraph.gen.types import Err from typegraph.gen.exports.core import MigrationAction, PrismaMigrationConfig from typegraph.graph.shared_types import BasicAuth from typegraph.graph.tg_artifact_upload import ArtifactUploader from typegraph.graph.typegraph import TypegraphOutput -from typegraph.wit import ErrorStack, SerializeParams, store, wit_utils +from typegraph.wit import SerializeParams, store, wit_utils from typegraph import version as sdk_version from typegraph.io import Log @@ -71,6 +70,9 @@ def tg_deploy(tg: TypegraphOutput, params: TypegraphDeployParams) -> DeployResul if typegate.auth is not None: headers["Authorization"] = typegate.auth.as_header_value() + # Make sure we have the correct credentials before doing anything + ping_typegate(url, headers) + serialize_params = SerializeParams( typegraph_path=params.typegraph_path, prefix=params.prefix, @@ -104,7 +106,7 @@ def tg_deploy(tg: TypegraphOutput, params: TypegraphDeployParams) -> DeployResul artifact_uploader.upload_artifacts() # deploy the typegraph - res = wit_utils.gql_deploy_query( + query = wit_utils.gql_deploy_query( store, params=QueryDeployParams( tg=tg_json, @@ -112,14 +114,11 @@ def tg_deploy(tg: TypegraphOutput, params: TypegraphDeployParams) -> DeployResul ), ) - if isinstance(res, Err): - raise ErrorStack(res.value) - req = request.Request( url=url, method="POST", headers=headers, - data=res.value.encode(), + data=query.encode(), ) response = exec_request(req) @@ -152,16 +151,13 @@ def tg_remove(typegraph_name: str, params: TypegraphRemoveParams): if typegate.auth is not None: headers["Authorization"] = typegate.auth.as_header_value() - res = wit_utils.gql_remove_query(store, [typegraph_name]) - - if isinstance(res, Err): - raise ErrorStack(res.value) + query = wit_utils.gql_remove_query(store, [typegraph_name]) req = request.Request( url=url, method="POST", headers=headers, - data=res.value.encode(), + data=query.encode(), ) response = exec_request(req).read().decode() @@ -186,3 +182,19 @@ def handle_response(res: Any, url=""): return json.loads(res) except Exception as _: raise Exception(f'Expected json object: got "{res}": {url}') + + +def ping_typegate(url: str, headers: dict[str, str]): + req = request.Request( + url=url, + method="POST", + headers=headers, + data=wit_utils.gql_ping_query(store).encode(), + ) + + try: + _ = request.urlopen(req) + except request.HTTPError as e: + raise Exception(f"Failed to access to typegate: {e}") + except Exception as e: + raise Exception(f"{e}: {req.full_url}") diff --git a/tests/e2e/self_deploy/self_deploy_bad_cred_test.ts b/tests/e2e/self_deploy/self_deploy_bad_cred_test.ts new file mode 100644 index 000000000..7d50d40fd --- /dev/null +++ b/tests/e2e/self_deploy/self_deploy_bad_cred_test.ts @@ -0,0 +1,40 @@ +// Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. +// SPDX-License-Identifier: MPL-2.0 +import { BasicAuth, tgDeploy } from "@typegraph/sdk/tg_deploy"; + +import { Meta } from "test-utils/mod.ts"; +import { tg } from "./self_deploy.ts"; +import { testDir } from "test-utils/dir.ts"; +import { join } from "@std/path/join"; +import { unreachable } from "@std/assert"; +import * as path from "@std/path"; +import { assertStringIncludes } from "@std/assert/string-includes"; + +Meta.test( + { + name: "typegate should fail after ping on bad credential", + }, + async (t) => { + const gate = `http://localhost:${t.port}`; + const auth = new BasicAuth("admin", "wrong password"); + const cwdDir = join(testDir, "e2e", "self_deploy"); + + try { + const _ = await tgDeploy(tg, { + typegate: { url: gate, auth }, + secrets: {}, + typegraphPath: path.join(cwdDir, "self_deploy.ts"), + migrationsDir: `${cwdDir}/prisma-migrations`, + defaultMigrationAction: { + apply: true, + create: true, + reset: false, + }, + }); + + unreachable(); + } catch(err) { + assertStringIncludes(JSON.stringify(err instanceof Error ? err.message : err), "Failed to access typegate: request failed with status 401 (Unauthorized)"); + } + }, +); diff --git a/tests/e2e/self_deploy/self_deploy_test.ts b/tests/e2e/self_deploy/self_deploy_test.ts index 07f865812..a16a8b7ea 100644 --- a/tests/e2e/self_deploy/self_deploy_test.ts +++ b/tests/e2e/self_deploy/self_deploy_test.ts @@ -21,7 +21,7 @@ Meta.test( const { serialized, response: gateResponseAdd } = await tgDeploy(tg, { typegate: { url: gate, auth }, secrets: {}, - typegraphPath: path.join(cwdDir, "self_deploy.mjs"), + typegraphPath: path.join(cwdDir, "self_deploy.ts"), migrationsDir: `${cwdDir}/prisma-migrations`, defaultMigrationAction: { apply: true,