Set Up Many to Many relations in Loopback 4 - loopbackjs

I'm setting up many to many relations in my loopback 4 application. Currently I am using this answer as a guide but after the repository and controller creation I don't know how to continue.
Currently I have three tables for the relation: Course, Language, and LanguageCourse. This means a Course can have many languages and a Language can belong to many courses.
My language-course.model.ts looks like this:
import {Course} from './course.model';
import {Language} from './language.model';
#model({settings: {}})
export class LanguageCourse extends Entity {
#property({
type: 'number',
id: true,
})
id?: number;
#belongsTo(() => Course)
courseId?: number;
#belongsTo(() => Language)
languageId?: number;
constructor(data?: Partial<LanguageCourse>) {
super(data);
}
}
export interface LanguageCourseRelations {
// describe navigational properties here
}
export type LanguageCourseWithRelations = LanguageCourse &
LanguageCourseRelations;
My course.model.ts looks like this (I have already set up a one to many relation in this model):
import {User, UserWithRelations} from './user.model';
#model({settings: {}})
export class Course extends Entity {
#property({
type: 'number',
id: true,
})
id?: number;
#property({
type: 'string',
})
name?: string;
#property({
type: 'string',
})
description?: string;
#belongsTo(() => User)
userId?: number;
#property({
type: 'string',
default: 'active',
})
state?: string;
#property({
type: 'string',
default: 'rookie',
})
level?: string;
#property({
type: 'string',
})
course_photo?: string;
constructor(data?: Partial<Course>) {
super(data);
}
}
export interface CourseRelations {
user?: UserWithRelations;
}
export type CourseWithRelations = Course & CourseRelations;
And my language.model.ts looks like this:
#model({settings: {}})
export class Language extends Entity {
#property({
type: 'number',
id: true,
})
id?: number;
#property({
type: 'string',
})
name?: string;
constructor(data?: Partial<Language>) {
super(data);
}
}
export interface LanguageRelations {
// describe navigational properties here
}
export type LanguageWithRelations = Language & LanguageRelations;
I would like to do a GET request to, for example /courses/{id} endpoint (and /courses as well) and have in the response all the languages that course has but I don't know how to make it work. Also I would like this to work in /languages endpoint.
Thanks for your time!

Unfortunately, there is no proper default many to many relations defined in loopback 4 documentation.
But it seems like you have already tried implementing to create a bridging connecting model language-course.model.ts which is good. Now you have to create the repository and controller for this model. Once that done you can write your own custom logic which will handle storing and retrieving of the data.
For example,
As you said you wish to query and fetch all list of all the languages based on a courses ID, your CourseLanguage model should look something like
import {Course} from './course.model';
import {Language} from './language.model';
#model({settings: {}})
export class LanguageCourse extends Entity {
#property({
type: 'number',
id: true,
})
id?: number;
#property({
type: 'string',
})
courses: string;
#property({
type: 'array',
itemType: 'string',
required:true
})
languages: string[];
constructor(data?: Partial<LanguageCourse>) {
super(data);
}
}
export interface LanguageCourseRelations {
// describe navigational properties here
}
export type LanguageCourseWithRelations = LanguageCourse &
LanguageCourseRelations;
What we are doing above is we are creating two new properties courses which will be the ID of the course and languages which will be an array holding all the languages for that course. Once this is done.
Next, you will have to work on the controller for this model
Follow these following steps for GET all languages based on course ID API
Create a custom controller with route /course/{id} as mentioned by you.
Inside that controller, write your custom code as follows:
#get('/course/{id}', {
responses: {
'204': {
description: 'Fetching the list of all the languages as per course ID',
},
},
})
async fetchListOfLanguagesAsPerCourseId(#param.path.string('id') id: string) {
/**
* Search for the course in `CourseLanguage` collection or table by using the course id
**/
let searchedCourseLanguageObject = await this.courseLanguageRepository.findOne({where: {courses: id}});
/**
* If the course does not exists, throw an error
**/
if(searchedCourseLanguageObject === null || undefined) {
throw new HttpErrors.NotFound("Course does not have any languages")
}
/**
* If the course exists, it will have array languages with all the IDs of the languages from LAnguages collection or table in the database related to the course
**/
let arrayOfLanguagesId = searchedCourseLanguageObject.languages;
/**
* Now go to the language table and fetch all the languages object whose IDs are present in the above array
**/
let listOfLanguages = await this.languagesRepository.find({where: {id: { inq: arrayOfLanguagesId }}});
/**
* Above code will look for all the languages ID in the languages table and return back objects of the language whose ID exist in the array arrayOfLanguagesId
**/
return listOfLanguages
}
Hopefully, this is not too intimidating, when you will implement it, it is easy. Write to me if you will still face any problem.
Thanks
Hopefully this helps.

