Prisma nested recursive relations depth - sql

I must query a group and all of its subgroups from the same model.
However, when fetching from Group table as shown below, Prisma doesn't include more than a 1-depth to the resulting Subgroups relation (subgroups of subgroups being left out). Subgroups attribute holds an array whose elements are of same type as the said model (recursive).
model Group {
id Int #id #default(autoincrement())
parentId Int?
Parent Group? #relation("parentId", fields: [parentId], references: [id])
Subgroups Group[] #relation("parentId")
}
GroupModel.findFirst({
where: { id: _id },
include: { Subgroups: true }
});
I guess this might be some sort of safeguard to avoid infinite recursive models when generating results. Is there any way of dodging this limitation (if it's one), and if so, how?
Thanks

You can query more than 1-depth nested subgroups by nesting include like so:
GroupModel.findFirst({
where: { id: _id },
include: { Subgroups: { include: { Subgroups: { include: Subgroups: { // and so on... } } } } }
});
But, as mentioned by #TasinIshmam, something like includeRecursive is not supported by Prisma at the moment.
The workaround would be to use $queryRaw (https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access#queryraw) together with SQL recursive queries (https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-RECURSIVE)

Related

Can I refer to the Row/Document internal variables when filtering in Prisma?

How can I use row/document variables in filters and sorting?
As you know in SQL we can filter on joins beside the foreign key Something like this
Select * From A LEFT JOIN B on A.key = B.foriegnKey AND B.key IN A.currentSelection
or even in mongo lookup
collection('A').aggregate([{
$lookup: {
from: "B",
localField: "key",
foreignField: "foreignKey",
let: { A_currentSelection: "$currentSelection" },
pipeline: [{
$match: {
$expr: { $in: ["$key", "$$A_currentSelection"] }
}
}],
as: "matches"
}
},
])
But you can't do the following in Prisma
prisma.A.findMany({
include: {
B: {
where: {
'$A.currentSelection': {
has: "$B.key"
}
}
}
}
})
Regardless of the query itself, the idea is that I can access the current row/document variables in the query, I also know that I can modify the structure of the database to get around these kinds of issues but the database is already structured in a specific manner that might break some parts of the code and it's also not viable to change the structure just because Prisma is not lacking in this part.
At first, I was using a raw query to get around this and know I've created more complex relationships in the schema to fix this in Prisma in this case, but if anyone knows a more elegant solution then I'd be grateful

Find entity with most relations filtered by criteria

model Player {
id String #id
name String #unique
game Game[]
}
model Game {
id String #id
isWin Boolean
playerId String
player Player #relation(fields: [playerId], references: [id])
}
I would like to find a player with most wins. How would I do that with prisma? If there is no prisma "native" way to do it, what is the most efficient way to do this with raw SQL?
The best I could think of is:
prisma.player.findMany({
include: {
game: {
where: {
isWin: true,
},
},
},
})
But it has huge downside that you need to filter and order results in Node manually, and also store all results in memory while doing so.
Using the groupBy API you can find the player with the most wins using two queries.
1. Activate orderByAggregateGroup
You'l need to use the orderByAggregateGroup preview feature and use Prisma version 2.21.0 or later.
Update your Prisma Schema as follows
generator client {
provider = "prisma-client-js"
previewFeatures = ["orderByAggregateGroup"]
}
// ... rest of schema
2. Find playerId of most winning player
Use a groupBy query to do the following:
Group games by the playerId field.
Find the count of game records where isWin is true.
Order them in descending order by the count mentioned in 2.
Take only 1 result (since we want the player with the most wins. You can change this to get the first-n players as well).
The combined query looks like this:
const groupByResult = await prisma.game.groupBy({
by: ["playerId"],
where: {
isWin: true,
},
_count: {
isWin: true,
},
orderBy: {
_count: {
isWin: "desc",
},
},
take: 1, // change to "n" you want to find the first-n most winning players.
});
const mostWinningPlayerId = groupByResult[0].playerId;
I would suggest checking out the Group By section of the Aggregation, grouping, and summarizing article in the prisma docs, which explains how group by works and how to use it with filtering and ordering.
3. Query player data with findUnique
You can trivially find the player using a findUnique query as you have the id.
const mostWinningPlayer = await prisma.player.findUnique({
where: {
id: mostWinningPlayerId,
},
});
Optionally, if you want the first "n" most winning players, just put the appropriate number in the take condition of the first groupBy query. Then you can do a findMany with the in operator to get all the player records. If you're not sure how to do this, feel free to ask and I'll clarify with sample code.

How to make complex nested where conditions with typeORM?

I am having multiple nested where conditions and want to generate them without too much code duplication with typeORM.
The SQL where condition should be something like this:
WHERE "Table"."id" = $1
AND
"Table"."notAvailable" IS NULL
AND
(
"Table"."date" > $2
OR
(
"Table"."date" = $2
AND
"Table"."myId" > $3
)
)
AND
(
"Table"."created" = $2
OR
"Table"."updated" = $4
)
AND
(
"Table"."text" ilike '%search%'
OR
"Table"."name" ilike '%search%'
)
But with the FindConditions it seems not to be possible to make them nested and so I have to use all possible combinations of AND in an FindConditions array. And it isn't possible to split it to .where() and .andWhere() cause andWhere can't use an Object Literal.
Is there another possibility to achieve this query with typeORM without using Raw SQL?
When using the queryBuilder I would recommend using Brackets
as stated in the Typeorm doc: https://typeorm.io/#/select-query-builder/adding-where-expression
You could do something like:
createQueryBuilder("user")
.where("user.registered = :registered", { registered: true })
.andWhere(new Brackets(qb => {
qb.where("user.firstName = :firstName", { firstName: "Timber" })
.orWhere("user.lastName = :lastName", { lastName: "Saw" })
}))
that will result with:
SELECT ...
FROM users user
WHERE user.registered = true
AND (user.firstName = 'Timber' OR user.lastName = 'Saw')
I think you are mixing 2 ways of retrieving entities from TypeORM, find from the repository and the query builder. The FindConditions are used in the find function. The andWhere function is use by the query builder. When building more complex queries it is generally better/easier to use the query builder.
Query builder
When using the query build you got much more freedom to make sure the query is what you need it to be. With the where you are free to add any SQL as you please:
const desiredEntity = await connection
.getRepository(User)
.createQueryBuilder("user")
.where("user.id = :id", { id: 1 })
.andWhere("user.date > :date OR (user.date = :date AND user.myId = :myId)",
{
date: specificCreatedAtDate,
myId: mysteryId,
})
.getOne();
Note that depending on your used database the actual SQL that you use here needs to be compatible. With that could also come a possible draw back of using this method. You will tie your project to a specific database. Make sure to read up about the aliases for tables you can set if you are using relations this would be handy.
Repository
You already saw that this is much less comfortable. This is because the find function or more specific the findOptions are using objects to build the where clause. This makes is harder to implement a proper interface to implement nested AND and OR clauses side by side. There for (I assume) they have chosen to split AND and OR clauses. This makes the interface much more declarative and means the you have to pull your OR clauses to the top:
const desiredEntity = await repository.find({
where: [{
id: id,
notAvailable: Not(IsNull()),
date: MoreThan(date)
},{
id: id,
notAvailable: Not(IsNull()),
date: date
myId: myId
}]
})
I cannot imagin looking a the size of the desired query that this code would be very performant.
Alternatively you could use the Raw find helper. This would require you to rewrite your clause per field, since you will only get access to the one alias at a time. You could guess the column names or aliases but this would be very poor practice and very unstable since you cannot directly control this easily.
if you want to nest andWhere statements if a condition is meet here is an example:
async getTasks(filterDto: GetTasksFilterDto, user: User): Promise<Task[]> {
const { status, search } = filterDto;
/* create a query using the query builder */
// task is what refer to the Task entity
const query = this.createQueryBuilder('task');
// only get the tasks that belong to the user
query.where('task.userId = :userId', { userId: user.id });
/* if status is defined then add a where clause to the query */
if (status) {
// :<variable-name> is a placeholder for the second object key value pair
query.andWhere('task.status = :status', { status });
}
/* if search is defined then add a where clause to the query */
if (search) {
query.andWhere(
/*
LIKE: find a similar match (doesn't have to be exact)
- https://www.w3schools.com/sql/sql_like.asp
Lower is a sql method
- https://www.w3schools.com/sql/func_sqlserver_lower.asp
* bug: search by pass where userId; fix: () whole addWhere statement
because andWhere stiches the where class together, add () to make andWhere with or and like into a single where statement
*/
'(LOWER(task.title) LIKE LOWER(:search) OR LOWER(task.description) LIKE LOWER(:search))',
// :search is like a param variable, and the search object is the key value pair. Both have to match
{ search: `%${search}%` },
);
}
/* execute the query
- getMany means that you are expecting an array of results
*/
let tasks;
try {
tasks = await query.getMany();
} catch (error) {
this.logger.error(
`Failed to get tasks for user "${
user.username
}", Filters: ${JSON.stringify(filterDto)}`,
error.stack,
);
throw new InternalServerErrorException();
}
return tasks;
}
I have a list of
{
date: specificCreatedAtDate,
userId: mysteryId
}
My solution is
.andWhere(
new Brackets((qb) => {
qb.where(
'userTable.date = :date0 AND userTable.type = :userId0',
{
date0: dates[0].date,
userId0: dates[0].type,
}
);
for (let i = 1; i < dates.length; i++) {
qb.orWhere(
`userTable.date = :date${i} AND userTable.userId = :userId${i}`,
{
[`date${i}`]: dates[i].date,
[`userId${i}`]: dates[i].userId,
}
);
}
})
)
That will produce something similar
const userEntity = await repository.find({
where: [{
userId: id0,
date: date0
},{
id: id1,
userId: date1
}
....
]
})

RavenDb DeleteByIndex on nested collection condition

Assume that I have a schema as below:
Father { // Type: 1
Id
}
Mother { // Type: 2
Id
}
Child {
Parents: [
{ ParentId, ParentType } // ParentType could be 1 or 2 acording to entity's type
]
}
How could I create an index that allow us to DeleteByIndex and accept lucene query such as: "Parents,ParentId:xyz AND Parents,ParentType:2"?
As I tried to create index as below:
Map = views => from view in views
select new
{
view.ParentId,
view.ParentType,
view.Parents
}
RavenDb failed to delete and said that "Parents,ParentId" is not indexed yet.
The reason for doing that is I would like to delete all children data when it is a child of one of {Mother, Father}.
The syntax Parents,ParentId is only applicable for dynamic indexes, using a static index, you are defining the field names, and you can name them however you want.
Map = views => from view in views
from parent in view.Parents
select new
{
parent .ParentId,
parent .ParentType
}
But check the docs about what fanout indexes if your system can have many parents.

MongoDB: How retrieve data that is newly constructed instead of original documents in the collection?

I have a collection in which documents are all in this format:
{"user_id": ObjectId, "book_id": ObjectId}
It represents the relationship between user and book, which is also one-to-many, that means, a user can have more than one books.
Now I got three book_id, for example:
["507f191e810c19729de860ea", "507f191e810c19729de345ez", "507f191e810c19729de860efr"]
I want to query out the users who have these three books, because the result I want is not the document in this collection, but a newly constructed array of user_id, it seems complicated and I have no idea about how to make the query, please help me.
NOTE:
The reason why I didn't use the structure like:
{"user_id": ObjectId, "book_ids": [ObjectId, ...]}
is because in my system, books increase frequently and have no limit in amount, in other words, user may read thousands of books, so I think it's better to use the traditional way to store it.
This question is not restricted by MongoDB, you can answer it in relational database thoughts.
Using a regular find you cannot get back all user_id fields who own all the book_id's because you normalized your collection (flattened it).
You can do it, if you use aggregation framework:
db.collection.aggregate([
{
$match: {
book_id: {
$in: ["507f191e810c19729de860ea",
"507f191e810c19729de345ez",
"507f191e810c19729de860efr" ]
}
}
},
{
$group: {
_id: "$user_id",
count: { $sum: 1 }
}
},
{
$match: {
count: 3
}
},
{
$group: {
_id: null,
users: { $addToSet: "$_id" }
}
}
]);
What this does is filters through the pipeline only for documents which match one of the three book_id values, then it groups by user_id and counts how many matches that user got. If they got three they pass to the next pipeline operation which groups them into an array of user_ids. This solution assumes that each 'user_id,book_id' record can only appear once in the original collection.