#How can I achieve an array of relationships in a Field? | PayloadCMS 3.0

160 messages · Page 1 of 1 (latest)

lofty patrol
#

Hi, I'm trying to create a way to implement translations for fields in Payload. What can I do to achieve that?
Example:

Consider as an example the following Json representing a Player.

"Player": {
  "name" : [
    { "id" : 1, "language" : "english", "key" : "name", "value" : "John" }, // Translation relationship
    { "id" : 2, "language" : "portuguese", "key" : "name", "value" : "João" }, // Translation relationship
  ],
  "age" : 18,
  "description" : [
    { "id" : 3, "language" : "english", "key" : "description", "value" : "Hello World!" }, // Translation relationship
    { "id" : 4, "language" : "portuguese", "key" : "description", "value" : "Olá Mundo!" }, // Translation relationship
  ]
}

How could I achieve a representation of this with as many languages and translations I wish?
I was looking into defining the Collection with the fields that can be translated as an array of relationships. But I'm not sure if that's even possible.

const languageFields: Field[] = [
  { name: 'name', type: 'text' },
  { name: 'isDefault', type: 'checkbox' }
]

const Languages: CollectionConfig = {
  slug: 'languages',
  auth: true,
  admin: { useAsTitle: 'name' },
  fields: languageFields,
}

const translationFields: Field[] = [
  { name: 'language', type: 'relationship', relationTo: 'languages', required: true },
  { name: 'key'     , type: 'text'        , },
  { name: 'value'   , type: 'text'        , },
]


const Translations: CollectionConfig = {
  slug: 'translations',
  auth: true,
  admin: { useAsTitle: 'name' },
  fields: translationFields,
};

const playerFields: Field[] = [
  { name: 'name', type: 'array', fields: [{ name: 'translation', type: 'relationship', relationTo: 'translations' }] },
  { name: 'age' , type: 'number' },
  { name: 'description', type: 'array', fields: [{ name: 'translation', type: 'relationship', relationTo: 'translations' }] }
]

const Players: CollectionConfig = {
  slug: 'players',
  auth: true,
  admin: { useAsTitle: 'name' },
  fields: playerFields,
}

export { Languages, Translations, Players }

This is my attempt. Not sure if this makes even sense. Could someone help me with this?

steep oasisBOT
balmy pewter
#

Hey @lofty patrol,

I would highly encourage you to take a look at the docs, as it looks like what you're trying to do is already covered by localization - in which case all you would have to do is configure it and author your content. See here: https://payloadcms.com/docs/configuration/localization

lofty patrol
balmy pewter
#

Yeah, looks like relationship and join fields will be your friends

lofty patrol
#

Could you give me an example of how join fields would work here? I'm reading the documentation but the example given looks a bit difficult to understand.

balmy pewter
#

Certainly! Say you have a players collection which holds all of the players of your game, and you have a languages collection which holds spoken languages that players may use. Now, suppose each player has a relationship to languages via a field named spokenLanguage:

collection data...
{
  name: 'spokenLanguage',
  type: 'relationship',
  relationTo: 'languages'
}

In your languages collection, it may be useful to see which players speak a particular language, and you would achieve this directional flow of information via a join field, like so:

... rest of languages collection field
{
  name: 'playersWhoSpeakMe',
  type: 'join',
  collection: 'players',
  on: 'spokenLanguage',
}
#

Does this make sense?

lofty patrol
balmy pewter
#

Join represents a bidirectional relationship, I'm not sure where you would put joins for those, like from what collection?

lofty patrol
balmy pewter
#

For on-to-many, you would juse use a relationship field with a hasMany option set to true

lofty patrol
#

The relationTo option is supposed to take the slug of the collection i want to reference, right?

balmy pewter
#

Yep! Or an array of slugs if it's hasMany

lofty patrol
#

In my case I'm getting a Collection slug already in use error.
But i'm looking around and I can't find another place where I'm using it.

balmy pewter
#

You mean in your relatinoship field?

#

Is it hasMany or a single?

lofty patrol
#

In this case. (sorry for the print)
Its throwing an error that "languages" is already a slug in use.

balmy pewter
#

Huh

#

This is strange, what if you change the name of that field?

