Office-JS API with Fetch to get data from server, replace fields in Word - fetch

I am new to the Office-JS API, but trying to develop a POC to demonstrate the ability to, with the click of a button, replace all fields in a Word document with corresponding data retrieved from an API.
So I have developed an API as an Azure Function, which I can call, passing a value, and it will return the field names and values as JSON for the record that matches the passed value. I have tested this already using Postman.
Now I am trying to get the Office-JS piece working. I started with the VS-2022 template.
My intended approach is that when the user clicks the button, the application will call the API and obtain the record (data) fields as a set of name/value pairs.
Then, I want loop through all of the (data) fields returned, and for each (data) field name, check to see if there is a (document) field in the document by that name; if so, replace the (document) field with the (data) field's value.
In the end, I realize it would likely be more efficient to loop the other way (loop through the doc fields, and then get the (data) field's value, etc.), but I figure I can tweak this once I get it working.
I also realize it's bad practice to perform a context.sync() within a loop, but again - I can clean that up once I get it working.
Right now, my code is reporting "Error: InvalidRequestContext: Cannot use the object across different request contexts."
My code follows:
(function () {
"use strict";
var messageBanner;
// The initialize function must be run each time a new page is loaded.
Office.initialize = function (reason) {
$(document).ready(function () {
// Initialize the notification mechanism and hide it
var element = document.querySelector('.MessageBanner');
messageBanner = new components.MessageBanner(element);
messageBanner.hideBanner();
// If not using Word 2016, use fallback logic.
if (!Office.context.requirements.isSetSupported('WordApi', '1.1')) {
$("#template-description").text("This sample displays the selected text.");
$('#button-text').text("Display!");
$('#button-desc').text("Display the selected text");
$('#highlight-button').click(displaySelectedText);
return;
}
$("#template-description").text("This POC demonstrates template capabilities for NLRB within WORD.");
$('#button-text').text("Merge Template!");
$('#button-desc').text("Replaces fields with approprate data.");
// Add a click event handler for the highlight button.
$('#highlight-button').click(loadCaseData("08-CA-036441"));
});
};
//CaseNum=08-CA-036441&DataType=1
function loadCaseData(caseNum) {
Word.run((context) => {
const ul = document.getElementById('caseData'),
url = `[URL to my API]`;
const createNode = element => { return document.createElement(element); };
const append = (parent, el) => { return parent.appendChild(el); };
fetch(url)
.then(response => { return response.json(); })
.then(json => {
let caseDataResult = json;
// Identify fields in the document
// Loop through case data for field substitution
var range = context.document.body;
context.load(range, 'text');
var searchResults;
for (let fieldName in caseDataResult) {
var searchFieldName = "«" + fieldName + "»";
let fieldValue = caseDataResult[fieldName];
return context.sync()
.then(function () {
// Queue a search command.
searchResults = range.search(searchFieldName, { matchCase: true, matchWholeWord: true });
// Queue a commmand to load the font property of the results.
context.load(searchResults, 'text');
})
.then(context.sync)
.then(function () {
var replaceCount = searchResults.items.length;
if (replaceCount && replaceCount >= 1) {
for (var replaceItem = 0; replaceItem < replaceCount; replaceItem++) {
searchResults[replaceItem].insertText(fieldValue,
Word.InsertLocation.replace);
}
}
})
.then(context.sync);
}
return context.sync();
})
.catch(errorHandler);
return context.sync();
});
}
//$$(Helper function for treating errors, $loc_script_taskpane_home_js_comment34$)$$
function errorHandler(error) {
// $$(Always be sure to catch any accumulated errors that bubble up from the Word.run execution., $loc_script_taskpane_home_js_comment35$)$$
showNotification("Error:", error);
console.log("Error: " + error);
if (error instanceof OfficeExtension.Error) {
console.log("Debug info: " + JSON.stringify(error.debugInfo));
}
}
// Helper function for displaying notifications
function showNotification(header, content) {
$("#notification-header").text(header);
$("#notification-body").text(content);
messageBanner.showBanner();
messageBanner.toggleExpansion();
}
})();
And the sample JSON returned from my data API:
{
"CaseAssgnedDt": "2011-07-22T17:12:19",
"AsgnUsrExcldFlg": "N",
"BuId": "0-R9NH",
"CaseFiledDt": "2006-03-09T00:00:00",
"InquiryId": "08-CA-036441",
"ChgofccmReqFlg": "N",
"DisputeUnitCity": "Cleveland",
"CaseClasification": "Unclassified",
"CaseClosedDt": null,
"Created": "2010-10-07T15:32:29",
"CreatedBy": "0-1",
"CrimeSubTypeCd": null,
"TypeCd": null,
"DbLastUpd": "2019-10-20T17:17:14.31",
"DbLastUpdSrc": "ScriptingService_PreInvokeMethod",
"CaseDescription": null,
"LastUpd": "2019-10-20T17:17:14",
"LastUpdBy": "1-CGA",
"LocalSeqNum": 1,
"ModificationNum": 11,
"CaseName": "Sample test case name",
"ParCaseId": null,
"PrAgencyId": "No Match Row Id",
"PrAgentId": "No Match Row Id",
"PrPostnId": "1-5D2V9F ",
"PrPrtnrId": "No Match Row Id",
"PrRepDnrmFlg": "Y",
"PrRepManlFlg": "Y",
"PrRepSysFlg": "Y",
"PrSgroupId": "No Match Row Id",
"PrSubjectId": "No Match Row Id",
"PrSuspctId": "No Match Row Id",
"IaCategory": "2",
"RewardExchangeDt": "2010-10-07T11:09:02",
"RowId": "1-2DCA-1327",
"CaseNumber": "08-CA-036441",
"CaseSource": "Visit",
"DisputeUnitState": "OH",
"CaseStatus": "Open",
"CaseSubType": "CA",
"CaseSubTypeCd": null,
"TerritoryTypeCd": "08",
"ThreatLvlCd": "Batch",
"CaseType": "C",
"BlockedFlag": null,
"CaseLongName": "Long sample test case name",
"XCaseNumCi": null,
"DojCaseType": null,
"ElectionTargetDt": null,
"HearingTargetDt": null,
"MethodType": null,
"XNameCi": null,
"Num8a3Discriminatees": null,
"Num8b2Discriminatees": null,
"NumOfEmployees": 146,
"PostElectionSelfCertification": null,
"Potential10j": "N",
"XPrPostnBrdId": "1-4P8HN1",
"XPrPostnSpvId": "1-1RAQT2",
"ElectionSelfCertification": null,
"XTypeCdCi": null,
"Moved2dh": 1,
"IdentityVal": 5792070,
"CdcRecordedFields": null,
"NxgenTestCase": "N",
"InquiryChargePetition": null,
"ChangeCaptureDatetime": "2019-10-20T13:17:15.83",
"RegionRecommendsPursuing10j": "N",
"SurrogateKey": 1731295
}
I appreciate any and all help to get this working, please.
Thank you.

This is another common symptom of nested calls of context.sync. Fix the nesting first. Also, performance is better when you push filtering logic as close to the data source as possible. Consider designing the Azure Function to take in a list of fields and send back only data for matching names. Then your client side code is greatly simplified because you can assume there is a matching field for every data record that is returned.

Related

How can I use AQL with multiple queries that using the result of one another?

I have 2 vertices and an edge named user, device, ownership respectively.
My business logic is when I receive device information, I upsert it with dateCreated and dateUpdated fields added. If I inserted that device then I insert new user with default values and create edge connection to it. If I update I simple return already connected user as a result.
Without losing atomicity how can I achieve this?
I tried single AQL query but without condition it is not possible it seems and traversal also is not supported with insert/update operation.
I can do separate queries but that loses atomicity.
var finalQuery = aql`
UPSERT ${deviceQuery}
INSERT MERGE(${deviceQuery},{dateCreated:DATE_NOW()})
UPDATE MERGE(${deviceQuery},{dateUpdated:DATE_NOW()})
IN ${this.DeviceModel}
RETURN { doc: NEW, type: OLD ? 'update' : 'insert' }`;
var cursor = await db.query(finalQuery);
var result = await cursor.next();
if (result.type == 'insert') {
console.log('Inserted documents')
finalQuery = aql`
LET user=(INSERT {
"_key":UUID(),
"name": "User"
} INTO user
RETURN NEW)
INSERT {
_from:${result.doc._id},
_to:user[0]._id,
"type": "belongs"
}INTO ownership
return user[0]`;
cursor = await db.query(finalQuery);
result = await cursor.next();
console.log('New user:',result);
}
You can try something like this
Upsert ....
FILTER !OLD
Let model = NEW
LET user= First(INSERT {
"_key":UUID(),
"name": "User"
} INTO user
RETURN NEW)
INSERT {
_from:model._id,
_to:user._id,
"type": "belongs"
}INTO ownership
return user
I end up separating the modification and selection queries.
var finalQuery = aql`
LET device=(
UPSERT ${deviceQuery}
INSERT MERGE(${deviceQuery},{dateCreated:DATE_NOW()})
UPDATE MERGE(${deviceQuery},{dateUpdated:DATE_NOW()})
IN ${this.DeviceModel}
RETURN { doc: NEW, type: OLD ? 'update' : 'insert' })
FILTER device[0].type=='insert'
LET user=(INSERT {
"_key":UUID(),
"name": "User"
} INTO user
RETURN NEW)
INSERT {
_from:device[0].doc._id,
_to:user[0]._id,
"type": "belongs"
}INTO ownership
return user[0]`;
var cursor = await db.query(finalQuery);
var result = await cursor.next();
if (result == null) {
const deviceId=this.DeviceModel.name+"/"+queryParams._key;
finalQuery = aql`
FOR v,e,p IN 1..1
OUTBOUND ${deviceId} ownership
FILTER e.type=="belongs"
RETURN v `;
cursor = await db.query(finalQuery);
result = await cursor.next();
isUpdate=true;
}
This way I ensure the atomicity. There are improvements for controling if cursor.extra.stats.writesExecuted true etc.

Non-reactive and reactive data in Meteor (same helper)

I've got one view displaying some pictures published by users with some data (let's image Instagram).
I already have these pictures as non-reactive data (otherwise you could see many updates) but these images have one button to like the picture. If I have this as non-reactive data I can't see when I click on "Like" the filled heart (I need to refresh).
This is my subscribe function:
this.subscribe('food', () => [{
limit: parseInt(this.getReactively('perPage')),
//skip: parseInt((this.getReactively('page') - 1) * this.perPage),
sort: this.getReactively('sort')
}, this.getReactively('filters'), this.getReactively('searchText'), this.getReactively('user.following')
]);
And this is my helper:
food() {
const food = Food.find({}, {reactive: true}, {
sort: this.sort
}).fetch().map(food => {
const owner = Meteor.users.findOne(food.owner, {fields: {username: 1, avatarS: 1, following: 1}});
food.avatarS = owner && owner.avatarS;
food.username = owner && owner.username;
if (food.likes.indexOf(Meteor.userId()) == -1) {
// user did not like this plate
food.liked = false;
} else {
// user liked this plate
food.liked = true;
}
return food;
});
}
Is possible to have a non-reactive model but with some reactive properties on it?
I'm using Angular 1.X with TS btw
Thanks in advance!
PS: is it normal that this works as non-reactive when I change reactive to true?
Modification to your code:
//console.log(food.likes);
this.subscribe('reactiveFoodData', {ownerId: food.owner, userId: Meteor.userId()}).subscribe(()=>{
console.log(this.user);
});
// THIS IS THE PUBLISH METHOD LOCATED IN THE SERVER SIDE:
Meteor.publish('reactiveFoodData', function(params: {ownerId:string, userId:string) {
const owner = Meteor.users.findOne(params.ownerId);
if (!owner) {
throw new Meteor.Error('404', 'Owner does not exist');
}
let result = {};
result.avatarS = owner.avatarS;
result.username = owner.username;
const food = Food.find({});
result.liked = !(food.likes.indexOf(params.userId) == -1);
return result;
});
You have few problems:
1. The reactive flag is true by default, you do not need to set it.
2. The function find is accepting only two arguments, not 3.
Should be:
const food = Food.find({}, {reactive: true, sort: this.sort})
If you need some, subset of data to be reactive only (from some collection). You could create a specific Method (which udpates only "likes").
https://guide.meteor.com/methods.html
UPDATE:
Here is how you write a method with return parameter (check two examples, with Future and without):
How to invoke a function in Meteor.methods and return the value
UPDATE2:
You have lost reactivity when you used fetch(). Because you moved from reactive cursor to just simple array over which you map values. Do not expect reactivity after fetch(). If you want fetch or do not want to use Cursors, you could wrap the find inside Tracker.autorun(()=>{}) or utilize publish/subscribe.
Note: But be careful, if you somehow manage to get "empty" cursor in find(), your Tracker.autorun will stop react reactively. Autorun works only if it has something to watch over.
The main point with method, is that if you want to have one time non-reactive action for something. You define the method on server:
Meteor.methods({
myMethod: ()=> {
return "hello";
}
});
And you can call it from client with:
Meteor.call('myMethod', (error, result) => {
console.log(result); // "hello"
});
Instead of working with pure collections. You could start using publish/subscribe. On server you publish 'likes' and on client you just listens to this new reactive view. E.g.,
Meteor.publish('likes', (options: {owner: string, likes: Array<any>}) => {
let result: any = {}
const owner = Meteor.users.findOne(options.owner, username: 1, avatarS: 1, following: 1}});
result.avatarS = options.owner && options.owner.avatarS;
result.username = options.owner && options.owner.username;
result.liked = !(options.likes.indexOf(Meteor.userId()) == -1)
return result;
});
On client side: Meteor.subscibe('likes', {food.owner, food.likes}).subscribe(()=>{});
This is just off the top of my head.
Have you tried looking at Tracker ? https://docs.meteor.com/api/tracker.html
But more specifically the method Tracker.nonreactive
https://docs.meteor.com/api/tracker.html#Tracker-nonreactive

