I'm looking into Loopback and stuck in finding a solution when two models (let's say Customer/Orders) have a one-to-many relationship but the primary key (id) of the parent isn't a foreign key in child table, but rather some non-key field.
For example, consider following two tables
Customer[ id(pk), name, social_security_number]
Order [ id(pk), social_security_number(fk)]
I'm not able to create the relationship based on the above scenario. Loopback isn't returning orders when I query against a particular customer. Here is my actual code snippet (partial). I'm using default id field as Primary Key
{
"name": "Customer",
"properties": {
"name": {
"type": "string",
"required": true
},
"social_security_number": {
"type": "number",
"required": true
}
},
"relations": {
"orders": {
"type": "hasMany",
"model": "Order",
"foreignKey": "social_security_number"
}
}
}
{
"name": "Order",
"base": "PersistedModel",
"properties": {
"social_security_number": {
"type": "number",
"required": true
}
},
"relations": {
"customer": {
"type": "belongsTo",
"model": "Customer",
"foreignKey": "social_security_number"
}
}
}
How can I make this work?
Related
I am using Loopback 3 and i have the following three models:
UserFile.json
{
"name": "UserFile",
"base": "PersistedModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {
"id": {
"type": "number",
"id": true,
"generated": true
},
"name": {
"type": "string",
"required": true
},
"size": {
"type": "number",
"default": 0
},
"uploadedAt": {
"type": "date",
"default": "$now"
}
},
"relations": {
"hasFile": {
"type": "hasMany",
"model": "UploadedFile",
"foreignKey": "fileId"
}
}
}
UploadedFile.json
{
"name": "UploadedFile",
"base": "PersistedModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {
"hashId": {
"type": "string",
"required": true
},
......
},
"relations": {
"file": {
"type": "belongsTo",
"model": "UserFile",
"foreignKey": "fileId"
}
}
}
PartitionedFile.json
{
"name": "PartitionedFile",
"base": "UploadedFile",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {
"partSize": {
"type": "number",
"default": 0
}
},
"relations": {
"file": {
"type": "belongsTo",
"model": "UserFile",
"foreignKey": "fileId"
}
}
}
The idea is that the UserFile has either one UploadedFile or (two or more) many PartitionedFile.
I want to be able to get from the UserFile, from the same relation the file, whether it is an UploadedFile or a PartitionedFile. Is it possible to achieve this just from the models' definitions or it can only be done with a remote method?
A relation from a model to another is bound to only one model; so you cannot have one relation called "file" to point to different models. In your example, it's bound to UploadedFile. In order for your model UserFile to have relations with both UploadedFile and PartitionedFile, you'll need two different relations.
For your UserFile to have either one UploadedFile or many PartitionedFile, it needs to have them both within its relations:
{
"name": "UserFile",
...
"relations": {
"uploadedFiles": {
"type": "hasOne", // Zero on one uploaded file
"model": "UploadedFile",
"foreignKey": "uploadedFileId"
},
"partitionedFiles": {
"type": "hasMany", // Zero or many partitioned files
"model": "PartitionedFile"
}
}
}
Then, to retrieve files from both UploadedFiles and PartitionedFiles through UserFile, you don't need a remote, but to include the models within your request.
For example, in ReactJS, it would look like this:
const response = await Axios.get(`/api/UserFile`, {
params: {
filter: {
include: [
{ relation: 'uploadedFiles' },
{ relation: 'partitionedFiles' },
]
}
}
}
Another solution would be to specify another model File that contains all these relations (UploadedFile, PartitionedFile, etc), so that the model UserFile can refer to the table File as one unique relation to retrieve the files...
We're porting our api from C# to Loopback ^v3.19.0 and have run into a blocker.
Many of our models have shared properties, so we've created a base model "Base" which they inherit from.
{
"name": "Base",
"base": "PersistedModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"mixins": {
"Timestamp": {}
},
"properties": {
"created-by": {
"type": "number",
"postgresql": {
"columnName": "created_by"
}
},
"created-date": {
"type": "date",
"postgresql": {
"columnName": "created_on_utc"
}
},
"updated-by": {
"type": "number",
"postgresql": {
"columnName": "updated_by"
}
},
"updated-date": {
"type": "date",
"postgresql": {
"columnName": "updated_on_utc"
}
},
"soft-deleted": {
"type": "boolean",
"postgresql": {
"columnName": "is_deleted"
}
},
"deleted-by": {
"type": "number",
"postgresql": {
"columnName": "deleted_by"
}
},
"deleted-date": {
"type": "date",
"postgresql": {
"columnName": "deleted_on_utc"
}
},
"tenant-id": {
"type": "number",
"postgresql": {
"columnName": "tenant_id"
}
}
},
...
}
Inside the Timestamp mixin (our own), those properties get set accordingly
module.exports = function(Model, options) {
Model.observe('before save', function event(ctx, next) {
const token = ctx.options && ctx.options.accessToken;
const userId = token && token.userId;
const now = new Date().toISOString();
if (ctx.instance) {
ctx.instance['created-by'] = userId;
ctx.instance['created-date'] = now;
ctx.instance['updated-by'] = userId;
ctx.instance['updated-date'] = now;
} else {
if (ctx.data['soft-deleted'] &&
ctx.data['soft-deleted'] === true) {
ctx.data['deleted-by'] = userId;
ctx.data['deleted-date'] = now;
ctx.data['is-active'] = false;
}
ctx.data['updated-by'] = userId;
ctx.data['updated-date'] = now;
}
next();
});
};
This works great when creating a new model. It was working great for updates (PATCH /modelname/:id), but unexpectedly broke and we can't figure out why. (This is consistent across all the models that inherit from this Base model.)
The mixin correctly sees the model and adds the updated properties like so
LoopbackJS | ************* 'before save' ctx.data **************
LoopbackJS | { 'is-active': false,
LoopbackJS | 'updated-by': 1,
LoopbackJS | 'updated-date': '2018-08-16T17:57:23.660Z' }
LoopbackJS | ************* END 'before save' ctx.data **************
But when loopback executes the update SQL, it somehow omits/removes the value for updated-by? (2nd param should be 1, not null)
LoopbackJS | 2018-08-16T17:57:23.666Z loopback:connector:postgresql SQL: UPDATE "public"."asset_types" SET "is_active"=$1,"updated_by"=$2,"updated_on_utc"=$3::TIMESTAMP WITH TIME ZONE,"tenant_id"=$4 WHERE "id"=$5
LoopbackJS | Parameters: [false,null,"2018-08-16T17:57:23.660Z",1,5]
updated_by in Postgres is nullable, so that shouldn't generate an error... but Loopback is sending a stringified function?
LoopbackJS | 2018-08-16T18:04:12.522Z loopback:connector:postgresql error: invalid input syntax for integer: "function () { [native code] }"
LoopbackJS | at Connection.parseE (/home/src/back-end/node_modules/pg/lib/connection.js:553:11)
LoopbackJS | at Connection.parseMessage (/home/src/back-end/node_modules/pg/lib/connection.js:378:19)
LoopbackJS | at TLSSocket.<anonymous> (/home/src/back-end/node_modules/pg/lib/connection.js:119:22)
LoopbackJS | at emitOne (events.js:115:13)
LoopbackJS | at TLSSocket.emit (events.js:210:7)
LoopbackJS | at addChunk (_stream_readable.js:264:12)
LoopbackJS | at readableAddChunk (_stream_readable.js:251:11)
LoopbackJS | at TLSSocket.Readable.push (_stream_readable.js:209:10)
LoopbackJS | at TLSWrap.onread (net.js:587:20)
If we don't touch the updated_by column, the SQL is correct and updates.
Incidentally, if we soft-delete and the deleted_by column is in play, the same thing happens there.
Feels like I'm spinning in circles here and probably overlooking something basic. Any suggestions?
EDIT
So it appears that it's not limited to a mixin... when we remove it completely and manually set the k:v pair in the payload (ie 'created-by': 1) we still get the same error back from Postgres.
The root cause of this was due to incorrect relationships.
I created this as a gist, but pasting it here too in case it helps someone else.
It's a PostgreSQL best-practice to use lowercase names, using snakecase if you need to. ie, my_column_name.
Also, since I'm using a JSON API client, I've installed the excellent loopback-component-jsonapi to handle the de/serialization stuff... but that just added additional complexities.
JSON API calls for dasherized property names. When you start with something like my-property-name, Loopback or the PostgreSQL driver (doesn't really matter) collapses the dasherized property down to mypropertyname by default.
This is bad... especially when you have an existing schema you're working with.
It's worse when you're working with relationships, because Loopback also appends the id suffix by default, so now you have issues unless you happen to have a mypropertynameid column.
An example
Let's say we have a Customer model. I needed endpoints that are lowercase (and dasherized, where applicable), so just change the plural to match here.
{
"name": "Customer",
"plural": "customers",
"base": "PersistedModel",
...
}
Inside of options.postgresql, you can set a tableName. Loopback will use the name value by default, but remember PostgreSQL doesn't like CamelCase. You need to override this unless you use lowercase model names.
(It's a religious preference, but I like my tables to be plurals. Fight me.)
{
...
"options": {
"validateUpsert": true,
"postgresql": {
"tableName": "customers"
}
}
...
}
Back to the properties, use the postgresql.columnName property to map to the correct column name in the db. If it's not a dasherized property name (ie status) then you can ignore the postgresql.columnName bit.
{
...
"properties": {
"is-active": {
"type": "boolean",
"default": false,
"postgresql": {
"columnName": "is_active"
}
}
}
}
Relationships can be a headache.
Let's say our Customer has people who work there. To do a basic one-many relationship between the models...
{
...
"relations": {
"people": {
"type": "hasMany",
"model": "Person",
"foreignKey": "customer_id"
}
},
...
}
people is the name of the relationship element of the JSON API payload.
A "gotcha" here for me was the foreignKey property.
The Loopback docs say it's optional - and it is - but if you leave it out then it adds the id suffix to the name (people) and then looks for that column in your customers table. That wasn't highlighted very well, but it was clear enough.
This part wasn't clear => I originally thought the foreignKey value pointed to the property of the Person model, so I had the dasherized customer-id property here. That's incorrect. It's literally asking you for the database column name, which feels like a bit of an antipattern... In the properties you had to define a columnName if you wanted to refer to the db columns under the ORM.
Also, note that the foreignKey property is reused in relationships but it means different things to different type contexts. In a hasMany, it's asking "Which column there maps to the primary key here?"
Final Customer model:
{
"name": "Customer",
"plural": "customers",
"base": "PersistedModel",
"options": {
"validateUpsert": true,
"postgresql": {
"tableName": "customers"
}
},
"properties": {
"name": {
"type": "string"
},
"is-active": {
"type": "boolean",
"default": false,
"postgresql": {
"columnName": "is_active"
}
}
},
"validations": [],
"relations": {
"people": {
"type": "hasMany",
"model": "Person",
"foreignKey": "customer_id"
}
},
"acls": [],
"methods": {}
}
The Person model on the other end of the relationship.
The foreignKey for a belongsTo relationship is asking the opposite question... "Which property here maps to the primary key there?"
Also, if you have properties you don't want exposed (especially if you've inherited a model and don't want/need all those properties for whatever reason) then you can hide them with the hidden element. See below.
{
"name": "Person",
"plural": "people",
"base": "User",
"idInjection": false,
"options": {
"validateUpsert": true,
"postgresql": {
"tableName": "people"
}
},
"hidden": [
"emailVerified",
"realm",
"username",
],
"properties": {
"first-name": {
"type": "string",
"postgresql": {
"columnName": "first_name"
}
},
"last-name": {
"type": "string",
"postgresql": {
"columnName": "last_name"
}
},
"email": {
"type": "string"
},
...
},
"validations": [],
"relations": {
"customer": {
"type": "belongsTo",
"model": "Customer",
"foreignKey": "customer_id"
}
},
"acls": [],
"methods": {}
}
I am having a nearly identical problem to my previous question. When a user model is created, the following error is returned:
ReferenceError: g is not defined
at new ModelConstructor (eval at createModelClassCtor (/usr/src/app/node_modules/loopback-datasource-juggler/lib/model-builder.js:671:21), <anonymous>:10:27)
at user.ModelBaseClass._initProperties (/usr/src/app/node_modules/loopback-datasource-juggler/lib/model.js:349:28)
at user.ModelBaseClass (/usr/src/app/node_modules/loopback-datasource-juggler/lib/model.js:60:8)
at user.Model (eval at createModelClassCtor (/usr/src/app/node_modules/loopback-datasource-juggler/lib/model-builder.js:671:21), <anonymous>:12:24)
at user.PersistedModel (eval at createModelClassCtor (/usr/src/app/node_modules/loopback-datasource-juggler/lib/model-builder.js:671:21), <anonymous>:12:24)
at user.User (eval at createModelClassCtor (/usr/src/app/node_modules/loopback-datasource-juggler/lib/model-builder.js:671:21), <anonymous>:12:24)
at new user (eval at createModelClassCtor (/usr/src/app/node_modules/loopback-datasource-juggler/lib/model-builder.js:671:21), <anonymous>:12:24)
at Function.DataAccessObject.create (/usr/src/app/node_modules/loopback-datasource-juggler/lib/dao.js:359:13)
at /usr/src/app/node_modules/loopback-datasource-juggler/lib/dao.js:1262:13
at /usr/src/app/node_modules/loopback-datasource-juggler/lib/dao.js:2175:62
at /usr/src/app/node_modules/loopback-datasource-juggler/lib/dao.js:2111:9
at /usr/src/app/node_modules/loopback-datasource-juggler/node_modules/async/dist/async.js:1012:9
at /usr/src/app/node_modules/loopback-datasource-juggler/node_modules/async/dist/async.js:359:16
at eachOfArrayLike (/usr/src/app/node_modules/loopback-datasource-juggler/node_modules/async/dist/async.js:928:9)
at eachOf (/usr/src/app/node_modules/loopback-datasource-juggler/node_modules/async/dist/async.js:990:5)
at _asyncMap (/usr/src/app/node_modules/loopback-datasource-juggler/node_modules/async/dist/async.js:1005:5)
at Object.map (/usr/src/app/node_modules/loopback-datasource-juggler/node_modules/async/dist/async.js:995:16)
at allCb (/usr/src/app/node_modules/loopback-datasource-juggler/lib/dao.js:2025:13)
at /usr/src/app/node_modules/loopback-connector-mongodb/lib/mongodb.js:1155:9
at result (/usr/src/app/node_modules/mongodb/lib/utils.js:414:17)
at executeCallback (/usr/src/app/node_modules/mongodb/lib/utils.js:406:9)
at handleCallback (/usr/src/app/node_modules/mongodb/lib/utils.js:128:55)
Here are my models:
user.json:
{
"name": "user",
"plural": "users",
"base": "User",
"idInjection": false,
"options": {
"validateUpsert": true
},
"properties": {
"id": {
"type" : "string",
"id" : true,
"required" : true,
"defaultFn" : "guid"
},
"type": {
"type" : "[string]",
"required" : true,
"default" : ["student"]
},
"full_name": {
"type": "string",
"required": false
},
"office" : {
"type": "string",
"required" : false
},
"profile_img": {
"type": "string",
"required": false
},
"departmentId": {
"type": "string",
"required": false
}
},
"validations": [],
"relations": {
"department": {
"type": "belongsTo",
"model": "department"
},
"syncedcalendar" : {
"type" : "hasMany",
"model" : "syncedcalendar"
},
"accessTokens": {
"type": "hasMany",
"model": "accessToken",
"foreignKey": "userId",
"options": {
"disableInclude": true
}
}
},
"acls": [],
"methods": {}
}
accessToken.json
{
"name": "accessToken",
"plural": "accessTokens",
"base": "AccessToken",
"properties": {},
"validations": [],
"idInjection": false,
"relations": {
"user": {
"type": "belongsTo",
"model": "user",
"foreignKey": "userId"
}
},
"acls": [],
"methods": []
}
The error above (in both this case and my previous issue) resulted because loopback could not parse the model's json file. In this case, it was the type property of user:
"type": {
"type" : "[string]",
"required" : true,
"default" : ["student"]
},
It seems the default cannot be an array. Removing this default and enforcing this default in a hook resolved the issue.
I went through an issue that resembled yours. And yes, the error stack gives too little useful info. It should instead say: "please stick to Loopback types" because that's the problem. You're stuck with Loopback types.
In your case, the parser couldn not handle an array. Well, it turns out it won't parse an un recognized type string either. For example, if you try to declare a property as integer:
"id":
{
"type": "Integer", <----- not a loopback type
"id": 1,
"mysql":
{
"columnName": "id",
"dataType": "int",
}
},
It will throw, because the native type is Number. You can format it as integer, but don't mess with its type.
"id":
{
"type": "Number", <---- one of loopback types. Integer is not one of them
"id": 1,
"mysql":
{
"columnName": "id",
"dataType": "int",
}
"format":"integer" <---- here's where you can format or cast as desired
},
If your database values are json-like (so they should rehidrate as arrays and objects) just declare them as Objects.
I have a contact db table and model. Employee model inherits from contact.
If i do GET employees/ it returns all the contacts.
How should I set up my employee.json if I want to return only the contacts with partnerId = 1?
{
"name": "employee",
"base": "contact",
"strict": false,
"idInjection": false,
"options": {
"validateUpsert": true,
"postgresql": {
"schema": "public",
"table": "contact"
}
},
"scope": {
"where": {
"partnerId": 1
}
},
//...
}
Debug says calling GET employees/ makes the following query:
SELECT "name", "position", "email", "password", "id" FROM "public"."contact" ORDER BY "id"
It does not seem that scope is added.
models/partner.json
{
"name": "partner",
// ...
"properties": {
"name": {
"type": "string",
"required": true
},
// ...
},
"validations": [],
"relations": {
"contacts": {
"type": "hasMany",
"model": "contact"
}
//...
},
"acls": [],
"methods": {}
}
Try using the where filter, either in the REST API
/employees?filter[where][partnerId]=1
or in your Employee.js
Employee.find({ where: {partnerId:1} });
https://docs.strongloop.com/display/APIC/Where+filter
I have an Order model which hasMany OrderItem models. But once a client wants to create an Order, it has to create an Order object first then for each product he added to his basket, he needs to create responding OrderItems separately. As you may notice it causes many reduntant requests. May be I can make a custom method for OrderItems which consumes a product list. But i was wondering if there is a built in mechanism for this like createMany since it is a very useful operation.
ORDER MODEL
{
"name": "Order",
"plural": "Orders",
"base": "PersistedModel",
"idInjection": true,
"properties": {
"customerId": {
"type": "number",
"required": true
},
"branchId": {
"type": "number",
"required": true
}
},
"validations": [],
"relations": {
"orderItems": {
"type": "hasMany",
"model": "OrderItem",
"foreignKey": "orderId"
}
},
"acls": [],
"methods": []
}
ORDERITEM MODEL
{
"name": "OrderItem",
"plural": "OrderItems",
"base": "PersistedModel",
"idInjection": true,
"properties": {
"UnitPrice": {
"type": "number"
},
"productId": {
"type": "number",
"required": true
},
"purchaseOrderId": {
"type": "number",
"required": true
},
"quantity": {
"type": "number"
}
},
"validations": [],
"relations": {
"product": {
"type": "belongsTo",
"model": "Product",
"foreignKey": "productId"
},
"purchaseOrder": {
"type": "belongsTo",
"model": "PurchaseOrder",
"foreignKey": ""
}
},
"acls": [],
"methods": []
}
Loopback "create" method accepts also an array of objects (see PersistedModel.create docs) so you should try creating one "create" call and send an array of OrderItems.