Prisma Nested Middleware
Middleware that is called for every nested relation in a Prisma query.
Vanilla Prisma middleware is great for modifying top-level queries but
becomes difficult to use when middleware must handle
nested writes
or modify where objects that reference relations.
See the existing issue regarding nested middleware
for more information.
This library creates middleware that is called for relations nested in the
params object, allowing you to modify params and results without having to
recurse through params objects yourself.

Table of Contents
Installation
This module is distributed via npm which is bundled with node and
should be installed as one of your project's dependencies:
npm install --save prisma-nested-middleware
@prisma/client is a peer dependency of this library, so you will need to
install it if you haven't already:
npm install --save @prisma/client
Usage
Pass a middleware function to
createNestedMiddleware, the returned middleware can be passed to Prisma client's $use method:
import { createNestedMiddleware } from 'prisma-nested-middleware'
client.$use(createNestedMiddleware(async (params, next) => {
const result = await next(params)
return result;
));
Params
The params object passed to the middleware function is a normal Prisma.MiddlewareParams object with the following
differences:
-
the action field adds the following options: 'connectOrCreate', 'connect', 'disconnect', 'include', 'select' and 'where'
-
there is an additional scope field that contains information specific to nested relations:
- the
parentParams field contains the params object of the parent relation
- the
modifier field contains any modifiers the params were wrapped in, for example some or every.
- the
logicalOperators field contains any logical operators between the current relation and it's parent, for example AND or NOT.
- the
relations field contains an object with the relation to the current model and from the model back to it's parent.
For more information on the modifier and logicalOperators fields see the Where section.
For more information on the relations field see the Relations section.
The type for the params object is:
type NestedParams = Omit<Prisma.MiddlewareParams, "action"> & {
action:
| Prisma.PrismaAction
| "where"
| "include"
| "select"
| "connect"
| "connectOrCreate"
| "disconnect";
scope?: {
parentParams: NestedParams;
relations: { to: Prisma.DMMF.Field; from: Prisma.DMMF.Field };
modifier?: "is" | "isNot" | "some" | "none" | "every";
logicalOperators?: ("AND" | "OR" | "NOT)[];
};
};
Nested Writes
The middleware function is called for every nested write
operation in the query. The action field is set to the operation being performed, for example "create" or "update".
The model field is set to the model being operated on, for example "User" or "Post".
For example take the following query:
const result = await client.user.update({
data: {
posts: {
update: {
where: { id: 1 },
data: { title: "Hello World" },
},
},
},
});
The middleware function will be called with:
{
action: 'update',
model: 'Post',
args: {
where: { id: 1 },
data: { title: 'Hello World' }
},
relations: {
to: { kind: 'object', name: 'posts', isList: true, ... },
from: { kind: 'object', name: 'author', isList: false, ... },
},
scope: [root params],
}
Some nested writes can be passed as an array of operations. In this case the middleware function is called for each
operation in the array. For example take the following query:
const result = await client.user.update({
data: {
posts: {
update: [
{ where: { id: 1 }, data: { title: "Hello World" } },
{ where: { id: 2 }, data: { title: "Hello World 2" } },
],
},
},
});
The middleware function will be called with:
{
action: 'update',
model: 'Post',
args: {
where: { id: 1 },
data: { title: 'Hello World' }
},
relations: {
to: { kind: 'object', name: 'posts', isList: true, ... },
from: { kind: 'object', name: 'author', isList: false, ... },
},
scope: [root params],
}
and
{
action: 'update',
model: 'Post',
args: {
where: { id: 2 },
data: { title: 'Hello World 2' }
},
relations: {
to: { kind: 'object', name: 'posts', isList: true, ... },
from: { kind: 'object', name: 'author', isList: false, ... },
},
scope: [root params],
}
Changing Nested Write Actions
The middleware function can change the action that is performed on the model. For example take the following query:
const result = await client.user.update({
data: {
posts: {
update: {
where: { id: 1 }
data: { title: 'Hello World' }
},
},
},
});
The middleware function could be used to change the action to upsert:
const middleware = createNestedMiddleware((params, next) => {
if (params.model === "Post" && params.action === "update") {
return next({
...params,
action: "upsert",
args: {
where: params.args.where,
create: params.args.data,
update: params.args.data,
},
});
}
return next(params);
});
The final query would be modified by the above middleware to:
const result = await client.user.update({
data: {
posts: {
upsert: {
where: { id: 1 },
create: { title: "Hello World" },
update: { title: "Hello World" },
},
},
},
});
When changing the action it is possible for the action to already exist. In this case the resulting actions are merged.
For example take the following query:
const result = await client.user.update({
data: {
posts: {
update: {
where: { id: 1 },
data: { title: "Hello World" },
},
upsert: {
where: { id: 2 },
create: { title: "Hello World 2" },
update: { title: "Hello World 2" },
},
},
},
});
Using the same middleware defined before the update action would be changed to an upsert action, however there is
already an upsert action so the two actions are merged into a upsert operation array with the new operation added to
the end of the array. When the existing action is already a list of operations the new operation is added to the end of
the list. The final query in this case would be:
const result = await client.user.update({
data: {
posts: {
upsert: [
{
where: { id: 2 },
create: { title: "Hello World 2" },
update: { title: "Hello World 2" },
},
{
where: { id: 1 },
create: { title: "Hello World" },
update: { title: "Hello World" },
},
],
},
},
});
Sometimes it is not possible to merge the actions together in this way. The createMany action does not support
operation arrays so the data field of the createMany action is merged instead. For example take the following query:
const result = await client.user.create({
data: {
posts: {
createMany: {
data: [{ title: "Hello World" }, { title: "Hello World 2" }],
},
create: {
title: "Hello World 3",
},
},
},
});
If the create action was changed to be a createMany action the data field would be added to the end of the existing
createMany action. The final query would be:
const result = await client.user.create({
data: {
posts: {
createMany: {
data: [
{ title: "Hello World" },
{ title: "Hello World 2" },
{ title: "Hello World 3" },
],
},
},
},
});
It is also not possible to merge the actions together by creating an array of operations for non-list relations. For
example take the following query:
const result = await client.user.update({
data: {
profile: {
create: {
bio: "My personal bio",
age: 30,
},
update: {
where: { id: 1 },
data: { bio: "Updated bio" },
},
},
},
});
If the update action was changed to be a create action using the following middleware:
const middleware = createNestedMiddleware((params, next) => {
if (params.model === "Profile" && params.action === "update") {
return next({
...params,
action: "create",
args: params.args.data,
});
}
return next(params);
});
The create action from the update action would need be merged with the existing create action, however since
profile is not a list relation we must merge together the resulting objects instead, resulting in the final query:
const result = await client.user.create({
data: {
profile: {
create: {
bio: "Updated bio",
age: 30,
},
},
},
});
Splitting Nested Write Actions
The middleware function can also split the action into multiple actions by passing an array of params to the next
function. For example take the following query:
const result = await client.user.update({
data: {
posts: {
delete: { id: 1 },
},
},
});
A middleware function that changes the delete into an update and disconnect could be defined as:
const middleware = createNestedMiddleware((params, next) => {
if (params.model === "Post" && params.action === "delete") {
return next([
{
...params,
action: "update",
args: {
where: params.args,
data: { deleted: true },
},
},
{
...params,
action: "disconnect",
args: params.args.where,
},
]);
}
return next(params);
});
The final query would be:
const result = await client.user.update({
data: {
posts: {
update: {
where: { id: 1 },
data: { deleted: true },
},
disconnect: { id: 1 },
},
},
});
Write Results
The next function of middleware calls for nested write actions always return undefined as their result. This is
because it the results returned from the root query may not include the data for a particular nested write.
For example take the following query:
const result = await client.user.update({
data: {
profile: {
create: {
bio: "My personal bio",
age: 30,
},
}
posts: {
updateMany: {
where: {
published: false,
},
data: {
published: true,
},
},
},
},
select: {
id: true,
posts: {
where: {
title: {
contains: "Hello",
},
},
select: {
id: true,
},
},
}
});
The profile field is not included in the select object so the result of the create action will not be included in
the root result. The posts field is included in the select object but the where object only includes posts with
titles that contain "Hello" and returns only the "id" field, in this case it is not possible to match the result of the
updateMany action to the returned Posts.
See Modifying Results for more information on how to update the results of queries.
Where
The where action is called for any relations found inside where objects in params.
Note that the where action is not called for the root where object, this is because you need the root action to know
what properties the root where object accepts. For nested where objects this is not a problem as they always follow the
same pattern.
To see where the where action is called take the following query:
const result = await client.user.findMany({
where: {
posts: {
some: {
published: true,
},
},
},
});
The where object above produces a call for "posts" relation found in the where object. The modifier field is set to
"some" since the where object is within the "some" field.
{
action: 'where',
model: 'Post',
args: {
published: true,
},
scope: {
parentParams: {...}
modifier: 'some',
relations: {...}
},
}
Relations found inside where AND, OR and NOT logical operators are also found and called with the middleware function,
however the where action is not called for the logical operators themselves. For example take the following query:
const result = await client.user.findMany({
where: {
posts: {
some: {
published: true,
AND: [
{
title: "Hello World",
},
{
comments: {
every: {
text: "Great post!",
},
},
},
],
},
},
},
});
The middleware function will be called with the params for "posts" similarly to before, however it will also be called
with the following params:
{
action: 'where',
model: 'Comment',
args: {
text: "Great post!",
},
scope: {
parentParams: {...}
modifier: 'every',
logicalOperators: ['AND'],
relations: {...}
},
}
Since the "comments" relation is found inside the "AND" logical operator the
middleware is called for it. The modifier field is set to "every" since the where object is in the "every" field and
the logicalOperators field is set to ['AND'] since the where object is inside the "AND" logical operator.
Notice that the middleware function is not called for the first item in the "AND" array, this is because the first item
does not contain any relations.
The logicalOperators field tracks all the logical operators between the parentParams and the current params. For
example take the following query:
const result = await client.user.findMany({
where: {
AND: [
{
NOT: {
OR: [
{
posts: {
some: {
published: true,
},
},
},
],
},
},
],
},
});
The middleware function will be called with the following params:
{
action: 'where',
model: 'Post',
args: {
published: true,
},
scope: {
parentParams: {...}
modifier: 'some',
logicalOperators: ['AND', 'NOT', 'OR'],
relations: {...},
},
}
The where action is also called for relations found in the where field of includes and selects. For example:
const result = await client.user.findMany({
select: {
posts: {
where: {
published: true,
},
},
},
});
The middleware function will be called with the following params:
{
action: 'where',
model: 'Post',
args: {
published: true,
},
scope: {...}
}
Where Results
The next function for a where action always resolves with undefined.
Include
The include action will be called for any included relation. The args field will contain the object or boolean
passed as the relation include. For example take the following query:
const result = await client.user.findMany({
include: {
profile: true,
posts: {
where: {
published: true,
},
},
},
});
For the "profile" relation the middleware function will be called with:
{
action: 'include',
model: 'Profile',
args: true,
scope: {...}
}
and for the "posts" relation the middleware function will be called with:
{
action: 'include',
model: 'Post',
args: {
where: {
published: true,
},
},
scope: {...}
}
Include Results
The next function for an include action resolves with the result of the include action. For example take the
following query:
const result = await client.user.findMany({
include: {
profile: true,
},
});
The middleware function for the "profile" relation will be called with:
{
action: 'include',
model: 'Profile',
args: true,
scope: {...}
}
And the next function will resolve with the result of the include action, in this case something like:
{
id: 2,
bio: 'My personal bio',
age: 30,
userId: 1,
}
For relations that are included within a list of parent results the next function will resolve with a flattened array
of all the models from each parent result. For example take the following query:
const result = await client.user.findMany({
include: {
posts: true,
},
});
If the root result looks like the following:
[
{
id: 1,
name: "Alice",
posts: [
{
id: 1,
title: "Hello World",
published: false,
userId: 1,
},
{
id: 2,
title: "My first published post",
published: true,
userId: 1,
},
],
},
{
id: 2,
name: "Bob",
posts: [
{
id: 3,
title: "Clean Code",
published: true,
userId: 2,
},
],
},
];
The next function for the "posts" relation will resolve with the following:
[
{
id: 1,
title: "Hello World",
published: false,
userId: 1,
},
{
id: 2,
title: "My first published post",
published: true,
userId: 1,
},
{
id: 3,
title: "Clean Code",
published: true,
userId: 2,
},
];
For more information on how to modify the results of an include action see the Modifying Results
Select
Similarly to the include action, the select action will be called for any selected relation with the args field
containing the object or boolean passed as the relation select. For example take the following query:
const result = await client.user.findMany({
select: {
posts: true,
profile: {
select: {
bio: true,
},
},
},
});
and for the "posts" relation the middleware function will be called with:
{
action: 'select',
model: 'Post',
args: true,
scope: {...}
}
For the "profile" relation the middleware function will be called with:
{
action: 'select',
model: 'Profile',
args: {
bio: true,
},
scope: {...}
}
There is another case possible for selecting fields in Prisma. When including a model it is supported to use a select
object to select fields from the included model. For example take the following query:
const result = await client.user.findMany({
include: {
profile: {
select: {
bio: true,
},
},
},
});
From v4 the "select" action is not called for the "profile" relation. This is because it caused two different kinds
of "select" action args, and it was not always possible to distinguish between them.
See Modifying Selected Fields for more information on how to handle selects.
Select Results
The next function for a select action resolves with the result of the select action. This is the same as the
include action. See the Include Results section for more information.
Relations
The relations field of the scope object contains the relations relevant to the current model. For example take the
following query:
const result = await client.user.create({
data: {
email: "test@test.com",
profile: {
create: {
bio: "Hello World",
},
},
posts: {
create: {
title: "Hello World",
},
},
},
});
The middleware function will be called with the following params for the "profile" relation:
{
action: 'create',
model: 'Profile',
args: {
bio: "Hello World",
},
scope: {
parentParams: {...}
relations: {
to: { name: 'profile', kind: 'object', isList: false, ... },
from: { name: 'user', kind: 'object', isList: false, ... },
},
},
}
and the following params for the "posts" relation:
{
action: 'create',
model: 'Post',
args: {
title: "Hello World",
},
scope: {
parentParams: {...}
relations: {
to: { name: 'posts', kind: 'object', isList: true, ... },
from: { name: 'author', kind: 'object', isList: false, ... },
},
},
}
Modifying Nested Write Params
When writing middleware that modifies the params of a query you should first write the middleware as if it were vanilla
middleware and then add conditions for nested writes.
Say you are writing middleware that sets a default value when creating a model for a particular model:
client.$use(
createNestedMiddleware((params, next) => {
if (params.scope) {
return next(params);
}
if (params.model !== "Invite") {
return next(params);
}
if (params.action === "create") {
if (!params.args.data.code) {
params.args.data.code = createCode();
}
}
if (params.action === "createMany") {
params.args.data.forEach((data) => {
if (!data.code) {
data.code = createCode();
}
});
}
if (params.action === "upsert") {
if (!params.args.create.code) {
params.args.create.code = createCode();
}
}
return next(params);
})
);
Then add conditions for the different args and actions that can be found in nested writes:
client.$use(
createNestedMiddleware((params, next) => {
if (params.model !== "Invite") {
return next(params);
}
if (params.action === "create") {
if (params.scope) {
if (!params.args.code) {
params.args.code = createCode();
}
} else {
if (!params.args.data.code) {
params.args.data.code = createCode();
}
}
}
[...]
if (params.action === "connectOrCreate") {
if (!params.args.create.code) {
params.args.create.code = createCode();
}
}
return next(params);
})
);
###Â Modifying Selected Fields
When writing middleware that modifies the selected fields of a model you must handle all actions that can contain a
select object, this includes:
select
include
findMany
findFirst
findUnique
findFirstOrThrow
findUniqueOrThrow
create
update
upsert
delete
This is because the select action is only called for relations found within a select object. For example take the
following query:
const result = await client.user.findMany({
include: {
comments: {
select: {
title: true,
replies: {
select: {
title: true,
},
},
},
},
},
});
For the above query the middleware function will be called with the following for the replies relation:
{
action: 'select',
model: 'Comment',
args: {
select: {
title: true,
},
},
scope: {...}
}
and the following for the comments relation:
{
action: 'include',
model: 'Comment',
args: {
select: {
title: true,
replies: {
select: {
title: true,
}
},
},
},
scope: {...}
}
So if you wanted to ensure that the "id" field is always selected you could write the following middleware:
client.$use(
createNestedMiddleware((params, next) => {
if ([
'select',
'include',
'findMany',
'findFirst',
'findUnique',
'findFirstOrThrow',
'findUniqueOrThrow',
'create',
'update',
'upsert',
'delete',
].includes(params.action)) {
if (typeof params.args === 'object' && params.args !== null && params.args.select) {
return next({
...params,
args: {
...params.args,
select: {
...params.args.select,
id: true,
},
},
});
}
}
return next(params)
})
);
Modifying Where Params
When writing middleware that modifies the where params of a query it is very important to first write the middleware as
if it were vanilla middleware and then handle the where action. This is because the where action is not called for
the root where object and so you will need to handle it manually.
Say you are writing middleware that excludes models with a particular field, let's call it "invisible" rather than
"deleted" to make this less familiar:
client.$use(
createNestedMiddleware((params, next) => {
if (params.scope) {
return next(params);
}
if (
params.action === "findFirst" ||
params.action === "findMany" ||
params.action === "updateMany" ||
params.action === "deleteMany" ||
params.action === "count" ||
params.action === "aggregate"
) {
return next({
...params,
where: {
...params.where,
invisible: false,
},
});
}
return next(params);
})
);
Then add conditions for the where action:
client.$use(
createNestedMiddleware((params, next) => {
if (params.action === "where") {
return next({
...params,
args: {
...params.args,
invisible: false,
},
});
}
if (
params.action === "findFirst" ||
params.action === "findMany" ||
params.action === "updateMany" ||
params.action === "deleteMany" ||
params.action === "count" ||
params.action === "aggregate"
) {
return next({
...params,
where: {
...params.where,
invisible: false,
},
});
}
return next(params);
})
);
Modifying Results
When writing middleware that modifies the results of a query you should take the following process:
- handle all the root cases in the same way as you would with vanilla Prisma middleware.
- handle nested results using the
include and select actions.
Say you are writing middleware that adds a timestamp to the results of a query. You would first handle the root cases:
client.$use(
createNestedMiddleware((params, next) => {
if (params.scope) {
return next(params);
}
const result = await next(params);
if (!result) return result;
if (
params.action === 'findFirst' ||
params.action === 'findUnique' ||
params.action === 'create' ||
params.action === 'update' ||
params.action === 'upsert' ||
params.action === 'delete'
) {
result.timestamp = Date.now();
return result;
}
if (params.action === 'findMany') {
const result = await next(params);
result.forEach(model => {
model.timestamp = Date.now();
})
return result;
}
return result;
})
)
Then you would handle the nested results using the include and select actions:
client.$use(
createNestedMiddleware((params, next) => {
const result = await next(params);
if (!result) return result;
[...]
if (
params.action === 'include' ||
params.action === 'select'
) {
if (Array.isArray(result)) {
result.forEach(model => {
model.timestamp = Date.now();
})
} else {
result.timestamp = Date.now();
}
return result
}
return result;
})
)
You could also write the above middleware by creating new objects for each result rather than mutating the existing
objects:
client.$use(
createNestedMiddleware((params, next) => {
[...]
if (
params.action === 'include' ||
params.action === 'select'
) {
if (Array.isArray(result)) {
return result.map(model => ({
...model,
timestamp: Date.now(),
}))
} else {
return {
...result,
timestamp: Date.now(),
}
}
}
return result;
})
)
NOTE: When modifying results from include or select actions it is important to either mutate the existing objects or
spread the existing objects into the new objects. This is because createNestedMiddleware needs some fields from the
original objects in order to correct update the root results.
Errors
If any middleware throws an error at any point then the root query will throw with that error. Any middleware that is
pending will have it's promises rejects at that point.
LICENSE
Apache 2.0