I am working on creating a RESTlet to load paginated customer records. One of the thing i need is the customer's defaultaddress. This field is available when I load a single customer like this:
customer = record.load({
type: record.Type.CUSTOMER,
id: context.id, // e.g. 1234
isDynamic: false
});
However, when I try to load all customers with pagination like this:
define(['N/record', 'N/search'], function(record, search) {
var currencies = {};
function getClients(context) {
var mySearch = search.create({
type: search.Type.CUSTOMER,
columns: [
{ name: 'companyname' },
{ name: 'vatregnumber' },
{ name: 'lastmodifieddate' },
{ name: 'currency' },
{ name: 'email' },
{ name: 'phone' },
{ name: 'defaultaddress' }
]
});
var searchResult = mySearch.run().getRange({
start: context.start || 0,
end: context.end || 100
});
var results = [];
for (var i = 0; i < searchResult.length; i++) {
results.push({
tax_number: searchResult[i].getValue({ name: 'vatregnumber' }),
name: searchResult[i].getValue({ name: 'companyname' }),
// snipped...
});
}
return results;
}
function loadCurrencyName(id) {
return record.load({
type: record.Type.CURRENCY,
id: id,
isDynamic: false
}).name;
}
return {
get: getClients
}
});
and execute the rest api call to the above RESETlet script; I get the following error:
{
"type":"error.SuiteScriptError",
"name":"SSS_INVALID_SRCH_COL",
"message":"An nlobjSearchColumn contains an invalid column, or is not in proper syntax: defaultaddress.",
"stack":[ ... snipped ... ],
"cause":{ ... snipped ... },
"id":"",
"notifyOff":false
}
Any idea how to best to load the default address of the customer, whilst loading all customers using the paged search feature?
{defaultaddress} is calculated when the customer record is loaded - it's not a stored field and is not available for saved searches. As the field help says, it's simply the default billing address and changes automatically according to what address in the address subtab you have checked 'Default Billing" on.
To work around this and display what you're looking for, you can simply replace defaultaddress with billaddress.
Related
I'm struggling with a script that is supposed to:
Search for a list of Customer's
Get a couple of Field Values, one of which is used in the next Search, the other is the ID
Search for a list of Custom Records, the criteria being one of the fields I just fetched
Get a field value
And use the Customer ID fetched earlier to assign the Custom Record field value to a Custom field on the Customer.
But it is dropping out on the second search, saying that it is returning "undefined" due to invalid Search Criteria. I'm assuming that the field I get from the first search is not working in the Criteria of the second search?
My code is below - is it an obvious one (as usual), or is it literally the wrong way to go about this?
/**
*#NApiVersion 2.x
*#NScriptType ScheduledScript
*/
define(['N/search'],
function getShipSuburbId(search) {
function execute() {
var customerSearchObj = search.create({
type: "customer",
filters:
[
["custentity_store_shipping_suburb","isnotempty",""]
],
columns:
[
search.createColumn({
name: "entityid"
}),
search.createColumn({name: "custentity_store_shipping_suburb"})
]
});
var custSearchResult = customerSearchObj.runPaged({pageSize: 1000});
log.debug({title: "customerSearchObj result count", details: custSearchResult.count});
var custNumPages = custSearchResult.pageRanges.length;
var custAllResults = [];
var i = 0;
while (i < custNumPages) {
custAllResults = custAllResults.concat(custSearchResult.fetch(i).data);
i++;
}
return custAllResults;
for (var j = 0; j < custAllResults.length; j++) {
var currentRecord = custAllResults[j].getValue({
name: "entityid"
});
var shipSub = custAllResults[j].getValue({
name: "custentity_store_shipping_suburb"
});
};
var shipSubIdSearch = search.create({
type: "customrecord_suburb",
filters:
[
["name","is",shipSub]
],
columns:
[
search.createColumn({
name: "internalid",
summary: "MAX",
label: "Internal ID"
})
]
});
var allSubIdResults = shipSubIdSearch.runPaged({pageSize: 1});
log.debug({title: "shipSubIdSearch result count", details: allSubIdResults.count});
var subNumPages = custSearchResult.pageRanges.length;
var subAllResults = [];
var m = 0;
while (m < subNumPages) {
subAllResults = subAllResults.concat(allSubIdResults.fetch(m).data);
m++;
}
return subAllResults;
for (var k = 0; k < subAllResults.length; k++) {
var shipSubId = subAllResults[k].getValue({
name: "internalid"
});
};
var setSuburbId = currentRecord.setValue({
fieldId: 'custentity_shipping_suburb_id',
value: shipSubId
});
return setSuburbId;
}
return {
execute : execute
};
});
NEW CODE BELOW
/**
*#NApiVersion 2.x
*#NScriptType ScheduledScript
*/
define(['N/search', 'N/record'],
function getShipSuburbId(search, record) {
function execute() {
var customerSearchObj = search.create({
type: "customer",
filters:
[
["custentity_store_shipping_suburb", "isnotempty", ""]
],
columns:
[
search.createColumn({
name: "entityid"
}),
search.createColumn({name: "custentity_store_shipping_suburb"})
]
}); // The first search, which draws a list of Customers
var custSearchResult = customerSearchObj.runPaged({pageSize: 1000}); // Run paged
log.debug({title: "customerSearchObj result count", details: custSearchResult.count});
var custNumPages = custSearchResult.pageRanges.length;
var custAllResults = [];
var i = 0;
while (i < custNumPages) {
custAllResults = custAllResults.concat(custSearchResult.fetch(i).data);
i++;
}
for (var j = 0; j < custAllResults.length; j++) {
var currentRecord = custAllResults[j].getValue({
name: "entityid"
});
var shipSub = custAllResults[j].getValue({
name: "custentity_store_shipping_suburb"
});
log.debug({title: "currentRecord", details: currentRecord});
log.debug({title: "shipSub", details: shipSub});
// I've left this "for" operation open for the next search - possible issue?
var shipSubIdSearch = search.create({
type: "customrecord_suburb",
filters:
[
["name", "is", shipSub]
],
columns:
[
search.createColumn({
name: "internalid",
summary: "MAX",
label: "Internal ID"
})
]
}); // Second search. This should only return one result each time it is run
var subIdRun = shipSubIdSearch.run();
log.debug({title: "subIdRun result count", details: subIdRun.count});
var shipSubId = subIdRun.each(
function (result) {
log.debug({
title: "Fetch ID",
details: result.getValue({name: "internalid"})
})
return true;
});
log.debug({title: "shipSubId result", details: shipSubId});
var myRecord = record.load({
type: 'customer',
id: currentRecord
}); // Load the Customer record, based on the id fetched in the first search
log.debug({title: "myRecord", details: myRecord});
myRecord.setValue({
fieldId: 'custentity_shipping_suburb_id',
value: shipSubId
}); // And set the value of the Custom field, based on value from second search
}
}
return {
execute : execute
};
});
And screenshot of Execution Log on New Script:
I tried to edit some of your code to get your closer to the answer. I've left comments explaining steps you'll need to take.
Try this and let me know how it goes!
/**
*#NApiVersion 2.1
*#NScriptType ScheduledScript
*/
define(['N/search', "N/record"], function (search, record) {
function execute() {
let customers = [];
let storeShippingSuburbIds = [];
let storeShippingSuburbId;
let searchCustomers = search.create({
type: "customer",
filters:
[
["custentity_store_shipping_suburb", "isnotempty", ""]
],
columns:
[
search.createColumn({name: "entityid"}),
search.createColumn({name: "custentity_store_shipping_suburb"})
]
});
var pagedData = searchCustomers.runPaged({pageSize: 1000});
pagedData.pageRanges.forEach(function (pageRange) {
let page = pagedData.fetch({index: pageRange.index});
page.data.forEach(function (result) {
customers.push([
result.getValue({name: "entityid"}),
result.getValue({name: "custentity_store_shipping_suburb"})
])
storeShippingSuburbIds.push(result.getValue({name: "custentity_store_shipping_suburb"}));
return true;
});
});
/*
* I think you want the search operator of anyof here.
*/
search.create({
type: "customrecord_suburb",
filters:
[
["name", "anyof", storeShippingSuburbIds]
],
columns:
[
search.createColumn({
name: "internalid",
summary: "MAX",
label: "Internal ID"
})
]
}).run().each(function (result) {
storeShippingSuburbId = result.getValue(result.columns[0]);
});
/*
* You'll need to use record.load() here or record.submitFields(), depending on a few things.
* But, it won't work as it is because it doesn't know what the "current record" is.
*/
let myRecord = record.load();
myRecord.setValue({
fieldId: 'custentity_shipping_suburb_id',
value: storeShippingSuburbId
});
myRecord.save();
}
return {
execute: execute
};
});
I have found the best way to approach multiple levels of searching, is to gather the results of each search into an array.
Once I have finished dealing with the results of one search, then process each element of the array one by one, whether that be searching, querying, editing, etc.
The advantages of this include:
If a scheduled script, this allows me to check the governance usage after the first search, and reschedule with the array of results as a parameter if necessary.
Handling other logic like sorting or formulas on the array elements, before continuing with other operations.
Potentially faster processing, particularly if you don't need to process any further operations on some results of the first search.
This is just personal preference, but it makes it easier to split elements of the script into separate functions and maintain readability and logical sequence.
I am trying to get the children of apostrophe pages to appear in my navigation object - however the _children array is always empty. My page does have child pages set up via the front end Pages UI.
My index.js for the lib/modules/apostrophe-pages module contains the following:
construct: function(self,options) {
// store the superclass method and call at the end
var superPageBeforeSend = self.pageBeforeSend;
self.pageBeforeSend = function(req, callback) {
// Query all pages with top_menu setting = true and add to menu collection
self.apos.pages.find(req, { top_menu: true }, {slug: 1, type: 1, _id: 1, title: 1})
.children(true)
.toArray(
function (err, docs) {
if (err) {
return callback(err);
}
req.data.navpages = docs;
return superPageBeforeSend(req, callback);
});
};
},
...
My top_menu attribute is set via apostrophe-custom-pages:
module.exports = {
beforeConstruct: function(self, options) {
options.addFields = [
{
name: 'subtitle',
label: 'Subtitle',
type: 'string'
},
{
name: 'css_class',
label: 'CSS Class',
type: 'string'
},
{
name: 'top_menu',
label: 'Include In Top Menu',
type: 'boolean'
}
].concat(options.addFields || []);
}
};
This gives me the pages I need with the top_menu setting.. but I want to get child pages too..
When debugging the code I can see that the docs._children array is present but is always empty, even though a page has child pages...
I have tried adding the following both to my app.js and to my index.js but it doesn't change the result:
filters: {
// Grab our ancestor pages, with two levels of subpages
ancestors: {
children: {
depth: 2
}
},
// We usually want children of the current page, too
children: true
}
How can I get my find() query to actually include the child pages?
Solved it..
I needed to add 'rank: 1, path: 1, level: 1' to the projection as per this page in the documentation: https://apostrophecms.org/docs/tutorials/howtos/children-and-joins.html#projections-and-children
I am stuck to save data in mongoDb. Here data is in array and i need to insert data if mongodb does not have. Please look code:-
var contactPersonData = [{
Name: 'Mr. Bah',
Organization: 'Ashima Limited - Point 2'
}, {
Name: 'Mr. Sel',
Organization: 'Ashima Limited - Point 2'
}, {
Name: 'Mr.ATEL',
Organization: 'Ashima Limited - Point 2'
}, {
Name: 'ANISH',
Organization: 'Ashima Limited - Point 2'
}, {
Name: 'sunny ji',
Organization: 'Ashima Limited - Point 2'
}, {
Name: 'ashish',
Organization: 'Ashima Limited - Point 2'
}]
console.log('filedata', contactPersonData);
var escapeData = [];
var tempArr = [];
function saveContact(personObj, mainCallback) {
var tempC = personObj['Organization'].trim();
var insertData = {};
Contact.findOne({ companyName: tempC })
.exec(function(err, contact) {
if (err)
return mainCallback(err);
console.log('find com', contact)
if (contact) {
//document exists
mainCallback(null, insertData);
} else {
var newContact = new Contact({ companyName: tempC, createdBy: '58ae5d18ba71d4056f30f7b1' });
newContact.save(function(err, contact) {
if (err)
return mainCallback(err);
console.log('new contact', contact)
insertData.contactId = contact._id;
insertData.name = personObj['Name'];
insertData.email = personObj['Email'];
insertData.contactNumber = { number: personObj['Phone'] };
insertData.designation = personObj['Designation'];
tempArr.push(insertData);
mainCallback(null, insertData);
})
}
});
}
async.map(contactPersonData, saveContact, function(err, result) {
console.log(err)
console.log(result)
},
function(err) {
if (err)
return next(err);
res.status(200).json({ unsaved: escapeData })
})
As per above code it has to insert six document instead of one. I think that above iteration not wait to complete previous one. So, the if condition is always false and else is executed.
Your saveContact() function is fine. The reason you get 6 documents instead of 1 document is that async.map() runs you code in parallel. All the 6 requests are made in parallel, not one after the another.
From the async.map() function's documentation -
Note, that since this function applies the iteratee to each item in parallel, there is no guarantee that the iteratee functions will complete in order.
As a result before the document is created in your database all queries are already run and all the 6 queries are not able to find that document since its still in the process of creation. Therefore your saveContact() method creates all the 6 documents.
If you run your code again then no more documents will be formed because by that time your document would be formed.
You should try running your code using async.mapSeries() to process your request serially. Just replace map() with mapSeries() in your above code. This way it will wait for one request to complete and then execute another and as a result only one document will be created. More on async.mapSeries() here.
You seem to be using async.map() wrong.
First, async.map() only has 3 parameters (i.e. coll, iteratee, and callback) so why do you have 4? In your case, coll is your contactPersonData, iteratee is your saveContact function, and callback is an anonymous function.
Second, the whole point of using async.map() is to create a new array. You are not using it that way and, instead, are using it more like an async.each().
Third, you probably should loop through the elements sequentially and not in parallel. Therefore, you should use async.mapSeries() instead of async.map().
Here's how I would revise/shorten your code:
var contactPersonData = [{
Name: 'Mr. Bah',
Organization: 'Ashima Limited - Point 2'
}, {
Name: 'Mr. Sel',
Organization: 'Ashima Limited - Point 2'
}, {
Name: 'Mr.ATEL',
Organization: 'Ashima Limited - Point 2'
}, {
Name: 'ANISH',
Organization: 'Ashima Limited - Point 2'
}, {
Name: 'sunny ji',
Organization: 'Ashima Limited - Point 2'
}, {
Name: 'ashish',
Organization: 'Ashima Limited - Point 2'
}];
function saveContact(personObj, mainCallback) {
var tempC = personObj.Organization.trim();
Contact.findOne({ companyName: tempC }, function (err, contact) {
if (err)
return mainCallback(err);
console.log('found contact', contact);
// document exists, so mark it as complete and pass the old item
if (contact)
return mainCallback(null, contact);
// document does not exist, so add it
contact = new Contact({ companyName: tempC, createdBy: '58ae5d18ba71d4056f30f7b1' });
contact.save(function (err, contact) {
if (err)
return mainCallback(err);
console.log('created new contact', contact)
// mark it as complete and pass a new/transformed item
mainCallback(null, {
contactId: contact._id,
name: personObj.Name,
email: personObj.Email, // ??
contactNumber: { number: personObj.Phone }, // ??
designation: personObj.Designation // ??
});
});
});
};
async.mapSeries(contactPersonData, saveContact, function (err, contacts) {
if (err)
return next(err);
// at this point, contacts will have an array of your old and new/transformed items
console.log('transformed contacts', contacts);
res.json({ unsaved: contacts });
});
In terms of ?? comments, it means you don't have these properties in your contactPersonData and would therefore be undefined.
I've got an stock application where I want to set some details about the stock and then insert all the items of the stock. I want to insert the stock details and the items in two different collection so then I can filter the items. I'm using the MEAN Stack where I've modified the crud module to accept some extra fields and also made the UI for filling the items array.This what I have so far:
scope.stockItems = [];
$scope.createStockItem = function () {
$scope.stockItems.push(
{
brand: $scope.brand,
style: $scope.style,
amount: $scope.amount
}
);
$scope.brand = false;
$scope.style = false;
$scope.amount = '';
};
// Create new Stock
$scope.create = function() {
// Create new Stock object
var stock = new Stocks ({
name: this.name,
details: this.details,
stockDate: this.stockDate
});
// Redirect after save
stock.$save(function(response) {
$location.path('stocks/' + response._id);
// Clear form fields
$scope.name = '';
}, function(errorResponse) {
$scope.error = errorResponse.data.message;
});
};
The stock model:
var StockSchema = new Schema({
name: {
type: String,
default: '',
required: 'Please fill Stock name',
trim: true
},
details: {
type: String,
default: '',
required: 'Please fill Stock details'
},
stockDate: Date
created: {
type: Date,
default: Date.now
},
user: {
type: Schema.ObjectId,
ref: 'User'
}
});
and the method in the server controller:
exports.create = function(req, res) {
var stock = new Stock(req.body);
stock.user = req.user;
stock.save(function(err) {
if (err) {
return res.status(400).send({
message: errorHandler.getErrorMessage(err)
});
} else {
res.jsonp(stock);
}
});
};
How can I send into the request and save the stockItems also?
By saying 'simultaneously' I think you are requiring transaction feature, which is really an RDBMS thing, and is not supported by MongoDB. If your application strongly relies on such features, I'm afraid MongoDB is not the right choice for you.
So back to your question, I don't understand why you have to store stock and stock item in 2 different collections. Store them in one collection would be a better choice. You can refer to the Data Model Design of MongoDB Manual for more information. If it's just to filter all the stock items, aggregation framework is designed for such purpose. As well as Map/Reduce. Here aggregation framework suits better for your issue. You would have something like:
db.stock.aggregate([
{$match: {...}}, // same as find criteria. to narrow down data range
{$unwind: "$items"}, // unwind items.
... // probably some other $match to filter the items
]);
I'm having some trouble with how best to implement this join/query in mongoose.
var mongoose = require('mongoose');
var addressSchema = new mongoose.Schema{
rank: Number,
address: String,
tag_name: String,
tag_url: String};
var tagSchema = new mongoose.Schema{
address: String,
name: String,
url: String};
I have a bunch of addresses saved and a bunch of tags saved. Some addresses have tags, most do not. I update the addresses and tags separately frequently. What I want to do is query some specific addresses and return them as an array with the tag fields filled in (the address tag fields are blank in the database).
So for example, I want to do this without making a db query for every address(101 db queries in the example). I'm not sure if $match or $in or populate is what I'm looking for. The below code is untested and may not work, but it should give you an idea of what I'm trying to do.
var db = require('../config/dbschema');
// find an array of addresses
db.addressModel.find({ rank: { $lte: 100 } }, function(err, addresses) {
// now fill in the tag fields and print
addTagField(0, addresses);
});
// recursive function to fill in tag fields
// address tag name and tag url fields are blank in the database
function addTagField(n, addresses) {
if(n < addresses.length) {
db.tagModel.find( { address: addresses[n].address }, function(err, tag) {
// if find a tag with this address, fill in the fields
// otherwise leave blank and check next address in array
if(tag) {
addresses[n].tag_name = tag.name;
addresses[n].tag_url = tag.url;
}
addTagField(n+1, addresses);
});
} else {
console.log(addresses);
}
}
http://mongoosejs.com/docs/api.html#aggregate_Aggregate-match
http://mongoosejs.com/docs/api.html#query_Query-in
http://mongoosejs.com/docs/api.html#document_Document-populate
I want to do what the above does with fewer db queries.
Your major problem is that you're not taking advantage of Mongoose's relationship mapping. Change your schemas just a bit and your problem will easily be solved. You can do it like this:
var tagSchema = new Schema({
name: String,
url: String,
})
var addressSchema = new Schema ({
rank: Number,
address: String,
tags: [tagSchema],
})
addressModel.find({rank: {$lte: 100}}, function(err, addresses) {
...
})
or this:
var tagSchema = new Schema({
name: String,
url: String,
})
var addressSchema = new Schema ({
rank: Number,
address: String,
tags: [{type: ObjectId, ref: 'Tag'}],
})
addressModel
.find({rank: {$lte: 100}})
.populate('tags', 'name url')
.exec(function(err, addresses) {
...
})
I didn't want to embed the docs. The is what I came up with.
db.addressModel.find({ rank: { $lte: 100 } }, function(err, addresses) {
if(err) return res.send(400);
if(!addresses) return res.send(404);
var addrOnlyAry = addresses.map(function(val, idx) { return val.address; });
db.tagModel.find( { address: { $in: addrOnlyAry } }, {}, function(err, tags) {
if(err) return res.send(400);
if(tags.length > 0) addresses = setTagFields(addresses, tags);
return res.json(addresses);
}
}
function setTagFields(addresses, tags) {
for(var i=0; i < tags.length; i++) {
for(var j=0; j < addresses.length; j++) {
if(addresses[j].address === tags[i].address) {
addresses[j].tag_name = tags[i].tag;
addresses[j].tag_url = tags[i].url;
break;
}
}
}
return addresses;
}