Related

TypeORM why is my relationship column undefined? foreign-key is undefined

I just use TypeORM and find the relationship column is undefined
#Entity({name: 'person'})
export class Person {
#PrimaryGeneratedColumn('uuid')
id!: string;
#OneToOne( () => User)
#JoinColumn()
user!: User;
#Column({
type: "enum",
enum: PersonTitle,
default: PersonTitle.Blank
})
title?: string;
#Column({type: 'varchar', default: ''})
first_name!: string;
#Column('varchar')
last_name!: string;
#ManyToOne(() => Organization, org => org.people, { nullable: true})
belong_organization!: Organization;
and I also have Organization entity:
export class Organization {
#PrimaryGeneratedColumn('uuid')
id!: string;
...
}
when I use Repository like:
const db = await getDatabaseConnection()
const prep = db.getRepository<Person>('person')
presult = await prep.findOne({where: {id}})
console.log(result)
my result is:
Person {
id: '75c37eb9-1d88-4d0c-a927-1f9e3d909aef',
user: undefined,
title: 'Mr.',
first_name: 'ss',
last_name: 'ls',
belong_organization: undefined, // I just want to know why is undefined? even I can find in database the column
expertise: [],
introduction: 'input introduction',
COVID_19: false,
contact: undefined
}
the database table like:
"id" "title" "first_name" "last_name" "expertise" "COVID_19" "userId" "belongOrganizationId" "introduction"
"75c37eb9-1d88-4d0c-a927-1f9e3d909aef" "Mr." "test" "tester" "nothing" "0" "be426167-f471-4092-80dc-7aef67f13bac" "8fc50c9e-b598-483e-a00b-1d401c1b3d61" "input introduction"
I want to show organization id, how typeORM do it? Foreign-Key is present undefined?
You need to either lazy load the relation or you need to specify the relation in the find
Lazy:
#Entity({name: 'person'})
class Person {
...
#ManyToOne(() => Organization, org => org.people, { nullable: true})
belong_organization!: Organization;
...
}
...
async logOrganization() {
const db = await getDatabaseConnection()
const prep = db.getRepository<Person>('person')
presult = await prep.findOne({where: {id}})
console.log(await result.belong_organization)
}
Find
const prep = db.getRepository<Person>('person')
presult = await prep.findOne({
where: { id },
relations: ["belong_organization"]
})
You could also always do an eager load, but i'd advise against this since then it would always do the join when it fetches a person.
If you want to query the belong_organizationId you need to add its field to the person entity. This field is usual something like belongOrganizationId
That would make
#Entity({name: 'person'})
class Person {
...
#Column()
belongOrganizationId:number
#ManyToOne(() => Organization, org => org.people, { nullable: true})
belong_organization!: Organization;
...
}
This would make it possible to query for its id too.
You could also query it more directly but this leaves you with some pretty ugly and unmaintainable code:
const findOptions: {
where :{
id,
'belong_organization.id': belong_organizationId
}
}

Mixins in loopback4

I want to add createdAt and updatedAt to each model on loopback 4
can not find name 'MixinTarget'.
Type parameter 'T' of exported function has or is using private name 'MixinTarget'.
If I try from documentation above error occurs.
MixinTaget must be imported from #loopback/core:
import {MixinTarget} from '#loopback/core';
import {Class} from '#loopback/repository';
export function TimeStampMixin<T extends MixinTarget<object>>(baseClass: T) {
return class extends baseClass {
// add a new property `createdAt`
// eslint-disable-next-line #typescript-eslint/ban-ts-comment
// #ts-ignore
public createdAt: Date;
constructor(...args: any[]) {
super(args);
this.createdAt = new Date();
}
printTimeStamp() {
console.log('Instance created at: ' + this.createdAt);
}
};
}
Further reading
As of the writing of this answer, the docs hasn't been updated to reflect the latest clarifications.
https://github.com/strongloop/loopback-next/blob/4932c6c60c25df885b4166b7c4c425eba457a73b/docs/site/Mixin.md
To resolve this issue, I didn't use the mixin approach. I added the following fields to my model.
#property({
type: 'date',
default: () => new Date(),
postgresql: {
columnName: 'updated_at',
},
})
updatedAt?: Date;
It should work as expected

