Cross schema joins with typeorm - postgresql

Steps to reproduce:
Create 2 schema a and b
Create tables user, photo in both schema with these respective entities UserA PhotoA, UserB PhotoB
// PhotoA
import {Entity, Column, PrimaryGeneratedColumn, ManyToOne} from "typeorm";
import { User as UserB } from "./User.b";
import { User as UserA } from "./User.a";
#Entity({schema: "a"})
export class Photo {
#PrimaryGeneratedColumn()
id: number;
#Column({
length: 100
})
name: string;
#ManyToOne(type => UserA)
userA: UserA;
#ManyToOne(type => UserB)
userB: UserB;
}
// UserB
import {Entity, Column, PrimaryGeneratedColumn, OneToMany} from "typeorm";
import { Photo as PhotoA } from "./Photo.a";
import { Photo as PhotoB } from "./Photo.b";
#Entity({schema: "b"})
export class User {
#PrimaryGeneratedColumn()
id: number;
#Column({
length: 100
})
name: string;
#OneToMany(type => PhotoA, photo => photo.userB)
photosA: PhotoA[]
#OneToMany(type => PhotoB, photo => photo.userB)
photosB: PhotoB[]
}
Run this code
import "reflect-metadata";
import * as typeorm from "typeorm";
import { Photo as PhotoA } from "./entities/Photo.a";
import { User as UserB } from "./entities/User.b";
import { PostgresConnectionOptions } from "typeorm/driver/postgres/PostgresConnectionOptions";
import { Photo as PhotoB } from "./entities/Photo.b";
import { User as UserA } from "./entities/User.a";
class Inl {
public async test() {
const connection = await typeorm.createConnection({
type: "postgres",
host: "localhost",
port: 5433,
username: "test",
password: "test",
database: "test",
synchronize: true,
logging: true,
entities: [ PhotoA, PhotoB, UserA, UserB ]
} as PostgresConnectionOptions);
const photoARepo = connection.getRepository(PhotoA);
const userBRepo = connection.getRepository(UserB);
const userBRow = new UserB();
userBRow.name = "User in schema B";
const userBSavedRow = await userBRepo.save(userBRow);
const photoARow = new PhotoA();
photoARow.name = "Photo in schema A";
photoARow.userB = userBSavedRow;
await photoARepo.save(photoARow);
const photoBRow = new PhotoB();
photoBRow.name = "Photo in schema B";
photoBRow.userB = userBSavedRow;
await photoARepo.save(photoARow);
const result = await userBRepo
.createQueryBuilder("userB")
.select("*")
.leftJoinAndSelect("a.photo", "photosA")
.leftJoinAndSelect("b.photo", "photosB")
.where({id: userBSavedRow.id})
.getOne();
console.log(result);
}
}
new Inl().test();
RESULTS
query: INSERT INTO "a"."photo"("name", "userAId", "userBId") VALUES ($1, DEFAULT, $2) RETURNING "id" -- PARAMETERS: ["Photo in schema A",6]
query: COMMIT
query: SELECT "Photo"."id" AS "Photo_id", "Photo"."name" AS "Photo_name", "Photo"."userAId" AS "Photo_userAId", "Photo"."userBId" AS "Photo_userBId" FROM "a"."photo" "Photo" WHERE "Photo"."id" IN ($1) -- PARAMETERS: [6]
(node:527) UnhandledPromiseRejectionWarning: Error: "a" alias was not found. Maybe you forgot to join it?
at QueryExpressionMap.findAliasByName (/home/lewis/Projects/internationalisation/src/query-builder/QueryExpressionMap.ts:341:19)
at JoinAttribute.getValue (/home/lewis/Projects/internationalisation/src/query-builder/JoinAttribute.ts:146:72)
at JoinAttribute.get [as relation] (/home/lewis/Projects/internationalisation/src/query-builder/JoinAttribute.ts:162:53)
at JoinAttribute.get [as metadata] (/home/lewis/Projects/internationalisation/src/query-builder/JoinAttribute.ts:175:18)
at SelectQueryBuilder.join (/home/lewis/Projects/internationalisation/src/query-builder/SelectQueryBuilder.ts:1299:27)
at SelectQueryBuilder.leftJoin (/home/lewis/Projects/internationalisation/src/query-builder/SelectQueryBuilder.ts:284:14)
at SelectQueryBuilder.leftJoinAndSelect (/home/lewis/Projects/internationalisation/src/query-builder/SelectQueryBuilder.ts:364:14)
at Inl.test (/home/lewis/Projects/internationalisation/index.ts:42:14)
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:160:7)
As you can see from the log, I'm having the error alias was not found with the above code. Anybody has hints on this?

