Skip to content

Commit eeb69c4

Browse files
fix(gate,sdk): fail fast on bad credentials before artifact upload (#961)
Solves [MET-793](https://linear.app/metatypedev/issue/MET-793/artifact-upload-allowed-with-wrong-credentials) #### Migration notes None - [x] The change comes with new or modified tests - [ ] Hard-to-understand functions have explanatory comments - [ ] End-user documentation is updated to reflect the change <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes - **New Features** - Added a ping functionality to verify typegate connectivity and credentials - Introduced a new server health check mechanism across multiple language implementations - **Improvements** - Simplified error handling in deployment and query-related functions - Enhanced pre-deployment validation process - **Testing** - Added test coverage for credential validation during deployment - Implemented new test scenarios for typegate connectivity checks <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 324dffa commit eeb69c4

File tree

11 files changed

+150
-28
lines changed

11 files changed

+150
-28
lines changed

src/typegate/src/runtimes/typegate.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ export class TypeGateRuntime extends Runtime {
114114
return this.execRawPrismaQuery;
115115
case "queryPrismaModel":
116116
return this.queryPrismaModel;
117-
117+
case "ping":
118+
return (_) => true;
118119
default:
119120
if (name != null) {
120121
throw new Error(`materializer '${name}' not implemented`);

src/typegate/src/typegraphs/typegate.json

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"execRawPrismaCreate": 64,
1616
"execRawPrismaUpdate": 65,
1717
"execRawPrismaDelete": 66,
18-
"queryPrismaModel": 67
18+
"queryPrismaModel": 67,
19+
"ping": 71
1920
},
2021
"id": [],
2122
"required": [
@@ -30,7 +31,8 @@
3031
"execRawPrismaCreate",
3132
"execRawPrismaUpdate",
3233
"execRawPrismaDelete",
33-
"queryPrismaModel"
34+
"queryPrismaModel",
35+
"ping"
3436
],
3537
"policies": {
3638
"typegraphs": [
@@ -68,6 +70,9 @@
6870
],
6971
"queryPrismaModel": [
7072
0
73+
],
74+
"ping": [
75+
0
7176
]
7277
}
7378
},
@@ -794,6 +799,16 @@
794799
"rowCount": [],
795800
"data": []
796801
}
802+
},
803+
{
804+
"type": "function",
805+
"title": "root_ping_fn",
806+
"input": 2,
807+
"output": 25,
808+
"runtimeConfig": null,
809+
"materializer": 14,
810+
"rate_weight": null,
811+
"rate_calls": false
797812
}
798813
],
799814
"materializers": [
@@ -931,6 +946,15 @@
931946
"idempotent": true
932947
},
933948
"data": {}
949+
},
950+
{
951+
"name": "ping",
952+
"runtime": 1,
953+
"effect": {
954+
"effect": "read",
955+
"idempotent": true
956+
},
957+
"data": {}
934958
}
935959
],
936960
"runtimes": [

src/typegate/src/typegraphs/typegate.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ def typegate(g: Graph):
146146
raise Exception(arg_info_by_path_id.value)
147147
arg_info_by_path_mat = Materializer(arg_info_by_path_id.value, effect=fx.read())
148148

149+
ping_mat_id = runtimes.register_typegate_materializer(store, TypegateOperation.PING)
150+
if isinstance(ping_mat_id, Err):
151+
raise Exception(ping_mat_id.value)
152+
ping_mat = Materializer(ping_mat_id.value, effect=fx.read())
153+
149154
serialized = t.gen(t.string(), serialized_typegraph_mat)
150155

151156
typegraph = t.struct(
@@ -419,4 +424,9 @@ def typegate(g: Graph):
419424
raw_prisma_delete_mat,
420425
),
421426
queryPrismaModel=query_prisma_model,
427+
ping=t.func(
428+
t.struct({}),
429+
t.boolean(), # always True
430+
ping_mat,
431+
),
422432
)

src/typegraph/core/src/runtimes/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,7 @@ impl crate::wit::runtimes::Guest for crate::Lib {
654654
WitOp::RawPrismaUpdate => (WitEffect::Update(false), Op::RawPrismaQuery),
655655
WitOp::RawPrismaDelete => (WitEffect::Delete(true), Op::RawPrismaQuery),
656656
WitOp::QueryPrismaModel => (WitEffect::Read, Op::QueryPrismaModel),
657+
WitOp::Ping => (WitEffect::Read, Op::Ping),
657658
};
658659

