diff --git a/src/lib/actionMiddleware.ts b/src/lib/actionMiddleware.ts index 54707bd..0855b77 100644 --- a/src/lib/actionMiddleware.ts +++ b/src/lib/actionMiddleware.ts @@ -4,6 +4,7 @@ import { NestedParams, NestedMiddleware } from "prisma-nested-middleware"; import { ModelConfig } from "./types"; import { addDeletedToSelect, + isDeletedFieldOverWritten, stripDeletedFieldFromResults, } from "./utils/nestedReads"; import { @@ -136,8 +137,7 @@ function createUpdateManyParams( where: { ...params.args?.where, // allow overriding the deleted field in where - [config.field]: - params.args?.where?.[config.field] || config.createValue(false), + ...(isDeletedFieldOverWritten(config.field, params.args?.where) ? {} : { [config.field]: config.createValue(false) }) }, }, }; @@ -268,8 +268,7 @@ function createFindFirstParams( where: { ...params.args?.where, // allow overriding the deleted field in where - [config.field]: - params.args?.where?.[config.field] || config.createValue(false), + ...(isDeletedFieldOverWritten(config.field, params.args?.where) ? {} : { [config.field]: config.createValue(false) }) }, }, }; @@ -283,6 +282,7 @@ export function createFindFirstMiddleware( }; } + /* FindMany middleware */ function createFindManyParams( @@ -297,8 +297,7 @@ function createFindManyParams( where: { ...params.args?.where, // allow overriding the deleted field in where - [config.field]: - params.args?.where?.[config.field] || config.createValue(false), + ...(isDeletedFieldOverWritten(config.field, params.args?.where) ? {} : { [config.field]: config.createValue(false) }) }, }, }; @@ -326,8 +325,7 @@ function createGroupByParams( where: { ...params.args?.where, // allow overriding the deleted field in where - [config.field]: - params.args?.where?.[config.field] || config.createValue(false), + ...(isDeletedFieldOverWritten(config.field, params.args?.where) ? {} : { [config.field]: config.createValue(false) }) }, }, }; @@ -359,7 +357,7 @@ function createCountParams( where: { ...where, // allow overriding the deleted field in where - [config.field]: where[config.field] || config.createValue(false), + ...(isDeletedFieldOverWritten(config.field, params.args?.where) ? {} : { [config.field]: config.createValue(false) }) }, }, }; @@ -387,7 +385,7 @@ function createAggregateParams( where: { ...where, // allow overriding the deleted field in where - [config.field]: where[config.field] || config.createValue(false), + ...(isDeletedFieldOverWritten(config.field, where) ? {} : { [config.field]: config.createValue(false) }) }, }, }; @@ -456,8 +454,7 @@ function createIncludeParams( where: { ...params.args?.where, // allow overriding the deleted field in where - [config.field]: - params.args?.where?.[config.field] || config.createValue(false), + ...(isDeletedFieldOverWritten(config.field, params.args?.where) ? {} : { [config.field]: config.createValue(false) }) }, }, }; @@ -518,8 +515,7 @@ function createSelectParams( where: { ...params.args?.where, // allow overriding the deleted field in where - [config.field]: - params.args?.where?.[config.field] || config.createValue(false), + ...(isDeletedFieldOverWritten(config.field, params.args?.where) ? {} : { [config.field]: config.createValue(false) }) }, }, }; diff --git a/src/lib/utils/nestedReads.ts b/src/lib/utils/nestedReads.ts index f5c289a..4c62cf2 100644 --- a/src/lib/utils/nestedReads.ts +++ b/src/lib/utils/nestedReads.ts @@ -34,3 +34,29 @@ export function stripDeletedFieldFromResults( return results; } + +export function isDeletedFieldOverWritten(field: string, where?: any): boolean { + if (!where) { + return false + } + if (where[field] !== undefined) { + return true + } + if (where.OR && Array.isArray(where.OR)) { + const isDeletedFieldOverWrittenInOR = where.OR.some((arg: any) => { + return isDeletedFieldOverWritten(field, arg) + }) + if (isDeletedFieldOverWrittenInOR) { + return true + } + } + if (where.AND && Array.isArray(where.AND)) { + const isDeletedFieldOverWrittenInAND = where.AND.some((arg: any) => { + return isDeletedFieldOverWritten(field, arg) + }) + if (isDeletedFieldOverWrittenInAND) { + return true + } + } + return false +} \ No newline at end of file diff --git a/test/e2e/queries.test.ts b/test/e2e/queries.test.ts index 6925768..52d3d37 100644 --- a/test/e2e/queries.test.ts +++ b/test/e2e/queries.test.ts @@ -426,6 +426,15 @@ describe("queries", () => { [firstUser.id, secondUser.id].sort() ); }); + it("findMany keeps soft deleted records when field is overwritten", async () => { + const foundUsers = await testClient.user.findMany({ + where: { name: { contains: "J" }, OR: [{ deleted: false }, { deleted: true }] }, + }); + expect(foundUsers).toHaveLength(3); + expect(foundUsers.map(({ id }) => id).sort()).toEqual( + [firstUser.id, secondUser.id, deletedUser.id].sort() + ); + }) }); describe("count", () => { diff --git a/test/unit/findMany.test.ts b/test/unit/findMany.test.ts index d5d3597..582c5b2 100644 --- a/test/unit/findMany.test.ts +++ b/test/unit/findMany.test.ts @@ -105,4 +105,68 @@ describe("findMany", () => { // params have not been modified expect(next).toHaveBeenCalledWith(params); }); + + it("allows explicitly querying for deleted records using OR modifier", async () => { + const middleware = createSoftDeleteMiddleware({ + models: { User: true }, + }); + + const params = createParams("User", "findMany", { + where: { id: 1, OR: [{ deleted: true }, { name: 'name' }] }, + }); + const next = jest.fn(() => Promise.resolve({})); + + await middleware(params, next); + + // params have not been modified + expect(next).toHaveBeenCalledWith(params); + }) + + it("allows explicitly querying for deleted records using nested OR modifier", async () => { + const middleware = createSoftDeleteMiddleware({ + models: { User: true }, + }); + + const params = createParams("User", "findMany", { + where: { id: 1, OR: [{ name: 'name' }, { OR: [{ deleted: { not: false } }] }] }, + }); + const next = jest.fn(() => Promise.resolve({})); + + await middleware(params, next); + + // params have not been modified + expect(next).toHaveBeenCalledWith(params); + }) + + it("allows explicitly querying for deleted records using AND modifier", async () => { + const middleware = createSoftDeleteMiddleware({ + models: { User: true }, + }); + + const params = createParams("User", "findMany", { + where: { id: 1, AND: [{ deleted: true }, { name: 'name' }] }, + }); + const next = jest.fn(() => Promise.resolve({})); + + await middleware(params, next); + + // params have not been modified + expect(next).toHaveBeenCalledWith(params); + }) + + it("allows explicitly querying for deleted records using nested AND modifier", async () => { + const middleware = createSoftDeleteMiddleware({ + models: { User: true }, + }); + + const params = createParams("User", "findMany", { + where: { id: 1, AND: [{ name: 'name' }, { OR: [{ deleted: { not: false } }] }] }, + }); + const next = jest.fn(() => Promise.resolve({})); + + await middleware(params, next); + + // params have not been modified + expect(next).toHaveBeenCalledWith(params); + }) }); diff --git a/test/unit/utils/nestedReads.test.ts b/test/unit/utils/nestedReads.test.ts new file mode 100644 index 0000000..1f594fb --- /dev/null +++ b/test/unit/utils/nestedReads.test.ts @@ -0,0 +1,51 @@ +import { isDeletedFieldOverWritten } from '../../../src/lib/utils/nestedReads' + +describe('nestedReads', () => { + describe('isDeletedFieldOverWritten', () => { + const DELETED_FIELD = 'deleted' + + it.each([ + [false, null], + [false, undefined], + [false, {}], + [false, { field: false }], + [false, { field: false, OR: [] }], + [false, { field: false, AND: [] }], + [true, { [DELETED_FIELD]: false }], + [true, { [DELETED_FIELD]: true }] + ]) + ("should return %p when where field is %s", (expectedResult, where) => { + const result = isDeletedFieldOverWritten(DELETED_FIELD, where) + + expect(result).toBe(expectedResult) + }) + + it("should return false when OR field doesn't contain the deleted field", () => { + const where = { OR: [{ field: 'value' }, { anotherField: 'value' }] } + const result = isDeletedFieldOverWritten(DELETED_FIELD, where); + + expect(result).toBe(false) + }) + + it("should return false when AND field doesn't contain the deleted field", () => { + const where = { AND: [{ field: 'value' }, { anotherField: 'value' }] } + const result = isDeletedFieldOverWritten(DELETED_FIELD, where); + + expect(result).toBe(false) + }) + + it("should return true when OR field contains the deleted field", () => { + const where = { OR: [{ field: 'value' }, { [DELETED_FIELD]: false }] } + const result = isDeletedFieldOverWritten(DELETED_FIELD, where); + + expect(result).toBe(true) + }) + + it("should return true when AND field contains the deleted field", () => { + const where = { AND: [{ [DELETED_FIELD]: false }, { anotherField: 'value' }] } + const result = isDeletedFieldOverWritten(DELETED_FIELD, where); + + expect(result).toBe(true) + }) + }) +}) \ No newline at end of file