I have followed the loopback offline sync example and create my own model with embedded document.
I created a Model named Project where ProjectMembers are embedded model. Here are my model:
Project.json
{
"name": "Project",
"base": "PersistedModel",
"strict": "throw",
"persistUndefinedAsNull": true,
"trackChanges": true,
"properties": {
...
},
"relations": {
"members": {
"type": "embedsMany",
"model": "ProjectMember",
"property": "members",
"options": {
"validate": true,
"forceId": false
}
}
}
}
ProjectMember.json
{
"name": "ProjectMember",
"base": "Model",
"idInjection": true,
"properties": {
...
},
"validations": [],
"relations": {},
"acls": [],
"methods": []
}
In the server side model-config.json I updated the datasource as below:
"Project": {
"dataSource": "my_db"
},
"ProjectMember": {
"dataSource": "transient"
}
And in the client side in lbclient/models/ I added 2 files local-project.json and remote-project.json as exactly same as local-todo.json and remote-todo.json.
I updated the client side model-config.json file as below:
"RemoteProject": {
"dataSource": "remote"
},
"LocalProject": {
"dataSource": "local"
}
In the client controller I run the following codes:
ProjectModel.create($scope.project)
.then(function(project) {
var owner = loginDetails.getLoginUser();// the member
owner.role = 'owner';
owner.status = 'active';
project.members.create(owner); //shows error: couldn't read property
$scope.project = {};
$scope.$apply();
});
It creates the Project but failed to create the embedded model. It displays "Couldn't read property create undefined"? Is there any way to create embedded model in the client side?
UPDATE
The embedded model works only on server side. But when the browserify create the browse.bundle.js, it fails to add the embedded model.
I followed the trial and error method and has come to a solution about offline embedded model.
As previously,I only defined the relations between Project and ProjectMember in the "common/models/" directory. What I find is, I have to define the relations in the client side model too. So I did the following steps and it works.
I created a json file - "lbclient/models/local-project-member.json".
{
"name": "LocalProjectMember",
"base": "ProjectMember"
}
Added the following lines in "lbclient/model-config.json".
"LocalProjectMember":{
"dataSource": "local"
}
Modified the "lbclient/models/local-project.json" file as below.
{
"name": "LocalProject",
"base": "Project",
"relations":{
"members": {
"type": "embedsMany",
"model": "LocalProjectMember",
"property": "memberList",
"options": {
"persist": true,
"validate": true,
"forceId": false
}
}
}
}
So the conclusion is if you want your embedded model work on offline you have to redefine the relations in the client side model.
Related
I have recently started working on strapi and was looking at the relations inside model in their documentation. My scenario is as follows. I have a model named course and another named tag. They have many to many relationship between them.This is what ./api/course/models/course.settings.json has when I made the relation between them named as tag2.
{
"connection": "default",
"collectionName": "course",
"info": {
"name": "course"
},
"options": {
"increments": true,
"timestamps": true
},
"attributes": {
"image_link": {
"type": "string"
},
"created_by": {
"columnName": "created_by_id",
"plugin": "users-permissions",
"model": "user"
},
"updated_by": {
"columnName": "updated_by_id",
"plugin": "users-permissions",
"model": "user"
},
"title": {
"type": "string"
},
"short_description": {
"type": "text"
},
"slug": {
"type": "string",
"unique": true
},
"tags2": {
"collection": "tag",
"via": "courses",
"dominant": true
}
}
}
When I specify the relation using the admin panel strapi itself made a junction table named as courses_tags_2_s__tags_courses.
Here is what tag model looks like
{
"connection": "default",
"collectionName": "tag",
"info": {
"name": "tag",
"mainField": "ui_label"
},
"options": {
"increments": true,
"timestamps": true
},
"attributes": {
"code": {
"type": "string"
},
"description": {
"type": "string"
},
"created_by": {
"plugin": "users-permissions",
"model": "user",
"columnName": "created_by_id"
},
"updated_by": {
"plugin": "users-permissions",
"model": "user",
"columnName": "updated_by_id"
},
"ui_label": {
"type": "string"
},
"courses": {
"via": "tags2",
"collection": "course"
}
}
}
I have a couple of questions
1) Is there a way I can set up the junction table as courses_tags ? i.e overriding the strapi one
2) I have set my mainField as "ui_label" in tag.settings.json but in the admin panel while editing course table content(rows in course table), in the related field of tag2 I see "code" field shown there instead of "ui_label". How to set the mainField?
Note: I have setup strapi with mysql server.
so to answer your first question, there is currently no way to override the join table between two models. This is totally auto-generated by Strapi.
For the second question, this part of the docs is out to date.
To manage display information you will have to use the content manager configuration in the admin panel.
Here a short video - https://www.youtube.com/watch?v=tzipS2CePRc&list=PL7Q0DQYATmvhlHxHqfKHsr-zFls2mIVTi&index=5&t=0s
For 1) Is there a way I can set up the junction table as courses_tags ? i.e overriding the strapi one:
You can specify the following option:
"collectionName": "courses_tags"
I am new to loopback. So as a part of the learning process, started creating a sample API. Also extended User model as Customer and AccessToken as CustomerAccessToken. But the problem is that access token is not being generated, when we tried to login data that is already send using a POST request.
Response body when login using credentials:
{
"id": "oiMDjErGGVkeSMtnt1SfHzGuERZf6OCId5FUulvir6A04htUbIV656FOBlXn9vDS",
"ttl": 1209600,
"created": "2019-05-09T09:41:05.184Z",
"userId": "5cd3f59594d45186b411bb02"
}
No accessToken is generated. I have removed the inbuilt User and AccessToken model and used extended ones.
model-config.json
{
"CustomerAccessToken": {
"dataSource": "db",
"public": false
}
"Customer": {
"dataSource": "db",
"public": true
}
}
customer.json
"relations": {
"accessTokens": {
"type": "hasMany",
"model": "CustomerAccessToken",
"foreignKey": "userId",
"options": {
"disableInclude": true
}
}
}
customerAccessToken.json
{
"name": "CustomerAccessToken",
"base": "AccessToken",
"properties": {},
"validations": [],
"relations": {
"user": {
"type": "belongsTo",
"model": "Customer",
"foreignKey": "userId"
}
},
"acls": [],
"methods": {}
}
One more query: Will login functionality work, when we try to extend User & AccessToken models or Do I need to write login functionality in the customer.js file in order to make it work.
Any help would be really appreciated.
Try to use the DEBUG variable to get more details about the error. from loopback root call
export DEBUG=loopback:security:access-context
npm start
to run the server in the debug mode. Then you can check what exactly is going on - which method is called and why you don't have access.
Probably it's ACL setting: by default, you cannot get users data in loopback, so you may want to add ACL rule to the Customer entity. I suggest to use loopback-cli for that - it's less error-prone. Then you select a model where you want to add a new rule and following the step-by-step instruction. You can read more about ACL here
Do not use CustomerAccessToken unless you want to implement multiple user models.
This is the configuration that works great for me:
model-config.json
{
"User": {
"dataSource": "mysql",
"public": false
},
"AccessToken": {
"dataSource": "mysql",
"public": false,
"relations": {
"Customer": {
"type": "belongsTo",
"model": "Customer",
"foreignKey": "userId"
}
}
},
"Customer": {
"dataSource": "mysql",
"public": true
}
}
server.js
app.use(loopback.token({
model: app.models.accessToken,
currentUserLiteral: 'me'
}))
Customer model must extend the User model:
customer.json
{
"name": "Customer",
"plural": "customers",
"base": "User"
}
In addition to Antonio Trapanis response. If you want to use your own AccessToken model to have to set it in server.js
app.use(loopback.token({
model: app.models.CustomerAccessToken,
}));
I followed this tutorial.
https://youtu.be/Jx39u8IssRg
I received Token in response body when logged in with 'user' through user controller.
I am able to to hit the end points of to-do model by passing that 'token' in the headers as 'Bearer Token'. I used "postman" to hit the to-do end points
( I was unable to insert/attach the received token into the to-do requests of loopback4 app)
This is what my model looks like
"name": "mClass",
"base": "PersistedModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {
"mClassName": {
"type": "string",
"required": true,
"index": {
"unique": true
}
},
"mClassUrl": {
"type": "string"
},
"mCreatedBy": {
"type": "string",
"required": true
},
"mCreatedAt": {
"type": "date",
"required": true,
"default": "$now"
},
"mUpdatedAt": {
"type": "date"
},
"mDeletedAt": {
"type": "date"
}
},
"validations": [],
"relations": {},
"acls": [
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "$unauthenticated",
"permission": "DENY"
}
],
"methods": {}
}
i want to make class name as unique. I tried index:{unique:true} but it seems that it is not working as i can still create classes with same name. Please help me on what changes to be made.
In case somebody is looking for the Loopback v4 solution to this problem. In Loopback v4 you amend the #property annotation with "index: {unqiue: true}":
#model()
export class Client extends Entity {
#property({
type: 'string',
required: true,
index: {
unique: true,
},
})
name: string;
}
Then you must update your schema:
npm run migrate
or recreate it:
npm run migrate -- --rebuild
PostgresQL is support and MySQL, too, I guess
Alternatively, in your mClass.js you can do the following:
module.exports = function (mClass) {
mClass.validatesUniquenessOf('mClassName');
};
What you need to do is to define index in your model.json:
"indexes": {
"indexName": {
"keys": {
"mClassName": 1
},
"options": {
"unique": true
}
}
}
Then you need to run autoupdate on your datasource when the server starts, great place for that would be a boot script, eg. ./server/boot/db-autoupdate.js:
'use strict';
module.exports = async function(app) {
// this will trigger the database structure update, like creating indexes (which is not handled by auto-migrate module
await app.dataSources.db.autoupdate();
};
From the docs:
id No Boolean Whether the property is a unique identifier. Default is false. See Id property below.
So I'd assume that instead of index: {...}, you have to use id: true.
What I am trying to figure out is how to get the id of the current authenticated user and use that when creating records in the DB as a foreign key of a different model?
To be more specific I need to get the id of the current authenticated user (model: CommonUser) and use that id as a FK when creating a new Event.
The relationships:
I have created a Model based on the User model called CommonUser. Common user has many Events. Event belongs to Common User.
So Event has a foreignKey called commonUserId.
How do I get the id of the user and use that when doing the insert?
I would have thought this would be automatic as part of the process as far as setting up relationships is concerned? Is that incorrect?
Also to complicate matters I have an Event Look-Up table (i will worry about this next so don't feel obligated to dive to deep) because Event also hasAndBelongsToMany through Event Lookup.
User
{
"name": "CommonUser",
"base": "User",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {},
"validations": [],
"relations": {
"events": {
"type": "hasMany",
"model": "Event",
"foreignKey": "eventId",
"through": "EventLookUp"
},
"friends": {
"type": "hasMany",
"model": "CommonUser",
"through": "Friend",
"foreignKey": "friendId"
}
},
"acls": [],
"methods": {}
}
Event
{
"name": "Event",
"base": "PersistedModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {
"name": {
"type": "string"
},
"radius": {
"type": "number",
"default": 50
},
"status": {
"type": "number"
},
"location": {
"type": "geopoint",
"required": true
}
},
"validations": [],
"relations": {
"owner": {
"type": "belongsTo",
"model": "CommonUser",
"foreignKey": "commonUserId"
},
"commonUsers": {
"type": "hasAndBelongsToMany",
"model": "CommonUser",
"foreignKey": "ownerId",
"through": "EventLookUp"
},
"galleries": {
"type": "hasOne",
"model": "Gallery",
"foreignKey": ""
},
"photos": {
"type": "hasMany",
"model": "Photo",
"foreignKey": "",
"through": "Gallery"
}
},
"acls": [],
"methods": {}
}
Event Lookup
{
"name": "EventLookUp",
"base": "PersistedModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {},
"validations": [],
"relations": {},
"acls": [],
"methods": {}
}
If I can be pointed in the right direction that would be fantastic. It's hard to find an answer reading through the documentation. I think I need to use an operation hook before insert and set the Event Models properties? What is loopback's best practice as far as this goes?
In loopback swagger when you login as a user using loopback's default users/login api , you get access token object as response.You can copy the id of access token and paste into the box in top right corner in swagger and set the access token.Thus internally your accesstoken is set in loopback and for your every request from swagger, loopback append the access token along with the request.In this way you can get the access token from ctx(context) in remote methods.
For create, findOrCreate, save an event obj:
Event.observe('before save', function updateUserId(ctx, next) {
let userId = ctx.options.accessToken.userId;`
if (ctx.instance) {
ctx.instance.commonUserId = userId;
}
next();
});
For updateAttributes for event obj:
Event.observe('before save', function updateUserId(ctx, next) {
let userId = ctx.options.accessToken.userId;
if (ctx.currentInstance) {
ctx.currentInstance.commonUserId = userId;
}
next();
});
I found a solution but I am unsure if it is best practice or if there is a better way to handle this.
Anyways I created an Operation Hook inside "/common/models/event.js" and appearently you can get access to the context within this hook.
If there is a better way to do this I would like to know.
'use strict';
module.exports = function (Event) {
Event.observe('before save', function updateForeignKeyCommonUserId(ctx, next) {
// console.log(ctx.options);
/* WHAT Options Looks Like */
// { accessToken:
// { id: 'Z20R1RsnumWEdDzR3TyCCbmZ0DTp4tOh2cviU6JGsrlNIYCs3KchQ7mdAnhTc1VQ',
// ttl: 1209600,
// created: 2018-06-26T17:41:28.298Z,
// userId: 3 },
// authorizedRoles: {} }
// console.log("THE USER ID IS: ");
// console.log(ctx.options.accessToken.userId); // Current User Id
var userId = ctx.options.accessToken.userId;
// So appearently "the context provides either an instance property or a pair of data and where properties"
// #todo: find out why there can be a 'instance' or 'data'.
if (ctx.instance) {
ctx.instance.commonUserId = userId; // instance
} else {
ctx.data.commonUserId = userId; // data
}
next();
});
};
If someone wouldn't mind explaining where context comes from and how it gets populated that would be awesome!
I try to figure out how to set a very simple nested "treenode" model in loopback with mongodb. The idea is there will be only one model (for this): treenode which can contain other treenodes. I would like to store them at once via mongodb nested documents:
- TreeNode (document):
Name: "A",
Nodes: [
{
Name: "A-A",
Nodes: [
{
Name: "A-A-A",
Nodes: []
},
{
Name: "A-A-B",
Nodes: []
},
{
Name: "A-A-C",
Nodes: []
}
},
{
Name: "A-B",
Nodes: []
},
]
Additionally each node at any level has relations to other models.
There will be many top-level root treenodes (documents). Which relation type and how should I use for this?
Unfortunately, there isn't much documentation on this topic yet. For now, see http://docs.strongloop.com/display/LB/Embedded+models+and+relations
You should define the nested models separately and then declare them as transient models. Then loopback should store them in its parent model, as explained http://loopback.io/doc/en/lb2/Embedded-models-and-relations.html#transient-versus-persistent-for-the-embedded-model
Define a transient data source
server/datasources.json
{
...
"transient": {
"name": "transient",
"connector": "transient"
}
}
server/model-config.json
{
...
"BaseNode": {
"dataSource": "db",
"public": true
},
"NestedNode": {
"dataSource": "transient",
"public": false
}
}
And the model definitions should be somthing like this:
common/models/NestedNode.json
{
"name": "NestedNode",
"base": "Model",
"properties": {
"name": {
"type": "string"
}
},
"relations": {
"nodes": {
"type": "referencesMany",
"model": "NestedNode",
"options": {
"validate": true,
"forceId": false
}
}
}
common/models/BaseNode.json
{
"name": "BaseNode",
"base": "PersistedModel",
"properties": {
"name": {
"type": "string"
}
},
...
"relations": {
"nestedNode": {
"type": "embedsMany",
"model": "Link",
"scope": {
"include": "linked"
}
}
},
...
}
You also may experience curcular reference problems.