659660
Ok(Store::register_materializer(Materializer::typegate(

src/typegraph/core/src/runtimes/typegate.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pub enum TypegateOperation {
2121
FindPrismaModels,
2222
RawPrismaQuery,
2323
QueryPrismaModel,
24+
Ping,
2425
}
2526

2627
impl MaterializerConverter for TypegateOperation {
@@ -44,6 +45,7 @@ impl MaterializerConverter for TypegateOperation {
4445
Self::FindPrismaModels => "findPrismaModels",
4546
Self::RawPrismaQuery => "execRawPrismaQuery",
4647
Self::QueryPrismaModel => "queryPrismaModel",
48+
Self::Ping => "ping",
4749
}
4850
.to_string(),
4951
runtime,

src/typegraph/core/src/utils/mod.rs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ impl crate::wit::utils::Guest for crate::Lib {
114114
.build(named_provider(&service_name)?)
115115
}
116116

117-
fn gql_deploy_query(params: QueryDeployParams) -> Result<String> {
118-
let query = r"
117+
fn gql_deploy_query(params: QueryDeployParams) -> String {
118+
let query = "
119119
mutation InsertTypegraph($tg: String!, $secrets: String!, $targetVersion: String!) {
120120
addTypegraph(fromString: $tg, secrets: $secrets, targetVersion: $targetVersion) {
121121
name
@@ -128,8 +128,8 @@ impl crate::wit::utils::Guest for crate::Lib {
128128

129129
let mut secrets_map = IndexMap::new();
130130
if let Some(secrets) = params.secrets {
131-
for item in secrets {
132-
secrets_map.insert(item.0, item.1);
131+
for (secret, value) in secrets {
132+
secrets_map.insert(secret, value);
133133
}
134134
}
135135

@@ -143,11 +143,11 @@ impl crate::wit::utils::Guest for crate::Lib {
143143
}),
144144
});
145145

146-
Ok(req_body.to_string())
146+
req_body.to_string()
147147
}
148148

149-
fn gql_remove_query(names: Vec<String>) -> Result<String> {
150-
let query = r"
149+
fn gql_remove_query(names: Vec<String>) -> String {
150+
let query = "
151151
mutation($names: [String!]!) {
152152
removeTypegraphs(names: $names)
153153
}
@@ -156,9 +156,20 @@ impl crate::wit::utils::Guest for crate::Lib {
156156
"query": query,
157157
"variables": json!({
158158
"names": names,
159-
}),
159+
}),
160+
});
161+
162+
req_body.to_string()
163+
}
164+
165+
fn gql_ping_query() -> String {
166+
let query = "query { ping }";
167+
let req_body = json!({
168+
"query": query,
169+
"variables": json!({}),
160170
});
161-
Ok(req_body.to_string())
171+
172+
req_body.to_string()
162173
}
163174

164175
fn metagen_exec(config: FdkConfig) -> Result<Vec<FdkOutput>> {

src/typegraph/core/wit/typegraph.wit

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,7 @@ interface runtimes {
472472
raw-prisma-update,
473473
raw-prisma-delete,
474474
query-prisma-model,
475+
ping,
475476
}
476477

477478
register-typegate-materializer: func(operation: typegate-operation) -> result<materializer-id, error>;
@@ -638,8 +639,9 @@ interface utils {
638639
secrets: option<list<tuple<string, string>>>,
639640
}
640641

641-
gql-deploy-query: func(params: query-deploy-params) -> result<string, error>;
642-
gql-remove-query: func(tg-name: list<string>) -> result<string, error>;
642+
gql-deploy-query: func(params: query-deploy-params) -> string;
643+
gql-remove-query: func(tg-name: list<string>) -> string;
644+
gql-ping-query: func() -> string;
643645

644646
record fdk-config {
645647
workspace-path: string,

src/typegraph/deno/src/tg_deploy.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ export async function tgDeploy(
8585
if (typegate.auth) {
8686
headers.append("Authorization", typegate.auth.asHeaderValue());
8787
}
88+
const url = new URL("/typegate", typegate.url);
89+
90+
// Make sure we have the correct credentials before doing anything
91+
await pingTypegate(url, headers);
8892

8993
if (refArtifacts.length > 0) {
9094
// upload the artifacts
@@ -103,7 +107,7 @@ export async function tgDeploy(
103107

104108
// deploy the typegraph
105109
const response = (await execRequest(
106-
new URL("/typegate", typegate.url),
110+
url,
107111
{
108112
method: "POST",
109113
headers,
@@ -165,3 +169,18 @@ export async function tgRemove(
165169

166170
return { typegate: response };
167171
}
172+
173+
export async function pingTypegate(
174+
url: URL,
175+
headers: Headers,
176+
) {
177+
await execRequest(
178+
url,
179+
{
180+
method: "POST",
181+
headers,
182+
body: wit_utils.gqlPingQuery(),
183+
},
184+
"Failed to access typegate",
185+
);
186+
}

src/typegraph/python/typegraph/graph/tg_deploy.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@
88
from platform import python_version
99

1010
from typegraph.gen.exports.utils import QueryDeployParams
11-
from typegraph.gen.types import Err
1211
from typegraph.gen.exports.core import MigrationAction, PrismaMigrationConfig
1312
from typegraph.graph.shared_types import BasicAuth
1413
from typegraph.graph.tg_artifact_upload import ArtifactUploader
1514
from typegraph.graph.typegraph import TypegraphOutput
16-
from typegraph.wit import ErrorStack, SerializeParams, store, wit_utils
15+
from typegraph.wit import SerializeParams, store, wit_utils
1716
from typegraph import version as sdk_version
1817
from typegraph.io import Log
1918

@@ -71,6 +70,9 @@ def tg_deploy(tg: TypegraphOutput, params: TypegraphDeployParams) -> DeployResul
7170
if typegate.auth is not None:
7271
headers["Authorization"] = typegate.auth.as_header_value()
7372

73+
# Make sure we have the correct credentials before doing anything
74+
ping_typegate(url, headers)
75+
7476
serialize_params = SerializeParams(
7577
typegraph_path=params.typegraph_path,
7678
prefix=params.prefix,
@@ -104,22 +106,19 @@ def tg_deploy(tg: TypegraphOutput, params: TypegraphDeployParams) -> DeployResul
104106
artifact_uploader.upload_artifacts()
105107

106108
# deploy the typegraph
107-
res = wit_utils.gql_deploy_query(
109+
query = wit_utils.gql_deploy_query(
108110
store,
109111
params=QueryDeployParams(
110112
tg=tg_json,
111113
secrets=[(k, v) for k, v in (params.secrets or {}).items()],
112114
),
113115
)
114116

115-
if isinstance(res, Err):
116-
raise ErrorStack(res.value)
117-
118117
req = request.Request(
119118
url=url,
120119
method="POST",
121120
headers=headers,
122-
data=res.value.encode(),
121+
data=query.encode(),
123122
)
124123

125124
response = exec_request(req)
@@ -152,16 +151,13 @@ def tg_remove(typegraph_name: str, params: TypegraphRemoveParams):
152151
if typegate.auth is not None:
153152
headers["Authorization"] = typegate.auth.as_header_value()
154153

155-
res = wit_utils.gql_remove_query(store, [typegraph_name])
156-
157-
if isinstance(res, Err):
158-
raise ErrorStack(res.value)
154+
query = wit_utils.gql_remove_query(store, [typegraph_name])
159155

160156
req = request.Request(
161157
url=url,
162158
method="POST",
163159
headers=headers,
164-
data=res.value.encode(),
160+
data=query.encode(),
165161
)
166162

167163
response = exec_request(req).read().decode()
@@ -186,3 +182,19 @@ def handle_response(res: Any, url=""):
186182
return json.loads(res)
187183
except Exception as _:
188184
raise Exception(f'Expected json object: got "{res}": {url}')
185+
186+
187+
def ping_typegate(url: str, headers: dict[str, str]):
188+
req = request.Request(
189+
url=url,
190+
method="POST",
191+
headers=headers,
192+
data=wit_utils.gql_ping_query(store).encode(),
193+
)
194+
195+
try:
196+
_ = request.urlopen(req)
197+
except request.HTTPError as e:
198+
raise Exception(f"Failed to access to typegate: {e}")
199+
except Exception as e:
200+
raise Exception(f"{e}: {req.full_url}")
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0.
2+
// SPDX-License-Identifier: MPL-2.0
3+
import { BasicAuth, tgDeploy } from "@typegraph/sdk/tg_deploy";
4+
5+
import { Meta } from "test-utils/mod.ts";
6+
import { tg } from "./self_deploy.ts";
7+
import { testDir } from "test-utils/dir.ts";
8+
import { join } from "@std/path/join";
9+
import { unreachable } from "@std/assert";
10+
import * as path from "@std/path";
11+
import { assertStringIncludes } from "@std/assert/string-includes";
12+
13+
Meta.test(
14+
{
15+
name: "typegate should fail after ping on bad credential",
16+
},
17+
async (t) => {
18+
const gate = `http://localhost:${t.port}`;
19+
const auth = new BasicAuth("admin", "wrong password");
20+
const cwdDir = join(testDir, "e2e", "self_deploy");
21+
22+
try {
23+
const _ = await tgDeploy(tg, {
24+
typegate: { url: gate, auth },
25+
secrets: {},
26+
typegraphPath: path.join(cwdDir, "self_deploy.ts"),
27+
migrationsDir: `${cwdDir}/prisma-migrations`,
28+
defaultMigrationAction: {
29+
apply: true,
30+
create: true,
31+
reset: false,
32+
},
33+
});
34+
35+
unreachable();
36+
} catch(err) {
37+
assertStringIncludes(JSON.stringify(err instanceof Error ? err.message : err), "Failed to access typegate: request failed with status 401 (Unauthorized)");
38+
}
39+
},
40+
);

tests/e2e/self_deploy/self_deploy_test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Meta.test(
2121
const { serialized, response: gateResponseAdd } = await tgDeploy(tg, {
2222
typegate: { url: gate, auth },
2323
secrets: {},
24-
typegraphPath: path.join(cwdDir, "self_deploy.mjs"),
24+
typegraphPath: path.join(cwdDir, "self_deploy.ts"),
2525
migrationsDir: `${cwdDir}/prisma-migrations`,
2626
defaultMigrationAction: {
2727
apply: true,

0 commit comments

Comments
 (0)