Trying to use loopback framework for simulating a backend service. I need to retrieve an object using POST method. I know REST services typically allow POST to update/create a resource, but here, I cannot use GET with resource details for retrieving data.
In my case, POST data contains a few query fields that have to be used to query an object and send json back. Is this possible with loopback? I cannot use GET with query parms due to security restrictions with sending data as query parms in a GET URL.
here is post request data
[ { customer:"sam", city:"noWhere", } ]
the POST event should query by customer and city, then return matching customer object
[ { customer:"sam", postcode:"352345", city:"noWhere", country:"US" } ]
I think that what you need is an express http method override middleware: https://github.com/expressjs/method-override
And defining middleware in loopback:
http://docs.strongloop.com/display/LB/Defining+middleware
You can override default loopback endpoint, like this
// Define custom remote method
Customer.fetch = function(oRequest, fnResponseCb) {
/* Do staff to find customer and finally call fnResponseCb(null, oCustomer) */
}
// Override custom remote method
Customer.remoteMethod('fetch', {
accepts: {
arg: 'oRequest',
type: 'object',
http: { source: 'body' }
},
returns: {
type: 'object',
root: true
},
http: {
path: '/',
verb: 'POST'
},
description : 'Fetch Customer'
});
Related
In loopback framework, is there a way to avoid updates for few fields
Below code allows updates for all fields that is passed in the API request body.
async updateById(
#param.path.number('id') id: number,
#requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(Todo, {partial: true}),
},
},
})
todo: Todo,
): Promise<void> {
await this.todoRepository.updateById(id, todo);
}
As far as I understand from your question, you want to update some part of the object in the database.
this.repo.updateById(id,objectYouWantToUpdate)
This is going to work perfectly, just send the data you want to update and not the whole object.
exclude key can help
schema: getModelSchemaRef(Todo, {partial: true, exclude: ['title']})
I have an existing GraphQL API that exposes a schema like this:
type AType {
id: Int!
name: String!
children: [BType]
}
type BType {
id: Int!
name: String!
}
type Query {
a(id: ID): A
as: [A]
}
plus some other omitted scalar fields in both. There are a couple of such API endpoints and I want to aggregate them all under a single AppSync instance, so that the front can query one endpoint with a single schema for all the data they need.
What I would like to do is if the front sends a query
as {
id,
name,
children {
id,
name
}
}
it would get relayed exactly to my existing GraphQL endpoint. The AWS examples linked on the AppSync page talk about GraphQL endpoints but the only linked code example is this, and it shows a resolver like this:
#**
Make an arbitrary HTTP call when this field is listed in a query selection set.
The "relativePath" is the resource path relative to the root URL provided when you
created the HTTP data source. Pass a "params" object to forward headers, body,
and query parameters to the HTTP endpoint.
*#
#if($context.identity.sub == $context.args.userId)
#set($payload = "query ListOrders {
listOrders(
userId: ""$context.args.userId""
orderDateTimeStatus: {
le : {
orderDateTime: ""$context.args.orderDateTime""
}
}
limit: 10
) {
items {
userId
status
orderDateTime
details
orderId
}
nextToken
}
}")
{
"version": "2018-05-29",
"method": "POST",
"resourcePath": "/graphql",
"params":{
"body": {
"query": "$util.escapeJavaScript($payload)"
},
"headers":{
"Content-Type": "application/json",
"x-access-token" : "$context.request.headers.x-access-token"
}
}
}
#else
$utils.unauthorized()
#end
This uses only the arguments passed into the query, not the query structure. If front requests a subset of fields of A, I only want to make a request for that subset of fields, not a request for all that then gets cut down by AppSync. Also it doesn't even contain nested objects -- would I have to fetch all the children every time just so that they get later ignored by AppSync?
Assume there are 4-5 microservices that have their own GraphQL endpoints that have different queries in their schemas. I'd like to map the requests 1-1 to them. How would I write such a resolver? AppSync doesn't really have a good playground environment where I could debug the resolver so I can't just look at the $ctx object and reverse-engineer the structure of the original query (AFAIK).
I found this question that asks just that, but the solution there doesn't work as intended, as mentioned by the author, and it seems to be dead.
I have set up an OpenAPI connector in Loopback 4 as described here and for unauthorized requests, it is working well; I managed to create the respective datasource, service and controller. My service is similar to the GeocoderProvider example, but, let's say, with the following service interface.
export interface MyExternalService {
search_stuff(params: {query?: string}): Promise<MyExternalServiceResponse>;
}
export interface MyExternalServiceResponse {
text: string;
}
From my controller, I invoke it like this, where this.myExternalService is the injected service (kind of unrelated, but can Loopback also implicitly parse a JSON response from an external API datasource?):
#get('/search')
async searchStuff(#param.query.string('query') query: string): Promise<void> {
return JSON.parse(
(await this.myExternalService.search_stuff({query})).text,
);
}
Now, the external endpoint corresponding to myExternalService.search_stuff needs an Authorization: Bearer <token> header, where the token is sent to Loopback by the client, i.e. it's not a static API key or so. Assuming I added #param.query.string('token') token: string to the parameter list of my searchStuff controller method, how can I forward that token to the OpenAPI connector? This is the relevant part of the underlying OpenAPI YAML definition file:
paths:
/search:
get:
security:
- Authorization: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/SearchResults'
operationId: search-stuff
components:
securitySchemes:
Authorization:
type: http
scheme: Bearer
I am now using the underlying execute function of the OpenAPI connector and manually intercept the request (the object that is passed to requestInterceptor is later passed directly to the http module by Swagger):
return JSON.parse(
(
await this.myExternalService.execute(
'search_stuff',
{query},
{
requestInterceptor: (req: {headers: {Authorization: string}}) => {
req.headers.Authorization = 'Bearer ' + token;
return req;
},
},
)
).text,
);
I also added the following method to the MyExternalService interface, inspired by the connector's actual execute function:
execute(
operationId: string,
parameters: object,
options: object,
): Promise<MyExternalServiceResponse>;
Some things I found:
Loopback internally uses the swagger-client module to do OpenAPI-based requests.
Specifically the securities option of Swagger's execute function expects a Security Definitions Object. There are some quirks with actually passing it to Swagger as well.
Internally, Swagger builds the final HTTP request that is sent out here in its source code. There, the securities key is mentioned, yet is is never actually used for the request. This means that manually specifying it in the third parameter of this.myExternalService.execute will change nothing.
I'll not accept this answer yet and I'm looking forward to finding a more Loopback-like approach.
I configured my service like this, to inject the basic authentication.
import {inject, lifeCycleObserver, LifeCycleObserver} from '#loopback/core';
import {juggler} from '#loopback/repository';
const SwaggerClient = require('swagger-client');
const config = {
name: 'jira',
connector: 'openapi',
spec: 'swagger-v2.json',
validate: false,
httpClient: (request: any) => {
request.headers["Authorization"] = "Basic " + Buffer.from("test:test").toString('base64');
return SwaggerClient.http(request);
},
};
#lifeCycleObserver('datasource')
export class JiraDataSource extends juggler.DataSource
implements LifeCycleObserver {
static dataSourceName = 'jira';
static readonly defaultConfig = config;
constructor(
#inject('datasources.config.jira', {optional: true})
dsConfig: object = config,
) {
super(dsConfig);
}
}
I want some props to be visible for GET method in loopback explorer, but I don't want to show them for POST method, for e.g. id property. How it can be done in loopback?
There are no built-in methods for this.
You need to do it in after remote for each remote method you want to be different from the default.
Model.afterRemote('GetMethod', function(ctx, instance, next){
var instance = ctx.result;
//reshape it
ctx.result = instance;
next();
});
UPDATE
If you want to affect this in explorer component so you need to create separate models with null datasource just for showing schema and use that in definition of remote method.
Model.remoteMethod('GetMethod', {
accepts: [
{
arg: 'req',
type: 'Object',
required: true,
http: {source: 'req'}
}
],
returns: {root: true, type: 'ModelDTOForSHow'},
http: {verb: 'get', status: 200, path: '/getter'}
});
And in ModelDTOForShow you hide some props and in another one some other props
How is it possible to access the loopback context (or simple Express req object) from within the model's logic?
It is critical to be able to know more about the request itself (current user identity more than anything else) inside the model's logic. When I override a built-in method (via custom script or from the model.js file) or develop a custom remote method, I would like to access the Express req object.
As loopback.getCurrentContext() is declared to be buggy, I cannot use it.
Ideas?
PS:
I find this page confusing: http://loopback.io/doc/en/lb2/Using-current-context.html
First it's said (and marked in red as important!) it is not recommended to use LoopBackContext.getCurrentContext() and then it's used it in each example!?
What's the point to give examples that do not work? Should we simply ignore the complete page? If so, what about the context? :)
Any clarification on this topic is much appreciated.
You can get access to express req object by using remote hooks
var loopback = require('loopback');
module.exports = function (MyModel) {
MyModel.beforeRemote('findOne', function (ctx, model, next) {
//access to ctx.req
console.log(ctx.req.headers)
next()
})
MyModel.beforeRemote('my-custom-remote-method', function (ctx, model, next) {
console.log(ctx.req.headers)
next()
})
}
Sure, you can use a beforeRemote hook to modify the ctx.args property. This property is the input of the remote method (that is, custom or built-in). This way, you can copy a part of the request inside this property, and it will be passed to the build-in method
Example 1 with the built-in method findOne.
MyModel.beforeRemote('findOne', function (ctx, model, next) {
ctx.args.filter.extrafield = ctx.req.headers['some-header'];
next();
});
Then override the findOne method since it's what you want to do
MyModel.on('dataSourceAttached', function(obj){
var findOne = MyModel.findOne;
MyModel.findOne = function(filter, cb) {
console.log(filter.extrafield); // Print what was in the header
return findOne.apply(this, arguments);
};
});
And finally call the method with curl
curl -H "some-header: 'hello, world!'" localhost:3000/api/MyModel/findOne
Example 2 with a custom remote printToken, to help you understand further
MyModel.beforeRemote('printToken', function (ctx, model, next) {
ctx.args.token = ctx.req.headers['some-header'];
next();
});
MyModel.printToken = function(token, cb) {
console.log(token);
cb();
}
MyModel.remoteMethod(
'printToken',
{
accepts: {arg: 'token', type: 'string', optional: true}
}
);
Then call the remote with curl, and pass the expected header
curl -H "some-header: 'hello, world!'" localhost:3000/api/MyModel/printToken
EDIT: There is a simpler solution that only works for custom remote
When defining your remote method, it is possible to tell loopback to pass elements of the http request to your remote directly as an input argument
MyModel.remoteMethod(
'printToken',
{
accepts: [
{arg: 'req', type: 'object', 'http': {source: 'req'}},
{arg: 'res', type: 'object', 'http': {source: 'res'}}
]
}
);
This way, your remote can access the req and res objects. This is documented here