Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 132 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,25 @@ For the "profile" relation the middleware function will be called with:
}
```

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:

```javascript
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](#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
Expand Down Expand Up @@ -1013,7 +1032,7 @@ client.$use(

// createMany and upsert do not change
[...]

// handle the "connectOrCreate" action
if (params.action === "connectOrCreate") {
if (!params.args.create.code) {
Expand All @@ -1027,6 +1046,115 @@ client.$use(
);
```

### 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:

```javascript
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:

```javascript
{
action: 'select',
model: 'Comment',
args: {
select: {
title: true,
},
},
scope: {...}
}
```

and the following for the comments relation:

```javascript
{
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:

```javascript
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
Expand Down Expand Up @@ -1060,7 +1188,7 @@ client.$use(
...params.where,
invisible: false,
},
})
});
}

// pass params to next middleware
Expand All @@ -1082,7 +1210,7 @@ client.$use(
...params.args,
invisible: false,
},
})
});
}

// handle root actions
Expand All @@ -1101,7 +1229,7 @@ client.$use(
...params.where,
invisible: false,
},
})
});
}

// pass params to next middleware
Expand Down
54 changes: 0 additions & 54 deletions src/lib/utils/extractNestedActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -443,60 +443,6 @@ export function extractRelationReadActions(
)
);
}

// push select nested in an include
if (action === "include" && arg.select) {
const nestedSelectActionInfo = {
params: {
model,
action: "select" as const,
args: arg.select,
runInTransaction,
dataPath: [],
scope: {
parentParams: readActionInfo.params,
relations: readActionInfo.params.scope.relations,
},
},
target: {
field: "include" as const,
action: "select" as const,
relationName: relation.name,
parentTarget,
},
};

nestedActions.push(nestedSelectActionInfo);

if (nestedSelectActionInfo.params.args?.where) {
const whereActionInfo = {
target: {
action: "where" as const,
relationName: relation.name,
readAction: "select" as const,
parentTarget: nestedSelectActionInfo.target,
},
params: {
model: nestedSelectActionInfo.params.model,
action: "where" as const,
args: nestedSelectActionInfo.params.args.where,
runInTransaction,
dataPath: [],
scope: {
parentParams: nestedSelectActionInfo.params,
relations: nestedSelectActionInfo.params.scope.relations,
},
},
};
nestedActions.push(whereActionInfo);
nestedActions.push(
...extractRelationWhereActions(
whereActionInfo.params,
whereActionInfo.target
)
);
}
}
});
});

Expand Down
4 changes: 1 addition & 3 deletions src/lib/utils/targets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,7 @@ export function buildOperationsPath(
}

export function buildQueryTargetPath(target: QueryTarget): string[] {
const path = target.parentTarget
? buildTargetPath(target.parentTarget)
: [];
const path = target.parentTarget ? buildTargetPath(target.parentTarget) : [];

if (!target.relationName) {
return [...path, target.action];
Expand Down
14 changes: 7 additions & 7 deletions test/e2e/smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,28 +152,28 @@ describe("smoke", () => {
});
});

describe('groupBy', () => {
describe("groupBy", () => {
beforeAll(() => {
testClient = new PrismaClient();
testClient.$use(
createNestedMiddleware((params, next) => {
if (params.action !== 'groupBy') {
throw new Error('expected groupBy action')
if (params.action !== "groupBy") {
throw new Error("expected groupBy action");
}
return next(params);
})
);
});

it('calls middleware with groupBy action', async () => {
it("calls middleware with groupBy action", async () => {
await expect(testClient.comment.findMany()).rejects.toThrowError(
'expected groupBy action'
"expected groupBy action"
);

const groupBy = await testClient.comment.groupBy({
by: ['authorId'],
by: ["authorId"],
orderBy: {
authorId: 'asc',
authorId: "asc",
},
});

Expand Down
32 changes: 0 additions & 32 deletions test/unit/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3125,37 +3125,5 @@ describe("actions", () => {
set(params, "args.data.profile", { delete: false })
);
});

it("replaces existing include with select changed to include", async () => {
const nestedMiddleware = createNestedMiddleware((params, next) => {
if (params.action === "select") {
return next({
...params,
action: "include",
});
}

return next(params);
});

const next = jest.fn((_: any) => Promise.resolve(null));
const params = createParams("User", "findUnique", {
where: { id: faker.datatype.number() },
include: {
posts: {
select: { deleted: true },
include: { author: true },
},
},
});

await nestedMiddleware(params, next);

expect(next).toHaveBeenCalledWith(
set(params, "args.include.posts", {
include: { deleted: true },
})
);
});
});
});
48 changes: 48 additions & 0 deletions test/unit/args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,54 @@ describe("params", () => {
});
});

it("allows middleware to modify select args nested in include select", async () => {
const nestedMiddleware = createNestedMiddleware((params, next) => {
if (params.action === "select" && params.model === "Comment") {
return next({
...params,
args: {
where: { deleted: true },
},
});
}
return next(params);
});

const next = jest.fn((_: any) => Promise.resolve(null));
const params = createParams("User", "create", {
data: {
email: faker.internet.email(),
},
include: {
posts: {
select: {
comments: true,
},
},
},
});

await nestedMiddleware(params, next);

expect(next).toHaveBeenCalledWith({
...params,
args: {
...params.args,
include: {
posts: {
select: {
comments: {
where: {
deleted: true,
},
},
},
},
},
},
});
});

it("allows middleware to add data to nested createMany args", async () => {
const nestedMiddleware = createNestedMiddleware((params, next) => {
if (params.action === "createMany") {
Expand Down
Loading