The problem here is that your are mixing up schema and alias in leftJoinAndSelect (the schema is resolved by TypeOrm, you don't need to specify this in your query if your entity is correctly configured). So this should work:
const result = await userBRepo
.createQueryBuilder("userB")
.leftJoinAndSelect("userB.photosA", "photosA")
.leftJoinAndSelect("userB.photosB", "photosB")
.where({id: userBSavedRow.id})
.getOne();

Related

Mongoose Schema properties validation with Typescript NextJS

i am trying to save new document to mongo db, the Schema validation is not working for me, i am trying ti make required true, but i still can add new document without the required field.
this is my schema:
// lib/models/test.model.ts
import { Model, Schema } from 'mongoose';
import createModel from '../createModel';
interface ITest {
first_name: string;
last_name: string;
}
type TestModel = Model<ITest, {}>;
const testSchema = new Schema<ITest, TestModel>({
first_name: {
type: String,
required: [true, 'Required first name'],
},
last_name: {
type: String,
required: true,
},
});
const Test = createModel<ITest, TestModel>('tests', testSchema);
module.exports = Test;
this is createModel:
// lib/createModel.ts
import { Model, model, Schema } from 'mongoose';
// Simple Generic Function for reusability
// Feel free to modify however you like
export default function createModel<T, TModel = Model<T>>(
modelName: string,
schema: Schema<T>
): TModel {
let createdModel: TModel;
if (process.env.NODE_ENV === 'development') {
// In development mode, use a global variable so that the value
// is preserved across module reloads caused by HMR (Hot Module Replacement).
// #ts-ignore
if (!global[modelName]) {
createdModel = model<T, TModel>(modelName, schema);
// #ts-ignore
global[modelName] = createdModel;
}
// #ts-ignore
createdModel = global[modelName];
} else {
// In production mode, it's best to not use a global variable.
createdModel = model<T, TModel>(modelName, schema);
}
return createdModel;
}
and this is my tests file:
import { connection } from 'mongoose';
import type { NextApiRequest, NextApiResponse } from 'next';
const Test = require('../../../lib/models/test.model');
import { connect } from '../../../lib/dbConnect';
const ObjectId = require('mongodb').ObjectId;
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
switch (req.method) {
case 'POST': {
return addPost(req, res);
}
}
}
async function addPost(req: NextApiRequest, res: NextApiResponse) {
try {
connect();
// const { first_name, last_name } = req.body;
const test = new Test({
first_name: req.body.first_name,
last_name: req.body.last_name,
});
let post = await test.save();
// return the posts
return res.json({
message: JSON.parse(JSON.stringify(post)),
success: true,
});
// Erase test data after use
//connection.db.dropCollection(testModel.collection.collectionName);
} catch (err) {
//res.status(400).json(err);
res.status(400).json({
message: err,
success: false,
});
}
}
in the Postman, i send a request body without the required field (first_name) and i still can add it.
any help?

typeOrm does not create entries in schema

I am using nestjs with typeOrm with Postgresql.
I am looking to save all data from the app in a different schema to 'public' as this is well known.
I have schema specified in an options file as below:
import { TypeOrmModuleOptions } from '#nestjs/typeorm';
export const typeOrmConfig: TypeOrmModuleOptions = {
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'xxxx',
password: 'xxx',
database: 'taskmanagement',
autoLoadEntities: true,
synchronize: true,
schema: 'sh',
};
I create the task as below:
async createTask(createTaskDto: CreateTaskDto): Promise<Task> {
// create directly using the entity
const { title, description } = createTaskDto;
const task = new Task();
task.title = title;
task.description = description;
task.status = TaskStatus.OPEN;
await task.save();
return task;
}
The Task entity is defined as:
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { TaskStatus } from './task-status.enum';
#Entity()
export class Task extends BaseEntity {
// #PrimaryGeneratedColumn('uuid')
// id: string;
#PrimaryGeneratedColumn()
id: number;
#Column()
title: string;
#Column()
description: string;
#Column()
status: TaskStatus;
}
The record is saved to the schema 'public', not my custom schema 'sh'.
I would expect typeOrm to use the schema from the config given that it writes to DB specified in the config options file.
Is this a bug with nestjs/typeOrm or am I missing a setting?
Run build script - this seems to get the revised config file to be used.

Insert into table via relation id's

