Declaring relationships in sequelize? - mysql

I've got three models I am trying to define a relationship between them.
Models:
User
Payment
Debtor
User has many Payments
Payment belongs to User
Payment has many Debtors
Debtor belongs to Payment
Debtor belongs to Payment
Do I need to define the relationship in the source and the target? i.e.
User.hasMany(Payment, {as: 'Payments'})
and also
Payment.belongsTo(User)
or can I get away with just defining it in one model?
I appear to get errors when trying to define it in both. The model I import is undefined and therefore when I try pass it into the "belongsTo()" or "hasMany()" functions I get the error called with something that's not an instance of Sequelize.Model
When I only include it in one model, it doesn't create the relationship correctly.
My models are as follows:
Payment model:
import { sequelize } from '../config';
import Sequelize from 'sequelize';
import User from './User';
const Payment = sequelize.define('payment', {
amount: {
type: Sequelize.FLOAT
},
reference: {
type: Sequelize.STRING
},
user_id: {
type: Sequelize.INTEGER
}
});
Payment.belongsTo(User)
export default Payment;
User model:
import { sequelize } from '../config';
import Sequelize from 'sequelize';
import Payment from './Payment';
const User = sequelize.define('user', {
email: {
type: Sequelize.STRING
},
username: {
type: Sequelize.STRING
},
firstName: {
type: Sequelize.STRING
},
lastName: {
type: Sequelize.STRING
},
facebookID: {
type: Sequelize.STRING
}
});
User.hasMany(Payment, {as: 'Payments'})
export default User;
// EDIT
When I try create a new user with a payment, I get the error payment is not associated to user!
exports.create = (req, res) => {
User.create({
email: 'test3#gmail.com',
firstName: "Test",
lastName: "Test",
username: 'fnord',
payment: [
{ amount: 10.00},
]
}, {
include: [ Payment ]
})
}

That happens because in payment you require User model and vice-versa, in user you require Payment (circular dependency). In order to avoid this situation you would have to declare both models in the same file or you could follow the Sequelize recommended way
module.exports = (sequelize, DataTypes) => {
return sequelize.define('modelName', {
// attributes
}, {
// additional options, like classMethods in which you could create the association
classMethods: {
associate: function(models) {
this.belongsTo(models.Model);
this.hasMany(models.Model);
}
}
});
}
In order to achieve it you would have to use the sequelize-cli module. If you install it, you could use sequelize init command which would generate a file which registers all models to the sequelize (all those models are initially in one location, however you can change it by modifying the auto generated file).
In this case, sequelize iterates over every model, registers it (with use of sequelize.import() method) and calls the associate function which creates relations between models. This way would prevent the situation you have mentioned.

if Someone is struggling with Sequelize associations like me, this might help for you as this is not mentioned any where in documentation that how you can make associations if you are using Sequelize.define i was also facing an issue and made this work for me
'use strict';
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('User', {
id: {
type: DataTypes.BIGINT,
primaryKey: true,
},
name: {
type: DataTypes.STRING
},
email: {
type: DataTypes.STRING,
unique : true,
},
password: {
type: DataTypes.STRING
},
status: {
type: DataTypes.STRING
},
});
let Bookmark = sequelize.model('Bookmark');
User.hasMany(Bookmark,{foreignKey : 'user_id', as 'bookmarks'});
return User;
};
hope it works!

Related

Angular 11, Typescript, Nodejs, mysql - If email domain correct on register component, show button based on 'mysql' Boolean value, in home component

