Loopback 4 - HasMany relation included in fields - postgresql

I am trying to setup the relation HasMany with the new Loopback 4 framework.
I have the following model:
import {Entity, model, property, belongsTo, hasMany} from
'#loopback/repository';
import {User} from "./user.model";
import {OrderProduct} from "./order-product.model";
#model({
name: 'sales_order'
})
export class Order extends Entity {
#property({
type: 'number',
id: true,
required: true,
})
id: number;
#property({
type: 'number',
required: true,
})
total_amount: number;
#belongsTo(() => User)
user_id: number;
#hasMany(() => OrderProduct, {keyTo: 'order_id'})
products?: OrderProduct[];
constructor(data?: Partial<Order>) {
super(data);
}
}
And the repository as follow:
import {DefaultCrudRepository, repository, HasManyRepositoryFactory, BelongsToAccessor} from '#loopback/repository';
import {Order, OrderProduct, User} from '../models';
import {DbDataSource} from '../datasources';
import {inject, Getter} from '#loopback/core';
import {OrderProductRepository} from "./order-product.repository";
import {UserRepository} from "./user.repository";
export class OrderRepository extends DefaultCrudRepository<
Order,
typeof Order.prototype.id
> {
public readonly user: BelongsToAccessor<
User,
typeof Order.prototype.id
>;
public readonly products: HasManyRepositoryFactory<
OrderProduct,
typeof Order.prototype.id
>;
constructor(
#inject('datasources.db') dataSource: DbDataSource,
#repository.getter(OrderProductRepository)
getOrderProductRepository: Getter<OrderProductRepository>,
#repository.getter('UserRepository')
userRepositoryGetter: Getter<UserRepository>,
) {
super(Order, dataSource);
this.products = this._createHasManyRepositoryFactoryFor(
'products',
getOrderProductRepository,
);
this.user = this._createBelongsToAccessorFor(
'user_id',
userRepositoryGetter,
);
}
}
When I do for example a get on orders, I have the errors: 500 error: column "products" does not exist and in digging a bit more, I can see that the SQL is trying to retrieve the fields products where it is just a relation.
Anybody have an idea if I am doing something wrong?
I am using pg as DB.

I believe this is a bug in LoopBack 4. When you decorate a class property with #hasMany, the decorator defines a model property under the hood. See here:
export function hasMany<T extends Entity>(
targetResolver: EntityResolver<T>,
definition?: Partial<HasManyDefinition>,
) {
return function(decoratedTarget: Object, key: string) {
property.array(targetResolver)(decoratedTarget, key);
// ...
};
}
When the connector is querying the database, it's trying to include the column products in the query, because it thinks products is a property.
The problem is already tracked by https://github.com/strongloop/loopback-next/issues/1909, please consider upvoting the issue and joining the discussion.

Related

Error retrieving data from DB using typeorm and type-graphql