#

from Language to something else

lofty patrol
#

Changed to 'lingua' and it gave the same error.

balmy pewter
#

Do you need it to be an auth collection? Can you try removing auth: true

#

Otherwise I'm not too sure what's happening here

lofty patrol
#

Just to be sure, the auth option makes it visible only if its authenticated. Right?

balmy pewter
#

No, auth option makes it so that payload recognizes it as a collection that can have users

#

As in it adds email and password fields to it automatically

lofty patrol
#

AH. then no it ain't needed

balmy pewter
#

You are looking for access

#

To get what you described above

lofty patrol
#

I removed it. Nothing changed

#

oh wait a min. I'm dumb

#

nvm

#

I was giving it duplicated

balmy pewter
#

Yep, that'll do it

lofty patrol
#

Huh. Thats a new one. I'm getting an error telling me that I'm not allowed to perform this action while trying to access /admin

balmy pewter
#

Check access controls and CRSF in config

lofty patrol
balmy pewter
#

Buildconfig, check access controls on your collections

lofty patrol
balmy pewter
lofty patrol
#

cors?

#

I don't have that on my buildConfig. But I did define app.use(cors( { origin: '*' } )); before initializing the Payload

balmy pewter
#

Do it in buildConfig

#

In general, the template I linked you to is a great resource

lofty patrol
#

The same problem persists

balmy pewter
#

Are you on v3 or v2?

lofty patrol
#

v3 I believe

balmy pewter
#

Mind sharing your buildConfig?

lofty patrol
#

Sure

#
import { ENV } from './env';
import WebPackConfig from "@/webpack.config"
import path from 'path'

import { payloadCloud } from '@payloadcms/plugin-cloud'
import { mongooseAdapter } from '@payloadcms/db-mongodb'

import { webpackBundler } from '@payloadcms/bundler-webpack'

import { slateEditor } from '@payloadcms/richtext-slate'
import { buildConfig } from 'payload/config'

import { Translations, Languages, Users, Teams, Persons } from './collections'

console.log("Before Config")

export default buildConfig({
    editor: slateEditor({}),
    collections: [ Translations, Users, Teams, Persons, Languages ],
    typescript: { outputFile: path.resolve(__dirname, 'payload-types.ts') },
    graphQL: { schemaOutputFile: path.resolve(__dirname, 'generated-schema.graphql') },
    plugins: [payloadCloud()],
    db: mongooseAdapter({ url: ENV.PAYLOAD_PUBLIC_DATABASE_URI, schemaOptions: { strict: false }, }),
    admin: {
        bundler: webpackBundler(),
    },
    cors: '*'
})

console.log("After Config")
#

I decided to check the console of the page while loading
I found this:

balmy pewter
#

Can you move your cors config to how it is in the template, create an env var for NEXT_PUBLIC_SERVER_URL as it's used quite a bit

#

The issue you have is symptomatic of misconfigured cors

lofty patrol
balmy pewter
#

It's not clear, I think it depends on credential headers and some other stuff

#

Wildcards on localhost

lofty patrol
balmy pewter
#

I agree with you, yes technically it should work, but it's not clear to me if it will here since the issue you are describing is typical of misconfigured cors. Might be that the config is expecting a real url

lofty patrol
#

Shouldn't this come from the access control of the collections?

balmy pewter
#

Did you check access controls? You've configued custom access for your collections?

#

If you've not configured anything custom for your collections, access controls will default to true

lofty patrol
#

But this was once running without anything related to cors

balmy pewter
#

Did this happen after you removed auth from login?

#

Do you have a users collection?

lofty patrol
balmy pewter
#

Is there a user in there that's already created?

lofty patrol
#

Its just as simple as this:

import { CollectionConfig } from 'payload/types'

const Users: CollectionConfig = {
    slug: 'users',
    auth: true,
    admin: {
        useAsTitle: 'email',
    },
    fields: [
        // Email added by default
        // Add more fields as needed
    ],
}

export { Users };
lofty patrol
#

Should I reset the MongoDB?

balmy pewter
#

Nice, try logging out, and then logging in with that user

#

I think maybe something with removing an auth collection went goofy

#

