Loopback, AngularJS and validation - angularjs

I followed this tutorial to create a project with Loopback and AngularJs. https://github.com/strongloop/loopback-example-angular
Now, I have an application with:
HTML files (with Bootstrap)
AngularJS controllers
AngularJS service (generated with syntax lb-ng server/server.js client/js/services/lb-services.js)
Model (located in ./common folder)
MongoDB backend
The model "Device" is defined in ./common/models/device.js
module.exports = function(Device) {
};
And in ./common/models/device.json
{
"name": "Device",
"base": "PersistedModel",
"idInjection": true,
"properties": {
"name": {
"type": "string",
"required": true
},
"description": {
"type": "string",
"required": true
},
"category": {
"type": "string",
"required": true
},
"initialDate": {
"type": "date"
},
"initialPrice": {
"type": "number",
"required": true
},
"memory": {
"type": "number"
}
},
"validations": [],
"relations": {},
"acls": [],
"methods": []
}
In the "AddDeviceController", I have an initialization part with:
$scope.device = new DeviceToBuy({
name: '',
description: '',
category: '',
initialPrice: 0,
memory: 8
initialDate: Date.now()
});
And I am able to save the $scope.device when executing the following method:
$scope.save = function() {
Device.create($scope.device)
.$promise
.then(function() {
console.log("saved");
$scope.back(); // goto previous page
}, function (error) {
console.log(JSON.stringify(error));
});
}
When everything is valid, the model is saved in the backend. If something is not valid in the $scope.device, I receive an error from my backend. So everything is working fine.
Now, I would like to use the model to perform client-side validation before sending my model to the backend and put some "error-class" on the bootstrap controls.
I tried something in the $scope.save function before sending to the backend:
if ($scope.device.isValid()) {
console.log("IsValid");
} else {
console.log("Not Valid");
}
But I get an exception "undefined is not a function" --> isValid() doesn't exist.
And I cannot find any example on how to execute this client-side validation.

LoopBack models are unopinionated and therefore do not provide client side validation out-of-box. You should use Angular validation mechanisms before calling $save.

Related

OData is returning Upper-case properties when filtering

I'm working on a project which has a front-end made in Reactjs + axios, and a backend with .NET Core 3.1 Web API which is configured to allow OData filtering.
Everything is working fine, but I only have one small problem.
If I consume, e.g: GET /api/questions without any OData filtering, I receive:
[
{
"id": "005b46c6-1811-42f2-b917-00178e916d2e",
"value": "List the requirements for Schedule III, IV, & V Prescriptions.",
"required": true,
"sequence": 0,
"questionType": {
"id": "4227e559-3b14-4e07-b90e-73151f723698",
"name": "Rating",
"value": "rating-question",
"description": "a question in where the user rates the answer"
},
"ratingSetting": {
"id": "5e6a883a-084d-4400-bdf2-78b97e2eba18",
"name": "Poor to Excellent",
"minValueText": "Poor",
"maxValueText": "Excellent",
"optionCount": 5
},
"answers": []
},
...
]
But if I perform some filtering, for example: GET /api/admin/questions?$select=id, value&$expand=questionType($select=name)&$expand=ratingSetting($select=name) the result contains an array where every property has changed to Upper-case:
[
{
"QuestionType": {
"Name": "Rating"
},
"RatingSetting": {
"Name": "Poor to Excellent"
},
"Id": "005b46c6-1811-42f2-b917-00178e916d2e",
"Value": "List the requirements for Schedule III, IV, & V Prescriptions."
},
...
]
As you can see, every property is upper case. This is causing me trouble when I try to bind the result object, because it's not the same question.id against question.Id, so I'm having an undefined exception in the front-end.
Is there any configuration that I have to perform in my backend to solve this?
Make sure you are using a contract resolver by specifying the appropriate NewtonSoftJsonOptions:
services.AddControllers(options =>
options.EnableEndpointRouting = false)
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
});
Install these libraries:
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Microsoft.AspNetCore.OData.NewtonsoftJson" Version="8.0.4" />
Then add this in startup.cs or program.cs:
services.AddControllers().AddOData(options =>
{
options.EnableQueryFeatures(maxTopValue: 500);
})
.AddODataNewtonsoftJson()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.ContractResolver =
new CamelCasePropertyNamesContractResolver();
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
});

How do i modify a raw data object returned by an ExtJS AJAX proxy into a JSON object to be consumed by a Tree Store