I'm using type-graphql in conjunction with typeorm, apollo-server-express and postgreSQL. I have a User and a Customer entity in a 1:n relationship, meaning one user can have multiple customers.
I can create users and customers just fine, but when attempting to retrieve the user associated to a customer using Apollo Server playground, I get an error message stating "Cannot return null for non-nullable field Customer.user."
When I check the database, the associated user id on the customer table is definitely not null (see attached image).
query {
customers {
customerId
customerName
user {
userId
}
}
}
Does anyone know what I'm doing wrong?
User.ts
import { Field, ID, ObjectType } from "type-graphql";
import { BaseEntity, Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { Customer } from "./Customer";
#ObjectType()
#Entity("users")
export class User extends BaseEntity {
#Field(() => ID)
#PrimaryGeneratedColumn("uuid")
userId: string;
#Field()
#Column({ unique: true })
email: string;
#Column({ nullable: false })
password: string;
#Field(() => Customer)
#OneToMany(() => Customer, customer => customer.user)
customers: Customer[]
}
Customer.ts
import { Field, ID, ObjectType } from "type-graphql";
import { BaseEntity, Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { User } from "./User";
#ObjectType()
#Entity("customers")
export class Customer extends BaseEntity {
#Field(() => ID)
#PrimaryGeneratedColumn("uuid")
customerId: string;
#Field()
#Column()
customerName: string;
#Field(() => User)
#ManyToOne(() => User, user => user.customers)
user: User;
}
CustomerResolver.ts
export class CustomerResolver {
#Query(() => [Customer])
async customers():Promise<Customer[]> {
try {
return await Customer.find();
} catch (error) {
console.log(error);
return error;
}
}
....
Setup / Version
Node: v14.17.0
"apollo-server-express": "^2.24.0",
"type-graphql": "^1.1.1",
"typeorm": "0.2.32"
postgreSQL: 13.2
In your resolver change the find operation like below:
return Customer.find({
relations: ["user"]
});
You should write a #FieldResolver which will fetch customers based on root user data.
https://typegraphql.com/docs/resolvers.html#field-resolvers

How can i use longitude and latitude with typeorm and postgres

My current entity looks like this:
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
#Entity()
export class Landmark extends BaseEntity {
#PrimaryGeneratedColumn('uuid')
id: string;
#Column()
longitude: number
#Column()
latitude: number
}
But i wonder if there is a better way to do this, with a special postgres type, that works with typeorm.
You're going to want to look into PostGIS and Spatial Column support in Typeorm:
https://github.com/typeorm/typeorm/blob/master/docs/entities.md#spatial-columns
PostGIS is an extension you can enable in your postgres database for dealing with spatial datatypes. Once you have PostGIS installed, you can use its special spatial data functions inside of a Typeorm query builder as you would any other PG function, backed up by GeoJSON.
Typeorm's postgres driver uses GeoJSON internally to work with PostGIS, so when you're defining Typeorm models, you need to add #types/geojson, which will let you correctly type your Typeorm columns as you asked.
For instance, you can import the Geometry type definition and type your column as so:
import { Geometry } from 'geojson';
...
#Column
location: Geometry
In your case, you might want to combine your latitude and longitude columns into a single column -- location -- which can use the point() function to combine latitude and longitude into a single Geometry type.
As a contrived example, you could do something like:
UPDATE customers SET location = 'point(37.7, 122.4)' where id = 123;
This would set the location column on your customers table (as an example) to a geometry(point) column type corresponding to the lat/lon position of San Francisco.
If you wanted to migrate existing double precision column values for lat/lon (which is how you should store lat/lon on their own) to a single location column of type geometry(point), you could use the ST_MakePoint function that comes out of the box with PostGIS.
i.e.
-- Assuming you have a lat and lon columns on the `customers` table that you are migrating to the PostGIS geometry(point) type
UPDATE customers SET location = ST_MakePoint(lat, lon) where id = 123;
Extending JosephHall Answer
Used postgres,postgis,typeORM,#types/geojson, Nest JS
import { Column, Entity, Index, PrimaryGeneratedColumn} from 'typeorm';
import { Point } from 'geojson';
#Entity({ name: 't_test_location' })
export class TestLocation {
#PrimaryGeneratedColumn('increment')
pk_id: number;
#Column({ type: 'varchar', name: 's_city' })
city: string;
#Index({ spatial: true })
#Column({
type: 'geography',
spatialFeatureType: 'Point',
srid: 4326,
nullable: true,
})
location:Point
}
Service class
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)
}
Controller
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);
}
}

NestJS with mongoose schema, interface and dto approach question