I also think the cors wildcard should work but here we are

lofty patrol
#

I can't. Normally when I load /admin it shows me the login page or the register page if no user is available

balmy pewter
#

Clear cookies, or open in incognito, restart server and give it a try

lofty patrol
#

Ok, let me restart stuff then

#

Nope, I restarted everything. Even the DB

#

Still the same problem

balmy pewter
#

Yeah I checked in the meantime, cors accepts string[] | "*" | CORSConfig

#

As for Access Controls, by default all it will do is check that a user is logged in essentially

#

Not too sure what's giving you the trouble

lofty patrol
#

Mind if I share with you my Collections files?

balmy pewter
#

Sure

lofty patrol
#

Person.ts

import { CollectionConfig, CollectionAfterChangeHook, Field } from 'payload/types';
import { cache } from '../middlewares';

const PERSON_MEMCLEAN = [
    'player_stats',
    'team_roster',
    'double_subs',
    'team_tactic_substitutes',
    'match_tactics',
    'match_scorers',
    'player_touchmap',
    'player_heatmap',
    'player_compare_match',
    'player_seasonstats',
    'player_compare_season',
    'top5',
    'team_tactic_l3',
];

const personFields: Field[] = [
    { name: 'matchName' , type: 'relationship', relationTo: 'translations', hasMany: true },
    { name: 'shortName' , type: 'relationship', relationTo: 'translations', hasMany: true },
    { name: 'firstName' , type: 'text' },
    { name: 'lastName'  , type: 'text' },
    { name: 'image'     , type: 'text' },
    { name: 'video'     , type: 'text' },
    { name: 'vbn_folder', type: 'text' },
    { name: 'team'      , type: 'relationship', relationTo: 'teams', admin: { readOnly: true } },
]

// Define the afterChange hook
const afterChangeHook: CollectionAfterChangeHook = async ({ doc }) => {
    cache.deletetargetkeys(PERSON_MEMCLEAN);
};

const Persons: CollectionConfig = {
    slug: 'persons',
    admin: {
        useAsTitle: 'matchName',
        defaultColumns: ['firstName', 'lastName', 'team'],
    },
    fields: personFields,
    hooks: { afterChange: [afterChangeHook] },
};

export { Persons };
#

Team.ts

import { CollectionConfig, CollectionAfterChangeHook, Field } from 'payload/types';
import { cache } from '../middlewares';

const teamFields: Field[] = [
    { name: 'name'        , type: 'relationship', relationTo: 'translations', hasMany: true },
    { name: 'initial'     , type: 'relationship', relationTo: 'translations', hasMany: true },
    { name: 'officialName', type: 'text' },
    { name: 'imagesfolder', type: 'text' },
    { name: 'color'       , type: 'text' },
    { name: 'textcolor'   , type: 'text' },
    { name: 'numbercolor' , type: 'text' },
    { name: 'badge'       , type: 'text' },
];

const afterChangeHook: CollectionAfterChangeHook = async ({ doc }) => {
    cache.deletekeys(['&target=']);
};

const Teams: CollectionConfig = {
    slug: 'teams',
    admin: { useAsTitle: 'officialName', defaultColumns: ['officialName'] },
    fields: teamFields,
    hooks: { afterChange: [afterChangeHook] },
};

export { Teams };
#

Translation.ts

import { CollectionConfig, CollectionAfterChangeHook, Field, CollectionAfterLoginHook } from 'payload/types'
import { cache } from '../middlewares';

// Languages Collection
const languageFields: Field[] = [
    { name: 'name', type: 'text' },
    { name: 'isDefault', type: 'checkbox' }
]

const Languages: CollectionConfig = {
    slug: 'languages',
    admin: { useAsTitle: 'name' },
    fields: languageFields,
}

const translationFields: Field[] = [
    { name: 'key'     , type: 'text' },
    { name: 'language', type: 'relationship', relationTo: 'languages' },
    { name: 'value'   , type: 'text' },
]


const afterChangeHook: CollectionAfterChangeHook = async ({ doc }) => { cache.deletekeys(['&target=']); };

const Translations: CollectionConfig = {
    slug: 'translations',
    admin: { useAsTitle: 'name' },
    fields: translationFields,
    hooks: { afterChange: [afterChangeHook] },
};