In an effort to create a treepanel, i configure it with a treestore whose AJAX proxy url receives json data i have no control of. But using Ext.data.reader.Json's transform property invokable before readRecords executes, gives an option to modify the passed raw (deserialized) data object from the AJAX proxy into a modified or a completely new data object. The transform config, gives the code snippet below:
Ext.create('Ext.data.Store', {
model: 'User',
proxy: {
type: 'ajax',
url : 'users.json',
reader: {
type: 'json',
transform: {
fn: function(data) {
// do some manipulation of the raw data object
return data;
},
scope: this
}
}
},
});
I would please like an example on how to go about modifying the return JSON object
[
{
"id": 3,
"attributes":
{},
"name": "user_one",
"login": "",
"email": "user_one#ats",
"phone": "0751223344",
"readonly": false,
"administrator": false,
"password": null
},
{
"id": 4,
"attributes":
{},
"name": "user_two",
"login": "",
"email": "user_two#ats",
"phone": "0751556677",
"readonly": false,
"administrator": false,
"password": null
}
]
into a JSON object fit for a treestore.
The hierarchical tree is to be rendered to show which user is under which admin using a condition administrator==true from the returned JSON, then a second AJAX request that returns that admin's users shown here.
[
{
"user_id": 3,
"admin_id": 1,
},
{
"user_id": 4,
"admin_id": 2,
}
]
Is the data nested at all? Otherwise why use a treepanel instead of a grid? To your question though, it'll depend on how you configure your treepanel but it would probably be something like this:
transform: {
fn: function(data) {
var treeRecords = Ext.Array.map(data, function(i){
return {
text: i.name,
leaf: true
//any other properties you want
}
});
var treeData = {
root: {
expanded: true,
children: treeRecords
}
};
return treeData;
},
scope: this
}

AJV validator and custom or user-friendly error message

I have the following schema and json to validate using ajv. I am developing a REST API that takes a JSON and gets validated against the schema and it returns the error (400- with the ajv error) or (200 - when successfully validated)
const schema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": [ "countries" ],
"definitions": {
"europeDef": {
"type": "object",
"required": ["type"],
"properties": { "type": {"const": "europe"} }
},
"asiaDef": {
"type": "object",
"required": ["type"],
"properties": { "type": {"const": "asia"} }
}
},
"properties": {
"countries": {
"type": "array",
"items": {
"oneOf":[
{ "$ref": "#/definitions/europeDef" },
{ "$ref": "#/definitions/asiaDef"}
]
}
}
}
}
const data = {
"countries":[
{"type": "asia1"},
{"type": "europe1"}
]
}
const isValid = ajv.validate(schema, data); //schema, data
if(! isValid){
console.log(ajv.errors);
}
and the error is:
[ { keyword: 'const',
dataPath: '/countries/0/type',
schemaPath: '#/definitions/europeDef/properties/type/const',
params: { allowedValue: 'europe' },
message: 'should be equal to constant' },
{ keyword: 'const',
dataPath: '/countries/0/type',
schemaPath: '#/definitions/asiaDef/properties/type/const',
params: { allowedValue: 'asia' },
message: 'should be equal to constant' },
{ keyword: 'oneOf',
dataPath: '/countries/0',
schemaPath: '#/properties/countries/items/oneOf',
params: { passingSchemas: null },
message: 'should match exactly one schema in oneOf' },
{ keyword: 'const',
dataPath: '/countries/1/type',
schemaPath: '#/definitions/europeDef/properties/type/const',
params: { allowedValue: 'europe' },
message: 'should be equal to constant' },
{ keyword: 'const',
dataPath: '/countries/1/type',
schemaPath: '#/definitions/asiaDef/properties/type/const',
params: { allowedValue: 'asia' },
message: 'should be equal to constant' },
{ keyword: 'oneOf',
dataPath: '/countries/1',
schemaPath: '#/properties/countries/items/oneOf',
params: { passingSchemas: null },
message: 'should match exactly one schema in oneOf' } ]
I know why the error is appearing (reason: as I have used 'asia1' & 'europe1' and it is not conforming the schema standard)
My question is, as I have derived this schema so I can pretty much understand the error. But for a third person it would definitely take some time to figure it out (and it may take more time, if the schema/errors are more complex).
If I returned that whole error message as a response as it is, it will be more complex error message to understand and to present to the enduser.
So, Is there any way by which I can provide more meaningful & user friendly error message to understand ?
ex: Invalid countries values found in JSON
I have checked: ajv-errors, better-ajv-errors but they are not providing the exact way I want?
Can someone suggest how to do that in a more user friendly way or any alternative mechanism?
I am using below code for generating the human readable error message
let msg: string = "Wrong body" // fallback error message;
if (errors && errors.length > 0) {
const error = errors[0];
msg = `${error.instancePath} ${error.message}`;
}
res.status(4xx).json({
errorMsg: msg,
});
I am using below dependencies to generate the validate functions in runtime with the below code
"dependencies": {
"ajv": "^8.11.0",
... // other dependencies
}
"devDependencies": {
"ajv-cli": "^5.0.0"
}
Below code gets run before project build and hence creating runtime generated validation files
const ajv = new Ajv({
schemas: schemas, // list of parsed json *.json schemas
code: { source: true, esm: true },
});
let moduleCode = standaloneCode(ajv);
Below is the few examples of error messaged diaplayed
Case of missing property:
"/items/0/price must have required property 'currency_code'"
Case of additional property: "/address must NOT have additional properties"
Case when quantity is fraction(allowed is +ve number): "/items/0/quantity must be integer"
Case when quantity is -ve(allowed is +ve number): "/items/0/quantity must be >= 1"
Case when passed value is not allowed(case of the enum): /items/0/price/currency_code must be equal to one of the allowed values

