Skip to content

Commit c8cb9ae

Browse files
committed
Merge pull request strongloop#223 from strongloop/feature/include-schema
Describe model schema in generated $resource
2 parents 279c810 + 61b3641 commit c8cb9ae

File tree

4 files changed

+119
-9
lines changed

4 files changed

+119
-9
lines changed

Diff for: lib/services.js

+57-8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
var fs = require('fs');
77
var ejs = require('ejs');
8+
var extend = require('util')._extend;
89

910
ejs.filters.q = function(obj) {
1011
return JSON.stringify(obj, null, 2);
@@ -17,37 +18,59 @@ ejs.filters.q = function(obj) {
1718
* var generateServices = require('loopback-sdk-angular').services;
1819
* var app = require('./server/server');
1920
*
20-
* var client = generateServices(app, 'lbServices', '/api');
21+
* var client = generateServices(app, {
22+
* ngModuleName: 'lbServices',
23+
* apiUrl: '/api'
24+
* });
2125
* require('fs').writeFileSync('client/loopback.js', client, 'utf-8');
2226
* ```
2327
*
28+
* To preserve backwards compatibility, the three-arg variant is still
29+
* supported:
30+
*
31+
* ```js
32+
* var client = generateServices(app, 'lbServices', '/api');
33+
* ```
34+
*
2435
* @param {Object} app The loopback application created via `app = loopback()`.
36+
* @options {Object} options
2537
* @param {string=} ngModuleName A name for the generated Angular module.
2638
* Default: `lbServices`.
2739
* @param {string=} apiUrl The URL where the client can access the LoopBack
2840
* server app. Default: `/`.
41+
* @param {Boolean} includeSchema Include model definition.
2942
* @returns {string} The generated javascript code.
3043
* @header generateServices
3144
*/
32-
module.exports = function generateServices(app, ngModuleName, apiUrl) {
33-
ngModuleName = ngModuleName || 'lbServices';
34-
apiUrl = apiUrl || '/';
45+
module.exports = function generateServices(app, options) {
46+
if (typeof options === 'string') {
47+
// legacy API: generateServices(app, ngModuleName, apiUrl)
48+
options = {
49+
ngModuleName: arguments[1],
50+
apiUrl: arguments[2],
51+
};
52+
}
3553

36-
var models = describeModels(app);
54+
options = extend({
55+
ngModuleName: 'lbServices',
56+
apiUrl: '/',
57+
}, options);
58+
59+
var models = describeModels(app, options);
3760

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

4366
return ejs.render(servicesTemplate, {
44-
moduleName: ngModuleName,
67+
moduleName: options.ngModuleName,
4568
models: models,
46-
urlBase: apiUrl.replace(/\/+$/, ''),
69+
urlBase: options.apiUrl.replace(/\/+$/, ''),
4770
});
4871
};
4972

50-
function describeModels(app) {
73+
function describeModels(app, options) {
5174
var result = {};
5275
app.handler('rest').adapter.getClasses().forEach(function(c) {
5376
var name = c.name;
@@ -99,6 +122,10 @@ function describeModels(app) {
99122

100123
buildScopes(result);
101124

125+
if (options.includeSchema) {
126+
buildSchemas(result, app);
127+
}
128+
102129
return result;
103130
}
104131

@@ -227,3 +254,25 @@ function findModelByName(models, name) {
227254
return models[n];
228255
}
229256
}
257+
258+
function buildSchemas(models, app) {
259+
for (var modelName in models) {
260+
var modelProperties = app.models[modelName].definition.properties;
261+
var schema = {};
262+
for (var prop in modelProperties) { // eslint-disable-line one-var
263+
schema[prop] = extend({}, modelProperties[prop]);
264+
// normalize types - convert from ctor (function) to name (string)
265+
var type = schema[prop].type;
266+
if (typeof type === 'function') {
267+
type = type.modelName || type.name;
268+
}
269+
// TODO - handle array types
270+
schema[prop].type = type;
271+
}
272+
273+
models[modelName].modelSchema = {
274+
name: modelName,
275+
properties: schema,
276+
};
277+
}
278+
}

Diff for: lib/services.template.ejs

+11
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,17 @@ if (typeof module !== 'undefined' && typeof exports !== 'undefined' &&
285285
<% }); // forEach methods name -%>
286286
<% } // for each scope -%>
287287
288+
<% if (meta.modelSchema) { -%>
289+
/**
290+
* @ngdoc object
291+
* @name <%-: moduleName %>.<%- modelName %>#schema
292+
* @propertyOf <%-: moduleName %>.<%- modelName %>
293+
* @description
294+
* The schema of the model represented by this $resource
295+
*/
296+
R.schema = <%- JSON.stringify(meta.modelSchema, null, 2) -%>;
297+
<% } -%>
298+
288299
return R;
289300
}]);
290301

Diff for: test.e2e/spec/services.spec.js

+39
Original file line numberDiff line numberDiff line change
@@ -971,6 +971,45 @@ define(['angular', 'given', 'util'], function(angular, given, util) {
971971
});
972972
});
973973

974+
describe('$resource generated with includeSchema:true', function() {
975+
var $injector;
976+
before(function() {
977+
return given.servicesForLoopBackApp(
978+
{
979+
models: {
980+
Product: {
981+
properties: {
982+
name: 'string',
983+
price: { type: 'number' },
984+
},
985+
},
986+
},
987+
includeSchema: true,
988+
})
989+
.then(function(createInjector) {
990+
$injector = createInjector();
991+
});
992+
});
993+
994+
it('has "schema" property with normalized LDL', function() {
995+
var Product = $injector.get('Product');
996+
var methodNames = Object.keys(Product);
997+
expect(methodNames).to.include.members(['schema']);
998+
var schema = Product.schema;
999+
expect(schema).to.have.property('name', 'Product');
1000+
expect(schema).to.have.property('properties');
1001+
console.log('schema properties', schema.properties);
1002+
expect(schema.properties).to.eql({
1003+
// "name: 'string'" was converted to full schema object
1004+
name: { type: 'String' },
1005+
// Type "number" was normalized to "Number"
1006+
price: { type: 'Number' },
1007+
// auto-injected id property
1008+
id: { id: 1, generated: true, type: 'Number' },
1009+
});
1010+
});
1011+
});
1012+
9741013
describe('for models with belongsTo relation', function() {
9751014
var $injector, Town, Country, testData;
9761015
before(function() {

Diff for: test.e2e/test-server.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ masterApp.post('/setup', function(req, res, next) {
6767
var name = opts.name;
6868
var models = opts.models;
6969
var enableAuth = opts.enableAuth;
70+
var includeSchema = opts.includeSchema;
7071
var setupFn = compileSetupFn(name, opts.setupFn);
7172

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

108109
try {
109-
servicesScript = generator.services(lbApp, name, apiUrl);
110+
if (includeSchema) {
111+
// the new options-based API
112+
servicesScript = generator.services(lbApp, {
113+
ngModuleName: name,
114+
apiUrl: apiUrl,
115+
includeSchema: includeSchema,
116+
});
117+
} else {
118+
// the old API, test it to verify backwards compatibility
119+
servicesScript = generator.services(lbApp, name, apiUrl);
120+
}
110121
} catch (err) {
111122
console.error('Cannot generate services script:', err.stack);
112123
servicesScript = 'throw new Error("Error generating services script.");';

0 commit comments

Comments
 (0)