nestjsx/crud typeorm relations query - nestjs

How do I have to write the query in order to get relations of the entity populated with?
Checked the below options, but none of them works.
localhost:3000/reservations?join=concert_id
localhost:3000/reservations?join=concert
If I change the concert_id key within the join object of #Crud() options to concert (as the name of the property in the Reservation.entity.ts file is just concert, not concert_id), then I get an error error: column reference "Reservation_id" is ambiguous.
Any help is much appreciated. Thanks in advance!
Reservation.entity.ts
#Entity('reservations')
export class Reservation {
#PrimaryGeneratedColumn()
id: number;
#Column({ name: 'booking_reference' })
bookingReference: string;
#ManyToOne(() => Concert, (concert) => concert.reservations)
#JoinColumn({ name: 'concert_id' })
concert: Concert;
}
Concert.entity.ts
#Entity('concerts')
export class Concert {
#PrimaryGeneratedColumn()
id: number;
#Column()
name: string;
#OneToMany(() => Reservation, (reservation) => reservation.concert)
reservations: Reservation[];
}
Reservation.controller.ts
#Crud({
model: {
type: Reservation
},
dto: {
create: CreateReservationDto
},
validation: { always: true },
query: {
join: {
concert_id: {
eager: true,
alias: "concert"
}
}
}
})
#ApiTags('Reservation')
#Controller('reservation')
export class ReservationController implements CrudController<Reservation> {
constructor(public service: ReservationService) {}

Related

Sequelize + typescript creating with nested model error: Object literal may only specify known properties

I have simplified two of my models User and Teacher looking like this :
import { InferAttributes, InferCreationAttributes, CreationOptional, NonAttribute, Association, HasManyGetAssociationsMixin, HasManyAddAssociationMixin, HasManyAddAssociationsMixin, HasManySetAssociationsMixin, HasManyRemoveAssociationMixin, HasManyRemoveAssociationsMixin, HasManyHasAssociationMixin, HasManyHasAssociationsMixin, HasManyCountAssociationsMixin, HasManyCreateAssociationMixin, HasOneGetAssociationMixin, HasOneSetAssociationMixin, HasOneCreateAssociationMixin } from "sequelize";
import { Models } from ".";
import Teacher from "./teacher";
export default class User extends Model<InferAttributes<User>, InferCreationAttributes<User>> {
id: CreationOptional<number>;
roles: string;
password: string;
username: string;
type: 'learner' | 'teacher' | 'designer';
getTeacher: HasOneGetAssociationMixin<Teacher>;
setTeacher: HasOneSetAssociationMixin<Teacher, number>;
createTeacher: HasOneCreateAssociationMixin<Teacher>;
teacher?: NonAttribute<Teacher>;
static associations: {
teacher: Association<User, Teacher>;
};
}
export default class Teacher extends FrelloModel<InferAttributes<Teacher>, InferCreationAttributes<Teacher>> {
id: CreationOptional<number>;
school_id: number;
getSchool: BelongsToGetAssociationMixin<School>;
setSchool: BelongsToSetAssociationMixin<School, number>;
createSchool: BelongsToCreateAssociationMixin<School>;
school?: NonAttribute<School>;
getUser: BelongsToGetAssociationMixin<User>;
setUser: BelongsToSetAssociationMixin<User, number>;
createUser: BelongsToCreateAssociationMixin<User>;
user?: NonAttribute<User>;
static associations: {
school: Association<Teacher, School>;
user: Association<Teacher, User>;
};
}
create(input: TeacherCreationInput): Promise<User> {
return models.user.create({
...input,
roles: '["TEACHER"]',
type: 'teacher',
teacher: {
school_id: input.school_id,
},
}, {
include: [{
model: models.teacher,
}],
});
}
When I try to create a user including the corresponding teacher, I get a typescript error:
Argument of type '{ roles: string; type: "teacher"; teacher: { school_id: number; ... 9 more ...; }' is not assignable to parameter of type 'Optional<InferCreationAttributes<User, { omit: never; }>, NullishPropertiesOf<InferCreationAttributes<User, { omit: never; }>>>'.
Object literal may only specify known properties, and 'teacher' does not exist in type 'Optional<InferCreationAttributes<User, { omit: never; }>, NullishPropertiesOf<InferCreationAttributes<User, { omit: never; }>>>'.ts(2345)
I can't get a correct solution from the docs for nested creation. Should teacher be of type CreationOptional ? If yes, which type should it be to take Teacher optionals into account ?

TypeORM create a record with the ForeignKey

I have two entities questionnaires and sections. A questionnaire has multiple sections so the relation is OneToMany.
When I try to create a new record for the questionnaire - it works just fine. I get an id and then I create a section record and reference the id there.
Response, when I try to create a section, is this
{
"name": "Section 1",
"questionnaire_id": 1,
"description": "Section 1 description",
"id": 1
}
As you see it returns questionnaire_id back but the problem is that that record in DB is questionnaire_id=null. If I change it manually and assign an id value to it then it works and I get my JSON document with sections in it.
If I fetch all records from the DB then I get this
[
{
"id": 1,
"name": "Section 1",
"description": "Section 1 description"
}
]
questionnaire_id is not present because it's null. 😕.
Do you know what I might be doing wrong? I have a feeling it has something to do with Entity relation but not sure what exactly.
#Entity({ name: 'questionnaires' })
export class Questionnaire {
#PrimaryGeneratedColumn()
id: number;
#Column({ type: 'varchar', length: 128 })
name: string;
#Column({ type: 'enum', enum: STATUS, default: STATUS.ACTIVE })
status: STATUS;
#OneToMany(() => Section, (section) => section.questionnaire)
#JoinColumn({ name: 'questionnaire_id' })
sections: Section[];
}
#Entity({ name: 'sections' })
export class Section {
#PrimaryGeneratedColumn()
id: number;
#Column({ type: 'varchar', length: 128 })
name: string;
#Column({ type: 'text', default: '' })
description: string;
#ManyToOne(() => Questionnaire, (questionnaire) => questionnaire.sections)
questionnaire: Questionnaire;
}
The goal is to get all the questionnaires with their' sections.
The way I'm trying to achieve is this
#Injectable()
export class QuestionnaireService {
constructor(#InjectRepository(Questionnaire) private readonly questionnaire: Repository<Questionnaire>) {}
create(createQuestionnaireDto: CreateQuestionnaireDto) {
return this.questionnaire.save(createQuestionnaireDto);
}
findAll() {
return this.questionnaire.find({
relations: ['sections'],
});
}
}
#Injectable()
export class SectionService {
constructor(#InjectRepository(Section) private readonly sectionRepository: Repository<Section>) {}
create(createSectionDto: CreateSectionDto) {
return this.sectionRepository.save(createSectionDto);
}
findAll() {
return this.sectionRepository.find();
}
}
export class CreateSectionDto {
#ApiProperty({
description: 'Section name',
example: 'Section 1',
})
#IsString()
#IsNotEmpty()
#MaxLength(128)
name: string;
#ApiProperty({
description: 'Relation to the questionnaire',
example: 1,
})
#IsNumber()
questionnaire_id: number;
#ApiProperty({
description: 'Section description',
example: 'Section 1 description',
default: '',
})
#IsString()
#IsOptional()
#MaxLength(512)
description?: string;
}
p.s.
I checked this one TypeORM insert row with foreign key
but did not work for me.
EDIT:
This is what query is executed when running insert command
query: START TRANSACTION
query: INSERT INTO "sections"("created_at", "updated_at", "name", "description", "questionnaire_id") VALUES (DEFAULT, DEFAULT, $1, $2, DEFAULT) RETURNING "created_at", "updated_at", "id", "description" -- PARAMETERS: ["Section 1","Section 1 description"]
query: COMMIT
I have "created_at", "updated_at" fields too but I did not include in the examples above.
As it's visible it includes questionnaire_id but for some reason the value is set to DEFAULT. Not passing the one I'm sending via http request.
SOLUTION
The problem was my approach of creating records. Since I've set up a DB relation and the Section's table is dependent on the Questionnaire's table - TypeORM does not allow me to create sections records with my FK value. The library handles it automatically if I created Questionnaire first and assigned Sections object to it.
With the configration above the line below works just fine
async create(createQuestionnaireDto: CreateQuestionnaireDto) {
const questionnaire = this.questionnaire.create(createQuestionnaireDto);
const sections = new Section();
sections.name = 'Default Section';
sections.summary = 'Default Section Summary';
sections.questionnaire = questionnaire;
await this.questionnaire.save(questionnaire);
await this.section.save(sections);
return questionnaire;
}
And the query it is running is this
query: START TRANSACTION
query: INSERT INTO "sections"("created_at", "updated_at", "name", "summary", "questionnaire_id") VALUES (DEFAULT, DEFAULT, $1, $2, $3) RETURNING "created_at", "updated_at", "id", "summary" -- PARAMETERS: ["Default Section","Default Section Summary",1]
query: COMMIT
As you can see there is no DEFAULT for questionnaire_id.
If I wanted my initial approach to make work I should not be using TypeORM relation decorators, which is a pretty bad idea.
As I have not used TypeORM very often so I am not totally sure about the answer but you can check this out.
So you probably doing wrong in defining your Entity relation. You are using #JoinColumn in Questionnaire Entity.
It should be defined in Sections Entity like below:
#Entity({ name: 'sections' })
export class Section {
#PrimaryGeneratedColumn()
id: number;
#Column({ type: 'varchar', length: 128 })
name: string;
#Column({ type: 'text', default: '' })
description: string;
#ManyToOne(() => Questionnaire, (questionnaire) => questionnaire.sections)
#JoinColumn({ name: 'questionnaire_id' })
questionnaire: Questionnaire;
}
The answer is based on this document https://typeorm.io/relations#joincolumn-options. Hope it solves your issue.
EDIT : (not related might be deprecated but mentioned in official docs)
Are you sure loading relations this way works ?
findAll() {
return this.questionnaire.find({
relations: ['sections'],
});
}
As I can see in documents HERE, they are doing something else.
findAll() {
return this.questionnaire.find({
relations: {
sections: true,
},
});
}
EDIT 2:
So if you now add a Column into your section entity like this it will work.
#Entity({ name: 'sections' })
export class Section {
#PrimaryGeneratedColumn()
id: number;
#Column({ type: 'varchar', length: 128 })
name: string;
#Column({ type: 'text', default: '' })
description: string;
#Column({ type: 'number' })
questionnaire_id: number;
#ManyToOne(() => Questionnaire, (questionnaire) => questionnaire.sections)
#JoinColumn({ name: 'questionnaire_id' })
questionnaire: Questionnaire;
}
Let me know if this works.

