Pagination in Google cloud endpoints + Datastore + Objectify - google-app-engine

I want to return a List of "Posts" from an endpoint with optional pagination.
I need 100 results per query.
The Code i have written is as follows, it doesn't seem to work.
I am referring to an example at Objectify Wiki
Another option i know of is using query.offset(100);
But i read somewhere that this just loads the entire table and then ignores the first 100 entries which is not optimal.
I guess this must be a common use case and an optimal solution will be available.
public CollectionResponse<Post> getPosts(#Nullable #Named("cursor") String cursor,User auth) throws OAuthRequestException {
if (auth!=null){
Query<Post> query = ofy().load().type(Post.class).filter("isReviewed", true).order("-timeStamp").limit(100);
if (cursor!=null){
query.startAt(Cursor.fromWebSafeString(cursor));
log.info("Cursor received :" + Cursor.fromWebSafeString(cursor));
} else {
log.info("Cursor received : null");
}
QueryResultIterator<Post> iterator = query.iterator();
for (int i = 1 ; i <=100 ; i++){
if (iterator.hasNext()) iterator.next();
else break;
}
log.info("Cursor generated :" + iterator.getCursor());
return CollectionResponse.<Post>builder().setItems(query.list()).setNextPageToken(iterator.getCursor().toWebSafeString()).build();
} else throw new OAuthRequestException("Login please.");
}
This is a code using Offsets which seems to work fine.
#ApiMethod(
name = "getPosts",
httpMethod = ApiMethod.HttpMethod.GET
)
public CollectionResponse<Post> getPosts(#Nullable #Named("offset") Integer offset,User auth) throws OAuthRequestException {
if (auth!=null){
if (offset==null) offset = 0;
Query<Post> query = ofy().load().type(Post.class).filter("isReviewed", true).order("-timeStamp").offset(offset).limit(LIMIT);
log.info("Offset received :" + offset);
log.info("Offset generated :" + (LIMIT+offset));
return CollectionResponse.<Post>builder().setItems(query.list()).setNextPageToken(String.valueOf(LIMIT + offset)).build();
} else throw new OAuthRequestException("Login please.");
}

Be sure to assign the query:
query = query.startAt(cursor);
Objectify's API uses a functional style. startAt() does not mutate the object.

Try the following:
Remove your for loop -- not sure why it is there. But just iterate through your list and build out the list of items that you want to send back. You should stick to the iterator and not force it for 100 items in a loop.
Next, once you have iterated through it, use the iterator.getStartCursor() as the value of the cursor.

Related

Calling an API based on data within cells A2:A100 and outputting to cells G2:G100

I've been trying to figure out how to get a Google AppsScript to pull in an API for keyword rank tracking directly within Google Sheets.
The loop is required to dynamically pull in information from column A and output the keyword ranking position into column G.
The keywords are in cells A2-A100. The ranking position (which is the only thing we are pulling from the API) we are popping into the corresponding row in column G, starting from G2. For testing purposes, we've got the loop set from 1 to 3.
We're at a bit of a loss as to why this isn't working as expected, and would really appreciate a nudge in the right direction!
The issue is that the very first result always returns 'keyword = undefined' within the API, and returning a result of '-1', meaning that the first row is not read. We've tried updating the r to 0, to 2, and changing the r references to no avail.
This makes us think that there must be something wrong with the loop, rather than the rest of the code, but please do correct me if this is not the case.
The script we've gotten so far is;
function callAPI() {
//New loop
for (r = 1; r <= 3; r++) {
{
//Find keyword, encode query and url
var query = keyword;
var url =
'https://api.avesapi.com/search?apikey={{APIKEYREMOVEDFORPRIVACY}}&type=web&' +
'google_domain=google.co.uk&gl=gb&hl=en&device=mobile&output=json&num=100&tracked_domain={{CLIENTDOMAIN}}.com&position_only=true&uule2=London,%20United%20Kingdom' +
'&query=' +
encodeURIComponent(query);
//Call API and add to log
var response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
Logger.log(response);
//Get column value for keyword
var sheet = SpreadsheetApp.getActiveSheet();
var keyword = sheet.getRange(1 + r, 1).getValue();
}
//Set value of column
var results = sheet.getRange(1 + r, 7).setValue(response);
}
}
Additional edit:
So this is crystal clear, the desired input is;
keyword in A2 is read using the API and the output found (ranking position) is fed into G2.
the loop should then read A3, find the corresponding ranking position within the API, and adds that value to G3
rinse and repeat until the end of the loop.
Hopefully this is enough to go on, would really appreciate any advice, thank you!
Basically from TheMaster's comments you switch up your statements to this:
function callAPI() {
var sheet = SpreadsheetApp.getActiveSheet();
var keywords = sheet.getRange(2,1,3).getValues();
var responses = [];
//New loop
for (r = 0; r <= 2; r++) {
//Find keyword, encode query and url
var query = keywords[r][0];
var url =
'https://api.avesapi.com/search?apikey={{APIKEYREMOVEDFORPRIVACY}}&type=web&' +
'google_domain=google.co.uk&gl=gb&hl=en&device=mobile&output=json&num=100&tracked_domain={{CLIENTDOMAIN}}.com&position_only=true&uule2=London,%20United%20Kingdom' +
'&query=' +
encodeURIComponent(query);
//Call API and add to log
var resp = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
Logger.log(resp);
responses.push([resp]);
}
//Set value of column
sheet.getRange(2,7,3).setValues(responses);
}
Note that I moved the sheet declaration outside the loop, it needs to be only called once.
EDIT: I updated the code to follow best practices in the tag info page. Note the usage of arrays as return values of getValues() and parameter of setValues().

