I'm trying to set the
name
field of a collection based on the title of a relation field... Related to this discussion:
https://payloadcms.com/community-help/discord/is-it-possible-to-populate-field-with-the-title-of-a-relation
However now I'm trying to do it while using the
payload-next-demo
as a starting point. When I try to create a collection in the admin UI and hit save I get the error:
hook is not a function
, and the collection is not saved.
I assume the source of the error is this hook file:
https://github.com/Snailedlt/next-payload-gv/blob/90a8eb104882e1c8137f475083216ec3179773d0/payload/collections/PriceZones/hooks/beforeChange.ts#L11Which is used here:
https://github.com/Snailedlt/next-payload-gv/blob/90a8eb104882e1c8137f475083216ec3179773d0/payload/collections/PriceZones/index.ts#L16-L18because it's mocked with webpack in the payload.config.ts file:
https://github.com/Snailedlt/next-payload-gv/blob/90a8eb104882e1c8137f475083216ec3179773d0/payload/payload.config.ts#L28-L37It's worth noting that the error also occurs when the hook file looks like this:
import { CollectionBeforeChangeHook } from 'payload/types'
export const BeforeChangeHook: CollectionBeforeChangeHook = async ({
data,
}) => {
return {
...data // just return the data without modifications
}
}
I don't understand the error though, nor how to fix it... So I hope someone's able and willing to help out 🙂
Full error:
[13:42:23] ERROR (payload): TypeError: hook is not a function
at eval (webpack-internal:///(api)/./node_modules/payload/dist/collections/operations/create.js:111:22)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async create (webpack-internal:///(api)/./node_modules/payload/dist/collections/operations/create.js:109:5)
at async handler (webpack-internal:///(api)/./node_modules/@payloadcms/next-payload/dist/handlers/[collection]/index.js:83:33)
Hey @snailedlt - I'll take a look at this for you, spinning up your repo now then I'll let you know what I find
Wow, thank you for going above and beyond to troubleshoot for me!
The error can be found on the latest commit of the
make_adjustments_for_gv
branch
i have faced the same error recently. However, instead of using return {...data} just don't return anything and it'll work. If you want to change the data. just use data.x = something or siblingData.xx = something
I am not sure if there's a change
That's good to know, and in most cases that will work fine. I need to set the name though, so my return looks like this:
return {
...data,
name: 'newName'
}
Here I think I need to pass
...data
before
name
so it doesn't override the existing data, or is it possible to change the name without returning it?
you can use siblingdata
How would that look? I'm unfamiliar with the syntax
async ({ siblingData }) => {
siblingData.name = "new name"
// You can also do your own validation
if(something){
siblingDate.name = ""
}
},
interesting. So this should work then right?
import { CollectionBeforeChangeHook } from 'payload/types'
import getPayloadClient from '../../../payloadClient';
export const BeforeChangeHook: CollectionBeforeChangeHook = async ({
data, // incoming data to update or create with
siblingData,
}) => {
const payload = await getPayloadClient();
const range = await payload.findByID({ collection: 'km-ranges', id: data.range })
const priceDescription: string = await data.priceType === 'fixed' ? 'nok' : 'nok/km' ?? 'fallback'
siblingData.name = range.name + ' - ' + data.price + ' ' + priceDescription
}
But I get this error:
Property 'siblingData' does not exist on type '{ data: Partial<any>; req: PayloadRequest; operation: CreateOrUpdateOperation; originalDoc?: any; context: RequestContext; }'.ts(2339)
(parameter) siblingData: any
No quick fixes available
@farhansyah is correct about reassigning your data - it does not seem to be the cause of your issue though, am still looking into it
Yeah siblingData isn't available in the
CollectionBeforeChangeHook
type:
export type BeforeChangeHook<T extends TypeWithID = any> = (args: {
data: Partial<T>;
req: PayloadRequest;
/**
* Hook operation being performed
*/
operation: CreateOrUpdateOperation;
/**
* Original document before change
*
* `undefined` on 'create' operation
*/
originalDoc?: T;
context: RequestContext;
}) => any;
sorry, here's the implementation:
type CollectionBeforeChangeHook<T extends TypeWithID = any> = (args: {
data: Partial<T>;
req: PayloadRequest;
operation: CreateOrUpdateOperation;
originalDoc?: T;
context: RequestContext;
}) => any
Ah, you are using CollectionHook. Sorrry, siblingData only exist on FieldHook
Ahh, makes sense. Thanks for the info still, every bit helps 🙂
But it should be similar, need to try to use whatis in the CollectionHook, but instead of returning data, you need to assign data.. that's the difference.
I guess for my usecase I could just use
data.name
to edit the name field, instead of editing
name
directly:
data.name = range.name + ' - ' + data.price + ' ' + priceDescription
However, like jesschow said, that still gives the same error for me :/
i tried using beforeValidate hook, this work for me..
are you user data.range exist ? it is not in the field
I changed the name to
kmRange
a while back, but forgot to change it here. Thanks for the heads up
Gonna try this now
Okey this works as long as I don't need to use the payload local API.
If I use the payload local API directly like this I get a webpack error:
import type { CollectionConfig } from 'payload/types'
import payload from "payload"
export const PriceZones: CollectionConfig = {
// The rest of the collection
hooks: {
beforeChange: [async ({data}) => {
const range = await payload.findByID({ collection: 'km-ranges', id: data.kmRange })
const priceDescription: string = await data.priceType === 'fixed' ? 'nok' : 'nok/km' ?? 'fallback'
data.name = range.name + ' - ' + data.price + ' ' + priceDescription}],
},
}
[12:21:29] ERROR (payload): TypeError: payload__WEBPACK_IMPORTED_MODULE_0__.default.findByID is not a function
at PriceZones.hooks.beforeChange (webpack-internal:///(api)/./payload/collections/PriceZones/index.ts:22:85)
at eval (webpack-internal:///(api)/./node_modules/payload/dist/collections/operations/create.js:111:22)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async create (webpack-internal:///(api)/./node_modules/payload/dist/collections/operations/create.js:109:5)
at async handler (webpack-internal:///(api)/./node_modules/@payloadcms/next-payload/dist/handlers/[collection]/index.js:83:33)
If I use the localAPI via getPayloadClient() I get an fs error:
import type { CollectionConfig } from 'payload/types'
import getPayloadClient from '../../payloadClient';
export const PriceZones: CollectionConfig = {
// The rest of the collection
hooks: {
beforeChange: [async ({data}) => {
const payload = await getPayloadClient();
const range = await payload.findByID({ collection: 'km-ranges', id: data.kmRange })
const priceDescription: string = await data.priceType === 'fixed' ? 'nok' : 'nok/km' ?? 'fallback'
data.name = range.name + ' - ' + data.price + ' ' + priceDescription}],
},
}
//payloadClient.ts
import { getPayload } from "payload/dist/payload";
import type {Payload} from "payload"
import config from './payload.config';
if (!process.env.MONGODB_URI) {
throw new Error('MONGODB_URI environment variable is missing')
}
if (!process.env.PAYLOAD_SECRET) {
throw new Error('PAYLOAD_SECRET environment variable is missing')
}
/**
* Global is used here to maintain a cached connection across hot reloads
* in development. This prevents connections growing exponentially
* during API Route usage.
*
* Source: https://github.com/vercel/next.js/blob/canary/examples/with-mongodb-mongoose/lib/dbConnect.js
*/
let cached = (global as any).payload
if (!cached) {
cached = (global as any).payload = { client: null, promise: null }
}
export const getPayloadClient = async (): Promise<Payload> => {
if (cached.client) {
return cached.client
}
if (!cached.promise) {
cached.promise = await getPayload({
// Make sure that your environment variables are filled out accordingly
mongoURL: process.env.MONGODB_URI as string,
secret: process.env.PAYLOAD_SECRET as string,
config: config,
})
}
try {
cached.client = await cached.promise
} catch (e) {
cached.promise = null
throw e
}
return cached.client
};
export default getPayloadClient;
Error:
- error ./node_modules/atomically/dist/utils/fs.js:4:0
Module not found: Can't resolve 'fs'
Did you mean './fs'?
Requests that should resolve in the current directory need to start with './'.
Requests that start with a name are treated as module requests and resolve within module directories (node_modules, D:\code\test_projects\next-payload-gv).
If changing the source code is not an option there is also a resolve options called 'preferRelative' which tries to resolve these kind of requests in the current directory too.
https://nextjs.org/docs/messages/module-not-found
Import trace for requested module:
./node_modules/atomically/dist/index.js
./node_modules/conf/dist/source/index.js
./node_modules/payload/dist/utilities/telemetry/index.js
./node_modules/payload/dist/utilities/telemetry/events/serverInit.js
./node_modules/payload/dist/payload.js
./payload/payloadClient.ts
./payload/collections/PriceZones/index.ts
./payload/payload.config.ts
./node_modules/payload/dist/admin/Root.js
./app/(payload)/admin/page.tsx
are you sure you are using the id: data.kmRange correctly?
payload.findbyId uses uniqueId like mongodb ObjectID value, unique string or number.
If yes, are the document you are looking for exist?
if you are intending to use just the values from the 'not yet created' doc, you can just get the value from the data object itself.
const priceDescription: string = data.priceType === 'fixed' ? 'nok' : 'nok/km' ?? 'fallback'
data.name = data.name + ' - ' + data.price + ' ' + priceDescription
Hey @snailedlt - I've got an alternative solution for you, is it alright if I open a PR on your repo?
Yes, that would be highly appreciated!
Thank you very much!
Not 100% sure if it's the correctway to do it, but it did work earlier (see this thread:
https://discord.com/channels/967097582721572934/1134115313437384876). Yes the document exists.
The intention is to use the name field of the kmRange releation, which afaik can't be retrieved other than through the local API. I'm very possibly wrong here though, so feel free to correct me 🙂
ah, then you can't use find by ID, since it is not the unique ID for the document.. you can use payload.find() for that
You'll get an array of result. Then check if the doc exist
But it worked earlier though. Why can't it be used here?
To be clear, this is the code that worked:
https://discord.com/channels/967097582721572934/1134115313437384876/1134480156187181238Inside a project based on the ecommerce template... while it doesn't work inside the project based on next-payload-demo. So the issue isn't the localapi method itself... but I'll wait and see what jess comes up with! 😄
you can try printing the result, whether you get all the values correctly, such as 'data.kmRange' and 'range'
it errors before it can do anything.
Even this errors out:
import { CollectionBeforeChangeHook } from 'payload/types'
export const BeforeChangeHook: CollectionBeforeChangeHook = async ({
data, // incoming data to update or create with
}) => {
console.log("test");
}
haha
I didn't have permission to open a PR on your repo, so here are the two files I changed:
1. PriceZones > index.ts
2. KMRanges.ts
I think the best way to achieve what you're looking for is to move everything to a virtual field. So, I have removed the collection hook and added hooks to the
name
fields in your
price-zones
and
km-ranges
collections. As you and @farhansyah said, we do not need to return any data, simply reassigning it will work. I have also changed your Local API request to a REST API request, with the structure of your project I believe the local API won't work.
Try replacing your two files with the ones I sent and let me know if it works for you!
Jess always saving the day!
Awesome, testing it right away!
Awesome, it works perfectly!
I would like if possible to only use the local api, could you tell me a little more about why that might not be possible?
I am able to use the local api in the frontend like this btw:
https://github.com/Snailedlt/next-payload-gv/blob/90a8eb104882e1c8137f475083216ec3179773d0/app/(site)/page.tsximport getPayloadClient from "../../payload/payloadClient";
export default async function Home() {
const payload = await getPayloadClient();
const kmRanges = await payload.find({
collection: "km-ranges",
});
return (
<div>
<h1>Ranges</h1>
<p>{JSON.stringify(kmRanges)}</p>
</div>
);
}
I'm not super familiar with how the
getPayloadClient()
works so I did some digging and you are correct - that should be fine to use, you might need to add it to your webpack aliases thought.
Also, I just tried this method of using the local API and it works:
const getFullTitle: FieldHook = async ({ siblingData, data, req: { payload } }) => {
if (data) {
try {
const kmRanges = await payload.find({
collection: "km-ranges",
});
if (kmRanges) {
const priceDescription: string = data.priceType === 'fixed' ? 'nok' : 'nok/km'
siblingData.name =`${kmRanges.docs[0].name} - ${data.price} ${priceDescription}`
}
} catch (err) {
console.log(err)
}
}
};
With hooks, you can de-structure
payload
directly from the
req
object.
Ohh that's great! I'm gonna be using that from now on. Again thanks a bunch for all the time and debugging spent helping!
Minor detail: Had to change it to findByID to get the correct kmRanges.name. Might be useful to know for others in the future 🙂
import type { CollectionConfig } from 'payload/types'
import { FieldHook } from 'payload/types';
const getFullTitle: FieldHook = async ({ siblingData, data, req: { payload } }) => {
if (data) {
try {
const kmRanges = await payload.findByID({ collection: 'km-ranges', id: data.kmRange });
if (kmRanges) {
const priceDescription: string = data.priceType === 'fixed' ? 'nok' : 'nok/km'
siblingData.name =`${kmRanges.name} - ${data.price} ${priceDescription}`
}
} catch (err) {
console.log(err)
}
}
};
Happy to help!
I found a bug
Sorting the name column doesn't work. Sorting any of the other columns work
Shoot, this is because a virtual field doesn't get saved to the database so there is nothing to sort
on. You can remove the whole
beforeChange
hook if you don't mind this value getting stored
gotcha, thanks
But still, the name column should probably be sortable even if it's a virtual field?
In an ideal world I mean 🙂
It's not possible. When you sort other columns it sorts the documents from the DB, it would be strange if these behaved differently and only sorted the ones that you see on screen. I do agree that it might be nice to come up with a way to mark fields as virtual and hide the sort icons, or show a different icon to denote why it is not sortable.
Star
Discord
online
Get help straight from the Payload team with an Enterprise License.