Sails.js Access Model on Service initialization - javascript

The question:
As I understand in sails.js during initialization process Services are initialized before Models.
Is there any possibility to change this behavior? To make Models load before Services.
If it's not, then how can I load particular settings from the database to use them to build instance of my class described in some Service during this Service initialization?
A little bit code for solidity:
api/models/Model.js
console.log("Model Identified");
module.exports = {
attributes: {
name: { type: 'string', required: true, size: 15 },
//Some extra secret fields
}
};
...
api/services/MyCoolService.js
console.log('service inits');
function MyCoolService(options){
//some extraordinary constructor logic may be ommited
}
MyCoolService.prototype.setOptions = function(options){
//Set values for MyCoolService fields.
}
//Some other methods
var myCoolServiceWithSettingsFromDb = new MyCoolService();
//That's the place
model.findOne(sails.config.myApplication.settingsId).exec(function(err,result){
if(!err)
myCoolServiceWithSettingsFromDb.setOptions(result);
});
module.exports = myCoolServiceWithSettingsFromDb;

It's because you instantiate object in service with constructor that needs sails that not exist. Try use this at MyCoolService;
module.exports = {
someOption: null,
method: function () {
var that = this;
sails.models.model.findOne(sails.config.myApplication.settingsId)
.exec(function (err, result) {
if (!err)
that.someOption = result;
});
}
};
that method can be called by sails.services.mycoolservice.method() or simply MyCoolService.method() to give your service some option from DB.
If you want to initiate them at Sails start, call that method at config/bootstrap.js

