Using python to implement GraphQL across multiple microservices, some use Ariadne, and some use graphene (and graphene-Django). Because of the microservice architecture, it's chosen that Apollo Federation will merge the schemas from the different microservices.
With Ariadne, it's very simple (being schema first), and a small example:
from ariadne import QueryType, gql, make_executable_schema, MutationType, ObjectType
from ariadne.asgi import GraphQL
query = QueryType()
mutation = MutationType()
sdl = """
type _Service {
sdl: String
}
type Query {
_service: _Service!
hello: String
}
"""
#query.field("hello")
async def resolve_hello(_, info):
return "Hello"
#query.field("_service")
def resolve__service(_, info):
return {
"sdl": sdl
}
schema = make_executable_schema(gql(sdl), query)
app = GraphQL(schema, debug=True)
Now this is picked up with no problem with Apollo Federation:
const { ApolloServer } = require("apollo-server");
const { ApolloGateway } = require("#apollo/gateway");
const gateway = new ApolloGateway({
serviceList: [
// { name: 'msone', url: 'http://192.168.2.222:9091' },
{ name: 'mstwo', url: 'http://192.168.2.222:9092/graphql/' },
]
});
(async () => {
const { schema, executor } = await gateway.load();
const server = new ApolloServer({ schema, executor });
// server.listen();
server.listen(
3000, "0.0.0.0"
).then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
})();
For which I can run graphql queries against the server on 3000.
But, with using graphene, trying to implement the same functionality as Ariadne:
import graphene
class _Service(graphene.ObjectType):
sdl = graphene.String()
class Query(graphene.ObjectType):
service = graphene.Field(_Service, name="_service")
hello = graphene.String()
def resolve_hello(self, info, **kwargs):
return "Hello world!"
def resolve_service(self, info, **kwargs):
from config.settings.shared import get_loaded_sdl
res = get_loaded_sdl() # gets the schema defined later in this file
return _Service(sdl=res)
schema = graphene.Schema(query=Query)
# urls.py
urlpatterns = [
url(r'^graphql/$', GraphQLView.as_view(graphiql=True)),
]
,... now results in an error from the Apollo Federation:
GraphQLSchemaValidationError: Type Query must define one or more fields.
As I checked into this matter, I found that apollo calls the microservice with a graphql query of:
query GetServiceDefinition { _service { sdl } }
Running it on the microservice via Insomnia/Postman/GraphiQL with Ariadne gives:
{
"data": {
"_service": {
"sdl": "\n\ntype _Service {\n sdl: String\n}\n\ntype Query {\n _service: _Service!\n hello: String\n}\n"
}
}
}
# Which expanding the `sdl` part:
type _Service {
sdl: String
}
type Query {
_service: _Service!
hello: String
}
and on the microservice with Graphene:
{
"data": {
"_service": {
"sdl": "schema {\n query: Query\n}\n\ntype Query {\n _service: _Service\n hello: String\n}\n\ntype _Service {\n sdl: String\n}\n"
}
}
}
# Which expanding the `sdl` part:
schema {
query: Query
}
type Query {
_service: _Service
hello: String
}
type _Service {
sdl: String
}
So, they both are the same thing for defining how to get sdl, I checked into the microservice response, and found that graphene response is sending the correct data too,
with the Json response "data" being equal to:
execution_Result: OrderedDict([('_service', OrderedDict([('sdl', 'schema {\n query: Query\n}\n\ntype Query {\n _service: _Service\n hello: String\n}\n\ntype _Service {\n sdl: String\n}\n')]))])
So what could the reason be for Apollo Federation not being able to successfully get this microservice schema?
This pip library can help https://pypi.org/project/graphene-federation/
Just use build_schema, and it'll add _service{sdl} for you:
import graphene
from graphene_federation import build_schema
class Query(graphene.ObjectType):
...
pass
schema = build_schema(Query) # add _service{sdl} field in Query
You are on the good path on the other answer, but it looks like you are going to need to strip out some stuff from the printed version.
here is the way I have used in a github issue
i sum up my code here:
schema = ""
class ServiceField(graphene.ObjectType):
sdl = String()
def resolve_sdl(parent, _):
string_schema = str(schema)
string_schema = string_schema.replace("\n", " ")
string_schema = string_schema.replace("type Query", "extend type Query")
string_schema = string_schema.replace("schema { query: Query mutation: MutationQuery }", "")
return string_schema
class Service:
_service = graphene.Field(ServiceField, name="_service", resolver=lambda x, _: {})
class Query(
# ...
Service,
graphene.ObjectType,
):
pass
schema = graphene.Schema(query=Query, types=CUSTOM_ATTRIBUTES_TYPES)
The solution is actually a slight hack the schema that is automatically generated via graphene. I thought I had tried this already and it still worked, but I just did it again now but it broke.
So if in Ariadne, I add
schema {
query: Query
}
into the sdl, Apollo Federation also raises Type Query must define one or more fields.. Without it, it works fine. So then I also went to graphene and in the resolve_service function I did:
def resolve_service(self, info, **kwargs):
from config.settings.shared import get_loaded_sdl
res = get_loaded_sdl()
res = res.replace("schema {\n query: Query\n}\n\n", "")
return _Service(sdl=res)
And now graphene works too, so I guess the problem was something I overlooked, it seems that Apollo Federation cannot handle schema grammar of:
schema {
query: Query
}
Update 1
A line I didn't notice on Apollo's website is that:
This SDL does not include the additions of the federation spec above. Given an input like this:
This is clear when combining the services together in Federation as it will raise the error:
GraphQLSchemaValidationError: Field "_Service.sdl" can only be defined once.
So, although in the full schema for the microservice with define _Service.sdl, we want that information gone for the string of the full-schema that is returned as the return String for _Service.sdl
Update 2
The Apollo Federation is now working fine, with making sure that the string returned by the sdl field does not contain federation specs.
In graphene, I think each implementation might differ, but in general you want to replace the following:
res = get_loaded_sdl()
res = res.replace("schema {\n query: Query\n}\n\n", "")
res = res.replace("type _Service {\n sdl: String\n}", "")
res = res.replace("\n _service: _Service!", "")
And in Ariadne, just need to define two sdl's, one containing the federation specs (for the schema returned by the service), and one without federation specs (the one returned by the sdl field)
In case anyone is wondering, this is because graphene v2 uses commas instead of ampersands in interfaces
interface x implements y, z {
...
}
and this syntax no longer works, a workaround is to monkey-patch get_sdl
import re
from myproject import Query, Mutation
from graphene_federation import service, build_schema
# monkey patch old get_sdl
old_get_sdl = service.get_sdl
def get_sdl(schema, custom_entities):
string_schema = old_get_sdl(schema, custom_entities)
string_schema = string_schema.replace('\n', ' ')
pattern_types_interfaces = r'type [A-Za-z]* implements ([A-Za-z]+\s*,?\s*)+'
pattern = re.compile(pattern_types_interfaces)
string_schema = pattern.sub(lambda matchObj: matchObj.group().replace(',', ' &'), string_schema)
return string_schema
service.get_sdl = get_sdl
schema = build_schema(Query, mutation=Mutation)
and it works.
Related
I'm struggling to make the fields of my request DTOs case insensitive.
export class ExampleDto {
dateOfBirth?: string
}
Now I want to accept
{ "dateofbirth": "19880101" }
{ "dateOfBirth": "19880101" }
{ "DATEOFBIRTH": "19880101" }
My first thought was to implement a middleware which just looks at the incoming body and "extends it" with lower & upper case mappings for all incoming fields.
But that doesn't meet my requirements due to camel case, which I definitely want to keep as the default.
Any ideas on how to do this?
You could create a custom Pipe where you try the different options and finally return the Dto instance:
export class CaseInsensitiveExampleDtoPipe implements PipeTransform{
transform(body: any, metadata: ArgumentMetadata): ExampleDto {
const dto = new ExampleDto();
dto.dateOfBirth = body.dateOfBirth || body.dateofbirth || body.DATEOFBIRTH;
return dto;
}
In your controller you can then use it as follows:
#UsePipes(new CaseInsensitiveExampleDtoPipe())
async postNewExample(#Body() exampleDto: ExampleDto) {
// ...
}
Since JavaScript properties start existing after their initialization, you cannot "see" the definition of dateOfBirth?: string and therefor you won't be able to match it against the received JSON.
A possible solution for that is to enforce the creation of the properties of all of your DTO's with a constructor:
export class ExampleDto {
dateOfBirth: string
constructor(dateOfBirth: string){
this.dateOfBirth = dateOfBirth;
}
}
Then, you'll be able to iterate over the ExampleDto's properties and match them with a pipe (the received type can be derived from metadata):
#Injectable()
export class IgnoreCasePipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
const dto = new metadata.metatype;
const dtoKeys = Object.getOwnPropertyNames(dto);
Object.keys(value).forEach(key => {
const realKey = dtoKeys.find(dtoKey => dtoKey.toLocaleLowerCase() === key.toLocaleLowerCase());
if (realKey) {
dto[realKey] = value[key];
}
});
return dto;
}
}
Either inject it globally in main.ts or wherever it's needed - just bear in mind that you'll need to create a constructor for each DTO.
Note: this would work for a single-level class. If you want to support something like people: PersonDto[] in your classes then you'll need to recursively find all of the nested keys and match them - something like this.
I don't get this. I have a service that injects entity repositories and has dedicated methods to do some business logic and functions.
Beside that I expose a method that just returns QueryBuilder - to avoid injecting repositories all over the place - for a few occasions when other service needs just a quick query:
type EntityFields = keyof MyEntity;
entityQueryBuilder(alias?: string, id?: number, ...select: EntityFields[]) {
const q = this.entityRepository.createQueryBuilder(alias);
if (id) {
q.where({id});
}
if (select) {
q.select(select);
}
return q;
}
Now when I am trying to use this and call:
const r = await service.entityQueryBuilder('a', 1, 'settings').getOne();
the result is always empty although in the log the generated SQL is correct.
However when I do:
const r = await service.entityQueryBuilder('a', 1, 'settings').execute();
I get (almost) what I need. I get array instead of an entity object directly but the data are there.
I am unhappy though as I need to map the result to the object I wanted, which is something that getOne() should do on my behalf. getMany() does not return results either.
What did I do wrong?
Edit:
FWIW here is the final solution I came up with based on the hint in accepted reply:
entityQueryBuilder(id?: number, ...select: EntityFields[]) {
const q = this.entityRepository.createQueryBuilder('alias');
if (id) {
q.where({id});
}
if (select) {
q.select(select.map(f => `alias.${f}`));
}
return q;
}
Admittedly it has hardcoded alias but that I can live with and is OK for my purpose.
Hope this helps someone in the future.
It happens because you put no really proper select. In your case, you need a.settings instead of settings:
const r = await service.entityQueryBuilder('a', 1, 'a.settings').getOne(); // it should works
I know this is a general question but I have exhausted google and tried many approaches.Any feedback is appreciated.
The HTTPClient is Angular 5+ so it returns an object created from the response JSON data. I get a massive JSON response from an endpoint I have no control over and I want to use about 20% of the response in my app and ignore the rest.
I am really trying hard to avoid using a series of templates or export objects or whatever and trying to force this massive untyped Observable into a typed object with hundreds of fields many being Arrays. All I need for the app is just a Array of very small objects with 3 fields per object. The 3 fields are all over within the JSON response and I want to map them to my object .map only seems to work when you are using the full response object and I can't find an example where .map does custom work besides in the case where you are mapping a few fields to 1 object and I am trying to map to an Array of my small objects.
UPDATED
Basically I want this service to return an object of Type DislayData to the module that subscribes to it but I get just an Object back. This is not what I ultimately need to do but if I can prove I can map the body of the response to my needed return type I can then start to break down the response body and return an Array of the Type I really need based on my silly DisplayData object. Thanks again!
export interface DislayData {
body: any;
}
...
export class DataService {
constructor(private http: HttpClient) { }
/** GET data from the black box */
getData(): Observable<DislayData> {
return this.http.get<HttpResponse<any>>(searchUrl, { observe: 'response' })
.pipe(
map(res => {
return res.body as DislayData;
}
tap(res => console.log(//do stuff with entire respoonse also)),
catchError(err => this.handleError(err)));
}
private handleError(error: HttpErrorResponse) {
...
Do you know the structure of the answering object?
If yes, you can do something like this:
item$ = new BehaviorSubject<any>({});
item = {
foo: 'a',
bar: 'b',
iton: [1, 2, 3],
boo: {
far: 'c'
}
};
logNewItem() {
this.item$
.pipe(
map(response => {
if (response.foo
&& response.iton
&& response.iton.length >= 3
&& response.boo
&& response.boo.far) {
let newItem = {
foo: response.foo,
iton2: response.iton[2],
far: response.boo.far
};
console.log(newItem); // output: Object { foo: "a", iton2: 3, far: "c" }
}
})
)
.subscribe();
this.item$.next(this.item);
}
Basically, you can simply make sure the properties exist, call them directly and map them to a better fitting object.
I heavily recommend creating an interface for the object you're receiving and an interface or class for the object you're mapping to. In that case you can also write the code more compact like this:
[...]
map(response: MyAPIResponse => {
let newItem = new NewItem(response);
console.log(newItem); // output: Object { foo: "a", iton2: 3, far: "c" }
}
})
[...]
class NewItem {
foo: string;
iton2: string;
far: string;
constructor(apiResponse: MyAPIResponse) {
//Validate parameter first
this.foo = apiResponse.foo;
this.iton2 = apiResponse.iton[2];
this.far = apiResponse.boo.far;
and make your code a lot more readable.
I am using Axios with NodeJs and trying to pass path parameters in axios.get() method. For example, if URL is url = '/fetch/{date}', I want to replace {date} with the actual date while calling axios.get(url).
I went through the source code on Github and StackOverflow, but couldn't find any method.
Is it possible to keep URLs with parameters as a placeholder and replace them while actually calling the get method of Axios?
Axios doesn't have this feature and it looks like the team don't want to add it.
With credit to previous responders for inspiration, to me this seems like the solution closest to what you (and me) are looking for:
1 - Where you want to store all your URLs and their parameters, define them as functions which use a template string to return the composed URL:
export var fetchDateUrl = (date) => `/fetch/${date}`;
If you need any type-specific formatting of the value being concatenated into the URL, this function is a good place to do it.
2 - Where you want to make the request, call the function with the correct parameters:
import { fetchDateUrl } from 'my-urls';
axios.get(fetchDateUrl(someDateVariable))...;
Another variation, if you really like the idea of naming the parameters at the call site, you can define the URL function to destructure an object like this:
var fetchDateUrl = ({date}) => `/fetch/${date}`;
which you'd then use like this:
axios.get(fetchDateUrl({date: someDateVariable}));
Use template strings
url = `/fetch/${date}`
Or just tag it on
url = '/fetch/'+ date
I think using axios interceptors is better to do this :
//create your instance
const instanceAxios = axios.create({
baseUrl: 'http://localhost:3001'
]);
instanceAxios.interceptors.request.use(config => {
if (!config.url) {
return config;
}
const currentUrl = new URL(config.url, config.baseURL);
// parse pathName to implement variables
Object.entries(config.urlParams || {}).forEach(([
k,
v,
]) => {
currentUrl.pathname = currentUrl.pathname.replace(`:${k}`, encodeURIComponent(v));
});
const authPart = currentUrl.username && currentUrl.password ? `${currentUrl.username}:${currentUrl.password}` : '';
return {
...config,
baseURL: `${currentUrl.protocol}//${authPart}${currentUrl.host}`,
url: currentUrl.pathname,
};
});
// use like :
instanceAxios.get('/issues/:uuid', {
urlParams : {
uuid: '123456789'
}
})
For typescript users, you will need to add this, in one of your .d.ts
declare module 'axios' {
interface AxiosRequestConfig {
urlParams?: Record<string, string>;
}
}
( this is a POC, not really tested, doesn't hesitate if you see something wrong )
You can use template strings ie:
let sellerId = 317737
function getSellerAnalyticsTotals() {
return axios.get(`http://localhost:8000/api/v1/seller/${sellerId}/analytics`);
}
Given some API /fetch/${date} you likely want to wrap your axios call in a function.
const fetchData = (date) => axios.get(`/fetch/${date}`);
fetchData(dateObject.toFormat('yyyy-mm-dd'))
.then(result => { ... });
This requires the calling code to format date correctly however. You can avoid this by using a DateTime library that handles date string parsing and do the format enforcement in the function.
const fetchData = (date) => axios.get(`/fetch/${date.toFormat('yyyy-mm-dd')}`);
fetchData(dateObject)
.then(result => { ... });
you can do like this:
getProduct = (id) => axios.get(`product/${id}`);
I always do it like this:
const res = await axios.get('https://localhost:3000/get', { params: { myParam: 123 } });
I find this to be much clearer than template strings.
More explanation here
Given the following object structure:
{
key1: "...",
key2: "...",
data: "..."
}
Is there any way to get this object from a CouchDB by quering both key1 and key2 without setting up two different views (one for each key) like:
select * from ... where key1=123 or key2=123
Kind regards,
Artjom
edit:
Here is a better description of the problem:
The object described above is a serialized game state. A game has exactly one creator user (key1) and his opponent (key2). For a given user I would like to get all games where he is involved (both as creator and opponent).
Emit both keys (or only one if equal):
function(doc) {
if (doc.hasOwnProperty('key1')) {
emit(doc.key1, 1);
}
if (doc.hasOwnProperty('key2') && doc.key1 !== doc.key2) {
emit(doc.key2, 1);
}
}
Query with (properly url-encoded):
?include_docs=true&key=123
or with multiple values:
?include_docs=true&keys=[123,567,...]
UPDATE: updated to query multiple values with a single query.
You could create a CouchDB view which produces output such as:
["key1", 111],
["key1", 123],
["key2", 111],
["key2", 123],
etc.
It is very simple to write a map view in javascript:
function(doc) {
emit(["key1", doc["key1"]], null);
emit(["key2", doc["key2"]], null);
}
When querying, you can query using multiple keys:
{"keys": [["key1", 123], ["key2", 123]]}
You can send that JSON as the data in a POST to the view. Or preferably use an API for your programming language. The results of this query will be each row in the view that matches either key. So, every document which matches on both key1 and key2 will return two rows in the view results.
I also was struggling with simular question, how to use
"select * from ... where key1=123 or key2=123".
The following view would allow you to lookup customer documents by the LastName or FirstName fields:
function(doc) {
if (doc.Type == "customer") {
emit(doc.LastName, {FirstName: doc.FirstName, Address: doc.Address});
emit(doc.FirstName, {LastName: doc.LastName, Address: doc.Address});
}
}
I am using this for a web service that queries all my docs and returns every doc that matches both the existence of a node and the query. In this example I am using the node 'detail' for the search. If you would like to search a different node, you need to specify.
This is my first Stack Overflow post, so I hope I can help someone out :)
***Python Code
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import httplib, json
from tornado.options import define,options
define("port", default=8000, help="run on the given port", type=int)
class MainHandler(tornado.web.RequestHandler):
def get(self):
db_host = 'YOUR_COUCHDB_SERVER'
db_port = 5984
db_name = 'YOUR_COUCHDB_DATABASE'
node = self.get_argument('node',None)
query = self.get_argument('query',None)
cleared = None
cleared = 1 if node else self.write('You have not supplied an object node.<br>')
cleared = 2 if query else self.write('You have not supplied a query string.<br>')
if cleared is 2:
uri = ''.join(['/', db_name, '/', '_design/keysearch/_view/' + node + '/?startkey="' + query + '"&endkey="' + query + '\u9999"'])
connection = httplib.HTTPConnection(db_host, db_port)
headers = {"Accept": "application/json"}
connection.request("GET", uri, None, headers)
response = connection.getresponse()
self.write(json.dumps(json.loads(response.read()), sort_keys=True, indent=4))
class Application(tornado.web.Application):
def __init__(self):
handlers = [
(r"/", MainHandler)
]
settings = dict(
debug = True
)
tornado.web.Application.__init__(self, handlers, **settings)
def main():
tornado.options.parse_command_line()
http_server = tornado.httpserver.HTTPServer(Application())
http_server.listen(options.port)
tornado.ioloop.IOLoop.instance().start()
if __name__ == '__main__':
main()
***CouchDB Design View
{
"_id": "_design/keysearch",
"language": "javascript",
"views": {
"detail": {
"map": "function(doc) { var docs = doc['detail'].match(/[A-Za-z0-9]+/g); if(docs) { for(var each in docs) { emit(docs[each],doc); } } }"
}
}
}