I'm implementing 'soft deletion' for the 'licenses' collection in the Customer Management Application we are currently building out.
Without going too deep into the nitty gritty, our software running on the client's server will send an EDA message to our Customer Management App when one of there licenses has been removed/deleted. When this occurs the corresponding License record's 'deleted' property (boolean/checkbox) is updated to true (which then makes it eligible for a manual hard deletion by an admin).
When a license is marked 'deleted' (but a hard manual deletion has not occurred), we would like to show the record on the admin panel as normal (i.e. when req.user._strategy === "local-jwt" ), but we do not want to return the record with any localAPI queries.
Because we are using the localAPI to query the licenses in many different places in our codebase, and because we will likely be implementing the same "soft deletion" on several other collections in the future, it would be preferable to implement a reusable strategy and not have to update every payload.find's 'where' statement.
My original thought was to use a collection beforeRead hook to accomplish this, however, I have not been able to get the hook to not return the document.
Is there a way to accomplish this using a beforeRead hook? If not, what would the recommended approach be?
Below is the beforeRead hooks I attempted to implement.
Thanks!
const removeDeletedFromResponse: CollectionBeforeReadHook = async ({
doc,
req,
query,
}) => {
if (req.user && req.user._strategy === "local-jwt" || !doc.deleted) {
return doc;
}
// ALSO ATTEMPTED:
// else {
// return null;
// }
// else {
// if ("and" in query) {
// query.and.push({ deleted: { equals: false } });
// } else {
// query.and = [{ deleted: { equals: false } }];
// }
// }
};
What was the issue with beforeReaD?
Did this condition fail?
if (req.user && req.user._strategy === "local-jwt" || !doc.deleted) {
return doc;
}
I was not able to get the beforeRead hook to NOT return the doc. The doc is returned regardless of whether or not the conditional was met.
Hmm
Even when you returned null?
I think im confused about the following
You want an action to occur once a doc is updated
Why is there an issue with beforeRead returning doc?
What about field level access control?
For example
Fields access control "read" have access to the doc in its entirety
And the user
Collection access control provides the user and document id
(for read)
You could, in your collection, define an access control function
and then put that on each field you want to hide
Otherwise
Just block requests to the default endpoints of the collection
And author custom endpoints on the collection
Which receive the same data as the normal endpoints by default
Hmm, though that may break Admin display
So i think access control is cleaner
Yes the doc was still returned, even when I returned null from the beforeRead hook.
No, I don't want an action to occur once a doc is updated. I'm looking for an easy way to exclude license records marked 'deleted' from all localApi find queries that are not performed by a user with a user._strategy equal to "local-jwt." So, if a user goes to the 'licenses' collection on the admin panel they should still be able to see license records that have been marked 'deleted'. But if a hook (for example) queries the licenses collection, licenses marked 'deleted' should not be returned.
I have 'read' field access controls implemented in some places already, but doesn't that only limit the ability to read a particular field? I want to exclude the entire document/record not just a field.
i see
ok give me a min
I know I could accomplish my goal by adding something like the following to every localApi find's where statement, but I was hoping to not have to add the same lines to dozens (potentially hundreds if we implement this on other collections) of places:
or: [
{
deleted: {
equals: false,
},
},
{
deleted: {
exists: false,
},
},
]
True
I'm booting up my sandbox proj
I have a couple of ideas
If the collection Read access control had access to the the doc like it does in update/create it would be ideal.
It has the id of the doc
Thanks so much by the way, I really appreciate you taking the time to lend a hand.
True, I could query the doc to find the deleted property if !req.user || req.user._strategy !== "local-jwt".
I'm going to give that a try, but wouldn't that essentially cause every doc to be queried twice if the above conditional is true?
Ok
I think I have an ida
idea*
It's not done yet but
Let me hear your thoughts
const afterReadHook: CollectionAfterReadHook = async ({
doc, // full document data
req, // full express request
query, // JSON formatted query
findMany, // boolean to denote if this hook is running against finding one, or finding many
}) => {
console.log(req.originalUrl);
return { ...doc, foo: 'bar' };
};
const Test: CollectionConfig = {
slug: 'test',
access: {
read: () => true,
},
fields: [
{
name: 'title',
label: 'Title',
type: 'text',
required: true,
},
],
hooks: {
afterRead: [afterReadHook],
},
};
This afterReadHook provides the doc
(and the req info)
I was able to manipulate what was returned from the API call via that spreaded property
So my idea is
Check the req url, or some info on the request to confirm it came from the Payload page
and return the document
otherwise, return an empty object
req.originalUrl may not be the play here, that, for example, returns
/payload/api/test/663e1eec7d50e7c537f23155?depth=0&draft=true&fallback-locale=null
So you could check if the doc ID (which i think is hidden?) exists in the request url
But that's a little hacky, there must be a better way to identify the request origin
I think though, this is getting close to a solution
yeah no prob man, the community is cool and if i cant help you, im sure one of the payload staff will
I'm going to play around with this a bit. I think this might be the hook I want to use. I was thinking afterRead ran AFTER the docs were returned. I'll report back if I find the solution. Much Appreciated!
Of course 😄
have you considered
beforeOperation
, as you can modify the args with it before any code in the operation will be executed? in this case you want to append the query to
args.where
if it's
find
operation.
i haven't read too much here so might have a wrong idea, but that's what i was using for cases like these
It looks like I'm having more or less the same issue with the afterRead hook. If I return null or undefined the doc is still included in the "docs" returned to the localAPI call. If I return an empty object, the empty object is returned in the "docs." I could definitely make it work returning an empty object but I would still need to update all my localAPI calls to handle/ignore empty objects which would negate the purpose of doing this and probably be just as much work /code repetition as just updating the 'where' statements of the localAPI calls.
Whats the issue of the localApi returning the doc?
I'll give it a try. Thanks.
Lets say I have 2 "licenses":
License A (not marked deleted)
License B (marked deleted)
What I would want to be returned from the local API find call is:
[{License A}]
If I return null/undefined from the hook the "docs" in the response from the localAPI find call looks like:
[{License A}, {License B}]
If I return an empty object for licenses marked deleted I get:
[{License A}, {}]
Cant you return the document with the field spreaded off?
The idea was to only return empty if the origin was from a REST call not in the admin panel
hmm
Yeah I can but, I don't want to include the document at all.
@423216344302092288
/solve The
beforeOperation
hook is the answer. Here's what I implemented, this will only add the where: {deleted: {equals: false}} for "reads" not initiated by a user and where there isn't already a deleted property in the where clause. This way if I need to query docs marked deleted I still can (as long as I explicitly include it in the original where statement). I'm going to expand this to check for a 'deleted' property at all levels of the where clause.
import { CollectionBeforeOperationHook } from "payload/types";
const removeDeletedFromResponse: CollectionBeforeOperationHook = async ({
args,
operation,
req,
}) => {
if (operation === "read") {
if (
(!req.user || req.user._strategy !== "local-jwt") &&
!("deleted" in args.where)
) {
args.where.deleted = {
equals: false,
};
}
}
return args;
};
Thanks again
@1049775120559898725and
@423216344302092288yall's help is much appreciated!
Yooo nicely done!
Nice to see it works,
beforeOperation
is actually a powerful thing but sometimes people forget about its existence
but i would improve your solution with adding
and
to the
where
instead of just
args.where.deleted
because you can already have
where
in the arg, and you want here to
appendthe query with
deleted
, not overwrite
like this
and: [originalWhere, { deleted: {...} }]
but up to you ofc
here's how payload does it, i believe you can even import it from payload/database
https://github.com/payloadcms/payload/blob/main/packages/payload/src/database/combineQueries.tsI updated the codeblock in my previous comment to recursively look for a "deleted" key in the where clause at all levels, I'm only adding the
where.deleted = {equals: false}
if there is not already a deleted key in the where clause. Maybe I'm mistaken but I don't think this would override the where clause, just the deleted property (which it won't because I'm checking if it exists first).
Either way, combining the originalWhere and new 'deleted' property into an 'and' array doesn't seem like a bad idea, I'm going to implement that.
Star
Discord
online
Get dedicated engineering support directly from the Payload team.