Thanks to Andi Nugroho Dirgantara,
I ended up with this solution (I still don't like it much, but it works):
api/services/MyCoolService.js
console.log('service inits');
function MyCoolService(options){
//some extraordinary constructor logic may be ommited
}
//All the same as in question
//The instance
var instance;
module.exports = module.exports = {
init: function(options) {
instance = new MyCoolService(options);
},
get: function() {
return instance;
},
constructor: MyCoolService
};
config/bootstrap.js
...
Model.findOrCreate({ id: 1 }, sails.config.someDefaultSettings).exec(function(err, result) {
if (err)
return sails.log.error(err);
result = result || sails.config.someDefaultSettings;
MyCoolService.init(result);
return sails.log.verbose("MyCoolService Created: ", TbcPaymentProcessorService.get());
});
...
tests/unit/service/MyCoolService.test.js
...
describe('MyCoolService', function() {
it('check MyCoolService', function(done) {
assert.notDeepEqual(MyCoolService.get(), sails.config.someDefaultSettings);
done();
});
});
...
It works: the service is instantiated once while bootstraping and it's instance is avaliable everywhere.
But to me this solution still weird... I still don't understand how to globally instantiate instance of my service (for use in a lot of controllers) and make it the best way.

Related

Jasmine and angular mocks : mocking a service that handles local storage

I have one service called wd$cache, that is basically a wrapper for localStorage.setItem and get.item.
Now I'm trying to test a controller that uses that service to achieve a certain result. The main problem is that I have an IF statement that gets triggered only if you have localstorage set already which is driving me nuts! (we are doing TDD here)
SERVICE
(function () {
angular
.module('hub')
.controller('promotionNotificationCtrl', promotionNotificationCtrl);
promotionNotificationCtrl.$inject = [
'hub$promotions',
'hub$client',
'wd$cache'
];
function promotionNotificationCtrl(
hub$promotions,
hub$client,
wd$cache) {
var vm = this;
activate();
//////////
function activate () {
hub$promotions.get(hub$client.brand, hub$client.subbrand).success(function (data) {
if (!wd$cache.get('hub$notification')) {
wd$cache.add('before', 123);
} else {
wd$cache.add('after', 321);
}
});
}
}
})();
TEST
describe('The promotion notification controller', function () {
var controller,
hub$client,
$httpBackend,
wd$cache,
mockData = [{
"foo": "bar"
},
{
"faa": "boo"
}];
beforeEach(module('hub'));
beforeEach(module('wired.core'));
beforeEach(module(function ($provide) {
hub$client = {
brand: 'bw',
subbrand: 'plus'
};
wd$cache = {
add: function () {
},
get: function () {
}
};
$provide.value('hub$client', hub$client);
$provide.value('wd$cache', wd$cache);
spyOn(wd$cache, 'add');
}));
beforeEach(inject(function ($controller, _$httpBackend_, _hub$promotions_) {
controller = $controller('promotionNotificationCtrl');
$httpBackend = _$httpBackend_;
hub$promotions = _hub$promotions_;
// request
$httpBackend.expectGET("/umbraco/api/promotions/get/?brand=bw&lang=en&subbrand=plus").respond(200, mockData);
$httpBackend.flush();
}));
it('should attempt to add a cache with a "before" key if no previous "hub$notification" cache was found', function () {
expect(wd$cache.add).toHaveBeenCalledWith('before', 123); //WORKING
})
it('should attempt to add a cache with a "after" key if a previous "hub$notification" cache was found', function () {
localStorage.setItem('hub$notification');
wd$cache.add('hub$notification');
expect(wd$cache.add).toHaveBeenCalledWith('after', 123); // NOT WORKING
// CANT GET THROUGH THE IF STATEMENT
})
});
Basically I can never get to 'Test Cases' after BeforeEach block, whatever I do. I've tried everything, since mocking it to use actual storage.
Any ideas?
You can provide a mock implementation that is already filled with some data:
var cache = {};
beforeEach(module(function ($provide) {
// ...
wd$cache = {
add: function (key, value) {
cache[key] = value;
},
get: function (key) {
return cache[key];
}
};
// add initial data here or in the individual tests, e.g.
// ...
}));
To set up the cache properly for a specific testcase you can use the cache field like this:
cache['hub$notification'] = 'whatever value makes sense here';
Of course you can also do this in beforeEach.
Currently you are trying to do it like this:
wd$cache.add('hub$notification');
expect(wd$cache.add).toHaveBeenCalledWith('after', 123);
This is problematic for two reasons:
You are not updating the cache because you are spying on the add method without .andCallThrough(). You should fix this (add .andCallThrough() after spy creation) otherwise updates from the controller will not affect the cache.
The spy records your call instead. You don't want this for setup code because it makes subsequent assertions more complicated.

Set a Backbone collection model with circular dependencies in requirejs

The thing is that I have a circular dependecy between some Backbone modules so I have to use "exports" as Requirejs scpecifies in its documentation http://requirejs.org/docs/api.html#circular. So the model 'A' will look like this:
define(function(require, exports) {
var B = require('B');
var A = Backbone.Model.extend({
});
exports.model = A;
});
And the collection 'B' like this:
define(function(require, exports) {
var A = require('A');
var B = Backbone.Model.extend({
model: A.model
});
exports.model = B;
});
The problem here is that by the time I have to specify the collection 'B' model property, the model 'A' isn't yet defined. This is the error I'm getting when I try to set the collection with models like this:
B.collection.set([{id: 1}, {id: 2}]);
Uncaught TypeError: 'undefined' is not an object (evaluating 'targetModel.prototype') (http://127.0.0.1:9999/bower_components/backbone/backbone.js:689)
Any ideas on how should I solve this problem?
From the example, it's not clear that B actually depends on A. If it's just a model:collection relationship, it might make sense to remove the dependency of the model on its collection. If it's at all possible to break the circular dependency, I would strongly encourage you to do so.
If the back-reference is truly required, though, one option might be to move the resources into the same module and do a sort of lazy export:
define(function() {
var lazyThings = {
A: null,
B: null
};
lazyThings.A = Backbone.Model.extend({
collection: things.B
});
lazyThings.B = Backbone.Collection.extend({
model: A
});
return lazyThings;
});
Alternatively, you could return lazyThings.B and later access the model from its prototype:
require('b', function (B) {
var A = B.prototype.model; // A
});
Finally, requirejs could be made to work by calling the respective dependencies lazily (i.e., after the modules are resolved):
// B
define(['a'], function (A) {
return function () {
return Backbone.Collection.extend({
model: A()
});
}
});
// A
define(['b'], function (B) {
return function () {
return Backbone.Model.extend({
model: B()
});
}
});
The following works for me, try to make it clear as possible.
You have a model, you have a collection. In order for them to both depend on each other + avoid a circular dependency, you need a 3rd "mediator" dependency. It's convenient in Backbone to have a model and easily lookup what collection it belongs to, and vice versa, but the problem of course is they have a circular dependency.
So before we had:
+model
+collection
__________
= circular
and after:
+model
+collection
+mediator
________
= OK
//collection
define([
'#allModels',
'#BaseCollection',
'#AppDispatcher',
'#allFluxConstants',
'app/js/flux/flux-helpers/collectionUpdater'
],
function (allModels, BaseCollection, AppDispatcher, allFluxConstants, collUpdater) {
var dispatchCallback = function (payload) {
return true;
};
var BaymaxComponentCollection = BaseCollection.extend({
model: allModels['BaymaxComponent'],
collectionName:'baymax-component',
url: '/baymax_component',
batchURL: '/batch/baymax_component',
initialize: function (models, opts) {
this.dispatchToken = AppDispatcher.register(dispatchCallback);
},
// collection is sorted by original insertion order.
comparator: 'order'
});
return new BaymaxComponentCollection();
});
//model
define([
'#BaseModel',
'#ModelCollectionMediator',
'#AppDispatcher'
],
function ( BaseModel, MCM) {
var BaymaxComponent = BaseModel.extend({
idAttribute: 'id',
urlRoot: '/baymax_component',
collectionName: 'baymax-component',
defaults: function () { //prevents copying default attributes to all instances of UserModel
return {}
},
initialize: function (attributes, opts) {
//*** the following line is crucial ***
this.collection = MCM.findCollectionByName(this.collectionName);
},
validate: function (attr) {
return undefined;
}
},
{ //class properties
});
return BaymaxComponent;
});
//mediator
define(function (require) {
return {
findCollectionByName: function (name) {
var allCollections = require('#allCollections');
return allCollections[name];
}
};
});

How is it posible to set a var property from within a function defined in that var?

I have a question regarding definitions of factories in AngularJS. I am unsure if this is a Javascript doubt or AngularJS, but I believe it's Angular.
Say I have a factory definition like this:
angular.module('myApp.services')
.factory('User', function($http) { // injectables go here
var backendUrl = "http://localhost:3000";
var service = {
// our factory definition
user: {},
setName: function(newName) {
service.user['name'] = newName;
},
setEmail: function(newEmail) {
service.user['email'] = newEmail;
},
save: function() {
return $http.post(backendUrl + '/users', {
user: service.user
});
}
};
return service;
});
How is it possible that the function setName is able to set service.user['email'], if service is actually defining setName itself?
This is basic javascript. Forget about the factory for a minute, only consider definition of service.
var service = {
//service will have an empty user js object
user: {},
//a key setName with the value as a function
//which sets a key value pair in user defined above
//service = { user: {name: 'Tony'} }
setName: function(newName) {
service.user['name'] = newName;
},
//a key setEmail with the value as a function
//which sets a key value pair in user defined above
//(consider setName has been called already)
//service = { user: {name: 'Tony', email: 'tony#stark.com'} }
setEmail: function(newEmail) {
service.user['email'] = newEmail;
},
//a key save with the value as a function
//which saves
save: function() {
//return something from a call mimicing $http.post
//return $http.post(backendUrl + '/users', { user: service.user });
}
}
Then, return or use service which has become an API by itself.
Check out dev tool console for log in this FIDDLE
setName function is going to be called by some other code like this,
User.setName("newname "); when this is called the service already defined and it's about setting new value to a property.
and you could use this keyword to make the code more meaningful,
setName: function(newName) {
this.user['name'] = newName;
}
I see one issue with your code, even though you have defined setName function you have the user variable is accessible to outside of the factory,now you can do like this as well,
User.user['name'] = newName;
I think what you need to do is make user a private variable,
angular.module('myApp.services')
.factory('User', function($http) { // injectables go here
var backendUrl = "http://localhost:3000";
var user={};
var service = {
// our factory definition
setName: function(newName) {
user['name'] = newName;
},
setEmail: function(newEmail) {
user['email'] = newEmail;
},
getUser:function(){
return user;
},
save: function() {
return $http.post(backendUrl + '/users', {
user: service.user
});
}
};
return service;
});
What you're describing is a behavior of JavaScript's closures.
Basically, a function has access to the variables outside of the function's scope. If those variables change, it's changed everywhere.
When the functions are created, service is still undefined
The functions create a closure around the service variable
service is then assigned to be the object
All the functions are now pointing to the service object
Here's a way to visualize this:
var service = null;
var getService = function() { return service; };
service = 5;
getService(); // returns 5
service = "service can change";
getService(); // returns "service can change";

Is this a best practice to cache request data in Backbone.js?

I have a requestCache: {} object in my router declaration. I have a reviews method mapped to a route with the same name (#reviews). I want to cache the results generated inside this method.
router.js
var AppRouter = Backbone.Router.extend({
currentView: null,
requestCache: {},
reviews: function() {
var self = this;
var reviewCollection = new ReviewCollection();
reviewCollection.url = '/profile/' + this.userid + '/reviews';
if('reviews' in self.requestCache) {
reviewCollection = self.requestCache['reviews'];
self.changeView(new ReviewsView({collection:reviewCollection}), 'reviews');
} else {
reviewCollection.fetch().done(function() {
self.requestCache['reviews'] = reviewCollection;
self.changeView(new ReviewsView({collection:reviewCollection}), 'reviews');
});
}
},
changeView just renders the view using the results.
This works fine. What I want to know is whether this is a good way of caching data?
Take a look at backbone-fetch-cache. It does what you want.
As suggested by SoundCloud team, they've craeted a store object to share models and collections through the code.
I've been using Backbone SingletonModel (https://github.com/reconbot/backbone-singleton)
It works just fine and you can make the same for your collections, defining a getInstance method and a _instance on its static part.
var MyCollection = Backbone.Collection.extend({}, {
_instance: null,
count: 0,
getInstance: function () {
if (!this._instance)
this._instance = new MyCollection();
this.count++;
return this._instance;
}
});

sails.js access controller method from controller method

How come in sails you cannot access other controller methods from within another one?
like this.
module.exports =
findStore: ->
# do somthing
index: ->
#findStore(); # Error: undefined
Compiled
module.exports = {
findStore: function() {},
index: function() {
return this.findStore(); // Error: undefined
}
};
If you can't do this, then why not? how else should I be doing this...
You can use sails.controllers.yourControllerName.findStore()
the sails global object has references to almost everything.
One of the best ways to organize your code in Sails, at least for me and my team, has been to have all the real business logic in Services (/api/services). Those objects can be accessed globally from any controller.
Also, a good practice is working with promises in services (as Sails use them on the model methods)
Just create a Store service (StoreService.js), with your code:
module.exports = {
findStore: function(storeId) {
// here you call your models, add object security validation, etc...
return Store.findOne(storeId);
}
};
Your Controllers should handle all that is related to requests, calling services, and returning apropriate responses.
For example, in you example, the controller could have this:
module.exports = {
index: function(req, res) {
if(req.param('id')) {
StoreService.findStore(req.param('id'))
.then(res.ok)
.catch(res.serverError);
} else {
res.badRequest('Missing Store id');
}
},
findStore: function(req, res) {
if(req.param('id')) {
StoreService.findStore(req.param('id'))
.then(res.ok)
.catch(res.serverError);
} else {
res.badRequest('Missing Store id');
}
},
};
That way, you have really simple controllers, and all business logic is managed by services.
Having the same problem for last few hours. I used the api/services folder.
It may not be exactly what you need but it is an option.
A good explanation is here. What services would one add to the api/services folder in sails.js
It's slightly annoying when you're just trying to build something quickly, but in the long run it forces good code organization practice (by making it harder to shove all business logic into a controller).
I would like to suggest a solution that works but not the best possible way to do it. We can use bind function to bind the context with the calling source as shown below :
generateUrl is present in the Controller A
function generateUrl(){
return 'www.google.com';
}
get URL is another method in Controller A
getURL(){
A.generateURL.bind(A.generateURL) //func call with optional arg
}
I hope this helps!
A more elegant way to solve this problem is using the keyword this before the function name.
Example:
one: function() {
console.log('First Function');
},
two: function() {
// call the function one in the same controller
this.one();
}
You can do something like this:
//ArticleController
module.exports = {
findStore: async () => {
return await findStoreFunc(req.param('id'));
},
index: async () => {
...
return await findStoreFunc(id);
}
};
const findStoreFunc = async (id) => {...}
And to use the function from another controller:
const ArticleController = require('./ArticleController');
//CustomerController
module.exports = {
index: async () => {
...
let article = await ArticleController.findStore(id);
...
}
};

Categories

Resources