Receiving errors on Apex class to match fields for two different custom objects

I've been stuck for 18 hours on this apex class. I would really appreciate some help to figure out what's wrong. Basically this code should match the fields between two objects and I don't know why I'm receiving the following errors:
Missing return statement required return type: String - Line 2
Expecting '}' but was: 'for' - Line 26
The output should show that when location and position title from Candidate object matches location and title from Position object then it will just show those values that match on a data table in a lightning web component I made on Visual Studio.
I'd like some help improving this code so I can run it on the dev console.
public class Matchposition {
public static String matchPositionsWithCandidate() {
Set<String> statuses = new Set<String> {'New', 'Open'};
List<Position__c> openPositions = [SELECT Id, Name, Location__c, Status__c FROM Position__c WHERE Status__c IN :statuses];
//system.debug(openPositions);
Set<String> openPositionAndLocation = new Set<String>();
Map<String, Position__c> openPositionMap = new Map<String, Position__c>();
for (Position__c position : openPositions) {
openPositionMap.put(position.Name + '-' + position.Location__c, position);
openPositionAndLocation.add(position.Name + '-' + position.Location__c);
}
}
//for(String key : openPositionMap.keySet()) {
// system.debug('*** Start ***');
// system.debug('key :' + key);
// system.debug('value :' + openPositionMap.get(key));
// system.debug('*** End ***');
//}
Map<String, List<Candidate__c>> candidatesMap = new Map<String, List<Candidate__c>>();
List<Candidate__c> candidates = [SELECT Position__c, Location__c, Mobile__c, First_Name__c, Last_Name__c, Email__c FROM Candidate__c];
for (Candidate__c candidate : candidates) {
if(candidatesMap.containsKey(candidate.Position__c + '-' + candidate.Location__c)) {
candidatesMap.get(candidate.Position__c + '-' + candidate.Location__c).add(candidate);
} else {
candidatesMap.put(candidate.Position__c + '-' + candidate.Location__c, new List<Candidate__c> {candidate});
}
}
system.debug('*************** OPEN POSITIONS ***************');
for (String key : openPositionAndLocation) {
system.debug('====> ' + openPositionMap.get(key).Name);
if(candidatesMap.containsKey(key)) {
system.debug('***** Candidates *****');
for(Candidate__c candidate : candidatesMap.get(key)) {
system.debug('------- Name : ' + candidate.First_Name__c + ' ' + candidate.Last_Name__c);
system.debug('------- Email : ' + candidate.Email__c);
system.debug('------- Mobile : ' + candidate.Mobile__c);
}
} else {
system.debug('********* No Candidates');
}
}
One issue you certainly have, apropos of nothing else, is overall structure. Removing the actual lines of code, here's how your class is laid out now:
public class Matchposition {
public static String matchPositionsWithCandidate() {
// Code here
}
// more code here - problem!
}
You have much of your logic loose in the body of your class, which is not valid in Apex. Additionally, the code in the class body references variables declared inside the method matchPositionsWithCandidate(), which are scoped (visible) to that method.
matchPositionsWithCandidate() is also declared to return a string, but in fact returns nothing at all.
Since you're not returning any data anywhere, one step you can take to try to get this code running is to ensure that all of your logic is inside the scope (the curly braces) of the method matchPositionsWithCandidate(), and declare that method to return void, not String.
You may also have unbalanced braces - it looks like there is one missing after the final else - if it wasn't simply omitted in the copy and paste.
I'm not sure why this code exists. While much of the logic is correct, it makes far more sense to model your data with a relationship between Candidate__c and Position__c, rather than just storing a name in both positions. Then, you don't need any "matching" code at all - just a simple SOQL relationship query.

Salesforce APEX method not bulkified

I have written an APEX Class that sends an email when a client is released. There is a method that I thought I had bulkified but I was told it does not. This is because this method calls another function which actually does the actual email creation and that is not bulkified. Can someone guide me as to how the SOQL queries can be taken out of the method?
global class LM_ChangeAccountRT {
private static final Profile sysAdmin = [select id from profile where name='System Administrator'];
#AuraEnabled
public static String resendEmails(List<String> accountIdList) {
String response = null;
try {
//Only send emails if user is either an ARMS Administor or System Administrator
if (System.label.ARMS_Administrator_Profile_Id == userinfo.getProfileId() ||
sysAdmin.Id == userinfo.getProfileId()) {
List<Account> accList = [SELECT Id,Client_Released__c, RecordTypeId,Client_Number__c, Client_Released__c, Email_Sent__c FROM Account WHERE Id IN:accountIdList];
for(Account acc: accList){
if (acc.Client_Number__c != null && acc.Client_Released__c && acc.Email_Sent__c == true) {
sendpdfgenerationEmails(acc); //this is the method thats not bulkified.
acc.Email_Sent__c = false;
response = 'Email Sent';
}else {
response= 'Access Denied';
}
}
update accList;
}
}catch(Exception e) {
System.debug(e.getMessage());
response = 'Error sending emails';
}
return response;
}
public static void sendpdfgenerationEmails(Account acc){
system.debug('start of confirmation card and pdf generation');
//Logic to find which VF template is used to send an email.
list<EmailTemplate> templateId = new list<EmailTemplate>();
string temppartner;
String partner_opt_in_attachment;
boolean sendFCAmail;
List<Dealership_PDF_Generation__c> custsettingdata = Dealership_PDF_Generation__c.getall().values();
System.debug('custom setting size = ' + custsettingdata.size());
// Fetch State
if(acc.Dealership_State__c!=null && acc.Dealership_Partner__c!=null)
{
for(Dealership_PDF_Generation__c tempcustsetting :custsettingdata)
{
if(acc.Dealership_Partner__c == tempcustsetting.Dealership_Partner__c && acc.Dealership_State__c==tempcustsetting.State__c && tempcustsetting.State__c=='WA' && acc.Dealership_State__c=='WA'){
//For WA State
// temppartner= '%' + tempcustsetting.TEMPLATE_Unique_name__c + '%';
temppartner= tempcustsetting.TEMPLATE_Unique_name__c;
if(acc.Dealership_Spiff_Payment__c == '% premium'){
partner_opt_in_attachment=tempcustsetting.opt_in_form_premium__c;
}else{
partner_opt_in_attachment=tempcustsetting.opt_in_form_nonpremium__c;
}
}
else if(acc.Dealership_Partner__c == tempcustsetting.Dealership_Partner__c && acc.Dealership_State__c==tempcustsetting.State__c && tempcustsetting.State__c=='TX' && acc.Dealership_State__c=='TX'){
//For TX State
//temppartner= '%' + tempcustsetting.TEMPLATE_Unique_name__c + '%';
temppartner= tempcustsetting.TEMPLATE_Unique_name__c;
if(acc.Dealership_Spiff_Payment__c == '% premium'){
partner_opt_in_attachment=tempcustsetting.opt_in_form_premium__c;
}else{
partner_opt_in_attachment=tempcustsetting.opt_in_form_nonpremium__c;
}
}
else if(acc.Dealership_Partner__c == tempcustsetting.Dealership_Partner__c && acc.Dealership_State__c!=tempcustsetting.State__c && tempcustsetting.State__c!='TX' && acc.Dealership_State__c!='TX' && acc.Dealership_State__c!='WA' &&tempcustsetting.State__c!='WA' ){
//For Non TX State
//temppartner= '%' + tempcustsetting.TEMPLATE_Unique_name__c + '%';
temppartner= tempcustsetting.TEMPLATE_Unique_name__c;
if(acc.Dealership_Spiff_Payment__c == '% premium'){
partner_opt_in_attachment=tempcustsetting.opt_in_form_premium__c;
}else{
partner_opt_in_attachment=tempcustsetting.opt_in_form_nonpremium__c;
}
system.debug('grabbed template: ' + temppartner);
}
if(acc.Dealership_Partner__c != null && temppartner!=null ){
templateId.add([Select id,DeveloperName from EmailTemplate where DeveloperName = :temppartner]); //This will probably cause governor limit issues. First problem
}
if (partner_opt_in_attachment != null) {
StaticResource sr = [Select s.Name, s.Id, s.Body From StaticResource s where s.Name =: partner_opt_in_attachment]; //'static_resource' is the name of the static resource PDF. This is another SOQL query that will cause problems
Blob tempBlob = sr.Body;
Messaging.EmailFileAttachment efa = new Messaging.EmailFileAttachment();
efa.setBody(tempBlob);
efa.setFileName('Opt-in.pdf');
List<Messaging.EmailFileAttachment> attachments = new List<Messaging.EmailFileAttachment>();
attachments.add(efa);
// add attachment to each email
for (Messaging.SingleEmailMessage email : emails) {
email.setFileAttachments(attachments);
}
}
system.debug('email sent: ' + emails.size());
Messaging.sendEmail(emails);
}
}
}
The reason why I am trying to bulkify this is because I have written a APEX scheduler that calls the resendemails method everyday at 7am to check which records need to have an email sent. I am afraid that if there are more than a 100 clients then it will cause problems and not send the emails. Any suggestions on how I can optimize the sendpdfemailgeenration() method?
Thank you
Yes, you are right - your's resendEmails() method is not bulkified.
Firstly, let me explain you why is that:
SOQL to get Accounts
Loop 1 on List of Account records
Call sendpdfgenerationEmails() method
Retrieve list of Dealership_PDF_Generation__c records
Loop 2 on List of Dealership_PDF_Generation__c records
SOQL to get StaticResources - Very bad! It's inside double loop!
Call Messaging.sendEmail() method - Very bad! It's inside double loop!
Update on List of Account records
You need to remember that:
1. You should never do SOQLs in loops! - Limit 100 SOQLs per transaction
2. You should never call Messaging.sendEmail() in loops! - Limit 10 calls per transaction
Now let me guide you how to refactor this method:
#AuraEnabled
public static String resendEmails(List<String> accountIdList) {
// 1. SOQL for List of Account records
// 2. Retrieve list of Dealership_PDF_Generation__c records
// 3. SOQL for List of StaticResources for all Names from Dealership_PDF_Generation__c records
// 4. Declaration of new List variable for Messaging.SingleEmailMessage objects
// 5. Loop 1 on List of Account records
// 6. Call new "prepareEmailsForAccount()" method, which prepares and returns list of Messaging.SingleEmailMessage objects
// 7. Add returned Messaging.SingleEmailMessage objects to list from point 4
// 8. End of loop 1
// 9. Call "Messaging.sendEmail()" method with list from point 4
// 10. Update on List of Account records
}
With this you will avoid SOQLs and calling Messaging.sendEmail() method in loops.

AppEngine full text search cursors broken in dev/unit test environment

I've noticed an inconsistency in the behavior of App Engine text search cursors in the devserver or unit test environment vs. production environments. The dev and unit test environments appear to exhibit a bug for cursors used in combination with sort expressions. Consider the following unit test code:
#Test
public void testQueryCursor( ) throws Exception
{
testQueryCursor("id_%02d"); // works
testQueryCursor("id_%d"); // fails
}
private void testQueryCursor( final String idFmt ) throws Exception
{
final int TEST_COUNT = 12;
final Index index =
SearchServiceFactory.getSearchService().getIndex(IndexSpec.newBuilder().setName("MY_TEST_IDX").build());
final List<String> docIds = new ArrayList<String>(TEST_COUNT);
try {
// populate some test data into an index
for (int i = 0; i < TEST_COUNT; i++) {
final String docId = String.format(idFmt, i);
final Document.Builder builder = Document.newBuilder().setId(docId);
builder.addField(Field.newBuilder().setName("some_field").setText("str1 " + docId)); // include varied docId in field for sorting
index.put(builder.build());
docIds.add(docId);
}
// for comparison to sorted search results
Collections.sort(docIds);
// define query options
final QueryOptions.Builder optionsBuilder =
QueryOptions
.newBuilder()
.setReturningIdsOnly(true)
.setLimit(10)
.setSortOptions(
SortOptions
.newBuilder()
.setLimit(20)
.addSortExpression(
SortExpression.newBuilder().setExpression("some_field")
.setDirection(SortDirection.ASCENDING).setDefaultValue("")));
// see https://developers.google.com/appengine/docs/java/search/results#Java_Using_cursors
// create an initial per-query cursor
Cursor cursor = Cursor.newBuilder().build();
final Iterator<String> idIter = docIds.iterator();
int batchIdx = 0;
do {
// build options and query
final QueryOptions options = optionsBuilder.setCursor(cursor).build();
final Query query = Query.newBuilder().setOptions(options).build("some_field : str1");
// search at least once
final Results<ScoredDocument> results = index.search(query);
int batchCount = 0;
for (final ScoredDocument match : results) {
batchCount++;
assertTrue(idIter.hasNext());
assertEquals(idIter.next(), match.getId());
System.out.println("Document " + match.getId() + " matched.");
}
System.out.println("Read " + batchCount + " results from batch " + ++batchIdx);
cursor = results.getCursor();
} while (cursor != null);
} finally {
index.delete(docIds);
}
}
If the assertEquals(idIter.next(), match.getId()); line is commented out the full output of the previously failing call the testQueryCursor("id_%d") can be observed and we see that the proper ordering of results appears to be ignored. What's more, the last search performed on the cursor repeats the last two elements retrieved from the previous search call. Since these two elements SHOULD BE the last two returned from the search perhaps this behavior is simply an artifact of the flaw which causes the improper sort.
This code can be easily run as a unit test as shown here or run from a JSP on the devserver and the behavior is consistent. When run as a JSP on a production instance of App Engine the behavior differs in that the search returns the correctly ordered results in all cases. It would be nice if the devserver environment and unit test tools were fixed to provide correct behavior consistent with production.

Objectify return List & Cursor

I am trying to use a cursor with Objectify and Google App Engine to return a subset of data and a cursor so that I can retrieve more data when the user is ready. I found an example here that looks exactly like what I need but I don't know how to return the final list plus the cursor. Here is the code I have:
#ApiMethod(name = "listIconThemeCursor") //https://code.google.com/p/objectify-appengine/wiki/Queries#Cursors
public CollectionResponse<IconTheme> listIconThemeCursor(#Named("cursor") String cursorStr) {
Query<IconTheme> query = ofy().load().type(IconTheme.class).limit(10);
if (cursorStr != null ) {
query.startAt(Cursor.fromWebSafeString(cursorStr));
}
List<IconTheme> result = new ArrayList<IconTheme>();
int count = 0;
QueryResultIterator<IconTheme> iterator = query.iterator();
while (iterator.hasNext()) {
IconTheme theme = iterator.next();
result.add(theme);
count++;
}
Cursor cursor = iterator.getCursor();
String encodeCursor = cursor.toWebSafeString();
return serial(tClass, result, encodeCursor);
}
Note that this was modified from a previous endpoint in which I returned the CollectionResponse of ALL the data. My dataset is large enough that this is no longer practical. Basically, I don't know what was in the user's function of 'serial(tClass, result, encodeCursor) that let it get returned to the user.
There is another example here but it doesn't appear to answer my question either.
I don't quite understand what you are asking, but I see one immediate bug in your code:
query.startAt(Cursor.fromWebSafeString(cursorStr));
...should be:
query = query.startAt(Cursor.fromWebSafeString(cursorStr));
Objectify command objects are immutable, functional objects.
After a long slog, I figured out that CollectionResponse has the cursor in it :(
Here is the complete code I used incorporating the comment from stickfigure above:
#ApiMethod(name = "listIconThemeCursor", path="get_cursor")
public CollectionResponse<IconTheme> listIconThemeCursor(#Named("cursor") String cursorStr) {
Query<IconTheme> query = ofy().load().type(IconTheme.class)
.filter("errors <", 10)
.limit(10);
if (cursorStr != null ) {
query = query.startAt(Cursor.fromWebSafeString(cursorStr));
}
List<IconTheme> result = new ArrayList<IconTheme>();
QueryResultIterator<IconTheme> iterator = query.iterator();
while (iterator.hasNext()) {
IconTheme theme = iterator.next();
result.add(theme);
}
Cursor cursor = iterator.getCursor();
CollectionResponse<IconTheme> response = CollectionResponse.<IconTheme> builder()
.setItems(result)
.setNextPageToken(cursor.toWebSafeString())
.build();
return response;
}

Resources