Below I have the Equipt entity it has three columns createdById, tribeId, userId.
I am trying to save a new row using the id's of each entity, and not the entities themselves:
This doesn't work:
let e = connection.getRepository(Equipt);
const check = await e.save({
userId: 1,
tribeId: 1,
createdById: 1,
})
This works:
let e = connection.getRepository(Equipt);
const check = await e.save({
user: user,
tribe: tribe,
createdBy: adminUser,
})
Entity:
import {ManyToOne, RelationId, JoinColumn, Entity} from "typeorm";
import {User} from './User';
import { Base } from "../base";
import { Tribe } from "./Tribe";
#Entity('Equipts')
export class Equipt extends Base {
#ManyToOne(type => User, { nullable: false })
#JoinColumn()
createdBy: User;
#RelationId((Equipt: Equipt) => Equipt.createdBy)
createdById: number;
#ManyToOne(type => Tribe, { nullable: false })
#JoinColumn()
Tribe: Tribe;
#RelationId((Equipt: Equipt) => Equipt.Tribe)
TribeId: number;
#ManyToOne(type => User, { nullable: false })
#JoinColumn()
user: User;
#RelationId((Equipt: Equipt) => Equipt.user)
userId: number;
}
Is there any way to insert using id's without having the pass the entire entity?

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.

Relationships GraphQL

The second week I try to link two collections in the apollo-server-express / MongoDB / Mongoose / GraphQL stack, but I do not understand how. I found a similar lesson with the REST API, what I need is called Relationships. I need this, but in GraphQL
watch video
How to add cars to the User?
I collected the test server, the code is here: https://github.com/gHashTag/test-graphql-server
Help
I have cloned your project and implemented some code and here what I changed to make relationship works. Note, I just did a basic code without validation or advance dataloader just to make sure non-complexity. Hope it can help.
src/graphql/resolvers/car-resolvers.js
import Car from '../../models/Car'
import User from '../../models/User'
export default {
getCar: (_, { _id }) => Car.findById(_id),
getCars: () => Car.find({}),
getCarsByUser: (user, {}) => Car.find({seller: user._id }), // for relationship
createCar: async (_, args) => {
// Create new car
return await Car.create(args)
}
}
src/graphql/resolvers/user-resolvers.js
import User from '../../models/User'
export default {
getUser: (_, { _id }) => User.findById(_id),
getUsers: () => User.find({}),
getUserByCar: (car, args) => User.findById(car.seller), // for relationship
createUser: (_, args) => {
return User.create(args)
}
}
src/graphql/resolvers/index.js
import UserResolvers from './user-resolvers'
import CarResolvers from './car-resolvers'
export default {
User:{
cars: CarResolvers.getCarsByUser // tricky part to link query relation ship between User and Car
},
Car:{
seller: UserResolvers.getUserByCar // tricky part to link query relation ship between User and Car
},
Query: {
getUser: UserResolvers.getUser,
getUsers: UserResolvers.getUsers,
getCar: CarResolvers.getCar,
getCars: CarResolvers.getCars
},
Mutation: {
createUser: UserResolvers.createUser,
createCar: CarResolvers.createCar,
}
}
src/graphql/schema.js
export default`
type Status {
message: String!
}
type User {
_id: ID!
firstName: String
lastName: String
email: String
cars: [Car]
}
type Car {
_id: ID
make: String
model: String
year: String
seller: User
}
type Query {
getUser(_id: ID!): User
getUsers: [User]
getCar(_id: ID!): Car
getCars: [Car]
}
type Mutation {
createUser(firstName: String, lastName: String, email: String): User
// change from _id to seller, due to base on logic _id conflict with CarId
createCar(seller: ID!, make: String, model: String, year: String): Car
}
schema {
query: Query
mutation: Mutation
}
`
src/middlewares.js
import bodyParser from 'body-parser'
import { graphqlExpress, graphiqlExpress } from 'apollo-server-express'
import { makeExecutableSchema } from 'graphql-tools'
import typeDefs from '../graphql/schema'
import resolvers from '../graphql/resolvers'
import constants from './constants'
export const schema = makeExecutableSchema({
typeDefs,
resolvers
})
export default app => {
app.use('/graphiql', graphiqlExpress({
endpointURL: constants.GRAPHQL_PATH
}))
app.use(
constants.GRAPHQL_PATH,
bodyParser.json(),
graphqlExpress(req => ({
schema,
context: {
event: req.event
}
}))
)
}
try to make something like this in your car resolver
export default {
getCar: ({ _id: ownId }, { _id }) =>
Car.findById(ownId || _id);
// here is the rest of your code
You need to add a resolver for the cars field on the User type.
const resolvers = {
Query: {
getUsers: ...
getCars: ...
...
},
Mutation: {
...
},
User: {
cars: ...
}
}