export { Languages, Translations };
#

User.ts

import { CollectionConfig } from 'payload/types'

const Users: CollectionConfig = {
    slug: 'users',
    auth: true,
    admin: {
        useAsTitle: 'email',
    },
    fields: [
        // Email added by default
        // Add more fields as needed
    ],
}

export { Users };
#

Do you think there's something I'm not supposed to do?

#

(Btw thank you for helping me, im sure the topic of this thread is long gone)

balmy pewter
#

What's cache from middlewares?

#

I'm not seeing anything here that stands out that would cause your issue

#

And it's my pleasure

lofty patrol
# balmy pewter What's cache from middlewares?

Its just a place where I have a few functions to treat the URL so I can store them

import mcache from 'memory-cache';

const verify = (duration) => {
    return (req, res, next) => {
        const key = '__express__' + req.originalUrl || req.url
        const cachedBody = mcache.get(key)
        if (cachedBody) {
            res.send(cachedBody)
        } else {
            res.sendResponse = res.send
            res.send = (body) => {
                mcache.put(key, body, duration * 1000);
                res.sendResponse(body)
            }
            next()
        }
    }
};

const deletekeys = (keys) => {
    keys.forEach(key => {
        let cachekeys = mcache.keys().filter(element => element.includes(key))
        cachekeys.forEach(element => {
            mcache.del(element)
        });
    });  
};

const deletetargetkeys = (targets) => {
    targets.forEach(target => {
        let cachekeys = mcache.keys().filter(element => element.includes(`&target=${target}`))
        cachekeys.forEach(element => {
            mcache.del(element)
        });
    });  
};

const deletetargetkeysandmatch = (target, keys) => {
    keys.forEach(key => {
        let cachekeys = mcache.keys().filter(element => element.includes(`&target=${target}`) && element.includes(key))
        cachekeys.forEach(element => {
            mcache.del(element)
        });
    });  
};

export {
    verify,
    deletekeys,
    deletetargetkeys,
    deletetargetkeysandmatch
};
#

Sorry for the late response btw

lofty patrol
#

Hi, @balmy pewter just to update you.
Ok, so I've been trying some stuff. Found this thread for someone using v2 that were also getting a forbidden request. The solution was adding an explicit access control to the collection. I did that to each collection.

const collection: CollectionConfig = {
    ...
    access: {
        read  : () => true, // allows public GET
        update: () => true, // allows public PATCH
        create: () => true, // allows public POST
        delete: () => true, // allows public DELETE
    }
}

And that did made the forbidden request disappear. Although now those transformed into 404 requests.

ERROR (payload): NotFound: The requested resource was not found.
    at findByID (D:\backoffice-keystone-opta\back-office-opta\node_modules\payload\src\collections\operations\findByID.ts:99:15)
    at processTicksAndRejections (node:internal/process/task_queues:95:5)
    at findByIDHandler (D:\backoffice-keystone-opta\back-office-opta\node_modules\payload\src\collections\requestHandlers\findByID.ts:26:17)
balmy pewter
#

This is weird

#

You haven't changed the default admin route have you?

#

I'm trying to think of what could be causing 404's

lofty patrol
#

/admin ?

balmy pewter
#

That issue with the access I haven't seen since v2 which makes me think you're on 2.x actually

#

Yeah /admin

lofty patrol
balmy pewter
#

Can you actually run npm list payload or pnpm why payload

#

You should see direct version nums

lofty patrol
#

Fu- apparently I am in fact using 2.0

#

wtf

balmy pewter
#

Oh man, yeah

#

This was eerily reminiscent of an issue I saw in v2

#

I think it can be solved, although I don't remember what the root cause was

lofty patrol
#

Can I upgrade it to v3?

balmy pewter
#

But v3 was released just yesterday

#

Yeah 1 sec

#

I have a doc for you

lofty patrol
#

Cuz I've been following the v3 docs xD

balmy pewter
#

Oh you may have been running into all sorts of unexpected issues if you've been following v3 docs

#

v3 just went stable yesterday

#

It may be worth just starting a new website template using npx or pnpm dlx create-payload-app