search as you type with elasticsearch,angularjs

I am working on search as you type functionality with angularjs and elastic search.I am passing the $viewValue to factory written in angular and it fetches data from angular.Please check code below.
services.factory('instantSearch',['$q', 'esFactory', '$location', function($q, elasticsearch, $location){
return{
instantResult : function(term){
var client = elasticsearch({
// host: $location.host() + ':9200'
host: 'localhost:9200'
});
var deferred = $q.defer();
client.search({
"index": 'stocks',
"type": 'stock',
"body": {
"from" : 0, "size" : 20,
"query": {
"bool":{
"should":[
{
"match_phrase":{
"name": term
}
},
{
"match_phrase":{
"symbol": term
}
},
{
"match":{
"industry": term
}
}
]
}
}
}
}).then(function(result) {
var hits = result.hits.hits;
deferred.resolve(hits);
},
function (err) {
console.trace(err.message);
}, deferred.reject);
return deferred.promise;
}
};
}]);
This code is working fine but the problem is that I get result when input matches complete term in elasticsearch index's field.So I want to implement token analyzer which will match token(ngram - 1,2,3) and provide result on typing of each character.
So to add analyzer code we have to add settings in te elasticserach index as below:
"settings": {
"analysis": {
"filter": {
"autocomplete_filter": {
"type": "edge_ngram",
"min_gram": 1,
"max_gram": 20
}
},
"analyzer": {
"autocomplete": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"autocomplete_filter"
]
}
}
}
}
But I am not getting the way to pass the argument here.Every example I checked shows output with curl command.How can we mix analyzer with the working code above.
Thanks for help.
Have you added the analyzer to the fields name, symbol and industry in your elastic search mapping?
curl -XPUT 'http://localhost:9200/index/type/_mapping?ignore_conflicts=true' -d'
{
"type": {
"properties": {
"name": {
"type": "string",
"analyzer": "autocomplete"
}
}
}
}'
Use ignore_conflicts=true without fail.
If you still face issues, then you might have to create a new index, add analyzer and filter to setting, create the desired mapping and then upload the data again.

LoopBack AngularJS extending User model

i'm facing some problems when using an extended user model in my AngularJS application.
here is my user.json:
{
"name": "user",
"base": "User",
"strict": false,
"idInjection": true,
"properties": {
"clientType": {
"type": "string",
"required": true
}
},
"validations": [],
"relations": {},
"acls": [
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
],
"methods": []
}
here is my model-config.json:
{
"_meta": {
"sources": [
"loopback/common/models",
"loopback/server/models",
"../common/models",
"./models"
]
},
"User": {
"dataSource": "mongo"
},
"AccessToken": {
"dataSource": "mongo",
"public": false
},
"ACL": {
"dataSource": "mongo",
"public": false
},
"RoleMapping": {
"dataSource": "mongo",
"public": false
},
"Role": {
"dataSource": "mongo",
"public": false
},
"Store": {
"dataSource": "mongo",
"public": true
},
"user": {
"dataSource": "mongo",
"public": true
}
}
this is my UserCtrl.js
angular.module('app.controllers.user', [])
.controller('UserCtrl', ['user', function (user) {
var vm = this;
vm.addUser = function () {
user.create({
firstName: vm.firstName,
lastName: vm.lastName,
email: vm.email,
password: vm.password,
userType: 'customer'
})
.$promise
.then(function (c) {
console.log('added user: ' + c.email);
});
};
}])
i'm getting the following error:
Error: [$injector:unpr] Unknown provider: userProvider <- user <- UserCtrl
if i use 'User' instead of 'user' it works, but it doesn't use my extended user-model with the specified ACL (READ for everyone)
i've read that you can specify var myUser = app.model.user to make sure that LoopBack uses the extended model. but i don't know how to do that in AngularJS since i specify the model as function parameter in the controller..
can you tell me how to use my extended user model within my AngularJS app?
thanks in advance!!
Do you have your user model generated inside Angular client library? If your application works when you use loopback auto-generated "User" model, then my best guess is that you have created your extended model "user", after you initially generated your angular services. If you are not using grunt task then you should regenerate angular services to update file with all changes and new models that you added since you last time generated the file.
Use lb-ng command to do it. As documentation suggests
For example, if your application has the standard LoopBack project layout, then in the /client sub-directory, enter these commands:
$ mkdir js
$ lb-ng ../server/server.js js/lb-services.js
You can find more information on the following link
http://docs.strongloop.com/display/public/LB/AngularJS+JavaScript+SDK
You need to define a Service, Factory, Provider, Value or Constant called 'user' in order for the service to be injectable in your controller. I do not see either of these in your post.
My suggestion is, if your extended user model is an instance of a class, then use a service:
app.service('user', fn);
If your extended user model is an object literal in JSON format, then use a factory:
app.factory('user', function() { return { ... }; });

Resources