Skip to content

Commit

Permalink
feat: avoid multiple types of select calls
Browse files Browse the repository at this point in the history
Currently there are two scenarios when a select operation type is found:
- When an include has a select within it
- When a select has a relation in it

This means that there is a situation where it is not possible to reason
about what to do with a select because the two scenarios look the same
based on the available information; both have the include operation as
the parent.

BREAKING CHANGE: remove the calls for include selects

Extensions that relied on calls with the "select" operation for select
objects within an include will no longer be able to use that call.
Instead use the parent "include" operation to modify the selected fields,
or use the "select" operation for the relation within that select object.
  • Loading branch information
olivierwilkinson committed Jan 13, 2024
1 parent 70e1ef9 commit d30522a
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 153 deletions.
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

0 comments on commit d30522a

Please sign in to comment.