Simplify your stack and build anything. Or everything.
Build tomorrow’s web with a modern solution you truly own.
Code-based nature means you can build on top of it to power anything.
It’s time to take back your content infrastructure.

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

default discord avatar
ashketshuplast year
90

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?

  • default discord avatar
    zed0547last year

    Hey

    @324517054667423744

    ,



    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
  • default discord avatar
    ashketshuplast year

    That doesn't serve me in this case. It will be useful for some other stuff later on, but rn I really need to make it in the DB level. Can't make it using Localization. I was looking into the

    Join

    field. Not sure if it is the right place to be looking at

  • default discord avatar
    zed0547last year

    Yeah, looks like

    relationship

    and

    join

    fields will be your friends

  • default discord avatar
    ashketshuplast year

    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.

  • default discord avatar
    zed0547last year

    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?

  • default discord avatar
    ashketshuplast year

    Ok, so would this still work if I have it on 2 different fields?


    p.e. in the example both the Player's fields 'name' and 'description' are translatable. Would they be both of type

    join

    ? and if so what would be the

    on

    i would refer to?

  • default discord avatar
    zed0547last year

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

  • default discord avatar
    ashketshuplast year

    In a relationship i could just say that certain field references an instance of a collection. Right?


    I just want an equivalent to a One-to-Many relationship. If you will. But independent from the collection. Rn I'm using the Player collection as an example but later on I want translation of other collection's fields.

  • default discord avatar
    zed0547last year

    For on-to-many, you would juse use a relationship field with a

    hasMany

    option set to true

  • default discord avatar
    ashketshuplast year

    Uhm ok, that is probably what I want.



    The

    relationTo

    option is supposed to take the

    slug

    of the collection i want to reference, right?

  • default discord avatar
    zed0547last year

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

  • default discord avatar
    ashketshuplast year

    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.

  • default discord avatar
    zed0547last year

    You mean in your relatinoship field?



    Is it hasMany or a single?

  • default discord avatar
    ashketshuplast year

    In this case. (sorry for the print)


    Its throwing an error that

    "languages"

    is already a slug in use.

  • default discord avatar
    zed0547last year

    Huh



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



    from Language to something else

  • default discord avatar
    ashketshuplast year

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

  • default discord avatar
    zed0547last year

    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

  • default discord avatar
    ashketshuplast year

    Just to be sure, the

    auth

    option makes it visible only if its authenticated. Right?

  • default discord avatar
    zed0547last year

    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

  • default discord avatar
    ashketshuplast year

    AH. then no it ain't needed

  • default discord avatar
    zed0547last year

    You are looking for

    access

    To get what you described above

  • default discord avatar
    ashketshuplast year

    I removed it. Nothing changed



    oh wait a min. I'm dumb



    nvm



    I was giving it duplicated

  • default discord avatar
    zed0547last year

    Yep, that'll do it

  • default discord avatar
    ashketshuplast year

    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
  • default discord avatar
    zed0547last year

    Check access controls and CRSF in config

  • default discord avatar
    ashketshuplast year

    On BuildConfig or the Initialization?

  • default discord avatar
    zed0547last year

    Buildconfig, check access controls on your collections

  • default discord avatar
    ashketshuplast year

    I haven't specified anything. Do I need to?

  • default discord avatar
    zed0547last year

    I think it's this giving you trouble:

    https://github.com/payloadcms/payload/blob/main/templates/website/src/payload.config.ts#L67

    Can you double check you have that

  • default discord avatar
    ashketshuplast year

    cors?



    I don't have that on my buildConfig. But I did define

    app.use(cors( { origin: '*' } ));

    before initializing the Payload

  • default discord avatar
    zed0547last year

    Do it in buildConfig



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

  • default discord avatar
    ashketshuplast year

    The same problem persists

  • default discord avatar
    zed0547last year

    Are you on v3 or v2?

  • default discord avatar
    ashketshuplast year

    v3 I believe

  • default discord avatar
    zed0547last year

    Mind sharing your buildConfig?

  • default discord avatar
    ashketshuplast year

    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:

  • default discord avatar
    zed0547last year

    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

  • default discord avatar
    ashketshuplast year

    I'm currently only working on localhost. And shouldn't the

    '*'

    do the trick?

  • default discord avatar
    zed0547last year

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



    Wildcards on localhost

  • default discord avatar
    ashketshuplast year

    What do you mean?

  • default discord avatar
    zed0547last year

    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

  • default discord avatar
    ashketshuplast year

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

  • default discord avatar
    zed0547last year

    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

  • default discord avatar
    ashketshuplast year

    But this was once running without anything related to cors

  • default discord avatar
    zed0547last year

    Did this happen after you removed auth from login?



    Do you have a

    users

    collection?

  • default discord avatar
    ashketshuplast year

    Yes I do

  • default discord avatar
    zed0547last year

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

  • default discord avatar
    ashketshuplast year

    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 };


    Yes



    Should I reset the MongoDB?

  • default discord avatar
    zed0547last year

    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

  • default discord avatar
    ashketshuplast year

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

  • default discord avatar
    zed0547last year

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

  • default discord avatar
    ashketshuplast year

    Ok, let me restart stuff then



    Nope, I restarted everything. Even the DB



    Still the same problem

  • default discord avatar
    zed0547last year

    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



    https://payloadcms.com/docs/access-control/overview#default-access-control

    Not too sure what's giving you the trouble

  • default discord avatar
    ashketshuplast year

    Mind if I share with you my Collections files?

  • default discord avatar
    zed0547last year

    Sure

  • default discord avatar
    ashketshuplast year
    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)

  • default discord avatar
    zed0547last year

    What's cache from middlewares?



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



    And it's my pleasure

  • default discord avatar
    ashketshuplast year

    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



    Hi,

    @654031862146007055

    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)
  • default discord avatar
    zed0547last year

    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

  • default discord avatar
    ashketshuplast year

    /admin ?

  • default discord avatar
    zed0547last year

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



    Yeah /admin

  • default discord avatar
    ashketshuplast year

    How can I make it to be sure?

  • default discord avatar
    zed0547last year

    Can you actually run npm list payload or pnpm why payload



    You should see direct version nums

  • default discord avatar
    ashketshuplast year

    Fu- apparently I am in fact using 2.0



    wtf

  • default discord avatar
    zed0547last year

    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

  • default discord avatar
    ashketshuplast year

    Can I upgrade it to v3?

  • default discord avatar
    zed0547last year

    But v3 was released just yesterday



    Yeah 1 sec



    I have a doc for you

  • default discord avatar
    ashketshuplast year

    Cuz I've been following the v3 docs xD

  • default discord avatar
    zed0547last year
    https://github.com/payloadcms/payload/blob/main/docs/migration-guide/overview.mdx

    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

  • default discord avatar
    ashketshuplast year

    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

  • default discord avatar
    zed0547last year

    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

  • default discord avatar
    ashketshuplast year

    I cant check it with certainty

  • default discord avatar
    zed0547last year

    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
  • default discord avatar
    ashketshuplast year

    But where?



    I dont see that anywhere

  • default discord avatar
    zed0547last year

    In package.json, look for payload package

  • default discord avatar
    ashketshuplast year
    {
      "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": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
    }
  • default discord avatar
    zed0547last year

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

  • default discord avatar
    ashketshuplast year

    ;-;



    Damn

  • default discord avatar
    zed0547last year

    Also v3 doesn't use Express anymore



    It's gonna be a nice upgrade though

  • default discord avatar
    ashketshuplast year

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

  • default discord avatar
    zed0547last year

    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

  • default discord avatar
    ashketshuplast year

    Its completely separated

  • default discord avatar
    zed0547last year

    Oh you're all good



    Payload v2 used Express internally



    That dep was dropped in v3

  • default discord avatar
    ashketshuplast year

    Oh so what does it use rn?

  • default discord avatar
    zed0547last year

    NextJS



    It's NextJS native

  • default discord avatar
    ashketshuplast year

    uh



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

  • default discord avatar
    zed0547last year

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



    I can only imagine the trouble you were running into

  • default discord avatar
    ashketshuplast year

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

  • default discord avatar
    zed0547last year

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

  • default discord avatar
    ashketshuplast year

    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

Star on GitHub

Star

Chat on Discord

Discord

online

Can't find what you're looking for?

Get dedicated engineering support directly from the Payload team.