Problem: a company can associate records that are not theirs, just by informing the id in the relationship
Scenario: My api is multi-company and each company manage your records (products, orders...), but these companies can only view and use records that belong to them. However, it is possible to enter an id of a product of another company in an order and should not
Example: The Apple company can only inform the iPhone product in its orders, if it tried to inform the Google Pixel should have some error message
Observations: I hope I do not need to validate each record associated with the order before saving because my actual schema has multiple relationships (20 tables associated with a order, some 2 level cascade) and this is only small example. It would be very complicated to search the database for each product and verify that it belongs to the company. I think it would be possible to prevent this recording with some index setting
Question: How can I prevent associating records from other companies with current company records, even reporting the id on body request?
I would appreciate your help, since I have a system in production with this situation.
company.entity.ts
#Entity()
export class Company {
#PrimaryGeneratedColumn()
id: number;
#Column()
name: string;
#OneToMany(() => Product, (product) => product.company)
products: Product[];
#OneToMany(() => Order, (order) => order.company)
orders: Order[];
}
product.entity.ts
#Entity()
export class Product {
#PrimaryGeneratedColumn()
id: number;
#Column()
description: string;
#ManyToOne(() => Company, (company) => company.products)
company: Company;
}
order.entity.ts
#Entity()
export class Order {
#PrimaryGeneratedColumn()
id: number;
#Column()
description: string;
#ManyToOne(() => Company, (company) => company.orders)
company: Company;
#ManyToMany(() => Product)
#JoinTable()
products: Product[];
}
order.controller.ts
#Controller('orders')
export class OrderController {
constructor(
#InjectRepository(Order)
public repository: Repository<Order>,
) { }
#Post()
public async create(#Body() body: Order): Promise<Order> {
body.id = undefined;
body = await this.repository.save(body);
return await this.repository.findOne(body.id, { relations: ['products'] });
}
#Put(':id')
public async update(#Param('id') id, #Body() body: Order): Promise<Order> {
body.id = Number(id);
await this.repository.save(body);
return await this.repository.findOne(id, { relations: ['products'] });
}
}
Company table
id: 1 description: Apple
id: 2 description: Google
Item table
id: 1 description: iPhone companyId: 1
id: 2 description: Google Pixel companyId: 2
POST http://localhost:3040/orders
Body Request:
Obs.: here the user informs the iPhone which is a product that he knows, and forces the product id 2 being an unknown product, because api GET validate which products are visible to Apple, in this case only the iPhone
{
"company": 1,
"description": "Order 001",
"products": [
{
"id": 1,
"description": "iPhone"
},
{
"id": 2
}
]
}
Body Response:
Obs.: even informing the product of another company, on the return of the api is presented the Google Pixel that could not be recorded at Apple's order. Here you should have an error message
{
"id": 2,
"description": "Order 001",
"products": [
{
"id": 1,
"description": "iPhone"
},
{
"id": 2,
"description": "Google Pixel"
}
]
}
Result Tables
Order table
id: 1 description: Order 001 companyId: 1
Order Product table
orderId: 2 productId: 1
orderId: 2 productId: 2
There are two issues to solve. The first is that the many-to-many relationship between order and product does not limit the product's company. You can probably do that using the right decorators, I believe you can specify table and column information in your #JoinTable decorator.
The second issue is there needs to be a way to associate API caller with the company they belong to. You can do this by using a JWT for authentication and embed the company id in the JWT's payload.
Hope this helps.
Related
I am facing an error with Prisma, it does not recognize my request which seems quite simple to me. I need to use "select" to retrieve only certain fields from the table.
Post model:
model Post {
id String #id #default(cuid())
title String
createdAt DateTime? #default(now())
categories CategoriesOnPosts[]
keywords KeywordsOnPosts[]
##map("posts")
}
Category model:
model Category {
id String #id #default(cuid())
name String
createdAt DateTime? #default(now())
posts CategoriesOnPosts[]
##map("categories")
}
CategoriesOnPosts model:
model CategoriesOnPosts {
postId String
categoryId String
post Post #relation(fields: [postId], references: [id])
category Category #relation(fields: [categoryId], references: [id])
##id([postId, categoryId])
##map("categoriesPosts")
}
My Prisma query:
export const getPosts = async () =>
await prisma.post.findMany({
select: {
id: true,
title: true,
categories: {
select: {
name: true,
slug: true,
},
},
createdAt: true,
},
orderBy: [
{
createdAt: 'desc',
},
],
});
I get the following error and I don't know how to fix it.
Unknown field categories for select statement on model Post.
Available options are listed in green.
im in trouble to find a way to make this, i want in "serieName" the name of the serie, but always return de id, i dont know what i need to do to make this relation correct, im trying to find a solution, but nothing works.
this is my models:
in Series model, i have a name, and in Figures_table i made a relation with him, and i want to get only the name when i pass the id for the field, so if Series have id: 1, name: "something", i want to show "something" not the id, but only show id.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model Series {
id Int #unique #default(autoincrement())
serie String
Figures_table Figures_table[]
}
model Manufacturers {
id String #unique
manufacturer String
Figures_table Figures_table[]
}
model Figures_table {
id String #unique
name String #db.LongText
category String #db.LongText
price String #db.LongText
specifications String #db.LongText
releaseInfo String #db.LongText
details String #db.LongText
serieName Int
manufacturerName String #db.VarChar(191)
createdAt DateTime #default(now())
manufacturer Manufacturers #relation(fields: [manufacturerName], references: [id])
series Series #relation(fields: [serieName], references: [id])
Images Images[]
}
model Images {
id Int #unique #default(autoincrement())
link String
figureID String
figureReferenceID Figures_table #relation(fields: [figureID], references: [id])
}
Here's a script which would include the name of the Series while fetching a record from the figures_table
import { PrismaClient, Prisma } from '#prisma/client';
const prisma = new PrismaClient({
log: ['query', 'info', 'warn'],
});
async function main() {
await prisma.figures_table.create({
data: {
category: 'Figures Table Category',
details: 'Figures Table Details',
name: 'Figures Table Name',
id: '1',
price: '1',
releaseInfo: 'Figures Table Release Info',
specifications: 'Figures Table Specifications',
series: {
create: {
serie: 'Series 1',
},
},
manufacturer: {
create: {
manufacturer: 'Manufacturer 1',
id: '1',
},
},
},
});
const figures_table = await prisma.figures_table.findUnique({
where: {
id: '1',
},
include: {
series: true,
},
});
console.log(figures_table);
}
main()
.catch((e) => {
throw e;
})
.finally(async () => {
await prisma.$disconnect();
});
We used the include section to get the details of Series relation.
Response:
{
id: '1',
name: 'Figures Table Name',
category: 'Figures Table Category',
price: '1',
specifications: 'Figures Table Specifications',
releaseInfo: 'Figures Table Release Info',
details: 'Figures Table Details',
serieName: 1,
manufacturerName: '1',
createdAt: 2022-10-11T12:33:37.768Z,
series: { id: 1, serie: 'Series 1' }
}
I have the following User and Group models that share a many-to-many relation:
model User {
id String #id #default(uuid())
email String #unique
groups UsersGroups[]
##map("user")
}
model Group {
id String #id #default(uuid())
name String
users UsersGroups[]
##map("group")
}
model UsersGroups {
user User #relation(fields: [userId], references: [id])
userId String #map(name: "user_id")
group Group #relation(fields: [groupId], references: [id])
groupId String #map(name: "group_id")
##id([userId, groupId])
##map("users_groups")
}
I'm having trouble using the connect API in Prisma to connect the users and groups. Here's what I have:
await prisma.group.update({
where: {
id: groupId,
},
data: {
users: {
connect: users.map((user) => ({ id: user.id })),
},
},
include: { users: true },
});
That doesn't work and here is the error I'm getting in the console:
PrismaClientValidationError:
Invalid `prisma.group.update()` invocation:
{
where: {
id: '64ce24c7-3054-42f2-b49f-4cdb52cf1bc7'
},
data: {
users: {
connect: [
{
id: '0b3f4a51-0efe-4b0a-8763-e71bc8091b86'
~~
}
]
}
},
include: {
users: true
}
}
Unknown arg `id` in data.users.connect.0.id for type UsersGroupsWhereUniqueInput. Available args:
type UsersGroupsWhereUniqueInput {
userId_groupId?: UsersGroupsUserIdGroupIdCompoundUniqueInput
}
From that above, it looks as though it's attempting to connect a user with id: '0b3f4a51-0efe-4b0a-8763-e71bc8091b86' (which is a user that exists) to the group with id: '64ce24c7-3054-42f2-b49f-4cdb52cf1bc7' (which also exists).
I'd be very grateful if someone could point out where I'm going wrong as I've been going in circles with this for a while now...
You are using an explicit many-to-many relation, cf. https://www.prisma.io/docs/concepts/components/prisma-schema/relations/many-to-many-relations#explicit-many-to-many-relations
I.e. you have defined the model UsersGroups yourself.
As a consequence, you would have to manage/create the records in this table yourself and connect it with the entry in the third table, e.g. like this (haven't tested it):
prisma.group.update({
where: {
id: groupId,
},
data: {
users: { create: { user: { connect: { id: userId } } } },
},
include: { users: true },
});
or if you want to loop over an list:
prisma.group.update({
where: {
id: groupId,
},
data: {
users: {
create: users.map((user) => ({
user: { connect: { id: user.id } },
})),
},
},
include: { users: true },
});
I would suggest to replace groups UsersGroups[] and users UserGroups[] with
userGroups UsersGroups[] in the schema to make it clearer.
As an alternative to explicit relationships you could try to use implicit many-to-many relations in the schema like this:
model User {
id String #id #default(uuid())
email String #unique
groups Group[]
##map("user")
}
model Group {
id String #id #default(uuid())
name String
users User[]
##map("group")
}
cf. https://www.prisma.io/docs/concepts/components/prisma-schema/relations/many-to-many-relations#implicit-many-to-many-relations
I am usingNESTJSWITHMYSQL`
I am trying to fetch the relational data of an table.
I've two columns related to a common entity.
while I try to fetch data I got follow errors.
am not able to understood how to fix this one.
Any help would be appriciated
{
"exception": {
"message": "ER_NONUNIQ_TABLE: Not unique table/alias: 'users'",
"code": "ER_NONUNIQ_TABLE",
"errno": 1066,
"sqlMessage": "Not unique table/alias: 'users'",
"sqlState": "42000",
"index": 0,}
}
Entities are as following
#Entity('leave')
export class LeaveEntity {
#PrimaryGeneratedColumn('increment')
id: number;
// relation between approver and user
#ManyToOne(() => UserEntity, u => u.id, {
eager: false,
cascade: false,
nullable: true
})
approvedBy: UserEntity[];
// relation between leave and users
#ManyToOne(() => UserEntity, user => user.id, {
eager: false,
cascade: false,
nullable: false
})
user: UserEntity[];
}
SERVICE CODE TO FETCH DATA
const qb = getRepository(LeaveEntity)
.createQueryBuilder("leave")
.skip(size * (page - 1))
.take(size)
.orderBy('leave.created_at', orderBy)
.leftJoinAndSelect("leave.approvedBy", "users")
.leftJoinAndSelect("leave.user", "users")
.select(['leave', 'users.email', 'users.id', 'users.firstName', 'users.lastName'])
const [items, total] = await qb.getManyAndCount();
return {items, total, page: +page, totalPages: Math.ceil(total/size)}
Because you name it same, take a look at your query builder:
.leftJoinAndSelect("leave.approvedBy", "users")
.leftJoinAndSelect("leave.user", "users")
So at the end your query will end up with smth like this:
SELECT * FROM leave LEFT JOIN users AS users LEFT JOIN users AS users ...
To fix that you have to name it different, fe:
.leftJoinAndSelect("leave.approvedBy", "apu") // as an approved users
.leftJoinAndSelect("leave.user", "users")
and then use your apu. prefix in rest of the query
After looking for a while I found a solution:
The second argument of leftJoinAndSelect() is an alias so you have to use different name let say usersA and usersB:
const qb = getRepository(LeaveEntity)
.createQueryBuilder("leave")
.skip(size * (page - 1))
.take(size)
.orderBy('leave.created_at', orderBy)
.leftJoinAndSelect("leave.approvedBy", "usersA")
.leftJoinAndSelect("leave.user", "usersB")
.select(
[
'leave',
'usersA.email',
'usersA.id',
'usersA.firstName',
'usersA.lastName',
'usersA.email',
'usersB.id',
'usersB.firstName',
'usersB.lastName'
]);
And here is my DOG own example tested and working:
export class Dog extends BaseEntity {
#PrimaryGeneratedColumn()
id: number;
#Column({ default: '' })
name: string;
#CreateDateColumn()
createdDate: Date;
#UpdateDateColumn()
updatedDate: Date;
#ManyToMany(type => User)
#JoinTable()
friends: User[];
#ManyToMany(type => User)
#JoinTable()
owners: User[];
#ManyToMany(type => Hobby)
#JoinTable()
hobbies: Hobby[];
}
And here my queryBuilder:
let queryBuilder = Dog
.createQueryBuilder('Dog')
.leftJoinAndSelect('Dog.hobbies', 'hobby')
.leftJoinAndSelect('Dog.friends', 'usersA')
.leftJoinAndSelect('Dog.owners', 'usersB')
.select(
[
'Dog.id',
'Dog.name',
'hobby.type',
'hobby.cost',
'usersA.id',
'usersA.username',
'usersB.id',
'usersB.username'
]
);
I'm working on a express (with TypeORM) + ReactJS app.
The problem is that I have 3 entities linked by OneToMany relationship like this:
customer
product(linked to customer)
model(linked to product)
import { Product } from './product.entity'
#Entity('customer')
export class Customer extends BaseEntity{
#PrimaryGeneratedColumn()
readonly id: number;
#Column ({name: 'name'})
name : string;
#Column ({name: 'test', nullable: true})
test : string;
#OneToMany(() => Product, product => product.customer)
// #JoinColumn({ name: 'product_id' })
products: Product[]
}
import {Customer} from './customer.entity'
import {Model} from './model.entity'
#Entity('product')
export class Product extends BaseEntity{
#PrimaryGeneratedColumn()
readonly id: number;
#Column ({name: 'name'})
name : string;
#Column ({name: 'test', nullable: true})
test : string;
#Column ({name: 'deleted', nullable: true})
deleted : string;
#ManyToOne(() => Customer, customer => customer.products)
#JoinColumn({ name: 'customer_id' })
customer: Customer;
#OneToMany(() => Model, model => model.product)
#JoinColumn({ name: 'customer_id' })
models: Model[]
}
import { Product } from "./product.entity";
#Entity('model')
export class Model extends BaseEntity{
#PrimaryGeneratedColumn()
readonly id: number;
#Column ({name: 'name'})
name : string;
#Column ({name: 'size', nullable: true})
size : string;
#Column ({name: 'deleted', nullable: true})
deleted : string;
#ManyToOne(() => Product, product => product.models)
#JoinColumn({ name: 'product_id' })
product: Product;
}
The save method from Express is:
static add = async(req: Request, res)=> {
const connection = getConnection();
const queryRunner = connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
let customer: Customer
customer = await queryRunner.manager.getRepository(Customer).findOneOrFail(req.body.id)
const productsReq: Array<Product> = req.body.products
productsReq.forEach(async (product: any) => {
let updatedProd = {...product, customer: customer}
const savedProd = await queryRunner.manager.getRepository(Product).save({...updatedProd})
product.models.forEach(async (model: any) => {
let updatedModel = {...model, product: savedProd}
await queryRunner.manager.getRepository(Model).save(updatedModel)
});
});
await queryRunner.commitTransaction();
return res.send('Saving done')
} catch (error) {
console.log(error)
await queryRunner.rollbackTransaction();
res.status(500).send('Some error occurs');
} finally {
}
}
Currently, in the DB, I have the following data:
1 customer with id 30
name
id
test
first customer
30
test column
1 product with id 119 linked to customer 30
id
name
test
customer_id
deleted
119
first product
test column
30
2 models with ids: 90 and 91 linked to product 119
id
name
size
deleted
product_id
91
second model witout id
2000
119
90
first model with id
1000
119
Next, from React i'm trying to update only the model with id 90 and add a new model.
(So i'm not sending to backend all the models, model with id 91 is not sent).
The JSON object sent from frontend to backend looks like this:
{
"id": 30,
"name": "first customer",
"test": "test column",
"products": [
{
"id" : 119,
"name": "first product",
"test": "test column",
"models": [
{
"id": 90,
"name": "first model with id",
"size": 1000
},
{
"name": "second model witout id",
"size": 2000
}
]
}
]
}
But the problem is in DB the foreing key "product_id" on table "model" is set to null for model with id 91 and a new row(92) is inserted.
The result is:
|id|name|size|deleted|product_id|
|--|----|----|-------|----------|
|91|second model witout id|2000|||
|90|first model with id|1000||119|
|92|second model witout id|2000||119|
How can I add a new model and update an existing one without sending all the existing models in DB ?
I think i found a solutions.
I changed the save method from Express like this:
static add = async(req: Request, res)=> {
const connection = getConnection();
const queryRunner = connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
let customer: Customer
let customerReq: any = req.body
customer = await queryRunner.manager.getRepository(Customer).findOneOrFail(req.body.id)
const productsReq: Array<Product> = req.body.products
productsReq.forEach(async (product: any) => {
let updatedProd = {...product, customer: customer}
let addedModels: Model[] = []
product.models.forEach(async (model: any) => {
const updatedModel = await queryRunner.manager.getRepository(Model).save({...model, product: product})
addedModels.push(updatedModel)
});
const existingProd = await queryRunner.manager.getRepository(Product).save({...updatedProd})
await (await existingProd.models).push(...addedModels)
const savedProd = await queryRunner.manager.getRepository(Product).save({...updatedProd})
});
await queryRunner.commitTransaction();
return res.send('Adding ok')
} catch (error) {
console.log(error)
await queryRunner.rollbackTransaction();
res.status(500).send('Something went terribly wrong');
} finally {
console.log('release')
// await queryRunner.release();
}
}
It's strange becouse 2 transaction are started:
query: START TRANSACTION
query: SELECT "Customer"."id" AS "Customer_id", "Customer"."name" AS "Customer_name", "Customer"."test" AS "Customer_test" FROM "customer" "Customer" WHERE "Customer"."id" IN ($1) -- PARAMETERS: [30]
query: SELECT "Customer"."id" AS "Customer_id", "Customer"."name" AS "Customer_name", "Customer"."test" AS "Customer_test" FROM "customer" "Customer" WHERE "Customer"."id" IN ($1) -- PARAMETERS: [30]
query: COMMIT
query: SELECT "Model"."id" AS "Model_id", "Model"."name" AS "Model_name", "Model"."size" AS "Model_size", "Model"."deleted" AS "Model_deleted", "Model"."product_id" AS "Model_product_id" FROM "model" "Model" WHERE "Model"."id" IN ($1) -- PARAMETERS: [132]
query: SELECT "Product"."id" AS "Product_id", "Product"."name" AS "Product_name", "Product"."test" AS "Product_test", "Product"."deleted" AS "Product_deleted", "Product"."customer_id" AS "Product_customer_id" FROM "product" "Product" WHERE "Product"."id" IN ($1) -- PARAMETERS: [119]
query: INSERT INTO "model"("name", "size", "deleted", "product_id") VALUES ($1, $2, DEFAULT, $3) RETURNING "id" -- PARAMETERS: ["second model witout id",2000,119]
release
Morgan --> POST 200 /test # Tue, 04 May 2021 14:28:08 GMT ::ffff:127.0.0.1 from undefined PostmanRuntime/7.28.0
query: START TRANSACTION
query: SELECT "Product"."id" AS "Product_id", "Product"."name" AS "Product_name", "Product"."test" AS "Product_test", "Product"."deleted" AS "Product_deleted", "Product"."customer_id" AS "Product_customer_id" FROM "product" "Product" WHERE "Product"."id" IN ($1) -- PARAMETERS: [119]
query: UPDATE "model" SET "size" = $2 WHERE "id" IN ($1) -- PARAMETERS: [132,8000]
query: COMMIT