Effectively using association population in SailsJS - sails.js

Association auto population is sexy during the early stages of the app development. Soon as the related models result in high number of associated records the api calls get a drastic performance hit. SailsJS provides a way to toggle this globally.
module.exports.blueprints.populate = true / false;
Ideal application would be to disable this option globally and load the related models on demand , is this possible ( Base use case would be how Laravel does things with Eager loading http://laravel.com/docs/5.0/eloquent#eager-loading ).

You should be able to override the blueprint configuration per controller #/disabling-blueprints-on-a-per-controller-basis
You may also override any of the settings from config/blueprints.js on a per-controller basis by defining a '_config' key in your controller defintion, and assigning it a configuration object with overrides for the settings in this file.
Try this in the controller where you want to activate populate:
module.exports = {
_config: {
populate: true
}
}

Related

Override Sails.js Waterline intercept Find method

I have a model named Product.
I want configurable across the board filtering for this model.
For example: sails.config.field = 2;
When I do GET /Product I want it to essentially do GET /Product?where={"field": 2}
The above works for blueprint by adding a policy, but I want consistent behavior when I use the waterline ORM
GET /Product
and Product.find() should return the same thing.
I can override the model: Product.find and it would work perfectly... IF there was some way for me to access the underlying find code.
The code I am using to intercept the blueprint is:
if (!req.query.where) {
req.query.where = `{"status":{">":0,">=":${sails.config.catalogVersions.status}}}`;
} else {
const parsedWhere = JSON.parse(req.query.where);
parsedWhere.status = {
'>': 0,
'>=': sails.config.catalogVersions.status,
};
req.query.where = JSON.stringify(parsedWhere);
}
I could very easily apply this to a Model.find interceptor.
Is there any way that once sails is loaded, I can access the root find method on a model even if its been overwritten at load time?
Maybe you could think on something like this one:
https://github.com/muhammadghazali/sails-hook-pagination
It's a hook and maybe works for your intended use.
I ended up using the hook:orm:loaded hook, to run some code which monkeypatched all of the models with a defaultScope which was stored in each of my models. It works well as I can easily modify the default criteria on all of the models and get consistent behavior across blueprint and waterline.
For code see:
Is there a way to override sails.js waterline endpoint with custom controller, but keep built in pagination, and filtering?

Disable logging on FileConfigurationSourceChanged - LogEnabledFilter

I want Administrators to enable/disable logging at runtime by changing the enabled property of the LogEnabledFilter in the config.
There are several threads on SO that explain workarounds, but I want it this way.
I tried to change the Logging Enabled Filter like this:
private static void FileConfigurationSourceChanged(object sender, ConfigurationSourceChangedEventArgs e)
{
var fcs = sender as FileConfigurationSource;
System.Diagnostics.Debug.WriteLine("----------- FileConfigurationSourceChanged called --------");
LoggingSettings currentLogSettings = e.ConfigurationSource.GetSection("loggingConfiguration") as LoggingSettings;
var fdtl = currentLogSettings.TraceListeners.Where(tld => tld is FormattedDatabaseTraceListenerData).FirstOrDefault();
var currentLogFileFilter = currentLogSettings.LogFilters.Where(lfd => { return lfd.Name == "Logging Enabled Filter"; }).FirstOrDefault();
var filterNewValue = (bool)currentLogFileFilter.ElementInformation.Properties["enabled"].Value;
var runtimeFilter = Logger.Writer.GetFilter<LogEnabledFilter>("Logging Enabled Filter");
runtimeFilter.Enabled = filterNewValue;
var test = Logger.Writer.IsLoggingEnabled();
}
But test reveals always the initially loaded config value, it does not change.
I thought, that when changing the value in the config the changes will be propagated automatically to the runtime configuration. But this isn't the case!
Setting it programmatically as shown in the code above, doesn't work either.
It's time to rebuild Enterprise Library or shut it down.
You are right that the code you posted does not work. That code is using a config file (FileConfigurationSource) as the method to configure Enterprise Library.
Let's dig a bit deeper and see if programmatic configuration will work.
We will use the Fluent API since it is the preferred method for programmatic configuration:
var builder = new ConfigurationSourceBuilder();
builder.ConfigureLogging()
.WithOptions
.DoNotRevertImpersonation()
.FilterEnableOrDisable("EnableOrDisable").Enable()
.LogToCategoryNamed("General")
.WithOptions.SetAsDefaultCategory()
.SendTo.FlatFile("FlatFile")
.ToFile(#"fluent.log");
var configSource = new DictionaryConfigurationSource();
builder.UpdateConfigurationWithReplace(configSource);
var defaultWriter = new LogWriterFactory(configSource).Create();
defaultWriter.Write("Test1", "General");
var filter = defaultWriter.GetFilter<LogEnabledFilter>();
filter.Enabled = false;
defaultWriter.Write("Test2", "General");
If you try this code the filter will not be updated -- so another failure.
Let's try to use the "old school" programmatic configuration by using the classes directly:
var flatFileTraceListener = new FlatFileTraceListener(
#"program.log",
"----------------------------------------",
"----------------------------------------"
);
LogEnabledFilter enabledFilter = new LogEnabledFilter("Logging Enabled Filter", true);
// Build Configuration
var config = new LoggingConfiguration();
config.AddLogSource("General", SourceLevels.All, true)
.AddTraceListener(flatFileTraceListener);
config.Filters.Add(enabledFilter);
LogWriter defaultWriter = new LogWriter(config);
defaultWriter.Write("Test1", "General");
var filter = defaultWriter.GetFilter<LogEnabledFilter>();
filter.Enabled = false;
defaultWriter.Write("Test2", "General");
Success! The second ("Test2") message was not logged.
So, what is going on here? If we instantiate the filter ourselves and add it to the configuration it works but when relying on the Enterprise Library configuration the filter value is not updated.
This leads to a hypothesis: when using Enterprise Library configuration new filter instances are being returned each time which is why changing the value has no effect on the internal instance being used by Enterprise Library.
If we dig into the Enterprise Library code we (eventually) hit on LoggingSettings class and the BuildLogWriter method. This is used to create the LogWriter. Here's where the filters are created:
var filters = this.LogFilters.Select(tfd => tfd.BuildFilter());
So this line is using the configured LogFilterData and calling the BuildFilter method to instantiate the applicable filter. In this case the BuildFilter method of the configuration class LogEnabledFilterData BuildFilter method returns an instance of the LogEnabledFilter:
return new LogEnabledFilter(this.Name, this.Enabled);
The issue with this code is that this.LogFilters.Select returns a lazy evaluated enumeration that creates LogFilters and this enumeration is passed into the LogWriter to be used for all filter manipulation. Every time the filters are referenced the enumeration is evaluated and a new Filter instance is created! This confirms the original hypothesis.
To make it explicit: every time LogWriter.Write() is called a new LogEnabledFilter is created based on the original configuration. When the filters are queried by calling GetFilter() a new LogEnabledFilter is created based on the original configuration. Any changes to the object returned by GetFilter() have no affect on the internal configuration since it's a new object instance and, anyway, internally Enterprise Library will create another new instance on the next Write() call anyway.
Firstly, this is just plain wrong but it is also inefficient to create new objects on every call to Write() which could be invoked many times..
An easy fix for this issue is to evaluate the LogFilters enumeration by calling ToList():
var filters = this.LogFilters.Select(tfd => tfd.BuildFilter()).ToList();
This evaluates the enumeration only once ensuring that only one filter instance is created. Then the GetFilter() and update filter value approach posted in the question will work.
Update:
Randy Levy provided a fix in his answer above.
Implement the fix and recompile the enterprise library.
Here is the answer from Randy Levy:
Yes, you can disable logging by setting the LogEnabledFiter. The main
way to do this would be to manually edit the configuration file --
this is the main intention of that functionality (developers guide
references administrators tweaking this setting). Other similar
approaches to setting the filter are to programmatically modify the
original file-based configuration (which is essentially a
reconfiguration of the block), or reconfigure the block
programmatically (e.g. using the fluent interface). None of the
programmatic approaches are what I would call simple – Randy Levy 39
mins ago
If you try to get the filter and disable it I don't think it has any
affect without a reconfiguration. So the following code still ends up
logging: var enabledFilter = logWriter.GetFilter();
enabledFilter.Enabled = false; logWriter.Write("TEST"); One non-EntLib
approach would just to manage the enable/disable yourself with a bool
property and a helper class. But I think the priority approach is a
pretty straight forward alternative.
Conclusion:
In your custom Logger class implement a IsLoggenabled property and change/check this one at runtime.
This won't work:
var runtimeFilter = Logger.Writer.GetFilter<LogEnabledFilter>("Logging Enabled Filter");
runtimeFilter.Enabled = false/true;

sails.js - How can I specify an order to the routes of installable hooks?

I have a few policies that I'm trying to convert to installable hooks.
One in particular is the authentication step that adds a session variable for who the user is. I need to ensure that it fires first because subsequent hooks/controllers look at the session to determine who the user is.
The docs on routes ( https://github.com/balderdashy/sails-docs/blob/master/concepts/extending-sails/Hooks/hookspec/routes.md ) say that I can specify if I want to run the route before or after all the custom routes, but I don't see a way to specify one hook before another hook.
I tried adding a "sails.on" in the initialize step of my two hooks as specified in the initialize docs ( https://github.com/balderdashy/sails-docs/blob/master/concepts/extending-sails/Hooks/hookspec/initialize.md ). While it did alter the order of the initialize phase, it didn't seem to have any impact on the order that routes were handled.
Any pointers would be greatly appreciated.
As you noted, there's no way to re-order hooks at the moment, but that doesn't mean you can't accomplish your goal in another way. The routes feature of hooks is very convenient, but in this case you will need to bind your routes using events, and then make your hooks dependent on each other to ensure that the routes load in the correct order. Many of the core Sails hooks work in just this way.
If you want to ensure that the hook firsthook to loads all of its routes before secondhook, you would put the following as the initialize method of firsthook:
initialize: function(cb) {
sails.on('router:before', function () {
sails.router.bind(routePath1, routeFn1);
sails.router.bind(routePath2, routeFn2);
sails.router.bind(routePath3, routeFn3);
etc...
});
}
And then do the same in secondhook, but wrapped in a sails.after block:
initialize: function(cb) {
sails.after('hook:firsthook:loaded', function() {
sails.on('router:before', function () {
sails.router.bind(routePath1, routeFn1);
sails.router.bind(routePath2, routeFn2);
sails.router.bind(routePath3, routeFn3);
etc...
});
}
}
Note that sails.after can accept either a single event name or an array to wait for.

SAPUI5 bindAggregation complete for a Table

I am binding an aggregation to a table . I couldn't find an event which is triggered after the binding is complete . There is "updateFinished" event for sap.m.List , which is exactly what I am looking for in a Table (and a dropodown). I thought of using attachRequestCompleted() on the model , but the model is used at other places where I do not want this event to trigger.
Is there anyway to trigger a event once the databinding is complete on a Table (and a dropdown)?
Any help is appreciated.
Thanks in advance.
update: There is "updateFinished" event for table extended from ListBase. I am still not sure how I missed it before I posted this question. But, the question is still valid for a dropdown and TableSelectDialog controls.
I also stumbled upon that problem, but in a different Context.
I have a Grid layout in which I dynamically load Panels via an oData Model.
Therefore I have entered the path in my XML Grid-View element.
<l:Grid id="grid" content="{some path...}">...</l:Grid>
Now I wanted to set the grid view busy and when the data is loaded revert this.
Therefore I use the Binding of the grid view.
In the Controllers onInit method I have added:
this._oGrid = this.getView().byId("grid");
this.getRouter().attachRouteMatched(this._onRouteMatch.bind(this));
Please note that the bind method is not available in every browser. You need to apply a polyfill. (See https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Function/bind)
Also Bind has nothing to do with the binding :D
I needed to do this because the Binding is not available in the onInit function.
The _onRouteMatched function:
var oContent = this._oGrid.getBinding("content");
oContent.attachDataReceived(function(oData) {
this._oGrid.setBusy(false);
}.bind(this));
Now when the data is received the busy option is set to false.
If you want to show a 'loading' indicator for your table while the data is still loading (and thus not bound), I think the best approach is the following:
Use a dedicated JSONModel which holds only UI-specific stuff, like toggling enabled/readonly/busy/etc properties of controls.
In your case, something like:
var oUIModelData = {
tableIsBusy : false,
//etc, things like :
btnSubmitEnabled : false,
someControlVisible : true
};
var oUIModel = new sap.ui.model.json.JSONModel();
oUIModel.setData(oUIModelData);
sap.ui.getCore().setModel(oUIModel, "UIModel");
In your table definition, bind the busy property to {UIModel>/tableIsBusy} and set the table's property busyIndicatorDelay to 0 to avoid any delays
Just before you do your OData service call, set the property tableBusy to true. This will immediately show the busy overlay to your table:
sap.ui.getCore().getModel("UIModel").setProperty(tableIsBusy, true);
//here your OData read call
In your OData service's requestCompleted (and if needed, also in requestFailed) event handlers, set the busy property of the UIModel back to false:
sap.ui.getCore().getModel("UIModel").setProperty(tableIsBusy, false);
The big benefit of this approach is (IMHO) instead of relying on each control to check whether the data has been loaded, simply do it during the actual load.
And by binding these UI-related things to a (separate) model saves you from writing lots of extra code ;-)
In general you could solve the problem by using batch processing on the OData service. According to https://sapui5.netweaver.ondemand.com/docs/guide/6c47b2b39db9404582994070ec3d57a2.html:
Use OData model v2.
var oModel = new sap.ui.model.odata.v2.ODataModel(myServiceUrl);
Define a set of deferred batch groups.
oModel.setDeferredBatchGroups(["myGroupId", "myGroupId2"]);
Add the batch group information to the corresponding bindings, e.g:
{
path:"/myEntities",
parameters: {
batchGroupId: "myGroupId"
}
}
All read/query actions on bindings in a certain batch group will be held back until a .submitChanges(.) call on the batch group is made.
oModel.submitChanges({
batchGroupId: "myGroupId",
success: mySuccessHandler,
error: myErrorHandler
});
Use the success/error handlers to execute actions.
This rather generic approach gives you additional control such as trigger actions, grouping and event handling on the OData model.

Navigation Property Filter

My question is this: How can you implement a default server-side "filter" for a navigation property?
In our application we seldom actually delete anything from the database. Instead, we implement "soft deletes" where each table has a Deleted bit column. If this column is true the record has been "deleted". If it is false, it has not.
This allows us to easily "undelete" records accidentally deleted by the client.
Our current ASP.NET Web API returns only "undeleted" records by default, unless a deleted argument is sent as true from the client. The idea is that the consumer of the service doesn't have to worry about specifying that they only want undeleted items.
Implementing this same functionality in Breeze is quite simple, at least for base entities. For example, here would be the implementation of the classic Todo's example, adding a "Deleted" bit field:
// Note: Will show only undeleted items by default unless you explicitly pass deleted = true.
[HttpGet]
public IQueryable<BreezeSampleTodoItem> Todos(bool deleted = false) {
return _contextProvider.Context.Todos.Where(td => td.Deleted == deleted);
}
On the client, all we need to do is...
var query = breeze.EntityQuery.from("Todos");
...to get all undeleted Todos, or...
var query = breeze.EntityQuery.from("Todos").withParameters({deleted: true})
...to get all deleted Todos.
But let's say that a BreezeSampleTodoItem has a child collection for the tools that are needed to complete that Todo. We'll call this "Tools". Tools also implements soft deletes. When we perform a query that uses expand to get a Todo with its Tools, it will return all Tools - "deleted" or not.
But how can I filter out these records by default when Todo.Tools is expanded?
It has occurred to me to have separate Web API methods for each item that may need expanded, for example:
[HttpGet]
public IQueryable<Todo> TodoAndTools(bool deletedTodos = false, bool deletedTools = false)
{
return // ...Code to get filtered Todos with filtered Tools
}
I found some example code of how to do this in another SO post, but it requires hand-coding each property of Todo. The code from the above-mentioned post also returns a List, not an IQueryable. Furthermore this requires methods to be added for every possible expansion which isn't cool.
Essentially what I'm looking for is some way to define a piece of code that gets called whenever Todos is queried, and another for whenever Tools is queried - preferably being able to pass an argument that defines if it should return Deleted items. This could be anywhere on the server-side stack - be it in the Web API method, itself, or maybe part of Entity Framework (note that filtering Include extensions is not supported in EF.)
Breeze cannot do exactly what you are asking for right now, although we have discussed the idea of allowing the filtering of "expands", but we really need more feedback as to whether the community would find this useful. Please add this to the breeze User Voice and vote for it. We take these suggestions very seriously.
Moreover, as you point out, EF does not support this.
But... what you can do is use a projection instead of an expand to do something very similar:
public IQueryable<Object> TodoAndTools(bool deleted = false
,bool deletedTools = false) {
var baseQuery = _contextProvider.Context.Todos.Where(td => td.Deleted == deleted);
return baseQuery.Select(t => new {
Todo: t,
Tools: t.Tools.Where( tool => tool.Deleted = deletedTools);
});
}
Several things to note here:
1) We are returning an IQueryable of Object instead of IQueryable of ToDo
2) Breeze will inspect the returned payload and automatically create breeze entities for any 'entityTypes' returned (even within a projection). So the result of this query will be an array of javascript objects each with two properties; 'ToDo' and 'Tools' where Tools is an array of 'Tool' entities. The nice thing is that both ToDo and Tool entities returned within the projection will be 'full' breeze entities.
3) You can still pass client side filters based on the projected property names. i.e.
var query = EntityQuery.from("TodoAndTools")
.where("Todo.Description", "startsWith", "A")
.using(em);
4) EF does support this.