loopback-sdk-builder
loopback-sdk-builder copied to clipboard
Missing types on remote methods
What type of issue are you creating?
- [ ] Bug
- [ ] Enhancement
- [ * ] Question
What version of this module are you using?
- [ ] 2.0.10 (Stable)
- [ ] 2.1.0-rc.n (2.1 Release Candidate n)
- [ * ] Other
Write other if any: 2.3.1
Please add a description for your issue:
I have tried several but did not succeed on getting the right types build from the SDK for remote methods. The way I declare them is like this:
User.remoteMethod("getBook", {
accepts: [{ arg: "options", type: "object", http: "optionsFromRequest" }],
returns: { arg: "data", type: "Book", root: true, description:"BusinessService" },
http: { path: "/getBook", verb: "post" }
});
But the result never has the type Book, it is always type any
public updateLastSeen(customHeaders?: Function): Observable<any> {
When the expected result should be:
public updateLastSeen(customHeaders?: Function): Observable<Book> {
And similar behavior for arrays.
This is kind of similar to 331 and 475 but I could not get find my answer there.
After looking at it for a while I notice in ./loopback-sdk-builder/lib/angular2/index.js
function buildObservableType(model, method) {
let type = 'any';
if (
method.name.match(/(^createMany$|^find)/g) ||
(
typeof method.returns === 'object' &&
(String(method.returns.type).toLowerCase() === 'array' || Array.isArray(method.returns.type))
)
) type = `${model.name}[]`;
if (method.name.match(/(^create$|upsert|^findBy|^findOne$)/g)) type = model.name;
return type;
}
Only adds the type for certain methods.
Is there something I am doing wrong when I defined the remote methods? If so why does it happens too that the return type is any when you have a relation for example User model has many Something, and then try to access the related model (Something) from the user.
After playing with it during some time, I think I have a solution that might be helpful. This is my new buildObservableType function
function buildObservableType(model, method) {
let type = 'any';
if (
method.name.match(/(^createMany$|^find)/g) ||
(
typeof method.returns === 'object' &&
(String(method.returns.type).toLowerCase() === 'array' || Array.isArray(method.returns.type))
)
) type = `${model.name}[]`;
if (method.name.match(/(^create$|upsert|^findBy|^findOne$)/g)) type = model.name;
if(type=='any' && method.returns && method.returns[0]){
type=method.returns[0].type;
type=method.isReturningArray() ? `${type}[]`: type;
console.log(`Warning Custom Type Applied for ${method.name}:${type}`)
}
return type;
}
It fixes all my problems, hopefully it will help someone
Since there might be some imports needed depending on the return type, function buildServiceImports
needs to be updated as well.
Here is a final copy of my ./loopback-sdk-builder/lib/angular2/index.js
in case it helps anyone:
/**
* @module Angular 2 Generator for loopback-sdk-builder
* @author Jonathan Casarrubias <@johncasarrubias> <github:jonathan-casarrubias>
* @license MIT
* @description
* Defines a SDK Schema and builds according configuration
*/
var fs = require('fs');
var path = require('path');
var mkdirp = require('mkdirp');
var rmdir = require('rimraf');
var ejs = require('ejs');
var utils = require('../utils');
var _ = require('underscore');
_.mixin(require('underscore.inflections'));
/**
* EJS Q Filter
* Deprecated in EJS 2 :(
*/
ejs.filters.q = obj => JSON.stringify(obj, null, 2);
ejs.filters.pluralize = text => _.pluralize(text);
/**
* Generate Client SDK for the given loopback application.
*/
module.exports = function generate(ctx) {
'use strict';
// Describe models and remove those blacklisted
ctx.models = utils.describeModels(ctx.app);
/**
* Directory Management
*/
ctx.outputFolder = path.resolve(ctx.outputFolder);
if (!ctx.quiet) {
console.log('Removing base directory %s', ctx.outputFolder);
}
rmdir.sync(ctx.outputFolder);
// Create required directories
let directories = [
ctx.outputFolder,
ctx.outputFolder + '/models',
ctx.outputFolder + '/services/core',
ctx.outputFolder + '/services/custom',
ctx.outputFolder + '/storage'
];
if (ctx.isIo === 'enabled') directories.push(ctx.outputFolder + '/sockets');
if (ctx.isNgrx === 'enabled' || ctx.isNgrx === 'orm') {
directories.push(ctx.outputFolder + '/actions');
directories.push(ctx.outputFolder + '/effects');
directories.push(ctx.outputFolder + '/guards');
directories.push(ctx.outputFolder + '/reducers');
directories.push(ctx.outputFolder + '/resolvers');
}
if (ctx.isNgrx === 'orm') {
directories.push(ctx.outputFolder + '/orm');
directories.push(ctx.outputFolder + '/orm/models');
}
directories.forEach(directory => mkdirp.sync(directory));
/**
* Fix to decide which AcccessToken to get, since usually is private, but not
* Always, so we need to import from the right place
*/
ctx.loadAccessToken = ctx.models.AccessToken ? false : true;
if (!ctx.quiet) {
console.log('DRIVER: ', ctx.driver);
}
const throughModels = {};
if (ctx.isNgrx === 'orm') {
Object.keys(ctx.models).forEach(modelName => {
for (const rel in ctx.models[modelName].sharedClass.ctor.relations) {
if (
ctx.models[modelName].sharedClass.ctor.relations[rel].modelThrough
) {
const throughModel =
ctx.models[modelName].sharedClass.ctor.relations[rel].modelThrough
.definition.name;
if (
typeof ctx.models[throughModel] === 'undefined' &&
!throughModels.hasOwnProperty(throughModel)
) {
throughModels[throughModel] = {
from: modelName,
to:
ctx.models[modelName].sharedClass.ctor.relations[rel]
.targetClass
};
}
}
}
});
}
/**
* LoopBack SDK Builder Schema for Angular 2 and ng2native 2
**/
let schema = [
/**
* SDK INDEXES
*/
{
template: './shared/index.ejs',
output: '/index.ts',
params: {
isIo: ctx.isIo,
isNgrx: ctx.isNgrx,
models: ctx.models,
driver: ctx.driver,
buildModuleImports,
buildNgModuleImports,
buildNgProviders
}
},
{
template: './shared/models/index.ejs',
output: '/models/index.ts',
params: {
isIo: ctx.isIo,
models: Object.assign({}, ctx.models, throughModels)
}
},
{
template: './shared/services/index.ejs',
output: '/services/index.ts',
params: {}
},
{
template: './shared/services/custom/index.ejs',
output: '/services/custom/index.ts',
params: { models: ctx.models }
},
{
template: './shared/services/core/index.ejs',
output: '/services/core/index.ts',
params: { isIo: ctx.isIo }
},
/**
* MODEL LIST SERVICES
*/
{
template: './shared/services/custom/models.ejs',
output: '/services/custom/SDKModels.ts',
params: { models: ctx.models }
},
/**
* SDK CONFIG
*/
{
template: './shared/config.ejs',
output: '/lb.config.ts',
params: { app: ctx.app }
},
/**
* SDK STATIC BASE AND CORE FILES
*/
{
template: './shared/models/base.ejs',
output: '/models/BaseModels.ts',
params: {
loadAccessToken: ctx.loadAccessToken,
isNgrx: ctx.isNgrx,
buildServiceDI
}
},
{
template: './shared/services/core/auth.ts',
output: '/services/core/auth.service.ts',
params: { loadAccessToken: ctx.loadAccessToken }
},
{
template: './shared/services/core/base.ejs',
output: '/services/core/base.service.ts',
params: {
isIo: ctx.isIo,
buildServiceDI,
buildBaseServiceImports
}
},
{
template: './shared/services/core/error.ejs',
output: '/services/core/error.service.ts',
params: {}
},
{
template: './shared/services/core/logger.ejs',
output: '/services/custom/logger.service.ts',
params: {}
},
/**
* STORAGE
*/
{
template: './shared/storage/storage.swaps.ts',
output: '/storage/storage.swaps.ts',
params: {}
}
];
// Add Browser Specific Code
if (ctx.driver.match(/ng2web|ng2universal/)) {
schema.push({
template: './shared/storage/cookie.browser.ts',
output: '/storage/cookie.browser.ts',
params: {}
});
schema.push({
template: './shared/storage/storage.browser.ts',
output: '/storage/storage.browser.ts',
params: {}
});
}
// Add Server Specific Code
if (ctx.driver === 'ng2universal') {
schema.push({
template: './shared/storage/cookie.node.ts',
output: '/storage/cookie.node.ts',
params: {}
});
}
// Add NativeScript Specific Code
if (ctx.driver === 'ng2native') {
schema.push({
template: './shared/storage/storage.native.ts',
output: '/storage/storage.native.ts',
params: {}
});
}
/**
* NGRX SUPPORT
*/
if (ctx.isNgrx === 'enabled' || ctx.isNgrx === 'orm') {
schema = schema.concat([
/**
* NGRX Utils
*/
{
template: './shared/state.ejs',
output: '/state.ts',
params: { models: ctx.models, throughModels }
},
{
template: './shared/util.ejs',
output: '/util.ts',
params: {}
},
/**
* NGRX Actions
*/
{
template: './shared/actions/auth.ejs',
output: '/actions/auth.ts',
params: {}
},
{
template: './shared/actions/base.ejs',
output: '/actions/base.ts',
params: {}
},
{
template: './shared/actions/error.ejs',
output: '/actions/error.ts',
params: {}
},
{
template: './shared/actions/index.ejs',
output: '/actions/index.ts',
params: { models: Object.assign({}, ctx.models, throughModels) }
},
/**
* NGRX Effects
*/
{
template: './shared/effects/auth.ejs',
output: '/effects/auth.ts',
params: {
userModelName: getUserModelName()
}
},
{
template: './shared/effects/base.ejs',
output: '/effects/base.ts',
params: {}
},
{
template: './shared/effects/resolver.ejs',
output: '/effects/resolver.ts',
params: {}
},
/**
* NGRX Guards
*/
{
template: './shared/guards/index.ejs',
output: '/guards/index.ts',
params: { models: ctx.models }
},
{
template: './shared/guards/auth.guard.ejs',
output: '/guards/auth.guard.ts',
params: {}
},
/**
* NGRX Reducers
*/
{
template: './shared/reducers/auth.ejs',
output: '/reducers/auth.ts',
params: {
userModelName: getUserModelName()
}
},
{
template: './shared/reducers/index.ejs',
output: '/reducers/index.ts',
params: {
models: Object.assign({}, ctx.models, throughModels),
isNgrx: ctx.isNgrx
}
},
{
template: './shared/reducers/base.ejs',
output: '/reducers/base.ts',
params: {}
},
{
template: './shared/reducers/baseThrough.ejs',
output: '/reducers/baseThrough.ts',
params: {}
},
/**
* NGRX Resolvers
*/
{
template: './shared/resolvers/auth-account.ejs',
output: '/resolvers/auth-account.ts',
params: {
userModelName: getUserModelName()
}
},
{
template: './shared/resolvers/index.ejs',
output: '/resolvers/index.ts',
params: {}
}
]);
}
if (ctx.isNgrx === 'orm') {
/**
* NGRX Orm
*/
schema = schema.concat([
{
template: './shared/orm/orm.ejs',
output: '/orm/orm.ts',
params: {
isIo: ctx.isIo,
models: ctx.models
}
},
{
template: './shared/orm/filter.ejs',
output: '/orm/filter.ts',
params: {}
},
{
template: './shared/orm/base.ejs',
output: '/orm/base.ts',
params: { isIo: ctx.isIo }
},
{
template: './shared/orm/index.ejs',
output: '/orm/index.ts',
params: {}
},
{
template: './shared/orm/models/index.ejs',
output: '/orm/models/index.ts',
params: { models: ctx.models }
}
]);
}
if (ctx.isNgrx === 'orm' && ctx.isIo === 'enabled') {
schema = schema.concat([
{
template: './shared/orm/io.ejs',
output: '/orm/io.ts',
params: {}
}
]);
}
/**
* REALTIME MODULE SUPPORT
*/
if (ctx.isIo === 'enabled') {
// Add generic code to any environment
schema = schema.concat([
{
template: './shared/sockets/connections.ts',
output: '/sockets/socket.connections.ts',
params: {}
},
{
template: './shared/sockets/socket.driver.ts',
output: '/sockets/socket.driver.ts',
params: {}
},
{
template: './shared/services/core/io.ejs',
output: '/services/core/io.service.ts',
params: {}
},
{
template: './shared/services/core/realtime.ts',
output: '/services/core/real.time.ts',
params: {}
},
{
template: './shared/models/fireloop.ejs',
output: '/models/FireLoop.ts',
params: {}
},
{
template: './shared/models/flref.ts',
output: '/models/FireLoopRef.ts',
params: {}
}
]);
// Add Browser Specific Code
if (ctx.driver.match(/ng2web|ng2universal/)) {
schema.push({
template: './shared/sockets/socket.browser.ts',
output: '/sockets/socket.browser.ts',
params: {}
});
}
// Add Server Specific Code
if (ctx.driver === 'ng2universal') {
schema.push({
template: './shared/sockets/socket.node.ts',
output: '/sockets/socket.node.ts',
params: {}
});
}
// Add NativeScript Specific Code
if (ctx.driver === 'ng2native') {
schema.push({
template: './shared/sockets/socket.native.ts',
output: '/sockets/socket.native.ts',
params: {}
});
}
}
/**
* SDK DYNAMIC FILES
*/
Object.keys(ctx.models).forEach(modelName => {
if (
ctx.models[modelName].sharedClass.ctor.settings.sdk &&
!ctx.models[modelName].sharedClass.ctor.settings.sdk.enabled
) {
if (!ctx.quiet) {
console.warn('LoopBack SDK Builder: %s model was ignored', modelName);
}
return;
} else {
if (!ctx.quiet) {
console.info('LoopBack SDK Builder: adding %s model to SDK', modelName);
}
schema.push(
/**
* SDK MODELS
*/
{
template: './shared/models/model.ejs',
output: '/models/' + capitalize(modelName) + '.ts',
params: {
model: ctx.models[modelName],
modelName: modelName,
plural:
ctx.models[modelName].sharedClass.ctor.settings.plural ||
ejs.filters.pluralize(modelName),
path:
ctx.models[modelName].sharedClass.ctor.settings.http &&
ctx.models[modelName].sharedClass.ctor.settings.http.path
? ctx.models[modelName].sharedClass.ctor.settings.http.path
: ctx.models[modelName].sharedClass.ctor.settings.plural,
buildPropertyType: buildPropertyType,
buildPropertyDefaultValue: buildPropertyDefaultValue,
buildRelationType: buildRelationType,
buildModelImports,
buildModelProperties,
capitalize
}
}
);
/**
* SDK CUSTOM SERVICES
*/
if (
ctx.fireloopOnly === 'disabled' ||
ctx.models[modelName].sharedClass.ctor.settings.base === 'User'
) {
schema.push({
template: './shared/services/custom/service.ejs',
output: '/services/custom/' + capitalize(modelName) + '.ts',
params: {
isIo: ctx.isIo,
model: ctx.models[modelName],
modelName: modelName,
moduleName: ctx.moduleName,
loadAccessToken: ctx.loadAccessToken,
buildPostBody,
buildUrlParams,
buildRouteParams,
buildMethodParams,
buildPayloadParams,
buildServiceDI,
buildServiceImports,
normalizeMethodName,
buildObservableType,
paramIsContext,
paramIsFunction
}
});
}
/**
* SDK NGRX ACTIONS, EFFECTS, REDUCERS, GUARDS, ORM
*/
if (ctx.isNgrx === 'enabled' || ctx.isNgrx === 'orm') {
schema.push({
template: './shared/actions/model.ejs',
output: '/actions/' + capitalize(modelName) + '.ts',
params: {
isIo: ctx.isIo,
model: ctx.models[modelName],
modelName: modelName,
moduleName: ctx.moduleName,
loadAccessToken: ctx.loadAccessToken,
buildUrlParams,
buildRouteParams,
buildMethodParams,
buildPayloadParams,
buildPayloadParamsWithoutTypes,
buildServiceDI,
buildServiceImports,
normalizeMethodName,
upperCasedMethodName,
buildObservableType,
paramIsContext,
paramIsFunction
}
});
schema.push({
template: './shared/effects/model.ejs',
output: '/effects/' + capitalize(modelName) + '.ts',
params: {
isIo: ctx.isIo,
model: ctx.models[modelName],
modelName: modelName,
moduleName: ctx.moduleName,
loadAccessToken: ctx.loadAccessToken,
buildUrlParams,
buildRouteParams,
buildMethodParams,
buildMethodParamsFromPayload,
buildServiceDI,
buildServiceImports,
normalizeMethodName,
upperCasedMethodName,
buildObservableType,
paramIsContext,
paramIsFunction,
capitalize
}
});
schema.push({
template: './shared/reducers/model.ejs',
output: '/reducers/' + capitalize(modelName) + '.ts',
params: {
isIo: ctx.isIo,
model: ctx.models[modelName],
modelName: modelName,
moduleName: ctx.moduleName,
loadAccessToken: ctx.loadAccessToken,
buildUrlParams,
buildRouteParams,
buildMethodParams,
buildMethodParamsFromPayload,
buildServiceDI,
buildServiceImports,
normalizeMethodName,
upperCasedMethodName,
buildObservableType,
paramIsContext,
paramIsFunction
}
});
schema.push({
template: './shared/guards/model.ejs',
output: '/guards/' + capitalize(modelName) + '.ts',
params: {
modelName: modelName
}
});
}
if (ctx.isNgrx === 'orm') {
schema.push({
template: './shared/orm/models/model.ejs',
output: '/orm/models/' + capitalize(modelName) + '.ts',
params: {
isIo: ctx.isIo,
model: ctx.models[modelName],
modelName: modelName,
moduleName: ctx.moduleName,
loadAccessToken: ctx.loadAccessToken,
buildUrlParams,
buildRouteParams,
buildMethodParams,
buildPayloadParams,
buildPayloadParamsWithoutTypes,
buildServiceDI,
buildServiceImports,
normalizeMethodName,
upperCasedMethodName,
buildObservableType,
paramIsContext,
paramIsFunction
}
});
}
}
});
/**
* THROUGH MODELS
*/
if (ctx.isNgrx === 'orm') {
Object.keys(throughModels).forEach(modelName => {
schema.push({
template: './shared/models/throughModel.ejs',
output: '/models/' + capitalize(modelName) + '.ts',
params: {
model: throughModels[modelName],
modelName: modelName[0].toUpperCase() + modelName.slice(1)
}
});
schema.push({
template: './shared/actions/throughModel.ejs',
output: '/actions/' + capitalize(modelName) + '.ts',
params: {
modelName: modelName[0].toUpperCase() + modelName.slice(1)
}
});
schema.push({
template: './shared/reducers/throughModel.ejs',
output: '/reducers/' + capitalize(modelName) + '.ts',
params: {
modelName: modelName[0].toUpperCase() + modelName.slice(1)
}
});
});
}
/**
* PROCESS SCHEMA
*/
schema.forEach(config => {
if (!ctx.quiet) {
console.info('Generating: %s', `${ctx.outputFolder}${config.output}`);
}
fs.writeFileSync(
`${ctx.outputFolder}${config.output}`,
ejs.render(
fs.readFileSync(require.resolve(config.template), {
encoding: 'utf-8'
}),
config.params
)
);
});
/**
* @method getUserModelName
* @description
* Discovers User model name
*/
function getUserModelName() {
let userModelName = '';
Object.keys(ctx.models).forEach(modelName => {
if (
ctx.models[modelName].sharedClass.ctor.settings.base === 'User' ||
ctx.models[modelName].sharedClass.ctor.settings.base === 'OAuthUser'
) {
userModelName = modelName;
}
});
return userModelName;
}
/**
* @method buildModelImports
* @description
* Define import statement for those model who are related to other scopes
*/
function buildModelImports(model) {
let relations = getModelRelations(model);
let loaded = {};
let modelName = capitalize(model.name);
loaded[modelName] = true;
let output = [];
if (relations.length > 0) {
relations.forEach((relationName, i) => {
let targetClass =
model.sharedClass.ctor.relations[relationName].targetClass;
if (!loaded[targetClass] && targetClass !== modelName) {
loaded[targetClass] = true;
output.push(` ${targetClass}`);
}
});
}
// Add GeoPoint custom type import
Object.keys(model.properties).forEach(property => {
var geoPointType = buildPropertyType(model.properties[property]);
var hasGeoPointType = geoPointType.indexOf('GeoPoint') !== -1;
if (hasGeoPointType) {
output.push(' GeoPoint');
}
});
if (output.length > 0) {
var imports = output.join(',\n');
output = ['import {', imports, "} from '../index';\n"];
}
return output.join('\n');
}
/**
* @method buildModelProperties
* @description
* Define properties for the given model
*/
function buildModelProperties(model, isInterface) {
let output = [];
// Work around to fix a LoopBack update that won't provide
// the password property anymore but is required for TypeScript purposes
if (model.isUser && !model.properties.password) {
model.properties.password = {
type: String
};
}
// Add Model Properties
Object.keys(model.properties).forEach(propertyName => {
if (model.isUser && propertyName === 'credentials') return;
let property = model.properties[propertyName];
let isOptional = isInterface && !property.required ? '?' : '';
let defaultValue = !isInterface
? ` = ${buildPropertyDefaultValue(property)}`
: '';
defaultValue =
ctx.defaultValue !== 'enabled' && ctx.defaultValue !== 'strict'
? ''
: defaultValue;
defaultValue =
ctx.defaultValue === 'strict' && !property.hasOwnProperty('default')
? ''
: defaultValue;
output.push(
` "${propertyName}"${isOptional}: ${buildPropertyType(
property
)}${defaultValue};`
);
});
// Add Model Relations
Object.keys(model.sharedClass.ctor.relations).forEach(relation => {
let relationType = buildRelationType(model, relation);
let defaultTypeValue =
!isInterface &&
ctx.defaultValue === 'enabled' &&
relationType.indexOf('Array') >= 0
? ' = []'
: '';
defaultTypeValue =
!isInterface &&
ctx.defaultValue === 'enabled' &&
relationType.indexOf('Array') === -1
? ' = null'
: defaultTypeValue;
output.push(
` ${relation}${
isInterface ? '?' : ''
}: ${relationType}${defaultTypeValue};`
);
});
return output.join('\n');
}
/**
* @method buildRelationType
* @description
* Discovers property type according related models that are public
*/
function buildRelationType(model, relationName) {
let relation = model.sharedClass.ctor.relations[relationName];
let targetClass = relation.targetClass;
let basicType = ctx.models[targetClass] ? targetClass : 'any';
let finalType = relation.type.match(/(hasOne|belongsTo)/g)
? basicType
: `${basicType}[]`;
return finalType;
}
/**
* @method buildObservableType
* @description
* Define observable type
*/
function buildObservableType(model, method) {
let type = 'any';
if (
method.name.match(/(^createMany$|^find)/g) ||
(
typeof method.returns === 'object' &&
(String(method.returns.type).toLowerCase() === 'array' || Array.isArray(method.returns.type))
)
) type = `${model.name}[]`;
if (method.name.match(/(^create$|upsert|^findBy|^findOne$)/g)) type = model.name;
if(type=='any' && method.returns && method.returns[0]){
type=method.returns[0].type;
type=method.isReturningArray() ? `${type}[]`: type;
console.log(`Warning Custom Type Applied for ${method.name}:${type}`)
}
return type;
}
/**
* @method buildServiceImports
* @description
* Define import statement for those model who are related to other scopes
* IMPORTANT: This method have a very specific flow, changing it may create
* multiple issues on multiple different use cases.
*/
function buildServiceImports(model, loadAccessToken, isIo) {
let modelName = capitalize(model.name);
let imports = [
{ module: 'Injectable, Inject, Optional', from: '@angular/core' },
{ module: 'HttpClient, HttpResponse', from: '@angular/common/http' },
{ module: 'SDKModels', from: './SDKModels' },
{ module: 'BaseLoopBackApi', from: '../core/base.service' },
{ module: 'LoopBackConfig', from: '../../lb.config' },
{ module: 'LoopBackAuth', from: '../core/auth.service' },
{
module: `LoopBackFilter, ${
model.isUser
? `SDKToken${
loadAccessToken && model.isUser ? ', AccessToken' : ''
}`
: ''
}`,
from: '../../models/BaseModels'
},
{ module: 'ErrorHandler', from: '../core/error.service' },
{ module: 'Subject', from: 'rxjs/Subject' },
{ module: 'Observable', from: 'rxjs/Observable' },
{ module: 'map', from: 'rxjs/operators' },
{ module: modelName, from: `../../models/${modelName}` }
];
if (isIo === 'enabled') {
imports.push({
module: 'SocketConnection',
from: '../../sockets/socket.connections'
});
}
let loaded = {};
loaded[modelName] = true;
getModelRelations(model).forEach((relationName, i) => {
let targetClass =
model.sharedClass.ctor.relations[relationName].targetClass;
// It is imperative to check first for through models, else we may miss some
// Through Models. This is because multiple relationships to the same model may have
// different Through models, in the next validation we avoid duplicating models, which
// can lead to miss some through models.
// Finally we will be adding through models only if these are Public
if (
model.sharedClass.ctor.relations[relationName].modelThrough &&
model.sharedClass.ctor.relations[relationName].modelThrough.sharedClass
.name !== 'Model' &&
ctx.models[
model.sharedClass.ctor.relations[relationName].modelThrough
.sharedClass.name
]
) {
let through = capitalize(
model.sharedClass.ctor.relations[relationName].modelThrough
.sharedClass.name
);
if (!loaded[through] && targetClass !== modelName) {
loaded[through] = true;
imports.push({ module: through, from: `../../models/${through}` });
}
}
// Now and after the through model was included is the right time to verify if the current model
// was loaded by another relationship, this way we don't duplicate the class during imports'
if (!loaded[targetClass] && targetClass !== modelName) {
loaded[targetClass] = true;
imports.push({
module: targetClass,
from: `../../models/${targetClass}`
});
}
});
model.methods.forEach(function(method) {
if (typeof method.returns === 'object' && method.returns && method.returns[0]) {
let type = method.returns[0].type
if(typeof type ==='string' && type.toLowerCase()!='any' &&
type.toLowerCase()!='object' && type.toLowerCase()!='string' &&
type.toLowerCase()!='number' && type.toLowerCase()!='date'){
if(imports.indexOf(t=>t.module==type)==-1){
imports.push({module:type,from:`../../models/${type}`})
}
}
}
})
return buildImports(imports);
}
/**
* @method buildModuleImports
* @description
* Define import statement for the SDK Module
*/
function buildModuleImports(models, isIo, driver) {
let imports = [
{ module: 'ErrorHandler', from: './services/core/error.service' },
{ module: 'LoopBackAuth', from: './services/core/auth.service' },
{ module: 'LoggerService', from: './services/custom/logger.service' },
{ module: 'SDKModels', from: './services/custom/SDKModels' },
{
module: 'InternalStorage, SDKStorage',
from: './storage/storage.swaps'
},
{ module: 'HttpClientModule', from: '@angular/common/http' },
{ module: 'CommonModule', from: '@angular/common' },
{ module: 'NgModule, ModuleWithProviders', from: '@angular/core' }
];
switch (driver) {
case 'ng2web':
imports.push({
module: 'CookieBrowser',
from: './storage/cookie.browser'
});
imports.push({
module: 'StorageBrowser',
from: './storage/storage.browser'
});
if (isIo === 'enabled') {
imports.push({
module: 'SocketBrowser',
from: './sockets/socket.browser'
});
}
break;
case 'ng2universal':
imports.push({
module: 'CookieBrowser',
from: './storage/cookie.browser'
});
imports.push({ module: 'CookieNode', from: './storage/cookie.node' });
imports.push({
module: 'StorageBrowser',
from: './storage/storage.browser'
});
if (isIo === 'enabled') {
imports.push({
module: 'SocketBrowser',
from: './sockets/socket.browser'
});
imports.push({ module: 'SocketNode', from: './sockets/socket.node' });
}
break;
case 'ng2native':
imports.push({
module: 'StorageNative',
from: './storage/storage.native'
});
imports.push({
module: 'NativeScriptHttpModule',
from: 'nativescript-angular/http'
});
if (isIo === 'enabled') {
imports.push({
module: 'SocketNative',
from: './sockets/socket.native'
});
}
break;
}
if (isIo === 'enabled') {
imports = imports.concat([
{ module: 'SocketDriver', from: './sockets/socket.driver' },
{ module: 'SocketConnection', from: './sockets/socket.connections' },
{ module: 'RealTime', from: './services/core/real.time' }
]);
}
Object.keys(models).forEach(modelName => {
let name = capitalize(modelName);
imports.push({ module: `${name}Api`, from: `./services/custom/${name}` });
});
return buildImports(imports);
}
/**
* @method buildNgModuleImports
* @description
* Define import statement for the SDK NG Modules
*/
function buildNgModuleImports(models, environment, isIo, driver) {
let imports = ['LoopBackAuth', 'LoggerService', 'SDKModels'];
if (isIo === 'enabled') {
imports.push('RealTime');
}
Object.keys(models).forEach(modelName =>
imports.push(`${capitalize(modelName)}Api`)
);
switch (environment) {
case 'browser':
if (driver === 'ng2web' || driver === 'ng2universal') {
imports.push('internalStorageProvider');
imports.push('{ provide: SDKStorage, useClass: StorageBrowser }');
if (isIo === 'enabled') {
imports.push('{ provide: SocketDriver, useClass: SocketBrowser }');
}
}
break;
case 'node':
if (driver === 'ng2universal') {
imports.push('{ provide: InternalStorage, useClass: CookieNode }');
if (isIo === 'enabled') {
imports.push('{ provide: SocketDriver, useClass: SocketNode }');
}
}
break;
case 'nativescript':
if (driver === 'ng2native') {
imports.push('{ provide: InternalStorage, useClass: StorageNative }');
imports.push('{ provide: SDKStorage, useClass: StorageNative }');
if (isIo === 'enabled') {
imports.push('{ provide: SocketDriver, useClass: SocketNative }');
}
}
break;
}
return imports.join(',\n ');
}
/**
* @method buildNgProviders
* @description
* Define import statement for the SDK NG Modules
*/
function buildNgProviders(isIo) {
let imports = ['ErrorHandler'];
if (isIo === 'enabled') {
imports.push('SocketConnection');
}
return imports.join(',\n ');
}
/**
* @method buildServiceDI
* @description
* Define import statement for the SDK NG Modules
*/
function buildServiceDI(isIo) {
let dependencies = ['@Inject(HttpClient) protected http: HttpClient'];
if (isIo === 'enabled') {
dependencies.push(
'@Inject(SocketConnection) protected connection: SocketConnection'
);
}
dependencies = dependencies.concat([
'@Inject(SDKModels) protected models: SDKModels',
'@Inject(LoopBackAuth) protected auth: LoopBackAuth',
'@Optional() @Inject(ErrorHandler) protected errorHandler: ErrorHandler'
]);
return dependencies.join(',\n ');
}
/**
* @method buildBaseServiceImports
* @description
* Define import statement for the SDK Module
**/
function buildBaseServiceImports(isIo) {
let imports = [
{ module: 'Injectable, Inject, Optional', from: '@angular/core' },
{
module:
'HttpClient, HttpHeaders, HttpRequest, HttpParams, HttpResponse, HttpParameterCodec',
from: '@angular/common/http'
},
{ module: 'NgModule, ModuleWithProviders', from: '@angular/core' },
{ module: 'ErrorHandler', from: './error.service' },
{ module: 'LoopBackAuth', from: './auth.service' },
{ module: 'LoopBackConfig', from: '../../lb.config' },
{
module: 'LoopBackFilter, AccessToken',
from: '../../models/BaseModels'
},
{ module: 'SDKModels', from: '../custom/SDKModels' },
{ module: 'Observable', from: 'rxjs/Observable' },
{ module: 'Subject', from: 'rxjs/Subject' },
{ module: 'ErrorObservable', from: 'rxjs/observable/ErrorObservable' },
{ module: 'catchError, map, filter', from: 'rxjs/operators' }
];
if (isIo === 'enabled') {
imports.push({
module: 'SocketConnection',
from: '../../sockets/socket.connections'
});
}
return buildImports(imports);
}
/**
* @method buildImports
* @description
* Transform an array of objects describing which should be imported into
* the actual template strings
*/
function buildImports(imports) {
return imports
.map(
item =>
`import ${item.from ? `{ ${item.module} }` : `'${item.module}'`}${
item.from ? ` from '${item.from}'` : ''
};`
)
.join('\n');
}
/**
* @method normalizeMethodName
* @description
* Normalizes method name from loopback form to a more human readable form
*/
function normalizeMethodName(methodName, capitalize) {
return methodName
.split('__')
.map((value, index) => {
return index < 2 && !capitalize
? value
: value.charAt(0).toUpperCase() + value.slice(1);
})
.join('');
}
/**
* @method upperCasedMethodName
* @description
* Upper cases method name from loopback form to a more human readable form
*/
function upperCasedMethodName(methodName) {
return methodName
.split('__')
.filter(value => value !== '')
.map((value, index) => {
if (index === 0) {
return value
.split(/(?=[A-Z])/)
.map(value => value.toUpperCase())
.join('_');
} else {
return value.toUpperCase();
}
})
.join('_');
}
/**
* @method buildMethodParams
* @description
* Set which params should be defined for the given remote method
*/
function buildMethodParams(model, methodName, params, isIo) {
//if (model.isUser && methodName === 'logout') return '';
let output = new Array();
let relations = getModelRelations(model);
let availableClasses = relations.map(
(relationName, index) =>
model.sharedClass.ctor.relations[relationName].targetClass
);
params = params.filter(param => {
return !paramIsContext(param) && !paramIsFunction(param);
});
relations.forEach(relationName => {
if (model.sharedClass.ctor.relations[relationName].modelThrough) {
let throughName = capitalize(
model.sharedClass.ctor.relations[relationName].modelThrough
.sharedClass.name
);
// Only add through models when they are Public
if (ctx.models[throughName]) {
availableClasses.push(
capitalize(
model.sharedClass.ctor.relations[relationName].modelThrough
.sharedClass.name
)
);
}
}
});
if (isIo)
params = params.filter(param => !param.arg.match(/(fk|data|options)/));
params.forEach((param, i, arr) => {
let type,
isArray = false;
if (param.type === 'object') {
type = param.arg === 'filter' ? 'LoopBackFilter' : 'any';
} else {
type =
param.type !== 'AccessToken' &&
param.type !== 'any' &&
param.type !== undefined
? capitalize(param.type)
: 'any';
}
if (
!type.match(/(^any$|LoopBackFilter)/) &&
availableClasses.indexOf(type) < 0
) {
type = 'any';
}
let value = '';
// Accept Array on createMany method.
if (methodName.match(/createMany/) && param.arg === 'data') {
isArray = true;
}
// Set default value, usually will be {}, but on login we include user
// Should not be undefined or will create request issues
if (
!param.required &&
(model.isUser && methodName === 'login') &&
param.arg === 'include'
) {
type = 'any';
value = " = 'user'";
} else if (type.match(/(any|LoopBackFilter)/)) {
value = !param.required ? ` = ${isArray ? '[]' : '{}'}` : '';
} else {
value = !param.required
? ` = ${isArray ? `new Array<${type}>()` : `new ${type}()`}`
: '';
}
output.push(`${param.arg}: ${type}${isArray ? '[]' : ''}${value}`);
});
// When login, there is a property not coming from LoopBack that is needed.
// so we need to add a rememberMe property to temporal o permanent store the user
if (model.isUser && methodName === 'login') {
output.push(`rememberMe: boolean = true`);
}
// When login, there is a property not coming from LoopBack that is needed.
// so we need to add a rememberMe property to temporal o permanent store the user
// UPDATE: it seems it is now coming from loopback, so now is duplicated
/*if ((model.isUser && methodName === 'login')) {
output.push(`rememberMe: boolean = true`);
}*/
if (!isIo) {
output.push(`customHeaders?: Function`);
}
return output.join(', ');
}
/**
* @method buildPayloadParams
* @description
* Set which params should be defined for the given remote method
*/
function buildPayloadParams(model, methodName, params, isIo) {
if (model.isUser && methodName === 'logout') return '';
let output = new Array();
let relations = getModelRelations(model);
let availableClasses = relations.map(
(relationName, index) =>
model.sharedClass.ctor.relations[relationName].targetClass
);
params = params.filter(param => {
return !paramIsContext(param) && !paramIsFunction(param);
});
relations.forEach(relationName => {
if (model.sharedClass.ctor.relations[relationName].modelThrough) {
let throughName = capitalize(
model.sharedClass.ctor.relations[relationName].modelThrough
.sharedClass.name
);
// Only add through models when they are Public
if (ctx.models[throughName]) {
availableClasses.push(
capitalize(
model.sharedClass.ctor.relations[relationName].modelThrough
.sharedClass.name
)
);
}
}
});
if (isIo)
params = params.filter(param => !param.arg.match(/(fk|data|options)/));
params.forEach((param, i, arr) => {
let type,
isArray = false;
if (param.type === 'object') {
type = param.arg === 'filter' ? 'LoopBackFilter' : 'any';
} else {
type =
param.type !== 'AccessToken' && param.type !== 'any'
? capitalize(param.type)
: 'any';
}
if (
!type.match(/(^any$|LoopBackFilter)/) &&
availableClasses.indexOf(type) < 0
) {
type = 'any';
}
let value = '';
// Accept Array on createMany method.
if (methodName.match(/createMany/) && param.arg === 'data') {
isArray = true;
}
output.push(`${param.arg}: ${type}${isArray ? '[]' : ''}${value}`);
});
if (!isIo) {
output.push(`customHeaders`);
}
return output.join(', ');
}
/**
* @method buildPayloadParamsWithoutTypes
* @description
* Set which params should be defined for the given remote method
*/
function buildPayloadParamsWithoutTypes(model, methodName, params, isIo) {
if (model.isUser && methodName === 'logout') return '';
let output = new Array();
let relations = getModelRelations(model);
let availableClasses = relations.map(
(relationName, index) =>
model.sharedClass.ctor.relations[relationName].targetClass
);
params = params.filter(param => {
return !paramIsContext(param) && !paramIsFunction(param);
});
relations.forEach(relationName => {
if (model.sharedClass.ctor.relations[relationName].modelThrough) {
let throughName = capitalize(
model.sharedClass.ctor.relations[relationName].modelThrough
.sharedClass.name
);
// Only add through models when they are Public
if (ctx.models[throughName]) {
availableClasses.push(
capitalize(
model.sharedClass.ctor.relations[relationName].modelThrough
.sharedClass.name
)
);
}
}
});
if (isIo)
params = params.filter(param => !param.arg.match(/(fk|data|options)/));
params.forEach((param, i, arr) => {
output.push(`${param.arg}`);
});
if (!isIo) {
output.push(`customHeaders`);
}
return output.join(', ');
}
/**
* @method buildMethodParamsFromPayload
* @description
* Set which params should be defined for the given remote method from the given payload
*/
function buildMethodParamsFromPayload(model, methodName, params, isIo) {
if (model.isUser && methodName === 'logout') return '';
let output = new Array();
let relations = getModelRelations(model);
let availableClasses = relations.map(
(relationName, index) =>
model.sharedClass.ctor.relations[relationName].targetClass
);
params = params.filter(param => {
return !paramIsContext(param) && !paramIsFunction(param);
});
relations.forEach(relationName => {
if (model.sharedClass.ctor.relations[relationName].modelThrough) {
let throughName = capitalize(
model.sharedClass.ctor.relations[relationName].modelThrough
.sharedClass.name
);
// Only add through models when they are Public
if (ctx.models[throughName]) {
availableClasses.push(
capitalize(
model.sharedClass.ctor.relations[relationName].modelThrough
.sharedClass.name
)
);
}
}
});
if (isIo)
params = params.filter(param => !param.arg.match(/(fk|data|options)/));
params.forEach((param, i, arr) => {
output.push(`action.payload.${param.arg}`);
});
return output.join(', ');
}
/**
* @method paramIsRoute
* @description
* Testing if the param is route type
*/
function paramIsRoute(param) {
return (
(param.http && param.http.source === 'path') ||
(param.arg && param.arg.match(/(^id$|fk|^file$|container)/))
);
}
/**
* @method paramIsFunction
* @description
* Testing if the param is function type
*/
function paramIsFunction(param) {
return typeof param.http === 'function';
}
/**
* @method paramIsContext
* @description
* Testing if the param is a http.context
*/
function paramIsContext(param) {
return (
typeof param.http !== 'undefined' &&
typeof param.http.source !== 'undefined' &&
param.http.source === 'context'
);
}
/**
* @method paramIsBody
* @description
* Testing if the param is a http.body or form
*/
function paramIsBody(param) {
return (
typeof param.http !== 'undefined' &&
typeof param.http.source !== 'undefined' &&
(param.http.source == 'body' || param.http.source == 'form')
);
}
/**
* @method paramIsQuery
* @description
* Testing if the param is a http.query or form
*/
function paramIsQuery(param) {
return (
(typeof param.http === 'undefined' && // Query is default, if http is not defined we treat it as query param
(param.arg && !param.arg.match(/(^id$|fk|^file$|container)/))) || // But only if it is not id, fk, file or container
(typeof param.http !== 'undefined' &&
typeof param.http.source !== 'undefined' &&
param.http.source == 'query')
);
}
/**
* @method buildPostBody
* @description
* Define which properties should be passed while posting data (POST, PUT, PATCH)
*/
function buildPostBody(postData) {
let output = [];
if (Array.isArray(postData)) {
postData = postData.filter(param => {
// Filter out route params and function params
if (
paramIsRoute(param) ||
paramIsFunction(param) ||
paramIsContext(param) ||
paramIsQuery(param)
) {
return false;
}
// Make sure the param is body or form data
return paramIsBody(param);
});
if (postData.length > 0) {
output.push('');
let l = postData.length;
let formData = [];
postData.forEach((property, i) => {
if (property.http.source == 'form') {
formData.push(property);
} else {
output.push(
` ${property.arg}: ${property.arg}${i < l - 1 ? ',' : ''}`
);
}
});
if (formData.length > 0) {
l = formData.length;
output.push(` data: {`);
formData.forEach((property, i) => {
output.push(
` ${property.arg}: ${property.arg}${i < l - 1 ? ',' : ''}`
);
});
output.push(` }`);
}
output.push(' ');
}
}
return output.join('\n');
}
/**
* @method buildUrlParams
* @description
* Define which properties should be passed using query string
*/
function buildUrlParams(model, methodName, urlParams) {
let output = [''];
// filter params that should not go over url query string
urlParams = urlParams.filter(param => {
// Filter out route params and function params
if (
paramIsRoute(param) ||
paramIsFunction(param) ||
paramIsContext(param) ||
paramIsBody(param)
) {
return false;
}
// Filter out body params
return paramIsQuery(param);
});
if (model.isUser && methodName === 'logout')
output.push(
` _urlParams.access_token = this.auth.getAccessTokenId();`
);
if (urlParams && urlParams.length > 0) {
urlParams.forEach((param, i) => {
output.push(
` if (typeof ${param.arg} !== 'undefined' && ${
param.arg
} !== null) _urlParams.${param.arg} = ${param.arg};`
);
});
}
return output.join('\n');
}
/**
* @method buildRouteParams
* @description
* Define which properties should be passed as route params
*/
function buildRouteParams(routeParams) {
let output = [];
if (routeParams) {
routeParams = routeParams.filter(param => {
if (
paramIsQuery(param) ||
paramIsFunction(param) ||
paramIsContext(param) ||
paramIsBody(param)
) {
return false;
}
return paramIsRoute(param);
});
if (routeParams.length > 0) {
output.push('');
routeParams.forEach((param, i) => {
output.push(
` ${param.arg}: ${param.arg}${
i < routeParams.length - 1 ? ',' : ''
}`
);
});
output.push(' ');
}
}
return output.join('\n');
}
/**
* @author João Ribeiro <[email protected], http://jonnybgod.ghost.io>,
* @license MIT
* @method buildPropertyType
* @description
* Define which properties should be passed as route params
*/
function buildPropertyType(property) {
if (!property || !property.type) {
return 'any';
}
switch (typeof property.type) {
case 'function':
switch (property.type.name) {
case 'String':
case 'Number':
case 'Boolean':
return property.type.name.toLowerCase();
case 'Date':
case 'GeoPoint':
return property.type.name;
default:
return 'any';
}
case 'object':
if (Array.isArray(property.type)) {
return `Array<${buildPropertyType(property.type[0])}>`;
}
return 'object';
default:
return 'any';
}
}
/*
* @author Julien Ledun <[email protected]>,
* @license MIT
* @method buildPropertyDefaultValue
* @description
* Define defaults null values for class properties
*/
function buildPropertyDefaultValue(property) {
let defaultValue = property.hasOwnProperty('default')
? property.default
: '';
switch (typeof property.type) {
case 'function':
switch (property.type.name) {
case 'String':
return `'${defaultValue}'`;
case 'Number':
return isNaN(Number(defaultValue)) ? 0 : Number(defaultValue);
case 'Boolean':
return Boolean(defaultValue);
case 'Date':
return isNaN(Date.parse(defaultValue))
? `new Date(0)`
: `new Date('${defaultValue}')`;
case 'GeoPoint':
default:
return '<any>null';
}
case 'object':
if (Array.isArray(property.type)) {
return '<any>[]';
}
return '<any>null';
default:
return '<any>null';
}
}
};
/**
* HELPERS
*/
function capitalize(string) {
return string[0].toUpperCase() + string.slice(1);
}
function getModelRelations(model) {
return Object.keys(model.sharedClass.ctor.relations).filter(
relationName =>
model.sharedClass.ctor.relations[relationName].targetClass &&
model.sharedClass.ctor.relations[relationName].targetClass !== model.name
);
}