Ktor-server RequestValidation not working - kotlin

I am using ktor (2.1.0) to create a small API. While doing so, I am trying to leverage Ktor-server cool functionalities, including RequestValidation.
That being said, it is not working and I can't figure out why since it looks to me very close to the examples in the documentation.
This is my server config:
embeddedServer(Netty, port = 8080) {
routing {
post("/stock") {
val dto = call.receive<CreateStockRequest>()
call.respond(HttpStatusCode.NoContent)
}
}
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
})
}
install(StatusPages) {
exception<RequestValidationException> { call, cause ->
call.respond(HttpStatusCode.BadRequest, cause.reasons.joinToString())
}
}
install(RequestValidation) {
validate<CreateStockRequest> { request ->
if (request.name.isBlank())
ValidationResult.Invalid("Stock must have a name")
if (request.symbol.isBlank())
ValidationResult.Invalid("Symbol must have a name")
ValidationResult.Valid
}
}
}
This is the request object being "received":
#Serializable
data class CreateStockRequest(val name: String, val symbol: String)
And this is the body being sent:
{
"name": "",
"symbol": "something"
}
I was expecting to get a BadRequest response, but I am getting a NoContent response as if everything was fine with the request.
Am I doing something wrong?

In your example, the server responds with BadRequest only if both "name" and "symbol" are blank. You can replace your validation logic with the when expression:
validate<CreateStockRequest> { request ->
when {
request.name.isBlank() -> ValidationResult.Invalid("Stock must have a name")
request.symbol.isBlank() -> ValidationResult.Invalid("Symbol must have a name")
else -> ValidationResult.Valid
}
}

I found the accepted answer to be a cleaner code solution, and that's why it is the accepted one.
That being said, the actual problem I was having is better explained by understanding that the compiler was getting confused with scopes.
On each "if" clause, I needed to return the validation result, but the compiler was not having it.
The reason for that was the fact that it got confused with the context to which my return statement referred.
My Solution was to use the '#' notation to specify the scope for the return:
validate<CreateStockRequest> { request ->
if (request.name.isBlank())
return#validate ValidationResult.Invalid("Stock must have a name")
if (request.symbol.isBlank())
return#validate ValidationResult.Invalid("Symbol must have a name")
return#validate ValidationResult.Valid
}
}

Related

BigCommerce Stencil - GraphQL query using front matter not returning anything

I'm not sure if it's a bug, but I'm not able to make GraphQL work in the Cornerstone template. I'm expecting an error or something getting returned at least, but nothing is being rendered at all from gql.
I am on the pages/product.html template, and I even tried this example from the docs:
---
product:
videos:
limit: {{theme_settings.productpage_videos_count}}
reviews:
limit: {{theme_settings.productpage_reviews_count}}
related_products:
limit: {{theme_settings.productpage_related_products_count}}
similar_by_views:
limit: {{theme_settings.productpage_similar_by_views_count}}
gql: "query productById($productId: Int!) {
site {
product(entityId: $productId) {
variants(first: 25) {
edges {
node {
sku
defaultImage {
url(width: 1000)
}
}
}
}
}
}
}
"
My goal is to have access to the paths/URL on each of the product's category because product.category is just an array of category names. Here's the query I am able to make work on the GraphQL playground (86 to be replaced by $productId in the front matter GraphQL query, I think?):
query getProductCategories {
site {
product(entityId: 86) {
categories {
edges {
node {
name
path
}
}
}
}
}
}
If there's no way around this, maybe I'll just try to do the fetching in the client side.
This now works correctly, as of 20-Sep-2021.
There was a bug, tracked as an issue here: https://github.com/bigcommerce/stencil-cli/issues/732 which has been resolved and closed.

How can I make protected routes in actix-web