Verify account endpoint in loopback

I am trying to implement verify account endpoint in loopback4 thorugh mailgun, since I am new in loopback4 and typescript in general, I am not sure if I am doing the right way. I want to retype the following code in the picture for loopback.I have already done with saving active flag in database and generating secretToken at signing up.
My code in loopback
#post('/users/verify')
async verify(
#requestBody(VerifyRequestBody) userData: User
): Promise<{ userData: object }> {
try {
const foundUser = await this.userRepository.findOne({
where: {
secretToken: userData.secretToken,
}
})
if (!foundUser) {
throw new HttpErrors.Forbidden(`No user found`)
}
foundUser.active = true;
foundUser.secretToken = ''
const savedUser = await this.userRepository.create(foundUser);
} catch (err) {
return err
}
}
User model, I am using MongoDB
import { Entity, model, property } from '#loopback/repository';
#model({ settings: {} })
export class User extends Entity {
#property({
type: 'string',
id: true,
})
id: string;
#property({
type: 'string',
required: true,
})
email: string;
#property({
type: 'string',
required: true,
})
password: string;
#property({
type: 'string',
required: true,
})
firstName: string;
#property({
type: 'string',
required: true,
})
lastName: string;
#property({
type: 'string',
required: false
})
secretToken: string;
#property({
type: 'boolean',
required: false,
default: false
})
active: boolean;
#property.array(String)
permissions: String[]
constructor(data?: Partial<User>) {
super(data);
}
}
export interface UserRelations {
// describe navigational properties here
}
export type UserWithRelations = User & UserRelations;
User repository
import { DefaultCrudRepository } from '#loopback/repository';
import { User, UserRelations } from '../models';
import { MongoDsDataSource } from '../datasources';
import { inject } from '#loopback/core';
export type Credentials = {
email: string,
password: string,
active: boolean
}
export type Verify = {
secretToken: string
}
export class UserRepository extends DefaultCrudRepository<
User,
typeof User.prototype.id,
UserRelations
> {
constructor(#inject('datasources.mongoDS') dataSource: MongoDsDataSource) {
super(User, dataSource);
}
}

Inject Relationships into Ember Payload

I have a component that shows and creates comments for a post, this component has a form for creating the new comments and sends them via POST to the backend, the normal payload would be:
{
data: {
attributes: {
created_at: "foo",
autor: "foo",
text: "foo"
},
relationships: {
post: {
data: { type: "posts", id: 1234 },
id: "1234",
type: "loans"
}
},
type: "comment"
}
}
The problem comes when you need to use the component in another view and more importantly when the name of the model is different, to say posts_breakdown, in this case, the payload would be:
{ data: {
attributes: {
created_at: "foo",
autor: "foo",
text: "foo"
},
relationships: {
post: {
data: null
}
},
type: "comment"
}
}
Clearly, in comments, there is no relation posts_breakdown, the first thing that I tried to add this relation to the model with posts_breakdown: belongsTo (posts_breakdown).
The problem is, that the backend can't recognize it and is not possible to modify it.
The backend is taking the values on the relationships to relate the comment with the post (post_id field into comment table)
My question: Is there some way to "trick" the backend and/or modify the payload, so think that the post_breakdown model is posted?
Below is a representation of how I have the defined models:
comment.js:
    export default DS.Model.extend ({
        author: DS.attr (),
        text: DS.attr (),
        created_at: DS.attr (),
        post: DS.belongsTo ('post'),
        posts_breakdown: DS.belongsTo ('posts_breakdown'),
    });
posts.js:
    export default DS.Model.extend ({
        text: DS.attr (),
        created_at: DS.attr (),
        author: DS.attr (),
        comments: DS.hasMany ('comments'),
    });