Added field not published to client?

I'm doing an aggregation in Meteor where I'm trying to find 'thingies' within a given distance and publish it to the client:
Meteor.publish("thingieSearch", function(userId) {
check(userId, String);
var subscription = this;
var thingies = {};
var userId = this.userId;
var usrAcc = Meteor.users.findOne({_id: userId});
var db = MongoInternals.defaultRemoteCollectionDriver().mongo.db;
var pipeline = [{
$geoNear: {
near: usrAcc.profile.location.geometry.coordinates,
distanceField: "calculatedDistance",
spherical: true,
limit: 100,
distanceMultiplier: 3959.2,
maxDistance: 0.075,
query: {
"status": "started",
"owner": {$ne: userId} },
}
}];
db.collection("thingies").aggregate(
pipeline,
Meteor.bindEnvironment(
function (err, result) {
console.log('result', result);
_.each(result, function (r) {
chases[r._id] = r;
subscription.added("thingieSearch", r._id, {
chase: r
});
})
}
)
);
subscription.ready();
});
When I do a console.log on the server side, it looks correct, the 'distanceField' that I specified as 'calculatedDistance' is calculated and shown as a field.
On the client side, I subscribe to this publication and I can see the thingies but I can't see the 'calculatedDistance' field.
Any idea why?
There are two things here.
1) Your publication will not reactively update data to the client, since you are using mongodb remote collection driver. If you intend it to be a non-reactive then you can use a meteor method instead of publication and call the method whenever userId changes.
2) I think you are using thingies collection on the client side and not seeing the calculatedDistance field. You need to create a client only collection (thingieSearch) to access the custom published results like this,
//On client side only
thingieSearch = new Mongo.Collection("thingieSearch");
thingieSearch.findOne(); // After the publication, you should be able to see the results with calculatedDistance
You should use whatever name you passed inside the subscription.added block to create collection. For example, if your publication has
subscription.added("thingieWithCalculateField", r._id, { chase: r });
you should do
//On client side only
thingieWithCalculateField = new Mongo.Collection("thingieWithCalculateField");
// instead of thingieSearch = new Mongo.Collection("thingieSearch");
See the counts-by-room publication in the Meteor.publish documentation for more details.