I need to verify if the user has permission for some routes.
I have made 3 "scopes" (guest, auth-user, admin) and now I don't know how to check if the user has access to these routes.
I'm trying to implement auth-middleware and this middleware should check if the user has the correct cookie or token. (I'm able to print out a cookie from request header), but I have no idea how to import, use actix_identity, and have access to id parameter inside this middleware.
I believe that my problem isn't only regarding Actix-identity, but I'm not able to pass parameters inside middleware.
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
let cookie_key = conf.server.key;
// Register http routes
let mut server = HttpServer::new(move || {
App::new()
// Enable logger
.wrap(Logger::default())
.wrap(IdentityService::new(
CookieIdentityPolicy::new(cookie_key.as_bytes())
.name("auth-cookie")
.path("/")
.secure(false),
))
//limit the maximum amount of data that server will accept
.data(web::JsonConfig::default().limit(4096))
//normal routes
.service(web::resource("/").route(web::get().to(status)))
// .configure(routes)
.service(
web::scope("/api")
// guest endpoints
.service(web::resource("/user_login").route(web::post().to(login)))
.service(web::resource("/user_logout").route(web::post().to(logout)))
// admin endpoints
.service(
web::scope("/admin")
// .wrap(AdminAuthMiddleware)
.service(
web::resource("/create_admin").route(web::post().to(create_admin)),
)
.service(
web::resource("/delete_admin/{username}/{_:/?}")
.route(web::delete().to(delete_admin)),
),
)
//user auth routes
.service(
web::scope("/auth")
// .wrap(UserAuthMiddleware)
.service(web::resource("/get_user").route(web::get().to(get_user))),
),
)
});
// Enables us to hot reload the server
let mut listenfd = ListenFd::from_env();
server = if let Some(l) = listenfd.take_tcp_listener(0).unwrap() {
server.listen(l)?
} else {
server.bind(ip)?
};
server.run().await
resources that I have tried:
Creating authentication middleware for Actix API
https://www.jamesbaum.co.uk/blether/creating-authentication-middleware-actix-rust-react/
Actix-web token validation in middleware https://users.rust-lang.org/t/actix-web-token-validation-in-middleware/38205
Actix middleware examples https://github.com/actix/examples/tree/master/middleware
Maybe I think completely wrong and auth-middleware isn't the best solution for my problem.
I hope that you can help me create "protected routes"
Try extractors instead
Trying to implement this pattern in Actix 3 I banged my head for awhile trying to use middleware, basically making a guard and then figuring out how to pass data from the middleware into the handler. It was painful and eventually I realized that I was working against Actix rather than with it.
Finally I learned out that the way to get information to a handler is to create a struct (AuthedUser, perhaps?) and implement the FromRequest trait on that struct.
Then every handler that asks for an AuthedUser in the function signature will be auth gated and if the user is logged in will have any user information you attach to AuthedUser in the FromRequest::from_request method.
Actix refers to these structs that implement FromRequest as extractors. It's a bit of magic that could use more attention in the guide.
The following does not use middleware(a little bit more work is needed) but it solves the problem with the bear minimum and seems to be the approach suggested in documentation:
#[macro_use]
extern crate actix_web;
use actix::prelude::*;
use actix_identity::{CookieIdentityPolicy, Identity, IdentityService};
use actix_web::{
dev::Payload, error::ErrorUnauthorized, web, App, Error, FromRequest, HttpRequest,
HttpResponse, HttpServer, Responder,
};
use log::{info, warn};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, pin::Pin, sync::RwLock};
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
struct Sessions {
map: HashMap<String, User>,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
#[serde(rename_all = "camelCase")]
struct Login {
id: String,
username: String,
scope: Scope,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
enum Scope {
Guest,
User,
Admin,
}
impl Default for Scope {
fn default() -> Self {
Scope::Guest
}
}
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
#[serde(rename_all = "camelCase")]
struct User {
id: String,
first_name: Option<String>,
last_name: Option<String>,
authorities: Scope,
}
impl FromRequest for User {
type Config = ();
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<User, Error>>>>;
fn from_request(req: &HttpRequest, pl: &mut Payload) -> Self::Future {
let fut = Identity::from_request(req, pl);
let sessions: Option<&web::Data<RwLock<Sessions>>> = req.app_data();
if sessions.is_none() {
warn!("sessions is empty(none)!");
return Box::pin(async { Err(ErrorUnauthorized("unauthorized")) });
}
let sessions = sessions.unwrap().clone();
Box::pin(async move {
if let Some(identity) = fut.await?.identity() {
if let Some(user) = sessions
.read()
.unwrap()
.map
.get(&identity)
.map(|x| x.clone())
{
return Ok(user);
}
};
Err(ErrorUnauthorized("unauthorized"))
})
}
}
#[get("/admin")]
async fn admin(user: User) -> impl Responder {
if user.authorities != Scope::Admin {
return HttpResponse::Unauthorized().finish();
}
HttpResponse::Ok().body("You are an admin")
}
#[get("/account")]
async fn account(user: User) -> impl Responder {
web::Json(user)
}
#[post("/login")]
async fn login(
login: web::Json<Login>,
sessions: web::Data<RwLock<Sessions>>,
identity: Identity,
) -> impl Responder {
let id = login.id.to_string();
let scope = &login.scope;
//let user = fetch_user(login).await // from db?
identity.remember(id.clone());
let user = User {
id: id.clone(),
last_name: Some(String::from("Doe")),
first_name: Some(String::from("John")),
authorities: scope.clone(),
};
sessions.write().unwrap().map.insert(id, user.clone());
info!("login user: {:?}", user);
HttpResponse::Ok().json(user)
}
#[post("/logout")]
async fn logout(sessions: web::Data<RwLock<Sessions>>, identity: Identity) -> impl Responder {
if let Some(id) = identity.identity() {
identity.forget();
if let Some(user) = sessions.write().unwrap().map.remove(&id) {
warn!("logout user: {:?}", user);
}
}
HttpResponse::Unauthorized().finish()
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
env_logger::init();
let sessions = web::Data::new(RwLock::new(Sessions {
map: HashMap::new(),
}));
HttpServer::new(move || {
App::new()
.app_data(sessions.clone())
.wrap(IdentityService::new(
CookieIdentityPolicy::new(&[0; 32])
.name("test")
.secure(false),
))
.service(account)
.service(login)
.service(logout)
.service(admin)
})
.bind("127.0.0.1:8088")?
.run()
.await
}
You can clone and run it here: https://github.com/geofmureithi/actix-acl-example
I think actix-web grants crate is perfect for you.
It allows you to check authorization using Guard, or a procedural macro (see examples on github).
It also integrates nicely with existing authorization middleware (like actix-web-httpauth).
A couple of examples for clarity:
proc-macro way
#[get("/secure")]
#[has_permissions("ROLE_ADMIN")]
async fn macro_secured() -> HttpResponse {
HttpResponse::Ok().body("ADMIN_RESPONSE")
}
Guard way
App::new()
.wrap(GrantsMiddleware::with_extractor(extract))
.service(web::resource("/admin")
.to(|| async { HttpResponse::Ok().finish() })
.guard(PermissionGuard::new("ROLE_ADMIN".to_string())))
And you can also take a look towards actix-casbin-auth (implementation of casbin integrated into actix)
Well this is in fact quite difficult to achieve in the newest actix-web version 3.0. What I did was copy the CookieIdentityPolicy middleware from the actix-web 1.0 version and modified it to my liking. However this is not plug & play code. Here and here is my version of it. Generally I would avoid actix-web, getting a thread / actor to spawn in the background and having it perform HTTP Requests are a nightmare. Then trying to share the results with handlers even more so.
middleware doesn't look very friendly with all the generics and internal types it defines, but it is a simple struct that wrap the next service to
be called. What is the next service is determined by the chain call when you create your App or define your routes. You use a generic S in your middleware which will be monomorphized at compile time so you don't have to care about which concrete type the middleware will protect.
The following middleware use a simple config passed to your App with .data() to check if the 'token' header contains the same magic value. It either go through the next service or return a not authorized error (futures).
use crate::config::Config;
use actix_service::{Service, Transform};
use actix_web::{
dev::{ServiceRequest, ServiceResponse},
error::ErrorUnauthorized,
web::Data,
Error,
};
use futures::future::{err, ok, Either, Ready};
use std::task::{Context, Poll};
pub struct TokenAuth;
impl<S, B> Transform<S> for TokenAuth
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{ type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = TokenAuthMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(TokenAuthMiddleware { service })
}
}
pub struct TokenAuthMiddleware<S> {
service: S,
}
impl<S, B> Service for TokenAuthMiddleware<S>
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
{ type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type Future = Either<S::Future, Ready<Result<Self::Response, Self::Error>>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&mut self, req: ServiceRequest) -> Self::Future {
if let Some(token) = req
.headers()
.get("token")
.and_then(|token| token.to_str().ok())
{
if let Some(config) = req.app_data::<Data<Config>>() {
if token == config.token {
return Either::Left(self.service.call(req));
}
}
}
Either::Right(err(ErrorUnauthorized("not authorized")))
}
}
to protect your functions is then as simple as
#[post("/upload", wrap="TokenAuth")]
async fn upload(mut payload: Multipart) -> Result<HttpResponse, Error> {
}
Note that you need actix_service 1.x for this to compile. actix_service 2 remove the request internal type to make it generic and I couldn't make it work with the wrap="" syntax

