diff --git a/lib/services.js b/lib/services.js index fc5a72b..2c36624 100644 --- a/lib/services.js +++ b/lib/services.js @@ -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); @@ -17,23 +18,45 @@ 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'), @@ -41,13 +64,13 @@ module.exports = function generateServices(app, ngModuleName, apiUrl) { ); 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; @@ -99,6 +122,10 @@ function describeModels(app) { buildScopes(result); + if (options.includeSchema) { + buildSchemas(result, app); + } + return result; } @@ -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, + }; + } +} diff --git a/lib/services.template.ejs b/lib/services.template.ejs index 28e2ea0..dbe16e5 100644 --- a/lib/services.template.ejs +++ b/lib/services.template.ejs @@ -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) -%>; +<% } -%> + return R; }]); diff --git a/test.e2e/spec/services.spec.js b/test.e2e/spec/services.spec.js index 0a00e19..14ab31a 100644 --- a/test.e2e/spec/services.spec.js +++ b/test.e2e/spec/services.spec.js @@ -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() { diff --git a/test.e2e/test-server.js b/test.e2e/test-server.js index 57d6784..0b74ea6 100644 --- a/test.e2e/test-server.js +++ b/test.e2e/test-server.js @@ -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) @@ -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.");';