Nestjs typeorm many to many order by count

I have two entities image and tag with many to many relationship
#Entity()
export class Image {
#PrimaryGeneratedColumn()
id: number;
#Column('text', { nullable: true })
caption: string;
#Column('text', { nullable: false })
url: string;
#Column('text', { nullable: false })
thumbnailUrl: string;
#ManyToOne(() => User, {
eager: true,
})
uploader: User;
#JoinTable()
#ManyToMany(() => Tag, (tag: Tag) => tag.images, {
cascade: true,
eager: true,
})
tags?: Tag[];
#CreateDateColumn()
created_at: Date;
#UpdateDateColumn()
updated_at: Date;
and Tag entity
#Entity()
export class Tag {
#PrimaryGeneratedColumn()
id: number;
#Column()
name: string;
#ManyToMany(() => Image, (image) => image.tags)
images: Image[];
}
Now I want to fetch popular tags. For that take 10 tags that have highest number of images created in last 7 days. For this i tried following in tag.repository.ts
getPopular() {
return (
this.createQueryBuilder('tag')
.addSelect((query) => {
return query
.select('COUNT(I.id)', 'count')
.from('image', 'I')
.where('I.created_at > :date', {
date: DateTime.now().minus({ weeks: 1 }).toISODate(),
});
}, 'imagecount')
.orderBy('imagecount', 'DESC')
.take(10)
.getMany()
);
But this will count all images that are created in last 7 days and will not consider if the image has that tag or not. So how should I load relation so that it count images with tags?
I would suggest you change your approach a little bit and try grouping after filtering by the last 7 days. Also, unless you have an explicit case where you will be adding images to a tag, I would recommend dropping the images property in your Tag entity since you already have a way to associate tags with an Image.
this.repository
.createQueryBuilder('image')
.where('image.created_at > :created_at', {
created_at: '2022-01-14 01:02:26', // make sure you set your own date here
})
.leftJoinAndSelect('image.tags', 'tag')
.groupBy('tag.id') // here is where we grup by the tag so we can count
.addGroupBy('tag.id')
.select('tag.id, count(tag.id)') // here is where we count :)
.orderBy('count(tag.id)', 'DESC')
.limit(10) // here is the limit
.execute();
These two sets of documentation could help you better in understanding how this query is built.
https://orkhan.gitbook.io/typeorm/docs/many-to-many-relations
https://github.com/typeorm/typeorm/blob/master/docs/select-query-builder.md#adding-group-by-expression

Storing GeoJson points and finding points within a given distance/radius | NODEJS, Postgres, NestJs, TypeOrm

Thank you in advance.
I have scavenged the internet for a working example/documentation for a way to store location point (longitude, latitude), find distance between two points, find points within a given distance.
I am using typeorm, nestjs, postgresql.
(I already tried Mariadb but St_distance_sphere is not working there so am going with postgresql)
this is my entity
#ApiProperty({
type: String,
title: 'current_location',
example: '{"type":"Point","coordinates":[28.612849, 77.229883]}',
})
#Index({ spatial: true })
#Column({
type: 'geometry',
srid: 4326,
nullable: true,
spatialFeatureType: 'Point',
transformer: {
to: (v: Point) => {
console.log(JSON.stringify(v));
return eval(`ST_GeomFromGeoJSON(${JSON.stringify(v)})`);
},
from: (v: any) => {
return { type: 'Point', coordinates: [v.x, v.y] } as Point;
},
},
})
current_location: string;
there seem to be too much postgres/postgis documentation but nothing useful for my case.
any help is much appreciated. I have been stuck on this for more than a week.
*note: I don't want to use JSONB datatype for its slower speed.
The following code will store in the DB and finds the locations within the range
Tech Stack nestJS,typeorm,postgres,postgis extension,#types/geojson
testlocation.entity.ts
import { Column, Entity, Index, PrimaryGeneratedColumn} from 'typeorm';
import { Geometry, Point } from 'geojson';
#Entity({ name: 't_test_location' })
export class TestLocation {
#PrimaryGeneratedColumn('increment')
pk_id: number;
#Column({ type: 'varchar', name: 's_city' })
city: string;
#Column({ type: 'double precision', name: 'd_lat' })
lat: number;
#Column({ type: 'double precision', name: 'd_long' })
long: number;
#Index({ spatial: true })
#Column({
type: 'geography',
spatialFeatureType: 'Point',
srid: 4326,
nullable: true,
})
location:Point
}
location.service.ts
import { Injectable } from '#nestjs/common';
import { InjectRepository } from '#nestjs/typeorm';
import { TestLocation } from 'src/model/testlocation.entity';
import { getManager, QueryBuilder, Repository } from 'typeorm';
import { Geometry, Point } from 'geojson';
#Injectable()
export class LocationService {
constructor(
#InjectRepository(TestLocation) private readonly repo: Repository<TestLocation>,
) {}
public async getAll() {
return await this.repo.find();
}
public async create(location:TestLocation){
const pointObject :Point= {
type: "Point",
coordinates: [location.long,location.lat]
};
location.location = pointObject;
return await this.repo.save(location)
}
public async getRange(lat:number,long:number,range:number = 1000) {
let origin = {
type: "Point",
coordinates: [long, lat]
};
let locations = await this.repo
.createQueryBuilder('t_test_location')
.select(['t_test_location.city AS city','ST_Distance(location, ST_SetSRID(ST_GeomFromGeoJSON(:origin), ST_SRID(location)))/1000 AS distance' ])
.where("ST_DWithin(location, ST_SetSRID(ST_GeomFromGeoJSON(:origin), ST_SRID(location)) ,:range)")
.orderBy("distance","ASC")
.setParameters({
// stringify GeoJSON
origin: JSON.stringify(origin),
range:range*1000 //KM conversion
})
.getRawMany();
return locations;
}
}
location.controller.ts
import { Body, Controller, Get, Post } from '#nestjs/common';
import { TestLocation } from 'src/model/testlocation.entity';
import { LocationService } from './location.service';
#Controller('location')
export class LocationController {
constructor(private serv: LocationService) {}
#Get()
public async getAll() {
return await this.serv.getAll();
}
#Post()
createLocation(#Body() location : TestLocation): void{
this.serv.create(location);
}
#Post('range')
public async getRange(#Body() location : {
lat:number,
long:number,
range:number
}){
return await this.serv.getRange(location.lat,location.long,location.range);
}
}