Spring Webflux Mono is ignoring ResponseEntity's status and returning 200 OK

I have a RestController which looks like this
#RestController
#RequestMapping("/user")
class UserController(val userService: UserService) {
#PostMapping("/register")
fun register(#RequestBody body: UsernamePasswordResource): Mono<*> {
return userService.createUser(body.username, body.password)
.map {ResponseEntity(it, HttpStatus.CREATED)}
.doOnError {
if (it is DuplicateUserException){
ResponseEntity(ErrorResource(it.message!!), HttpStatus.BAD_REQUEST)
} else {
ResponseEntity(ErrorResource("Internal server error"), HttpStatus.INTERNAL_SERVER_ERROR)
}
}
}
}
When I create a using using this endpoint 200 OK in response and this as body:
{
"headers": {},
"body": {
"username": "test7",
"passwordEncoded": "4HxMMUI09pltEr9pyKIxsQ==",
"description": ""
},
"statusCode": "CREATED",
"statusCodeValue": 201
}
Why is the status of my ResponseEntity ignored and 200 is returned? How do i fix this? Thanks in advance.
Spring provides an HTTP 200 (OK) response by default when an endpoint returns successfully. To return a custom HTTP status, use the #ReponseStatus annotation above your function and pass your status code in the parenthesis. Your code snippet should look like:
#RestController
#RequestMapping("/user")
class UserController(val userService: UserService) {
#PostMapping("/register")
#ResponseStatus(HttpStatus.CREATED)
fun register(#RequestBody body: UsernamePasswordResource): Mono<*> {
return userService.createUser(body.username, body.password)
.map {ResponseEntity(it, HttpStatus.CREATED)}
.doOnError {
if (it is DuplicateUserException){
ResponseEntity(ErrorResource(it.message!!), HttpStatus.BAD_REQUEST)
} else {
ResponseEntity(ErrorResource("Internal server error"), HttpStatus.INTERNAL_SERVER_ERROR)
}
}
}
}
I figured it out. The endpoint was returning Mono<*>
After I changed my service to return AuthenticationResource instead of UserResource
I could also make the endpoint return Mono<ResponseEntity<AuthenticationResource>>.
After that the ResponseEntity was respected and used as normal.