I am new into nestJS and mongoDB and its not clear for me why do we need to declare DTO, schema and interface for each collection we want to save in our mongoDB. IE. I have a collection (unfortunately I've named it collection but it does not matter) and this is my DTO:
export class CollectionDto {
readonly description: string;
readonly name: string;
readonly expiration: Date;
}
interface:
import { Document } from 'mongoose';
export interface Collection extends Document {
readonly description: string;
readonly name: string;
readonly expiration: Date;
}
and schema:
import * as mongoose from 'mongoose';
export const CollectionSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
description: {
type: String,
required: false,
},
expiration: {
type: String,
required: true,
}
});
My doubt is that do we really need as many as three objects with almost the same contents? It looks strange at first sight.
I was working with mongoose a lot in plain nodejs basis and as well I'm starting to work with NestJS. Mongoose defines two things so that you can use mongodb to create, query, update and delete documents: Schema and Model. You already have your schema, and for model in plain mongoose should be as:
import * as mongoose from 'mongoose';
export const CollectionSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
description: {
type: String,
required: false,
},
expiration: {
type: String,
required: true,
}
});
const Collection = mongoose.model('collections', CollectionSchema);
Collection here will be mongoose model. So far so good.
In NestJs, and if you are going to follow API best practices, you will use a DTO (Data Transfer Object). NestJs in doc mention that is preferable to use classes than interfaces, so you don't need interfaces here. When you define Mongoose schema, you can also define Model/Schema:
import { Prop, Schema, SchemaFactory } from '#nestjs/mongoose';
import { Document } from 'mongoose';
export type CollectionDocument = Collection & Document;
#Schema()
export class Collection {
#Prop()
name: string;
#Prop()
description: number;
#Prop()
expiration: string;
}
export const CollectionSchema = SchemaFactory.createForClass(Collection);
And for your services and controllers you use both (model and DTO):
import { Model } from 'mongoose';
import { Injectable } from '#nestjs/common';
import { InjectModel } from '#nestjs/mongoose';
import { Collection, CollectionDocument } from './schemas/collection.schema';
import { CollectionDto } from './dto/collection.dto';
#Injectable()
export class CollectionService {
constructor(#InjectModel(Collection.name) private collectionModel: Model<CollectionDocument>) {}
async create(createColDto: CollectionDto): Promise<Collection> {
const createdCollection = new this.collectionModel(createColDto);
return createdCollection.save();
}
async findAll(): Promise<Collection[]> {
return this.collectionModel.find().exec();
}
}
After this, you can user Swagger to automatic doc of your APIs.
NestJS Mongo Techniques

Why DTOs are not throwing Validation error in nestjs?

I am using DTO in my code, and I am getting the response as expected but in code DTOs are not throwing error for example
export class CreateCatDto {
readonly name: string;
readonly age: number;
readonly breed: string;
}
In this name, age, the breed is a required field and each has their data type but while running on the postman when I am not passing all the required field or only one field into postman body I am not getting any errors like age is required if I have passed other two fields or I have given value of the parameter not according to data type like:- age : twenty five then also it should throw error but I am not getting.
So, This is class created for
import { ApiProperty } from '#nestjs/swagger';
export class Cat {
#ApiProperty({ example: 'Kitty', description: 'The name of the Cat' })
name: string;
#ApiProperty({ example: 1, description: 'The age of the Cat' })
age: number;
#ApiProperty({
example: 'Maine Coon',
description: 'The breed of the Cat',
})
breed: string;
}
This is controller in which I am importing class and Dto.
import { Body, Controller, Get, Param, Post } from '#nestjs/common';
import {
ApiBearerAuth,
ApiOperation,
ApiResponse,
ApiTags,
} from '#nestjs/swagger';
import { CatsService } from './cats.service';
import { Cat } from './classes/cat.class';
import { CreateCatDto } from './dto/create-cat.dto';
#ApiBearerAuth()
#ApiTags('cats')
#Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
#Post()
#ApiOperation({ summary: 'Create cat' })
#ApiResponse({ status: 403, description: 'Forbidden.' })
async create(#Body() createCatDto: CreateCatDto): Promise<Cat> {
return this.catsService.create(createCatDto);
}
}
I don't know why you selected nestjs-swagger tag, DTO by itself will not validate inputs, maybe you need to use a ValidationPipe with the class-validator package as suggested on docs https://docs.nestjs.com/techniques/validation#validation
It's as simple as putting a decorator on your code now:
import { IsEmail, IsNotEmpty } from 'class-validator';
export class CreateCatDto {
#IsNotEmpty()
#IsString()
readonly name: string;
#IsNotEmpty()
#IsInt()
readonly age: number;
#IsNotEmpty()
readonly breed: string;
You can see all the items here: https://github.com/typestack/class-validator#validation-decorators
And if you want to sanitize the request body, you should use a serializer to help:
https://docs.nestjs.com/techniques/serialization#serialization
This will show or hide your DTO properties based on decorators of each field. You need to install class-transformer package.
import { Exclude } from 'class-transformer';
export class UserEntity {
id: number;
firstName: string;
lastName: string;
#Exclude()
password: string;
constructor(partial: Partial<UserEntity>) {
Object.assign(this, partial);
}
}
It's important to remember that interceptors will run on your request and response.

TypeORM: [364] ERROR: missing FROM-clause entry for table "userorganisation" at character 401

I just hit a pretty interesting problem when using TypeORM and joining tables that I've set up for a Postgres database. I figured it out but thought I'd post the info here for anyone else that had a similar problem.
I have 3 tables set up on my database: user, organisation, user_organisation.
The idea for this is that a user can belong to many organisations and the table called user_organisation maps users to these organisations. So my entities look like this,
user.entity.ts
import { TimestampedEntity } from '#shared/entities/timestamped.entity';
import { Organisation } from '#user/entities/organisation.entity';
import { UserOrganisation } from '#user/entities/user-organisation.entity';
import { Column, Entity, Index, JoinTable, ManyToMany, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
#Entity()
#Index(['email', 'password'])
export class User extends TimestampedEntity {
#PrimaryGeneratedColumn()
userId: number;
#Column({
length: 64
})
username: string;
#Column({
length: 500
})
email: string;
#Column({
length: 255
})
password: string;
#Column({
type: 'json',
})
details: any;
#Column({
nullable: true
})
refreshToken: string;
#OneToMany(type => UserOrganisation, userOrganisation => userOrganisation.user)
#JoinTable()
userOrganisations: UserOrganisation[];
}
user-organisation.entity.ts
import { Organisation } from '#user/entities/organisation.entity';
import { User } from '#user/entities/user.entity';
import { Column, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm';
#Entity()
export class UserOrganisation {
#ManyToOne(type => User, user => user.userOrganisations, { primary: true })
user: User;
#ManyToOne(type => Organisation, organisation => organisation.userOrganisations, { primary: true })
organisation: Organisation;
}
organisation.entity.ts
import { TimestampedEntity } from '#shared/entities/timestamped.entity';
import { UserOrganisation } from '#user/entities/user-organisation.entity';
import { User } from '#user/entities/user.entity';
import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
#Entity()
export class Organisation extends TimestampedEntity {
#PrimaryGeneratedColumn()
orgId: number;
#Column({
length: 255
})
name: string;
#Column({
type: 'json'
})
details: any;
#OneToMany(type => UserOrganisation, userOrganisation => userOrganisation.organisation)
userOrganisations: UserOrganisation[];
}
I was then trying to run the following query,
this.userRepository.createQueryBuilder('user')
.where('user.email = :email', { email })
.innerJoin(UserOrganisation, 'userOrganisation', 'user.userId = userOrganisation.userUserId')
.getOne();
And this is the error message I got,
ERROR: missing FROM-clause entry for table "userorganisation" at character 401
The final query printed out like this,
SELECT "user"."createdAt" AS "user_createdAt"
, "user"."updatedAt" AS "user_updatedAt"
, "user"."userId" AS "user_userId"
, "user"."username" AS "user_username"
, "user"."email" AS "user_email"
, "user"."password" AS "user_password"
, "user"."details" AS "user_details"
, "user"."refreshToken" AS "user_refreshToken"
FROM "user" "user"
INNER JOIN "user_organisation" "userOrganisation"
ON "user"."userId" = userOrganisation.userUserId
WHERE "user"."email" = $1
The way I fixed it is described below.
What I noticed in the query,
SELECT "user"."createdAt" AS "user_createdAt", "user"."updatedAt" AS "user_updatedAt", "user"."userId" AS "user_userId", "user"."username" AS "user_username", "user"."email" AS "user_email", "user"."password" AS "user_password", "user"."details" AS "user_details", "user"."refreshToken" AS "user_refreshToken" FROM "user" "user" INNER JOIN "user_organisation" "userOrganisation" ON "user"."userId" = userOrganisation.userUserId WHERE "user"."email" = $1
Was that there was a difference between the user table and the userOrganisation table in the join criteria,
"user"."userId" = userOrganisation.userUserId
The user table was automatically wrapped in quotation marks but userOrganisation was not... So I changed my code to the following,
this.userRepository.createQueryBuilder('user')
.where('user.email = :email', { email })
.innerJoin(UserOrganisation, 'userOrganisation', '"user"."userId" = "userOrganisation"."userUserId"')
.getOne();
If you look above, I've added the quotation marks in the join criteria. It's all working now :)
Hope this helps!
Well done. However when using joins in typeorm you have to write the conditions as they are defined then setting objects.
this.userRepository.createQueryBuilder('user')
.where('user.email = :email', { email })
.innerJoin(UserOrganisation, 'userOrganisation', 'user.userId=userOrganisation.user.userId')
.getOne();
With this you do not need to add quotation marks.
The generated sql is not showing the quotes because it does not know how to populate the conditions.
I hope this helps.