typeorm one-on-one entity not found on getManager.find

I have this typeorm entity:
#Entity({ name: 'trans' })
export class Transaction {
#PrimaryGeneratedColumn({ type: 'int', name: 'trans_id' })
id: number;
#OneToOne(type => Credit, credit => credit.transaction, { cascade: true })
credit: Credit
}
With the following relation:
#Entity({ name: 'trans_credit' })
export class Credit {
#PrimaryColumn({ type: 'int', name: 'trans_id' })
id: number;
#OneToOne(type => Transaction, transaction => transaction.credit)
#JoinColumn({ name: 'trans_id' })
transaction: Transaction;
}
There is no FK defined on the trans_credit table. Corresponding tables are:
CREATE TABLE `trans` (`trans_id` int(10) unsigned NOT NULL AUTO_INCREMENT) AUTO_INCREMENT=1
CREATE TABLE `trans_credit` (`trans_id` int(10) unsigned NOT NULL)
All entities and table declarations presented here are simplified of course.
When I try to query:
const fromEntity = getRepository(Transaction).find({
where: [
{ status: 'pending', credit: { j5: 1 } }
]
});
I get this:
Error: Relation with property path trans_id in entity was not found
Why oh why is this happening?
You specified your id column name of Credit as trans_id. And you defined the name of JoinColumn as trans_id too.
I think trans_id should be credit_id in your case:
#Entity({ name: 'trans_credit' })
export class Credit {
#PrimaryColumn({ type: 'int', name: 'credit_id' }) // <- updated the column name
id: number;
#OneToOne(type => Transaction, transaction => transaction.credit)
#JoinColumn({ name: 'trans_id' })
transaction: Transaction;
}

Resources