Set programmatically jsonValidation for dynamic mapping

I am creating a new vscode extension, and I need to extend the standard usage of the jsonValidation system already present in vscode.
Note : I am talking about the system defined in package.json :
"contributes" : {
"languages": [
{
"id" : "yml",
"filenamePatterns": ["module.service"]
},
{
"id" : "json",
"filenamePatterns": ["module.*"]
}
],
"jsonValidation": [
{
"fileMatch": "module.test",
"url": "./resources/test.schema"
}
]
}
Now, I need to create a dynamic mapping, where the json fields filematch/url are defined from some internal rules (like version and other internal stuff). The standard usage is static : one fileMatch -> one schema.
I want for example to read the version from the json file to validate, and set the schema after that :
{
"version" : "1.1"
}
validation schema must be test-schema.1.1 instead of test-schema.1.0
note : The question is only about the modification of the configuration provided by package.json from the extensions.ts
Thanks for the support
** EDIT since the previous solution was not working in all cases
There is one solution to modify the package.json at the activating of the function.
export function activate(context: vscode.ExtensionContext) {
const myPlugin = vscode.extensions.getExtension("your.plugin.id");
if (!myPlugin)
{
throw new Error("Composer plugin is not found...")
}
// Get the current workspace path to found the schema later.
const folderPath = vscode.workspace.workspaceFolders;
if (!folderPath)
{
return;
}
const baseUri : vscode.Uri = folderPath[0].uri;
let packageJSON = myPlugin.packageJSON;
if (packageJSON && packageJSON.contributes && packageJSON.contributes.jsonValidation)
{
let jsonValidation = packageJSON.contributes.jsonValidation;
const schemaUri : vscode.Uri = vscode.Uri.joinPath(baseUri, "/schema/value-0.3.0.json-schema");
const schema = new JsonSchemaMatch("value.ospp", schemaUri)
jsonValidation.push(schema);
}
}
And the json schema class
class JsonSchemaMatch
{
fileMatch: string;
url : string;
constructor(fileMatch : string, url: vscode.Uri)
{
this.fileMatch = fileMatch;
this.url = url.path;
}
}
Another important information is the loading of the element of contributes is not reread after modification, for example
class Language
{
id: string;
filenamePatterns : string[];
constructor(id : string, filenamePatterns: string[])
{
this.id = id;
this.filenamePatterns = filenamePatterns;
}
}
if (packageJSON && packageJSON.contributes && packageJSON.contributes.languages)
{
let languages : Language[] = packageJSON.contributes.languages;
for (let language of languages) {
if (language.id == "json") {
language.filenamePatterns.push("test.my-json-type")
}
}
}
This change has no effect, since the loading of file association is already done (I have not dig for the reason, but I think this is the case)
In this case, creating a settings.json in the workspace directory can do the job:
settings.json
{
"files.associations": {
"target.snmp": "json",
"stack.cfg": "json"
}
}
Be aware that the settings.json can be created by the user with legitimate reason, so don't override it, just fill it.

