diff --git a/README.md b/README.md index c531fbd..d0e5a9b 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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) { @@ -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 @@ -1060,7 +1188,7 @@ client.$use( ...params.where, invisible: false, }, - }) + }); } // pass params to next middleware @@ -1082,7 +1210,7 @@ client.$use( ...params.args, invisible: false, }, - }) + }); } // handle root actions @@ -1101,7 +1229,7 @@ client.$use( ...params.where, invisible: false, }, - }) + }); } // pass params to next middleware diff --git a/src/lib/utils/extractNestedActions.ts b/src/lib/utils/extractNestedActions.ts index 614d052..8ad3ec2 100644 --- a/src/lib/utils/extractNestedActions.ts +++ b/src/lib/utils/extractNestedActions.ts @@ -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 - ) - ); - } - } }); }); diff --git a/src/lib/utils/targets.ts b/src/lib/utils/targets.ts index bb1d711..ca91e40 100644 --- a/src/lib/utils/targets.ts +++ b/src/lib/utils/targets.ts @@ -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]; diff --git a/test/e2e/smoke.test.ts b/test/e2e/smoke.test.ts index f9f942e..3b363b6 100644 --- a/test/e2e/smoke.test.ts +++ b/test/e2e/smoke.test.ts @@ -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", }, }); diff --git a/test/unit/actions.test.ts b/test/unit/actions.test.ts index e5c77c6..2344945 100644 --- a/test/unit/actions.test.ts +++ b/test/unit/actions.test.ts @@ -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 }, - }) - ); - }); }); }); diff --git a/test/unit/args.test.ts b/test/unit/args.test.ts index 7ab7a09..b272a83 100644 --- a/test/unit/args.test.ts +++ b/test/unit/args.test.ts @@ -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") { diff --git a/test/unit/calls.test.ts b/test/unit/calls.test.ts index df91710..1af98f0 100644 --- a/test/unit/calls.test.ts +++ b/test/unit/calls.test.ts @@ -106,7 +106,7 @@ describe("calls", () => { rootParams: createParams("User", "groupBy", { by: ["email"], orderBy: { email: "asc" }, - }) + }), }, { description: "nested create in create", @@ -2383,24 +2383,6 @@ describe("calls", () => { from: getModelRelation("Post", "author"), }, }, - { - action: "select", - model: "Post", - argsPath: "args.include.posts.select", - relations: { - to: getModelRelation("User", "posts"), - from: getModelRelation("Post", "author"), - }, - scope: { - action: "include", - model: "Post", - argsPath: "args.include.posts", - relations: { - to: getModelRelation("User", "posts"), - from: getModelRelation("Post", "author"), - }, - }, - }, { action: "select", model: "Comment", @@ -2507,33 +2489,6 @@ describe("calls", () => { }, }, }, - { - action: "select", - model: "Comment", - argsPath: "args.include.posts.include.comments.select", - relations: { - to: getModelRelation("Post", "comments"), - from: getModelRelation("Comment", "post"), - }, - scope: { - action: "include", - model: "Comment", - argsPath: "args.include.posts.include.comments", - relations: { - to: getModelRelation("Post", "comments"), - from: getModelRelation("Comment", "post"), - }, - scope: { - action: "include", - model: "Post", - argsPath: "args.include.posts", - relations: { - to: getModelRelation("User", "posts"), - from: getModelRelation("Post", "author"), - }, - }, - }, - }, ], }, { @@ -3637,24 +3592,6 @@ describe("calls", () => { from: getModelRelation("Post", "author"), }, }, - { - action: "select", - model: "Post", - argsPath: "args.include.posts.select", - relations: { - to: getModelRelation("User", "posts"), - from: getModelRelation("Post", "author"), - }, - scope: { - action: "include", - model: "Post", - argsPath: "args.include.posts", - relations: { - to: getModelRelation("User", "posts"), - from: getModelRelation("Post", "author"), - }, - }, - }, { action: "select", model: "Comment",