Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions lib/services.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,117 @@ function describeModels(app) {
return;
}

// The URL of prototype methods include sharedCtor parameters like ":id"
// Because all $resource methods are static (non-prototype) in ngResource,
// the sharedCtor parameters should be added to the parameters
// of prototype methods.
c.methods.forEach(function fixArgsOfPrototypeMethods(method) {
var ctor = method.restClass.ctor;
if (!ctor || method.sharedMethod.isStatic) return;
method.accepts = ctor.accepts.concat(method.accepts);
});

result[name] = c;
});

buildScopes(result);

return result;
}

var SCOPE_METHOD_REGEX = /^prototype.__([^_]+)__(.+)$/;

function buildScopes(models) {
for (var modelName in models) {
buildScopesOfModel(models, modelName);
}
}

function buildScopesOfModel(models, modelName) {
var modelClass = models[modelName];

modelClass.scopes = {};
modelClass.methods.forEach(function(method) {
buildScopeMethod(models, modelName, method);
});

return modelClass;
}

// reverse-engineer scope method
// defined by loopback-datasource-juggler/lib/scope.js
function buildScopeMethod(models, modelName, method) {
var modelClass = models[modelName];
var match = method.name.match(SCOPE_METHOD_REGEX);
if (!match) return;

var op = match[1];
var scopeName = match[2];
var modelPrototype = modelClass.sharedClass.ctor.prototype;
var targetClass = modelPrototype[scopeName]._targetClass;

if (modelClass.scopes[scopeName] === undefined) {
if (!targetClass) {
console.error(
'Warning: scope %s.%s is missing _targetClass property.' +

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. Maybe include the version numbers in here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, I would include it if the version was easily available. I mean all I have is the app object here. I could probably hack the Node module system to find out what module created the app object and then where the loopback-datasource-juggler is installed, so that I can read its package.json file and get the version, but considering the complexity, I'd rather avoid that.

'\nThe Angular code for this scope won\'t be generated.' +
'\nPlease upgrade to the latest version of' +
'\nloopback-datasource-juggler to fix the problem.',
modelName, scopeName);
modelClass.scopes[scopeName] = null;
return;
}

if (!findModelByName(models, targetClass)) {
console.error(
'Warning: scope %s.%s targets class %j, which is not exposed ' +
'\nvia remoting. The Angular code for this scope won\'t be generated.',
modelName, scopeName, targetClass);
modelClass.scopes[scopeName] = null;
return;
}

modelClass.scopes[scopeName] = {
methods: {},
targetClass: targetClass
};
} else if (modelClass.scopes[scopeName] === null) {
// Skip the scope, the warning was already reported
return;
}

var apiName = scopeName;
if (op == 'get') {
// no-op, create the scope accessor
} else if (op == 'delete') {
apiName += '.destroyAll';
} else {
apiName += '.' + op;
}

method.deprecated = 'Use ' + modelName + '.' + apiName + '() instead.';

// build a reverse record to be used in ngResource
// Product.__find__categories -> Category.::find::product::categories
var reverseName = '::' + op + '::' + modelName + '::' + scopeName;

var reverseMethod = Object.create(method);
reverseMethod.name = reverseName;
delete reverseMethod.deprecated;
reverseMethod.internal = 'Use ' + modelName + '.' + apiName + '() instead.';

var reverseModel = findModelByName(models, targetClass);
reverseModel.methods.push(reverseMethod);

var scopeMethod = Object.create(method);
scopeMethod.name = reverseName;
delete scopeMethod.deprecated;
modelClass.scopes[scopeName].methods[apiName] = scopeMethod;
}