Currently trying to add functionality in my application that would check for a certain domain email address during registration, and based on this, the user will see a specific button on the 'home' page later. (Something like; If #hotmail.com email, patchValue('1'). Then If '0', hide button, if '1', show button.)
The email 'check' is done in my 'isTestEmail' function, in the 'register.component.ts' and the check is working correctly based on my console.log output.
Next, in the same function, I want to then set a value (isLecturer) in the accounts DB table, from '0' to '1' if the desired email address was entered.
Then in my 'home.component.html', show or hide a specific button (using ngIf), based on that mySQL account table property (isLecturer) value of 0 or 1 (true/false).
My problem is that I am unsure of the correct syntax to put in my 'isTestEmail' function, that will change the value in the database, from 0 to 1.
I'm also unsure of what code to add in the 'lab-swap-home.component.ts', to assist the detection of the NgIf statement in the 'lab-swap-home.component.html'.
I'm not confident in my proposed solution and could be overthinking this. I would love to see any easier suggestions to implement this functionality.
Database > Accounts table > isLecturer config:
register.component.ts:
import { Component, OnInit } from '#angular/core';
import { Router, ActivatedRoute } from '#angular/router';
import { FormBuilder, FormGroup, Validators } from '#angular/forms';
import { first } from 'rxjs/operators';
import { AccountService, AlertService } from '#app/_services';
import { MustMatch } from '#app/_helpers';
import { Account } from '#app/_models/account'; //
#Component({ templateUrl: 'register.component.html' })
export class RegisterComponent implements OnInit {
form: FormGroup;
loading = false;
submitted = false;
email: any;
//isLecturer: boolean = true; //optional way, unsure.
//isLecturer: any;
constructor(
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private accountService: AccountService,
private alertService: AlertService
) { }
ngOnInit() {
this.form = this.formBuilder.group({
title: ['', Validators.required],
firstName: ['', Validators.required],
lastName: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6)]],
confirmPassword: ['', Validators.required],
acceptTerms: [false, Validators.requiredTrue],
//isLecturer: ['', Validators.required],
}, {
validator: MustMatch('password', 'confirmPassword')
});
}
public TestFunc() {
console.log('Function is called')
}
public isTestEmail() {
// var re = /^(([^<>()[\]\\.,;:\s#\"]+(\.[^<>()[\]\\.,;:\s#\"]+)*)|(\".+\"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
// if(re.test(email)){
//Email valid. Procees to test if it's from the right domain (Second argument is to check that the string ENDS with this domain, and that it doesn't just contain it)
if(this.form.controls.email.value.indexOf("#hotmail.com", this.form.controls.email.value.length - "#hotmail.com".length) !== -1){
//VALID
console.log("TEST EMAIL VALID");
this.isLecturer.patchValue('1'); // Intended way.
this.isLecturer = false; // optional way, unsure how to implement.
}
else{
console.log("TEST EMAIL INVALID");
}
//}
}
// convenience getter for easy access to form fields
get f() { return this.form.controls; }
onSubmit() {
this.submitted = true;
console.log('LOOK!')
this.TestFunc(); // calling function this way works!
this.isTestEmail();
//this.accountService.isTestEmail
//this.accountService.TestFunc
// reset alerts on submit
this.alertService.clear();
// stop here if form is invalid
if (this.form.invalid) {
return;
}
this.loading = true;
this.accountService.register(this.form.value)
.pipe(first())
.subscribe({
next: () => {
this.alertService.success('Registration successful, please check your email for verification instructions', { keepAfterRouteChange: true });
this.router.navigate(['../login'], { relativeTo: this.route });
},
error: error => {
this.alertService.error(error);
this.loading = false;
}
});
}
}
(model) account.ts:
import { Role } from './role';
export class Account {
id: string;
title: string;
firstName: string;
lastName: string;
email: string;
role: Role;
jwtToken?: string;
isLecturer?: boolean;
}
lab-swap-home.component.html button example:
<button (click)="openModal('custom-modal-1')" *ngIf="isLecturer === '1'" class="btn btn-primary mr-2 mb-3 ml-3" >Create available lab slots</button>
account.model.js (backend):
const { DataTypes } = require('sequelize');
module.exports = model;
function model(sequelize) {
const attributes = {
email: { type: DataTypes.STRING, allowNull: false },
passwordHash: { type: DataTypes.STRING, allowNull: false },
title: { type: DataTypes.STRING, allowNull: false },
firstName: { type: DataTypes.STRING, allowNull: false },
lastName: { type: DataTypes.STRING, allowNull: false },
acceptTerms: { type: DataTypes.BOOLEAN },
isLecturer: { type: DataTypes.BOOLEAN },
isStudent: { type: DataTypes.BOOLEAN },
role: { type: DataTypes.STRING, allowNull: false },
verificationToken: { type: DataTypes.STRING },
verified: { type: DataTypes.DATE },
resetToken: { type: DataTypes.STRING },
resetTokenExpires: { type: DataTypes.DATE },
passwordReset: { type: DataTypes.DATE },
created: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
updated: { type: DataTypes.DATE },
isVerified: {
type: DataTypes.VIRTUAL,
get() { return !!(this.verified || this.passwordReset); }
}
};
const options = {
// disable default timestamp fields (createdAt and updatedAt)
timestamps: false,
defaultScope: {
// exclude password hash by default
attributes: { exclude: ['passwordHash'] }
},
scopes: {
// include hash with this scope
withHash: { attributes: {}, }
}
};
return sequelize.define('account', attributes, options);
}
I believe to have resolved my own issue by moving the 'isTestEmail' function into the 'home' component, and checking the current users account email, by calling the function during page initialization. Then the button is hidden / shown based on the current users email domain.
Managed to get the correct syntax, In html:
*ngIf="isLecturer === true"
And home.component.ts:
isLecturer: boolean = false
public isTestEmail() {
if(this.account.email.indexOf("#mail.com", this.account.email.length - "#mail.com".length) !== -1){
this.isLecturer = true;
}
else{
console.log("TEST EMAIL INVALID");
}
//}
}

