diff --git a/README.md b/README.md index c5390a6..486e396 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/src/lib/utils/extractNestedOperations.ts b/src/lib/utils/extractNestedOperations.ts index 9c85bed..76a14f6 100644 --- a/src/lib/utils/extractNestedOperations.ts +++ b/src/lib/utils/extractNestedOperations.ts @@ -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 - ) - ); - } - } }); }); diff --git a/test/unit/args.test.ts b/test/unit/args.test.ts index bfc5a64..9e9518c 100644 --- a/test/unit/args.test.ts +++ b/test/unit/args.test.ts @@ -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) => { diff --git a/test/unit/calls.test.ts b/test/unit/calls.test.ts index 65fdcfb..7c553b3 100644 --- a/test/unit/calls.test.ts +++ b/test/unit/calls.test.ts @@ -36,8 +36,9 @@ type OperationCall = { logicalOperators?: LogicalOperator[]; }; -function nestedParamsFromCall( rootParams: NestedParams, call: OperationCall @@ -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", @@ -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"), - }, - }, - }, - }, ], }, { @@ -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", @@ -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, diff --git a/test/unit/operations.test.ts b/test/unit/operations.test.ts index 0b55bac..c656572 100644 --- a/test/unit/operations.test.ts +++ b/test/unit/operations.test.ts @@ -3227,39 +3227,5 @@ describe("operations", () => { set(params.args, "data.profile", { delete: false }) ); }); - - it("replaces existing include with select changed to include", async () => { - const allOperations = withNestedOperations({ - $rootOperation: (params) => { - return params.query(params.args); - }, - $allNestedOperations: (params) => { - if (params.operation === "select") { - return params.query(params.args, "include"); - } - - return params.query(params.args); - }, - }); - - const query = jest.fn((_: any) => Promise.resolve(null)); - const params = createParams(query, "User", "findUnique", { - where: { id: faker.datatype.number() }, - include: { - posts: { - select: { deleted: true }, - include: { author: true }, - }, - }, - }); - - await allOperations(params); - - expect(query).toHaveBeenCalledWith( - set(params.args, "include.posts", { - include: { deleted: true }, - }) - ); - }); }); });