Skip to content

Describe model schema in generated $resource #223

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 2, 2016
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
65 changes: 57 additions & 8 deletions lib/services.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

var fs = require('fs');
var ejs = require('ejs');
var extend = require('util')._extend;

ejs.filters.q = function(obj) {
return JSON.stringify(obj, null, 2);
Expand All @@ -17,37 +18,59 @@ ejs.filters.q = function(obj) {
* var generateServices = require('loopback-sdk-angular').services;
* var app = require('./server/server');
*
* var client = generateServices(app, 'lbServices', '/api');
* var client = generateServices(app, {
* ngModuleName: 'lbServices',
* apiUrl: '/api'
* });
* require('fs').writeFileSync('client/loopback.js', client, 'utf-8');
* ```
*
* To preserve backwards compatibility, the three-arg variant is still
* supported:
*
* ```js
* var client = generateServices(app, 'lbServices', '/api');
* ```
*
* @param {Object} app The loopback application created via `app = loopback()`.
* @options {Object} options
* @param {string=} ngModuleName A name for the generated Angular module.
* Default: `lbServices`.
* @param {string=} apiUrl The URL where the client can access the LoopBack
* server app. Default: `/`.
* @param {Boolean} includeSchema Include model definition.
* @returns {string} The generated javascript code.
* @header generateServices
*/
module.exports = function generateServices(app, ngModuleName, apiUrl) {
ngModuleName = ngModuleName || 'lbServices';
apiUrl = apiUrl || '/';
module.exports = function generateServices(app, options) {
if (typeof options === 'string') {
// legacy API: generateServices(app, ngModuleName, apiUrl)
options = {
ngModuleName: arguments[1],
apiUrl: arguments[2],
};
}

var models = describeModels(app);
options = extend({
ngModuleName: 'lbServices',
apiUrl: '/',
}, options);

var models = describeModels(app, options);

var servicesTemplate = fs.readFileSync(
require.resolve('./services.template.ejs'),
{ encoding: 'utf-8' }
);

return ejs.render(servicesTemplate, {
moduleName: ngModuleName,
moduleName: options.ngModuleName,
models: models,
urlBase: apiUrl.replace(/\/+$/, ''),
urlBase: options.apiUrl.replace(/\/+$/, ''),
});
};

function describeModels(app) {
function describeModels(app, options) {
var result = {};
app.handler('rest').adapter.getClasses().forEach(function(c) {
var name = c.name;
Expand Down Expand Up @@ -99,6 +122,10 @@ function describeModels(app) {

buildScopes(result);

if (options.includeSchema) {
buildSchemas(result, app);
}

return result;
}

Expand Down Expand Up @@ -227,3 +254,25 @@ function findModelByName(models, name) {
return models[n];
}
}

function buildSchemas(models, app) {
for (var modelName in models) {
var modelProperties = app.models[modelName].definition.properties;
var schema = {};
for (var prop in modelProperties) { // eslint-disable-line one-var
schema[prop] = extend({}, modelProperties[prop]);
// normalize types - convert from ctor (function) to name (string)
var type = schema[prop].type;
if (typeof type === 'function') {
type = type.modelName || type.name;
}
// TODO - handle array types
schema[prop].type = type;
}

models[modelName].modelSchema = {
name: modelName,
properties: schema,
};
}
}
11 changes: 11 additions & 0 deletions lib/services.template.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,17 @@ if (typeof module !== 'undefined' && typeof exports !== 'undefined' &&
<% }); // forEach methods name -%>
<% } // for each scope -%>

<% if (meta.modelSchema) { -%>
/**
* @ngdoc object
* @name <%-: moduleName %>.<%- modelName %>#schema
* @propertyOf <%-: moduleName %>.<%- modelName %>
* @description
* The schema of the model represented by this $resource
*/
R.schema = <%- JSON.stringify(meta.modelSchema, null, 2) -%>;
Copy link
Contributor

Choose a reason for hiding this comment

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

would it be better to use <%-: meta.modelSchema | q %>?

Copy link
Member Author

Choose a reason for hiding this comment

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

good catch, will apply your proposal.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, on the second thought, I think it's better to explicitly call JSON.stringify here. The intention of the q filter is to produce a quoted string. The fact that it calls JSON.stringify under the hood is an implementation detail that we should not rely on, as it may change in the future (in theory).

Copy link
Contributor

Choose a reason for hiding this comment

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

agreed, make sense

<% } -%>

return R;
}]);

Expand Down
39 changes: 39 additions & 0 deletions test.e2e/spec/services.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,45 @@ define(['angular', 'given', 'util'], function(angular, given, util) {
});
});

describe('$resource generated with includeSchema:true', function() {
var $injector;
before(function() {
return given.servicesForLoopBackApp(
{
models: {
Product: {
properties: {
name: 'string',
price: { type: 'number' },
},
},
},
includeSchema: true,
})
.then(function(createInjector) {
$injector = createInjector();
});
});

it('has "schema" property with normalized LDL', function() {
var Product = $injector.get('Product');
var methodNames = Object.keys(Product);
expect(methodNames).to.include.members(['schema']);
var schema = Product.schema;
expect(schema).to.have.property('name', 'Product');
expect(schema).to.have.property('properties');
console.log('schema properties', schema.properties);
expect(schema.properties).to.eql({
// "name: 'string'" was converted to full schema object
name: { type: 'String' },
// Type "number" was normalized to "Number"
price: { type: 'Number' },
// auto-injected id property
id: { id: 1, generated: true, type: 'Number' },
});
});
});

describe('for models with belongsTo relation', function() {
var $injector, Town, Country, testData;
before(function() {
Expand Down
13 changes: 12 additions & 1 deletion test.e2e/test-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ masterApp.post('/setup', function(req, res, next) {
var name = opts.name;
var models = opts.models;
var enableAuth = opts.enableAuth;
var includeSchema = opts.includeSchema;
var setupFn = compileSetupFn(name, opts.setupFn);

if (!name)
Expand Down Expand Up @@ -106,7 +107,17 @@ masterApp.post('/setup', function(req, res, next) {
}

try {
servicesScript = generator.services(lbApp, name, apiUrl);
if (includeSchema) {
// the new options-based API
servicesScript = generator.services(lbApp, {
ngModuleName: name,
apiUrl: apiUrl,
includeSchema: includeSchema,
});
} else {
// the old API, test it to verify backwards compatibility
servicesScript = generator.services(lbApp, name, apiUrl);
}
} catch (err) {
console.error('Cannot generate services script:', err.stack);
servicesScript = 'throw new Error("Error generating services script.");';
Expand Down