post_breakdown.js
    export default DS.Model.extend ({
        most_commented_post: DS.attr (),
        last_commented_post: DS.attr (),
        frequent_users: DS.attr (),
        comments: DS.hasMany ('comments'),
    });
Ok, i already figured out the way to modify the payload send it to the backend.
Ember have Serializers!
Following this guide, i can modify the data into the payload, erase it, add it or whatever i need:
https://guides.emberjs.com/release/models/customizing-serializers/
I my case, firs i need to add the relationship into the comment's model, in this line:
`posts_breakdown: DS.belongsTo ('posts_breakdown')`
then generate a serializer for comment's model with ember-cli:
`ember generate serializer comment`
finally, into the serializer if the payload contains data into the post_breakdown relationship, delete it and pass it to post relationship, in this way, the payload was the same:
import DS from 'ember-data';
export default DS.JSONAPISerializer.extend({
/*
This two functions, are necesary because Ember Data changes the underscore
between variable names by dashes. In fact, it's a Ember suggestion.
*/
keyForAttribute: function (key) {
return key;
},
keyForRelationship: function (key) {
return key;
},
serialize(snapshot, options) {
let json = this._super(...arguments);
/* This makes possible to store comments when the comments-panel-loan component is used
in loan_breakdown view with post_breakdown model:
*/
if (json.data.relationships.post_breakdown.data) {
json.data.relationships.loan = {
data: {
type: "posts",
id: json.data.relationships.post_breakdown.data.id }
};
delete json.data.relationships.post_breakdown;
}
return json;
},
});

How Do I Construct my Ember Models and API Data to represent data on the relationship?

I'm new to jsonapi and how the structure works and trying to get a relationship to load properly. I'm expecting ember-data to follow provided url's in the relationship.links of my objects to fetch the required information but I'm getting unexpected results.
I have Users, Territories, and a User/Territory relationship defined like this:
// User Model
const UserModel = DS.Model.extend({
username: DS.attr('string'),
territories: DS.hasMany('user-territories')
}
// Territory Model
const TerritoryModel = DS.Model.extend({
name: DS.attr('string')
}
// User-Territory Model
const UserTerritoryModel = DS.Model.extend({
notes: DS.attr('string'),
startDate: DS.attr('date'),
user: DS.belongsTo('user'),
territory: DS.belongsTo('territory')
}
I then have mock data (using http-mock) that looks like this:
// api/users/
data: {
type: "users",
id: 1,
attributes: {
username: "thisIsMyUsername"
}
},
relationships: {
territories: {
links: {
self: "http://localhost:4200/api/users/1/territories"
}
}
}
// api/users/1/territories
data: {
type: "user-territories",
id: 1,
attributes: {
notes: "This is a note",
startDate: "2017-01-01"
}
},
relationships: {
user: {
link: {
self: "http://localhost:4200/api/users/1"
}
},
territory: {
link: {
self: "http://localhost:4200/api/territories/1"
}
}
}
// api/territories/1
data: {
type: "territories",
id: 1,
attributes: {
name: "Territory #1"
}
}
In my User route, I want to request the UserModel and have access to the UserTerritory Relationship data and the Territory itself. The api calls are not what I expect though:
this.get('store').findRecord('user', 1, { include: "territories" });
EXPECTED:
api/users/1
api/users/1/territories
ACTUAL:
api/users/1
api/users/1?include=territories
If I call the user-territories model I get this:
EXPECTED:
api/users/1/territories
ACTUAL:
api/user-territories/1
If you use included, ember-data basically thinks you want to tell the server to side-load data. If you return a links, just resolve the relationship. However the relationships have to be inside the data. Also the self link is for the relationship itself, to return the data use related.
So first you do something like user = store.findRecord('user', '1'), this will fetch to api/users/. Then you should return something like this:
// api/users/
{
data: {
type: "users",
id: 1,
attributes: {
username: "thisIsMyUsername"
}
relationships: {
territories: {
links: {
related: "http://localhost:4200/api/users/1/territories"
}
}
}
}
}
Next you do user.get('territories'). This will return a promise, and fetch http://localhost:4200/api/users/1/territories, or whatever was inside that related link. However know, what ember-data will expect you to return user-territories here, because thats what you specified with territories: DS.hasMany('user-territories'). You should know what you directly can model an many-to-many relationship in ember-data without a third table.