Use projection for meteor publish subscribe

I have a UI element framework, that works only with specific data model ( expects an object with "text" key). This is different from the data model that exists in my mongodb.
So my first idea was to use a projection and publish it to the listeners.
var db = MongoInternals.defaultRemoteCollectionDriver().mongo.db;
db.Element.aggregate({$project: { _id:1, text:"$description"}});
The Problem is that this is not a cursor, but just simple ejson object. What strategy should i use to give the UI framework needed data and to have a reactivity/data binding from both sides.
In your publication, instead of returning a cursor, you can use the lower level publish api to have finer control over the results.
For instance, when you return a cursor from a publication, Meteor calls the _publishCursor function which looks like this:
Mongo.Collection._publishCursor = function (cursor, sub, collection) {
var observeHandle = cursor.observeChanges({
added: function (id, fields) {
sub.added(collection, id, fields);
},
changed: function (id, fields) {
sub.changed(collection, id, fields);
},
removed: function (id) {
sub.removed(collection, id);
}
});
// We don't call sub.ready() here: it gets called in livedata_server, after
// possibly calling _publishCursor on multiple returned cursors.
// register stop callback (expects lambda w/ no args).
sub.onStop(function () {observeHandle.stop();});
// return the observeHandle in case it needs to be stopped early
return observeHandle;
};
So, you can modify your publication, to basically do the same thing, but also publish a text field, which gets its value from the description field, like so:
Given the following collection:
MyCollection = new Mongo.Collection("my_collection");
Your publish function might look like this:
Meteor.publish("myPub", function () {
var sub = this;
var observeHandle = myCollection.find().observeChanges({
added: function (id, fields) {
fields.text = fields.description; // assign text value here
sub.added("my_collection", id, fields);
},
changed: function (id, fields) {
fields.text = fields.description; // assign text value here
sub.changed("my_collection", id, fields);
},
removed: function (id) {
sub.removed("my_collection", id);
}
});
sub.ready()
// register stop callback (expects lambda w/ no args).
sub.onStop(function () {observeHandle.stop();});
};

How do I do a parameter based publication in Meteor and remove old subscription document?

I want to implement a parameter based publication in Meteor but I am running into some problems.
Here is what I have.
As the user types the keyup event that subscribes to publication and passes the value of the input.
'keyup #customerSearch': function(event, template){
var keyword = template.find('#customerSearch').value;
if(keyword){
if(keyword.length >= 3){
Meteor.subscribe('sessioncustomers', keyword);
}
}
}
The publication uses this keyword to return the records.
Meteor.publish("sessioncustomers", function(keyword){
if(keyword ){
if(keyword.length >= 3){
query.name = new RegExp(regExpQuoted(keyword), 'i' );
Customers.find(query);
} else {
return null;
}
}else{
return null;
}
});
The problem.
It works and documents are received except when the client changes the keyword or rather as the keywords changes the publication publishes additional documents that match the keywords but the client collection never removes the old documents.
How do I get the old documents that no longer match out of the client collection?
I thought that because the parameters of the subscription had changed that the non-matching documents would be unsubscribed and only the new matching documents would be subscribed.
In your keyup callback you need to "unsubscribe" to the previous publication,
otherwise you'll keep the old documents.
var sessionCustomersHandler = false;
'keyup #customerSearch': function(event, template) {
var keyword = template.find('#customerSearch').value;
if (keyword && keyword.length >= 3)
var newSessionCustomersHandler = Meteor.subscribe('sessioncustomers', keyword);
if (sessionCustomersHandler)
sessionCustomersHandler.stop();
sessionCustomersHandler = newSessionCustomersHandler;
}
Moreover, don't forget to check(keyword, String) in your publish function, for security.
Meteor.publish("sessioncustomers", function(keyword){
check(keyword, String)
if (keyword.length >= 3)
return Customers.find({
name: new RegExp(regExpQuoted(keyword), 'i' )
});
});
Make a local unnamed client collection
this.SessionCustomers = new Meteor.Collection(null);
Call a server method to get the results you want. Make the callback clear (remove all) and then insert to that local collection.
return Meteor.call('sessioncustomers', query, function(err, data) {
if (err) {
return console.log(err.message);
} else {
SessionCustomers.remove({});
var item, _i, _len;
for (_i = 0, _len = data.length; _i < _len; _i++) {
item = array[_i];
SessionCustomers.insert(item);
}
}
});

Resources