From 31dffcda8b452e4c2ade76cc7b1a6b428bd654d9 Mon Sep 17 00:00:00 2001 From: misterapp Date: Wed, 8 Nov 2023 17:23:49 +0800 Subject: [PATCH 1/3] feat: support custom-defined createValue function --- src/lib/utils/resultFiltering.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/utils/resultFiltering.ts b/src/lib/utils/resultFiltering.ts index 369be92..2ff57ed 100644 --- a/src/lib/utils/resultFiltering.ts +++ b/src/lib/utils/resultFiltering.ts @@ -10,7 +10,7 @@ export function shouldFilterDeletedFromReadResult( return ( !params.args.where || typeof params.args.where[config.field] === "undefined" || - !params.args.where[config.field] + params.args.where[config.field] === config.createValue(false) ); } From 7de6931ec0b783111a33ca5e76131856ef92f3e3 Mon Sep 17 00:00:00 2001 From: misterapp Date: Wed, 8 Nov 2023 19:10:36 +0800 Subject: [PATCH 2/3] fix: bugfix --- src/lib/utils/resultFiltering.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib/utils/resultFiltering.ts b/src/lib/utils/resultFiltering.ts index 2ff57ed..11154d9 100644 --- a/src/lib/utils/resultFiltering.ts +++ b/src/lib/utils/resultFiltering.ts @@ -17,11 +17,13 @@ export function shouldFilterDeletedFromReadResult( export function filterSoftDeletedResults(result: any, config: ModelConfig) { // filter out deleted records from array results if (result && Array.isArray(result)) { - return result.filter((item) => !item[config.field]); + return result.filter( + (item) => item[config.field] === config.createValue(false) + ); } // if the result is deleted return null - if (result && result[config.field]) { + if (result && result[config.field] !== config.createValue(false)) { return null; } From 8afd8b8f2b1e24577c90e48d7b452a0f8e906c8c Mon Sep 17 00:00:00 2001 From: Olivier Wilkinson Date: Sat, 13 Jan 2024 18:04:43 +0000 Subject: [PATCH 3/3] fix: use lodash.isEqual for date value comparison Update resultFiltering helpers to use lodash isEqual to compare values from createValue function. Lodash isEqual does Date comparison in a way that works for `new Date(0) === new Date(0)`. Update README with explanation of new behaviour of createValue. Update nestedReads tests to use Date(0) as the value for non-deleted records, ensuring we have test coverage for that behaviour. --- README.md | 21 ++++++++++++++++++--- prisma/schema.prisma | 9 +++++---- src/lib/utils/resultFiltering.ts | 9 +++++---- test/e2e/nestedReads.test.ts | 19 ++++++++++++++----- 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 06132bd..a6ee8c9 100644 --- a/README.md +++ b/README.md @@ -122,8 +122,13 @@ client.$use( ``` The `field` property is the name of the field to use for soft delete, and the `createValue` property is a function that -takes a deleted argument and returns the value for whether the record is soft deleted or not. The `createValue` method -must return a falsy value if the record is not deleted and a truthy value if it is deleted. +takes a deleted argument and returns the value for whether the record is soft deleted or not. + +The `createValue` method must return a deterministic value if the record is not deleted. This is because the middleware +uses the value returned by `createValue` to modify the `where` object in the query, or to manually filter the results +when including or selecting a toOne relationship. If the value for the field can have multiple values when the record is +not deleted then this filtering will fail. Examples for good values to use when the `deleted` arg in `createValue` is +false are `false`, `null`, `0` or `Date(0)`. It is possible to setup soft delete for multiple models at once by passing a config for each model in the `models` object: @@ -226,7 +231,6 @@ client.$use( For more information for why updating through toOne relationship is disabled by default see the [Excluding Soft Deleted Records in a `findUnique` Operation](#excluding-soft-deleted-records-in-a-findunique-operation) section. - To allow to one updates or compound unique index fields globally you can use the `defaultConfig` to do so: ```typescript @@ -271,6 +275,17 @@ model Comment { } ``` +If the Comment model was configured to use a `deletedAt` field where the value is `Date(0)` by default and a `Date()` +when the record is deleted you would need to add the following to your Prisma schema: + +```prisma +model Comment { + deletedAt DateTime @default(dbgenerated("to_timestamp(0)")) + // Note that the example above is for PostgreSQL, you will need to use the appropriate default function for your db. + [other fields] +} +``` + Models configured to use soft delete that are related to other models through a toOne relationship must have this relationship defined as optional. This is because the middleware will exclude soft deleted records when the relationship is included or selected. If the relationship is not optional the types for the relation will be incorrect and you may diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 928adaa..ba67093 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -52,8 +52,9 @@ model Comment { } model Profile { - id Int @id @default(autoincrement()) - bio String? - deleted Boolean @default(false) - users User[] + id Int @id @default(autoincrement()) + bio String? + deleted Boolean @default(false) + deletedAt DateTime @default(dbgenerated("to_timestamp(0)")) + users User[] } diff --git a/src/lib/utils/resultFiltering.ts b/src/lib/utils/resultFiltering.ts index 14a2f17..5a0cb7e 100644 --- a/src/lib/utils/resultFiltering.ts +++ b/src/lib/utils/resultFiltering.ts @@ -1,4 +1,5 @@ import { NestedParams } from "prisma-nested-middleware"; +import isEqual from "lodash/isEqual"; import { ModelConfig } from "../types"; @@ -10,20 +11,20 @@ export function shouldFilterDeletedFromReadResult( !params.scope?.relations.to.isList && (!params.args.where || typeof params.args.where[config.field] === "undefined" || - params.args.where[config.field] === config.createValue(false)) + isEqual(params.args.where[config.field], config.createValue(false))) ); } export function filterSoftDeletedResults(result: any, config: ModelConfig) { // filter out deleted records from array results if (result && Array.isArray(result)) { - return result.filter( - (item) => item[config.field] === config.createValue(false) + return result.filter((item) => + isEqual(item[config.field], config.createValue(false)) ); } // if the result is deleted return null - if (result && result[config.field] !== config.createValue(false)) { + if (result && !isEqual(result[config.field], config.createValue(false))) { return null; } diff --git a/test/e2e/nestedReads.test.ts b/test/e2e/nestedReads.test.ts index f58dab0..15d2046 100644 --- a/test/e2e/nestedReads.test.ts +++ b/test/e2e/nestedReads.test.ts @@ -11,7 +11,16 @@ describe("nested reads", () => { beforeAll(async () => { testClient = new PrismaClient(); testClient.$use( - createSoftDeleteMiddleware({ models: { Comment: true, Profile: true } }) + createSoftDeleteMiddleware({ + models: { + Comment: true, + // use truthy value for non-deleted to test that they are still filtered + Profile: { + field: "deletedAt", + createValue: (deleted) => (deleted ? new Date() : new Date(0)), + }, + }, + }) ); user = await client.user.create({ @@ -66,7 +75,7 @@ describe("nested reads", () => { // restore soft deleted profile await client.profile.updateMany({ where: {}, - data: { deleted: false }, + data: { deletedAt: new Date(0) }, }); }); afterAll(async () => { @@ -107,7 +116,7 @@ describe("nested reads", () => { await client.profile.updateMany({ where: {}, data: { - deleted: true, + deletedAt: new Date(), }, }); @@ -214,7 +223,7 @@ describe("nested reads", () => { await client.profile.updateMany({ where: {}, data: { - deleted: true, + deletedAt: new Date(), }, }); @@ -402,7 +411,7 @@ describe("nested reads", () => { await client.profile.update({ where: { id: profile!.id }, data: { - deleted: true, + deletedAt: new Date(), }, });