function findModelByName(models, name) {
for (var n in models) {
if (n.toLowerCase() == name.toLowerCase())
return models[n];
}
}
232 changes: 136 additions & 96 deletions lib/services.template
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ var module = angular.module(<%-: moduleName | q %>, ['ngResource']);
*/
module.factory(
<%-: modelName | q %>,
['LoopBackResource', 'LoopBackAuth', function(Resource, LoopBackAuth) {
return Resource(
['LoopBackResource', 'LoopBackAuth', '$injector', function(Resource, LoopBackAuth, $injector) {
var R = Resource(
urlBase + <%-: meta.ctor.getFullPath() | q %>,
<% /*
Constructor arguments are hardcoded for now.
Expand All @@ -54,100 +54,7 @@ module.factory(
<% meta.methods.forEach(function(action) {
var methodName = action.name.split('.').join('$');
-%>
/**
* @ngdoc method
* @name lbServices.<%- modelName %>#<%- methodName %>
* @methodOf lbServices.<%- modelName %>
*
* @description
*
<% if (!action.description) {
action.description = '<em>\n' +
'(The remote method definition does not provide any description.)\n' +
'</em>';
} -%>
* <%-: action.description | replace:/\n/g, '\n * ' %>
*
<%
var params = action.accepts;
var postData;
if (action.getHttpMethod() == 'POST' || action.getHttpMethod() == 'PUT') {
params = params.filter(function(arg) {
return arg.http && (arg.http.source == 'query' || arg.http.source == 'path');
});
postData = action.accepts.filter(function(arg) {
return params.indexOf(arg) == -1;
});
}
-%>
* @param {Object=} parameters Request parameters.
<% if (params.length == 0) { -%>
*
* This method does not accept any parameters.
* Supply an empty object or omit this argument altogether.
<% } else { params.forEach(function(arg) { -%>
*
* - `<%- arg.arg %>` – `{<%- getJsDocType(arg) %>}` - <%-
(arg.description || '').replace(/\n/g, '\n * ') %>
<% }); } -%>
<% if (modelName === 'User' && methodName === 'login') { -%>
*
* - `rememberMe` - `boolean` - Whether the authentication credentials
* should be remembered in localStorage across app/browser restarts.
* Default: `true`.
<% } -%>
<% if (postData) { -%>
*
* @param {Object} postData Request data.
<% if (postData.length == 0) { -%>
*
* This method does not accept any data. Supply an empty object.
<% } else if (postData.length == 1 && postData[0].http &&
postData[0].http.source == 'body') { -%>
*
* This method expects a subset of model properties as request parameters.
<% } else {
postData.forEach(function(arg) { -%>
*
* - `<%- arg.arg %>` – `{<%- getJsDocType(arg) %>}` - <%-
(arg.description || '').replace(/\n/g, '\n * ') %>
<% });
}
} -%>
*
<% var returnType = action.isReturningArray() ? 'Array.<Object>': 'Object'; -%>
* @param {Function(<%- returnType %>, Object)=} successCb
* Success callback with two arguments: `value`, `responseHeaders`.
*
* @param {Function(Object)=} errorCb Error callback with one argument:
* `httpResponse`.
*
* @return {<%- returnType %>} An empty reference that will be
* populated with the actual data once the response is returned
* from the server.
*
<% if (!action.returns || action.returns.length == 0) { -%>
* This method returns no data.
<% } else if (action.returns[0].root) { -%>
<% if (action.returns[0].description) { -%>
* <%- action.returns[0].description
.replace(/\n/g, '\n * ').trimRight() %>
<% } else { -%>
* <em>
* (The remote method definition does not provide any description.
* This usually means the response is a `<%- modelName %>` object.)
* </em>
<% } -%>
<% } else { -%>
* Data properties:
<% action.returns.forEach(function(arg) { -%>
*
* - `<%- arg.arg %>` – `{<%- getJsDocType(arg) %>}` - <%-
(arg.description || '').replace(/\n/g, '\n * ') %>
<% });
}
-%>
*/
<% ngdocForMethod(modelName, methodName, action); -%>
<%-: methodName | q %>: {
url: urlBase + <%-: action.getFullPath() | q %>,
method: <%-: action.getHttpMethod() | q %>,
Expand Down Expand Up @@ -214,6 +121,29 @@ postData.forEach(function(arg) { -%>
<% } -%>
}
);

<% for (var scopeName in meta.scopes) {
var scope = meta.scopes[scopeName];
if (!scope) continue;
var scopeMethods = scope.methods;
// Angular names always start with a capital letter
var targetClass = scope.targetClass[0].toUpperCase() + scope.targetClass.slice(1);

// sort the names to make sure the get method creating the scope
// is emitted first (R.categories before R.categories.create)
Object.keys(scopeMethods).sort().forEach(function(methodName) {
var action = scopeMethods[methodName];
ngdocForMethod(modelName, methodName, action, targetClass);
-%>
R.<%- methodName %> = function() {
var TargetResource = $injector.get(<%-: targetClass | q %>);
var action = TargetResource[<%-: action.name | q %>];
return action.apply(R, arguments);
};
<% }); // forEach methods name -%>
<% } // for each scope -%>

return R;
}]);

<% } // for modelName in models -%>
Expand Down Expand Up @@ -300,4 +230,114 @@ function getJsDocType(arg) {
if (!arg.required) type += '=';
return type;
}

function ngdocForMethod(modelName, methodName, action, responseModelName) {
// always add an empty line before the ngdoc comment:
-%>

<%
if (action.internal) {
-%>
// INTERNAL. <%- action.internal %>
<%
return;
}
-%>
/**
* @ngdoc method
* @name lbServices.<%- modelName %>#<%- methodName %>
* @methodOf lbServices.<%- modelName %>
<% if (action.deprecated) { -%>
* @deprecated <%- action.deprecated %>
<% } -%>
*
* @description
*
<% if (!action.description) {
action.description = '<em>\n' +
'(The remote method definition does not provide any description.)\n' +
'</em>';
} -%>
* <%-: action.description | replace:/\n/g, '\n * ' %>
*
<%
var params = action.accepts;
var postData;
if (action.getHttpMethod() == 'POST' || action.getHttpMethod() == 'PUT') {
params = params.filter(function(arg) {
return arg.http && (arg.http.source == 'query' || arg.http.source == 'path');
});
postData = action.accepts.filter(function(arg) {
return params.indexOf(arg) == -1;
});
}
-%>
* @param {Object=} parameters Request parameters.
<% if (params.length == 0) { -%>
*
* This method does not accept any parameters.
* Supply an empty object or omit this argument altogether.
<% } else { params.forEach(function(arg) { -%>
*
* - `<%- arg.arg %>` – `{<%- getJsDocType(arg) %>}` - <%-
(arg.description || '').replace(/\n/g, '\n * ') %>
<% }); } -%>
<% if (modelName === 'User' && methodName === 'login') { -%>
*
* - `rememberMe` - `boolean` - Whether the authentication credentials
* should be remembered in localStorage across app/browser restarts.
* Default: `true`.
<% } -%>
<% if (postData) { -%>
*
* @param {Object} postData Request data.
<% if (postData.length == 0) { -%>
*
* This method does not accept any data. Supply an empty object.
<% } else if (postData.length == 1 && postData[0].http &&
postData[0].http.source == 'body') { -%>
*
* This method expects a subset of model properties as request parameters.
<% } else {
postData.forEach(function(arg) { -%>
*
* - `<%- arg.arg %>` – `{<%- getJsDocType(arg) %>}` - <%-
(arg.description || '').replace(/\n/g, '\n * ') %>
<% });
}
} -%>
*
<% var returnType = action.isReturningArray() ? 'Array.<Object>': 'Object'; -%>
* @param {Function(<%- returnType %>, Object)=} successCb
* Success callback with two arguments: `value`, `responseHeaders`.
*
* @param {Function(Object)=} errorCb Error callback with one argument:
* `httpResponse`.
*
* @return {<%- returnType %>} An empty reference that will be
* populated with the actual data once the response is returned
* from the server.
*
<% if (!action.returns || action.returns.length == 0) { -%>
* This method returns no data.
<% } else if (action.returns[0].root) { -%>
<% if (action.returns[0].description) { -%>
* <%- action.returns[0].description
.replace(/\n/g, '\n * ').trimRight() %>
<% } else { -%>
* <em>
* (The remote method definition does not provide any description.
* This usually means the response is a `<%- responseModelName || modelName %>` object.)
* </em>
<% } -%>
<% } else { -%>
* Data properties:
<% action.returns.forEach(function(arg) { -%>
*
* - `<%- arg.arg %>` – `{<%- getJsDocType(arg) %>}` - <%-
(arg.description || '').replace(/\n/g, '\n * ') %>
<% });
}
-%>
*/
<% } // end of ngdocForMethod -%>
Loading