#

And then just copy + pasting your configs

#

Instead of trying to migrate outright

#

Might save you some time

#

Shoot, should've asked sooner - sorry I didn't catch that

lofty patrol
#

wait... so when I was following the docs instalation process I did the npx create-payload-app which asked me if i remember correctly which version i wanted to proceed with. I can with like 95% certainty say that I picked v3

#

I started 1 week ago on PayloadCMS

balmy pewter
#

Ahhh, but cpa was still in beta then, and cpa may have not been configured to scaffold a v3 app from the base command

#

You would've had to run create-payload-app@beta

lofty patrol
#

I cant check it with certainty

balmy pewter
#

You could check package.json

#

Or lockfile

#

If it's v3 beta -> it'll be tagged with @beta.123 or some number
v3 is on 3.0.1
v2 is 2.x.x

lofty patrol
#

I dont see that anywhere

balmy pewter
#

In package.json, look for payload package

lofty patrol
#
{
  "name": "BackOffice-Opta",
  "description": "A blank template to get started with Payload",
  "version": "1.0.0",
  "main": "dist/server.js",
  "license": "MIT",
  "scripts": {
    "dev": "env-cmd nodemon src/server.ts ",
    "build:payload": "env-cmd payload build",
    "build:server": "tsc",
    "build": "yarn copyfiles && yarn build:payload && yarn build:server",
    "serve": "env-cmd node dist/server.js",
    "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
    "generate:types": "env-cmd payload generate:types",
    "generate:graphQLSchema": "env-cmd payload generate:graphQLSchema",
    "payload": "env-cmd payload"
  },
  "dependencies": {
    "@payloadcms/bundler-webpack": "^1.0.0",
    "@payloadcms/db-mongodb": "^1.0.0",
    "@payloadcms/plugin-cloud": "^3.0.0",
    "@payloadcms/richtext-slate": "^1.0.0",
    "assert": "^2.1.0",
    "axios": "^1.7.7",
    "cors": "^2.8.5",
    "cross-env": "^7.0.3",
    "dotenv": "^16.4.5",
    "env-cmd": "^10.1.0",
    "express": "^4.19.2",
    "google-spreadsheet": "^3.3.0",
    "luxon": "^3.5.0",
    "memory-cache": "^0.2.0",
    "mongoose": "^8.8.0",
    "node-cron": "^3.0.3",
    "os-browserify": "^0.3.0",
    "payload": "latest",
    "promise.allsettled": "^1.0.7",
    "querystring-es3": "^0.2.1",
    "stream-browserify": "^3.0.0",
    "url": "^0.11.4",
    "util": "^0.12.5",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@types/express": "^4.17.21",
    "@types/node": "^22.9.0",
    "copyfiles": "^2.4.1",
    "nodemon": "^2.0.20",
    "ts-node": "^9.1.1",
    "typescript": "^4.9.5",
    "webpack": "^5.96.1",
    "webpack-cli": "^5.1.4"
  },
  "packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
balmy pewter
#

Judging from the plugins, the fact that you don't have react/react-dom and no next package in deps... you're on v2

lofty patrol
#

;-;

#

Damn

balmy pewter
#

Also v3 doesn't use Express anymore

#

It's gonna be a nice upgrade though

lofty patrol
#

I'm still using express tho, I'm using for a separated API running in the same stuff

balmy pewter
#

Ahh, that's fine

#

Might need to have some seperation from the Payload stuff

#

But Payload is headless so it doesn't care too much

lofty patrol
balmy pewter
#

Oh you're all good

#

Payload v2 used Express internally

#

That dep was dropped in v3

lofty patrol
balmy pewter
#

NextJS

#

It's NextJS native

lofty patrol
#

uh

#

No wonder i was having so much trouble with stuff that was supposed to work

balmy pewter
#

My mind is being blown right now, you have the patience of a saint

#

I can only imagine the trouble you were running into

lofty patrol
#

Ahahah, no problem. People tell me that on a daily basis

balmy pewter
#

Let me know if you run into anymore issues going forward!

lofty patrol
#

Ok, ill probably be opening a different thread for the problems going forward tho

#

This one is getting really extensive the topic is no longer relevant