That's the signature for oneUrl function: oneUrl(route, url)
And from the documentation:
oneUrl(route, url): This will create a new Restangular object that is
just a pointer to one element with the specified URL.
To me, it seems useless to set Route when you are giving a url for the resource. Why does it exist in the argument list? Why is it mandatory? And how can it be used?
In my use of oneUrl I've found the route name is used to build the URL for subsequent PUT and DELETE operations. For example (pseudo code):
// "GET /api/v2/users/3/ HTTP/1.1" 200 184
var user = Restangular.oneUrl('myuser', 'http://localhost:8000/api/v2/users/3').get();
user.name = 'Fred';
// the following uses the route name and not the URL:
// "PUT /api/v2/myuser HTTP/1.1 404 100
user.put();
I was surprised by this behavior. I expected put() to use the same URL as get(); which would be helpful in my case.
My API uses absolute URLs within the JSON payloads to navigate to all related resources and I wanted to use oneUrl() to GET/PUT instances without recreating the routes in the JS code. But I'm pretty new to Restangular so I might not have the mental model correct.
Related
I'm using Angular's Resource module to access a web API, but I'm having problems as the API uses URIs as the primary key.
Whenever I try to make a call to this API, passing in a URI as a string parameter, I'm getting 400 Bad Request errors. On closer inspection, Resource is escaping all the forward slashes in the URI but not the colon at the start. It's doing a GET on a URL that looks like this: http://myserver/api/objects/http:%2F%2Fexample.comk%2FmyURI%2F, which is of course invalid. I've also tried escaping the colon with a backslash, but that doesn't work either.
How can I make Resource escape my parameters properly? I've tried replacing the colon with %3A before making the call, but that results in the % being encoded again, returning 404 Not Found.
The service handling Resource looks like this:
angular.module('adminApp').factory('MyObject', myObject);
function myObject($resource) {
return $resource('/api/objects/:uri');
};
and I'm calling it like this:
MyObject.get({ uri: myUri }, function(result) {
...
});
I've got around this issue by passing the URI as a query parameter instead of as part of the request URL. I did this by changing my resource service to this:
angular.module('adminApp').factory('MyObject', myObject);
function myObject($resource) {
return $resource('/api/objects');
};
and leaving the calling code this the same. ngResource then creates a GET that looks like http://myserver/api/objects?uri=http:%2F%2Fexample.comk%2FmyURI%2F, which is fine.
Basically, if you're using unusual characters in your API parameters, put them in a query string rather than in the URL! :-)
When uploading to GCS (Google Cloud Storage) using the BlobStore's createUploadURL function, I can provide a callback together with header data that will be POSTed to the callback URL.
There doesn't seem to be a way to do that with GCS's signed URL's
I know there is Object Change Notification but that won't allow the user to provide upload specific information in the header of a POST, the way it is possible with createUploadURL's callback.
My feeling is, if createUploadURL can do it, there must be a way to do it with signed URL's, but I can't find any documentation on it. I was wondering if anyone may know how createUploadURL achieves that callback calling behavior.
PS: I'm trying to move away from createUploadURL because of the __BlobInfo__ entities it creates, which for my specific use case I do not need, and somehow seem to be indelible and are wasting storage space.
Update: It worked! Here is how:
Short Answer: It cannot be done with PUT, but can be done with POST
Long Answer:
If you look at the signed-URL page, in front of HTTP_Verb, under Description, there is a subtle note that this page is only relevant to GET, HEAD, PUT, and DELETE, but POST is a completely different game. I had missed this, but it turned out to be very important.
There is a whole page of HTTP Headers that does not list an important header that can be used with POST; that header is success_action_redirect, as voscausa correctly answered.
In the POST page Google "strongly recommends" using PUT, unless dealing with form data. However, POST has a few nice features that PUT does not have. They may worry that POST gives us too many strings to hang ourselves with.
But I'd say it is totally worth dropping createUploadURL, and writing your own code to redirect to a callback. Here is how:
Code:
If you are working in Python voscausa's code is very helpful.
I'm using apejs to write javascript in a Java app, so my code looks like this:
var exp = new Date()
exp.setTime(exp.getTime() + 1000 * 60 * 100); //100 minutes
json['GoogleAccessId'] = String(appIdentity.getServiceAccountName())
json['key'] = keyGenerator()
json['bucket'] = bucket
json['Expires'] = exp.toISOString();
json['success_action_redirect'] = "https://" + request.getServerName() + "/test2/";
json['uri'] = 'https://' + bucket + '.storage.googleapis.com/';
var policy = {'expiration': json.Expires
, 'conditions': [
["starts-with", "$key", json.key],
{'Expires': json.Expires},
{'bucket': json.bucket},
{"success_action_redirect": json.success_action_redirect}
]
};
var plain = StringToBytes(JSON.stringify(policy))
json['policy'] = String(Base64.encodeBase64String(plain))
var result = appIdentity.signForApp(Base64.encodeBase64(plain, false));
json['signature'] = String(Base64.encodeBase64String(result.getSignature()))
The code above first provides the relevant fields.
Then creates a policy object. Then it stringify's the object and converts it into a byte array (you can use .getBytes in Java. I had to write a function for javascript).
A base64 encoded version of this array, populates the policy field.
Then it is signed using the appidentity package. Finally the signature is base64 encoded, and we are done.
On the client side, all members of the json object will be added to the Form, except the uri which is the form's address.
var formData = new FormData(document.forms.namedItem('upload'));
var blob = new Blob([thedata], {type: 'application/json'})
var keys = ['GoogleAccessId', 'key', 'bucket', 'Expires', 'success_action_redirect', 'policy', 'signature']
for(field in keys)
formData.append(keys[field], url[keys[field]])
formData.append('file', blob)
var rest = new XMLHttpRequest();
rest.open('POST', url.uri)
rest.onload = callback_function
rest.send(formData)
If you do not provide a redirect, the response status will be 204 for success. But if you do redirect, the status will be 200. If you got 403 or 400 something about the signature or policy maybe wrong. Look at the responseText. If is often helpful.
A few things to note:
Both POST and PUT have a signature field, but these mean slightly different things. In case of POST, this is a signature of the policy.
PUT has a baseurl which contains the key (object name), but the URL used for POST may only include bucket name
PUT requires expiration as seconds from UNIX epoch, but POST wants it as an ISO string.
A PUT signature should be URL encoded (Java: by wrapping it with a URLEncoder.encode call). But for POST, Base64 encoding suffices.
By extension, for POST do Base64.encodeBase64String(result.getSignature()), and do not use the Base64.encodeBase64URLSafeString function
You cannot pass extra headers with the POST; only those listed in the POST page are allowed.
If you provide a URL for success_action_redirect, it will receive a GET with the key, bucket and eTag.
The other benefit of using POST is you can provide size limits. With PUT however, if a file breached your size restriction, you can only delete it after it was fully uploaded, even if it is multiple-tera-bytes.
What is wrong with createUploadURL?
The method above is a manual createUploadURL.
But:
You don't get those __BlobInfo__ objects which create many indexes and are indelible. This irritates me as it wastes a lot of space (which reminds me of a separate issue: issue 4231. Please go give it a star)
You can provide your own object name, which helps create folders in your bucket.
You can provide different expiration dates for each link.
For the very very few javascript app-engineers:
function StringToBytes(sz) {
map = function(x) {return x.charCodeAt(0)}
return sz.split('').map(map)
}
You can include succes_action_redirect in a policy document when you use GCS post object.
Docs here: Docs: https://cloud.google.com/storage/docs/xml-api/post-object
Python example here: https://github.com/voscausa/appengine-gcs-upload
Example callback result:
def ok(self):
""" GCS upload success callback """
logging.debug('GCS upload result : %s' % self.request.query_string)
bucket = self.request.get('bucket', default_value='')
key = self.request.get('key', default_value='')
key_parts = key.rsplit('/', 1)
folder = key_parts[0] if len(key_parts) > 1 else None
A solution I am using is to turn on Object Changed Notifications. Any time an object is added, a Post is sent to a URL - in my case - a servlet in my project.
In the doPost() I get all info of objected added to GCS and from there, I can do whatever.
This worked great in my App Engine project.
I have a REST API endpoint for creating an empty object. What is the "standard" url scheme for this GET method? I'm currently using a factory in an angularjs app to make the call to the server.
Right now I have the following scheme:
GET
Item/new/
My $resource:
ngServices.factory("TESTfactory", function ($resource) {
return $resource("testNewItem/new", {}, {
create: {method: 'GET'}
}
}
A successful call to the above resource:
$scope.newItem = TESTfactory.newItem.create();
Any other suggestions would be much appreciated.
I've looked at the following links, which did not specifically list a url scheme for getting empty objects:
REST API Overview
Quick Reference section in this doc
A GET method should never create something. GET is supposed to be nullipotent, which means that it should have no side-effects. Creating a resource is certainly a side effect.
So, the standard http call would be
POST
Items/create
or
POST
Items/new
or better yet just
POST
Items/
I have a Nancy Fx application that acts as a pure API endpoint (application/json only, no text/html or browser etc access intended) and a module that returns e.g. the following:
return
userExists
? Negotiate.WithStatusCode(HttpStatusCode.OK)
: Negotiate.WithStatusCode(HttpStatusCode.NotFound);
However, I noticed one particularity - client's that have an Accept-Header set to 'application/json' and perform a GET request here, do get a text/html response back, even worse - in the .NotFound case a Nancy-specific/own 404 error is returned, in case of .OK an exception occurs due to missing Views.
What makes it even stranger for me is that inside my custom IStatusCodeHandler I "see" that the context.Response is a JsonResponse, somewhere down the pipeline this gets handled and (attempted to be) transformed further to text/html somehow though, and I wonder why.
Is there any way I can prevent the transformation to text/html?
This is because Nancy has a DefaultStatusCodeHandler that handles 500 and 404 responses. It's the last thing that runs in the Nancy pipeline before the host takes over the response.
What you're seeing is because the handler gets a 404 response (albeit a JsonResponse), and it can't know whether it's a hard (a route simply didn't exist) or a soft (a route existed but returned 404) status code, so it transforms it to the default 404 page. You might argue that it should check the accept header before doing so, but right now it isn't.
If you don't want this behavior, you can remove the default status code handler by overriding the InternalConfiguration property in your bootstrapper:
protected override NancyInternalConfiguration InternalConfiguration
{
get
{
return NancyInternalConfiguration
.WithOverrides(config => config.StatusCodeHandlers.Clear());
}
}
I have a Cloud Endpoints method that looks like this:
//HTTP POST
#ApiMethod(name = "hylyts.insert")
public Hylyt insertHylyt(#Named("url") String url, Hylyt hylyt, User user)
throws OAuthRequestException{
log.info("Trying to save hylyt '"+hylyt+"' with id '"+hylyt.getId());
if (user== null) throw new OAuthRequestException("Your token is no good here.");
hylyt.setArticle(getArticleKey(url, user));
ofy().save().entity(hylyt);
return hylyt;
}
I call it from the Javascript Client Library using this:
gapi.client.hylytit.hylyts.insert({PARAMS}).execute(callback);
Now, if I structure {PARAMS} as suggested in the docs (second example),
{
'url': url,
'resource': {
'hylyt': {
'contentType': 'application/json',
'data': hylyt
}
}
}
I get a null object in the endpoint (not to mention that the whole point of this library is to make these calls simple, which this structure clearly violates).
When I structure {PARAMS} as these answers suggest,
{
'url': url,
'resource': hylyt
}
I get a null object in the endpoint again. The correct syntax is this:
{
'url': url,
'id': hylyt.id
'text': hylyt.text
}
Which just blows my mind. Am I doing this all wrong? Is this a bug? Is it only happening because gapi is also passing the auth token in the background?
Yes, I could use the request syntax instead, but, again, why even use the library if it's just as complex as making the XHRs in pure javascript? I wouldn't mind the complexity if Google explained in the docs why things are happening. But the docs, paraphrased, just say use these methods and the auth, CORS, and XHR magic will happen behind closed doors.
Is the API method correctly recognized as POST method?
The resource parameter which is sent as POST body won't work correctly in a GET request.
The way it looks you are actually sending a GET request with the Hylyt properties in the query string.
To make sure you can change the method annotation to this:
#ApiMethod(name = "hylyts.insert", httpMethod = HttpMethod.POST)
Yup, agreed it's a bug. caused me great pains as well.
So i guess the work around is to create a combined object to pass to your api all named and un named parameters. Rather than hardcode each.. a quick loop might be better.
var param = {};
param["url"] = url;
for (var prop in hylyt) {
param[prop] = hylyt[prop];
}
gapi.client.hylytit.hylyts.insert(param).execute(callback);
That mashing together of parameters / objects can become a slick function if you really want.. but it's a band aid for what I'd consider a defect.
I see in the related question (cloud endpoints resource attribute for transmitting named params & body not working), you actually logged a defect.. Good stuff. Though there still appears no movement on this one. fingers crossed for someday!
The bug has been resolved. The correct syntax is
gapi.client.hylytit.hylyts.insert({url: url}, hylyt).execute(callback);