Skip to content
Open
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
148 changes: 148 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,25 @@ For the "profile" relation the `$allNestedOperations()` function will be called
}
```

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` operation is _not_ called for the "profile" relation. This is because it caused two different kinds
of `select` operation 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 `query` function for a `select` operation resolves with the result of the `select` operation. This is the same as the
Expand Down Expand Up @@ -1018,6 +1037,135 @@ const client = _client.$extends({
});
```

### Modifying Selected Fields

When writing an extension that modifies the selected fields of a model you must handle all operations that can contain a
select object, this includes:

- `select`
- `include`
- `findMany`
- `findFirst`
- `findUnique`
- `findFirstOrThrow`
- `findUniqueOrThrow`
- `create`
- `update`
- `upsert`
- `delete`

This is because the `select` operation 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 `$allNestedOperations()` hook will be called with the following for the "replies" relation:

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

and the following for the "comments" relation:

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

```javascript
const client = _client.$extends({
query: {
$allModels: {
$allOperations: withNestedOperations({
async $rootOperation(params) {
if (
[
"findMany",
"findFirst",
"findUnique",
"findFirstOrThrow",
"findUniqueOrThrow",
"create",
"update",
"upsert",
"delete",
].includes(params.operation) &&
typeof params.args === "object" &&
params.args !== null &&
params.args.select
) {
return params.query({
...params.args,
select: {
...params.args.select,
id: true,
},
});
}

return params.query(params.args);
},
async $allNestedOperations(params) {
if (
["select", "include"].includes(params.operation) &&
typeof params.args === "object" &&
params.args !== null &&
params.args.select
) {
return params.query({
...params.args,
select: {
...params.args.select,
id: true,
},
});
}
},
}),
},
},
});
```

### Modifying Where Params

When writing extensions that modify the where params of a query you should first write the `$rootOperation()` hook as
Expand Down
52 changes: 0 additions & 52 deletions src/lib/utils/extractNestedOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,58 +444,6 @@ export function extractRelationReadOperations<
)
);
}

// push select nested in an include
if (operation === "include" && arg.select) {
const nestedSelectOperationInfo = {
params: {
model,
operation: "select" as const,
args: arg.select,
scope: {
parentParams: readOperationInfo.params,
relations: readOperationInfo.params.scope.relations,
},
query: params.query,
},
target: {
field: "include" as const,
operation: "select" as const,
relationName: relation.name,
parentTarget,
},
};

nestedOperations.push(nestedSelectOperationInfo);

if (nestedSelectOperationInfo.params.args?.where) {
const whereOperationInfo = {
target: {
operation: "where" as const,
relationName: relation.name,
readOperation: "select" as const,
parentTarget: nestedSelectOperationInfo.target,
},
params: {
model: nestedSelectOperationInfo.params.model,
operation: "where" as const,
args: nestedSelectOperationInfo.params.args.where,
scope: {
parentParams: nestedSelectOperationInfo.params,
relations: nestedSelectOperationInfo.params.scope.relations,
},
query: params.query,
},
};
nestedOperations.push(whereOperationInfo);
nestedOperations.push(
...extractRelationWhereOperations(
whereOperationInfo.params,
whereOperationInfo.target
)
);
}
}
});
});

Expand Down
45 changes: 45 additions & 0 deletions test/unit/args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,51 @@ describe("args", () => {
});
});

it("can modify select args nested in include select", async () => {
const allOperations = withNestedOperations({
$rootOperation: (params) => params.query(params.args),
$allNestedOperations: (params) => {
if (params.operation === "select" && params.model === "Comment") {
return params.query({
where: { deleted: true },
});
}
return params.query(params.args);
},
});

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

await allOperations(params);

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

it("can add data to nested createMany args", async () => {
const allOperations = withNestedOperations({
$rootOperation: (params) => {
Expand Down
74 changes: 7 additions & 67 deletions test/unit/calls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ type OperationCall<Model extends Prisma.ModelName> = {
logicalOperators?: LogicalOperator[];
};

function nestedParamsFromCall<Model extends Prisma.ModelName,
ExtArgs extends Types.Extensions.InternalArgs = Types.Extensions.DefaultArgs
function nestedParamsFromCall<
Model extends Prisma.ModelName,
ExtArgs extends Types.Extensions.InternalArgs = Types.Extensions.DefaultArgs
>(
rootParams: NestedParams<ExtArgs>,
call: OperationCall<Model>
Expand Down Expand Up @@ -2396,24 +2397,6 @@ describe("calls", () => {
from: getModelRelation("Post", "author"),
},
},
{
operation: "select",
model: "Post",
argsPath: "args.include.posts.select",
relations: {
to: getModelRelation("User", "posts"),
from: getModelRelation("Post", "author"),
},
scope: {
operation: "include",
model: "Post",
argsPath: "args.include.posts",
relations: {
to: getModelRelation("User", "posts"),
from: getModelRelation("Post", "author"),
},
},
},
{
operation: "select",
model: "Comment",
Expand Down Expand Up @@ -2520,33 +2503,6 @@ describe("calls", () => {
},
},
},
{
operation: "select",
model: "Comment",
argsPath: "args.include.posts.include.comments.select",
relations: {
to: getModelRelation("Post", "comments"),
from: getModelRelation("Comment", "post"),
},
scope: {
operation: "include",
model: "Comment",
argsPath: "args.include.posts.include.comments",
relations: {
to: getModelRelation("Post", "comments"),
from: getModelRelation("Comment", "post"),
},
scope: {
operation: "include",
model: "Post",
argsPath: "args.include.posts",
relations: {
to: getModelRelation("User", "posts"),
from: getModelRelation("Post", "author"),
},
},
},
},
],
},
{
Expand Down Expand Up @@ -3650,24 +3606,6 @@ describe("calls", () => {
from: getModelRelation("Post", "author"),
},
},
{
operation: "select",
model: "Post",
argsPath: "args.include.posts.select",
relations: {
to: getModelRelation("User", "posts"),
from: getModelRelation("Post", "author"),
},
scope: {
operation: "include",
model: "Post",
argsPath: "args.include.posts",
relations: {
to: getModelRelation("User", "posts"),
from: getModelRelation("Post", "author"),
},
},
},
{
operation: "select",
model: "Comment",
Expand Down Expand Up @@ -4115,10 +4053,12 @@ describe("calls", () => {
],
},
])(
"calls middleware with $description",
"calls $allNestedOperations with $description",
async ({ rootParams, nestedCalls = [] }) => {
const $rootOperation = jest.fn((params) => params.query(params.args));
const $allNestedOperations = jest.fn((params) => params.query(params.args));
const $allNestedOperations = jest.fn((params) =>
params.query(params.args)
);
const allOperations = withNestedOperations({
$rootOperation,
$allNestedOperations,
Expand Down
Loading