Add a field in a many-to-one relation in NestJS

I am trying to develop a small application to record cooking recipes. To do this, I declared 2 entities with nestJS allowing me to manage the recipes and another to manage the ingredients. I also created a 3rd entity to record the quantities of ingredients needed :
Database diagram
// recipe.entity.js
#Entity()
export class Recipe {
#PrimaryGeneratedColumn()
id: number
#Column('datetime')
createdAt: Date
#Column('datetime')
updatedAt: Date
#Column('varchar', { length: 100 })
title: string;
#Column('varchar', {nullable: true})
image: string;
#OneToMany(type => RecipeIngredients, recipeIngredients => recipeIngredients.recipe)
ingredients: RecipeIngredients[];
}
// ingredient.entity.js
#Entity()
export class Ingredient {
#PrimaryGeneratedColumn()
id: number
#Column('datetime')
createdAt: Date
#Column('datetime')
updatedAt: Date
#Column('varchar', { length: 100 })
name: string;
#Column('varchar', {nullable: true})
image: string;
#OneToMany(type => RecipeIngredients, recipeIngredients => recipeIngredients.ingredient)
recipes: RecipeIngredients[];
}
// recipe_ingredients.entity.js
#Entity()
export class RecipeIngredients {
#PrimaryGeneratedColumn()
id: number
#ManyToOne(type => Recipe, recipe => recipe.ingredients)
recipe: Recipe
#ManyToOne(type => Ingredient)
ingredient: Ingredient
#Column()
quantity: string;
}
First, I would like to be able to retrieve a recipe with the list of necessary ingredients:
const recipe = await this.recipesRepository.createQueryBuilder('recipe')
.where('recipe.id = :recipeId', {recipeId: _id})
.leftJoin('recipe.ingredients', 'recipe_ingredients')
.leftJoin('recipe_ingredients.ingredient', 'ingredient')
.getMany();
But this method returns only my recipe object without the ingredients...
[
{
"id": 1,
"createdAt": "2020-04-30T09:12:22.000Z",
"updatedAt": "2020-04-30T09:12:22.000Z",
"title": "Test",
"image": null
}
]
From there, I'm lost ... How can I get the list of my ingredients (at least the name and quantity fields) directly from my service?
Thank you in advance for your help.
Using leftJoin will able you to join data without its selection. It selects the recipe if it has ingredients, but won't return its ingredients.
As read in TypeORM's documentation :
You can join data without its selection. To do that, use leftJoin or
innerJoin:
const user = await createQueryBuilder("user")
.innerJoin("user.photos", "photo")
.where("user.name = :name", { name: "Timber" })
.getOne();
This will generate:
SELECT user.* FROM users user
INNER JOIN photos photo ON photo.user = user.id
WHERE user.name = 'Timber'
This will select Timber if he has photos, but won't return his photos.
To select the ingredients try using leftJoinAndSelect instead.

NestJs update Many-To-Many relation with join table

I have two entities - Property and Owner. One Property can have a lot of Owners and Owner can have a lot of Properties. For join use property_owner table. How to update this many-to-many relation using NestJS/TypeORM?
#Entity('property')
export class Property extends EntityModel {
#Column({ length: 255, nullable: false })
name: string;
#ManyToMany(type => Owner, { cascade: true })
#JoinTable({
name: 'property_owner',
joinColumn: { name: 'propertyId', referencedColumnName: 'id'},
inverseJoinColumn: { name: 'ownerId', referencedColumnName: 'id'},
})
owners: Owner[];
}
#Entity('owner')
export class Owner extends EntityModel {
#Column({ length: 255, nullable: false })
name: string;
#ManyToMany(type => Property, { cascade: true })
#JoinTable({
name: 'property_owner',
joinColumn: { name: 'ownerId', referencedColumnName: 'id'},
inverseJoinColumn: { name: 'propertyId', referencedColumnName: 'id'},
})
properties: Property[];
}
Below my service's methods for save and update:
public create(req: Request): Promise<Dto> {
const dto: CreateDto = {
...req.body,
owners: this.formatJoinData(req.body.owners) //[1,2,3] => [{id:1},{id:2},{id:3}]
};
const entity = Object.assign(new Entity(), dto);
return this.repo.save(entity);
}
public update(req: Request): Promise<UpdateResult> {
const dto: EditDto = {
...req.body,
owners: this.formatJoinData(req.body.owners) //[1,2,3] => [{id:1},{id:2},{id:3}]
};
const id = req.params.id;
const entity = Object.assign(new Entity(), dto);
return this.repo.update(id, entity);
}
Saving new Property work fine, but when I try update property I get error
[ExceptionsHandler] column "propertyId" of relation "property" does not exist
Owners data in both cases looks like [{id:1},{id:2},{id:3}]. I think problem in save/update methods results. Save method return to us Entity with id and update method return to us UpdateResult which not contain Entity id. But may be we can transform/additionally define this value somewhere...
I found solution. Need to call save method instead update.
In my case update will be looks like
import {plainToClass} from 'class-transformer';
public async update(req: Request): Promise<Dto> {
const found = await this.repo.findOneOrFail(req.params.id, {
relations: ['owners', 'electricMeter'],
});
const dto = {
...found,
...req.body,
owners: this.formatJoinData(req.body.owners) //[1,2,3] => [{id:1},{id:2},{id:3}]
updatedBy: this.getUser(req),
updatedDate: Date.now(),
};
return this.repo.save(plainToClass(Entity, dto));
}
This code can be improved, but think that main idea is clear.
https://typeorm.io/#/many-to-many-relations documentation doesn't say more than use JoinTable decorator, and we don't know what you havr in your request, but it looks like you're passing wrong values. These fields are virtual, at the end with m2m relationship third table is created to handle relationship.