Get GraphQL whole schema query

I want to get the schema from the server.
I can get all entities with the types but I'm unable to get the properties.
Getting all types:
query {
__schema {
queryType {
fields {
name
type {
kind
ofType {
kind
name
}
}
}
}
}
}
How to get the properties for type:
__type(name: "Person") {
kind
name
fields {
name
type {
kind
name
description
}
}
}
How can I get all types with the properties in only 1 request? Or ever better: How can I get the whole schema with the mutators, enums, types ...
Update
Using graphql-cli is now the recommended workflow to get and update your schema.
The following commands will get you started:
# install via NPM
npm install -g graphql-cli
# Setup your .graphqlconfig file (configure endpoints + schema path)
graphql init
# Download the schema from the server
graphql get-schema
You can even listen for schema changes and continuously update your schema by running:
graphql get-schema --watch
In case you just want to download the GraphQL schema, use the following approach:
The easiest way to get a GraphQL schema is using the CLI tool get-graphql-schema.
You can install it via NPM:
npm install -g get-graphql-schema
There are two ways to get your schema. 1) GraphQL IDL format or 2) JSON introspection query format.
GraphQL IDL format
get-graphql-schema ENDPOINT_URL > schema.graphql
JSON introspection format
get-graphql-schema ENDPOINT_URL --json > schema.json
or
get-graphql-schema ENDPOINT_URL -j > schema.json
For more information you can refer to the following tutorial: How to download the GraphQL IDL Schema
This is the query that GraphiQL uses (network capture):
query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
subscriptionType {
name
}
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
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
}
}
}
}
}
}
}
}
You can use GraphQL-JS's introspection query to get everything you'd like to know about the schema:
import { introspectionQuery } from 'graphql';
If you want just the information for types, you can use this:
{
__schema: {
types: {
...fullType
}
}
}
Which uses the following fragment from the introspection query:
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
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
}
}
}
}
}
}
}
}
`;
If that seems complicated, it's because fields can be arbitrarility deeply wrapped in nonNulls and Lists, which means that technically even the query above does not reflect the full schema if your fields are wrapped in more than 7 layers (which probably isn't the case).
You can see the source code for introspectionQuery here.
Using apollo cli:
npx apollo schema:download --endpoint=http://localhost:4000/graphql schema.json
Update
After getting sick of modifying my previous script all the time, I caved and made my own CLI tool gql-sdl. I still can't find a different tool that can download GraphQL SDL with zero config but would love for one to exist.
Basic usage:
$ gql-sdl https://api.github.com/graphql -H "Authorization: Bearer ghp_[redacted]"
directive #requiredCapabilities(requiredCapabilities: [String!]) on OBJECT | SCALAR | ARGUMENT_DEFINITION | INTERFACE | INPUT_OBJECT | FIELD_DEFINITION | ENUM | ENUM_VALUE | UNION | INPUT_FIELD_DEFINITION
"""Autogenerated input type of AbortQueuedMigrations"""
input AbortQueuedMigrationsInput {
"""The ID of the organization that is running the migrations."""
ownerId: ID!
"""A unique identifier for the client performing the mutation."""
clientMutationId: String
}
...
The header argument -H is technically optional but most GraphQL APIs require authentication via headers. You can also download the JSON response instead (--json) but that's a use case already well served by other tools.
Under the hood this still uses the introspection query provided by GraphQL.js, so if you're looking to incorporate this functionality into your own code see the example below.
Previous answer
Somehow I wasn't able to get any of the suggested CLI tools to output the schema in GraphQL's Schema Definition Language (SDL) instead of the introspection result JSON. I ended up throwing together a really quick Node script to make the GraphQL library do it for me:
const fs = require("fs");
const { buildClientSchema, getIntrospectionQuery, printSchema } = require("graphql");
const fetch = require("node-fetch");
async function saveSchema(endpoint, filename) {
const response = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: getIntrospectionQuery() })
});
const graphqlSchemaObj = buildClientSchema((await response.json()).data);
const sdlString = printSchema(graphqlSchemaObj);
fs.writeFileSync(filename, sdlString);
}
saveSchema("https://example.com/graphql", "schema.graphql");
getIntrospectionQuery() has the complete introspection query you need to get everything, and then buildClientSchema() and printSchema() turns the JSON mess into GraphQL SDL.
Wouldn't be too difficult to make this into a CLI tool itself but that feels like overkill.
You can use the Hasura's graphqurl utility
npm install -g graphqurl
gq <endpoint> --introspect > schema.graphql
# or if you want it in json
gq <endpoint> --introspect --format json > schema.json
Full documentation: https://github.com/hasura/graphqurl
You can download a remote GraphQL server's schema with the following command. When the command succeeds, you should see a new file named schema.json in the current working directory.
~$ npx apollo-cli download-schema $GRAPHQL_URL --output schema.json
You can use GraphQL-Codegen with the ast-plugin
npm install --save graphql
npm install --save-dev #graphql-codegen/cli
npx graphql-codegen init
Follow the steps to generate the codegen.yml file
Once the tool is installed, you can use the plugin to download the schema which is schema-ast
The best is to follow the instruction on the page to install it… but basically:
npm install --save-dev #graphql-codegen/schema-ast
Then configure the codegen.yml file to set which schema(s) is/are the source of truth and where to put the downloaded schema(s) file:
schema:
- 'http://localhost:3000/graphql'
generates:
path/to/file.graphql:
plugins:
- schema-ast
config:
includeDirectives: true
I was also looking and came across this Medium article on GraphQL
The below query returned many details regarding schema, queries and their input & output params type.
fragment FullType on __Type {
kind
name
fields(includeDeprecated: true) {
name
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
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
}
}
}
}
}
}
}
}
query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
types {
...FullType
}
directives {
name
locations
args {
...InputValue
}
}
}
}
You can use IntelliJ plugin JS GraphQL then IDEA will ask you create two files "graphql.config.json" and "graphql.schema.json"
Then you can edit "graphql.config.json" to point to your local or remote GraphQL server:
"schema": {
"README_request" : "To request the schema from a url instead, remove the 'file' JSON property above (and optionally delete the default graphql.schema.json file).",
"request": {
"url" : "http://localhost:4000",
"method" : "POST",
"README_postIntrospectionQuery" : "Whether to POST an introspectionQuery to the url. If the url always returns the schema JSON, set to false and consider using GET",
"postIntrospectionQuery" : true,
"README_options" : "See the 'Options' section at https://github.com/then/then-request",
"options" : {
"headers": {
"user-agent" : "JS GraphQL"
}
}
}
After that IDEA plugin will auto load schema from GraphQL server and show the schema json in the console like this:
Loaded schema from 'http://localhost:4000': {"data":{"__schema":{"queryType":{"name":"Query"},"mutationType":{"name":"Mutation"},"subscriptionType":null,"types":[{"kind":"OBJECT","name":"Query","description":"","fields":[{"name":"launche
Refer to https://stackoverflow.com/a/42010467/10189759
Would like to point out that if authentications are needed, that you probably cannot just use the config file generated from graphql init
You might have to do something like this, for example, using the github graphql API
{
"projects": {
"graphqlProjectTestingGraphql": {
"schemaPath": "schema.graphql",
"extensions": {
"endpoints": {
"dev": {
"url": "https://api.github.com/graphql",
"headers": {
"Authorization": "Bearer <Your token here>"
}
}
}
}
}
}
}
If you want to do it by your self, read these code:
There is a modular state-of-art tool 「graphql-cli」, consider looking at it. It uses package 「graphql」's buildClientSchema to build IDL .graphql file from introspection data.
graphql-cli get-schema :integrated into graphql-cli part 1
graphql-config EndpointsExtension :integrated into graphql-cli part 2
The graphql npm package's IntrospectionQuery does
query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
subscriptionType {
name
}
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
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
}
}
}
}
}
}
}
}
source
You could use apollo codegen:client. See https://github.com/apollographql/apollo-tooling#apollo-clientcodegen-output