How to define models without use Sequelize ? (Node.JS with MySQL)

I work with Node.JS and MySQL with "mysql" package.
Is there is a way to define models with class ??
I read a lot and all opinion say i need sequellize for this.
In angular i use data model like this:
export class Stage {
public name: string;
public consultation: string;
public approval: string;
public stageOrder: number;
}
Is there is a way to make it work in nodejs?
Thanks
Yes there is, you need to use sequellize and defined a class in this way:
const Stage = sequelize.define('stage', {
name: {
type: Sequelize.STRING,
allowNull: false
},
consultation: {
type: Sequelize.STRING
},
approval: {
type: Sequelize.STRING
},
stageOrder: {
type: Sequelize.INTEGER
}
}, {
// options
});
You can read the documentation here.

Angular 6: Fetch Hash Table Data from JSON respond Backend

I have This JSON respond from my backend:
//User_Courses
[
{
id: 1,
name: "Ice King",
email: "pretty_princess1234#gmail.com"
completedCourses: [1,3],
unlockedCourses: [1,3,4,5,6],
completedLessons: [{"1" => [1,2,3]}, {"3" => [1,2,3,4,5,6,7]}, {"4" => [1]}]
},
{
id: 2,
name: "Mr. Crocker",
email: "fairy_godparents111#gmail.com"
completedCourses: [3],
unlockedCourses: [1,3,4],
completedLessons: [{"3" => [1,2,3,4,5,6,7]}, {"4" => [1,2]}]
}
]
// completed lessons are all the lesson the user finished.
// courses can be in progress or completed.
I want to fetch data from backend and subscribe it to this interface.
I don't sure how to implement the data structure and how to access data.
This is the interface I created:
export interface IUser {
id: number;
name: string;
email: string;
completedCourses: number[];
unlockedCourses: number[];
completedLessons: // <----- don't know what type to write
}
I want to know how to implement this, subscribe data with service and access data (in order to change it later and add data).
Thank you so much!
Create model for CompletedLesson (as mentioned in the comments):
interface ICompletedLesson {
[name: string]: number[];
}
interface IUser {
id: number;
name: string;
email: string;
completedCourses: number[];
unlockedCourses: number[];
completedLessons: ICompletedLesson[];
}
Then, create a service, something like this:
#Injectable()
export class UserService {
constructor(private http: HttpService) { }
fetchUserCourses(): Observable<IUser[]> {
return this.http.get<IUser[]>(`URL_TO_THE_USER_COURSES%);
}
}
And, wherever you are fetching data (some component for example):
fetchUserCourses() {
// userService is injected in this component's constructor
this.userService.fetchUserCourses().subscribe(users => {
// do something with result, yes, something like
this.users = users;
});
}
In the JSON you provided, to access the first lesson of the Mr. Crocker completed lessons (this.users are all users you retrieved from backend):
const firstCompletedLesson = this.users[1].completedLessons[0]; // {"3": [1,2,3,4,5,6,7]}
const lessons = firstCompletedLesson["3"]; // [1,2,3,4,5,6,7]
const firstLesson = lessons[0]; // 1
Furhermore, you can access "3" like this:
Object.keys(firstCompletedLesson)[0]; // 3
and you can add to array using push:
lessons.push(8); // [1,2,3,4,5,6,7,8]
and to add new completed lesson use:
this.users[1].completedLessons.push({ "5": [1, 2, 3] });
Hope this helps.