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.

Are recursive relationships possible?

default discord avatar
anodyne_one3 years ago
4
Use Case:

I am creating a game and using PayloadCMS to manage content and data for the game. One part of this is various "factions" that players and NPCs can be a part of.



Part of what I'd like to manage in PayloadCMS is data around the relationships between factions. By doing this, each faction should be able to have a field that references a different faction. I thought a relationship field might work for this but that might not be the best solution and seems to currently be impossible.



What Happens:

When attempting to create a recursive relationship field, I get an error message as follows:



Field Faction Id has invalid relationship 'factions'., stack=InvalidFieldRelationship: Field Faction Id has invalid relationship 'factions'.

I suppose that this makes sense because "factions" is not yet defined as a collection when this field is defined.



Code Example

const fields: Field[] = [
  {
    name: 'id',
    type: 'text',
    required: true,
    unique: true,
    label: 'Should be underscored, used as a reference in attrs, npcs and quests.'
  },
  {
    name: 'name',
    type: 'text',
    required: true,
    label: 'This is what the player sees.'
  },
  {
    name: 'relationships',
    type: 'array',
    fields: [{
      name: 'factionId',
      type: 'relationship',
      relationTo: 'factions',
      maxDepth: 0,
    }, {
      name: 'reputation',
      type: 'number',
      min: 0,
      max: 100,
      defaultValue: 50,
    }],
    label: 'Relationships with other factions',
    hooks: {
      afterRead: [({ value: relationships }) => {
        // Transform array of relationships into an object where the factionId is key:
        return relationships.reduce((acc: Record<string, number>, { factionId, reputation }) => ({
          ...acc,
          [factionId]: reputation,
        }), {});
      }]
    }
  }
];


I imagine there may be a way for me to write some kind of custom field that allows for this, or have a separate collection to describe the relationships between factions, or hardcode some of it (e.g. maintain an array of factionIds that duplicates the data in the CMS though that's not ideal).



Anyway, is it possible to do this with a "relationships" field or a different built-in type?



As an aside, I found a workaround that minimizes hardcoding anything while working for my use case. Still curious about this possibility but it's far from urgent. Thanks



I am running into an error with my workaround



Collection "factions" > Field "factionRelationships" > "value" does not match any of the allowed types

import { FACTION_NAMES, REPUTATIONS } from "lib/Factions";
import { Field } from "payload/dist/fields/config/types";

// The CMS expects string values for its select options:
const FACTION_REPUTATIONS = Object.entries(REPUTATIONS).map(([key, value]) => ({ label: key, value: String(value) }));

// Ensure that the faction ID used in each document or for factionRelationships exists in code:
const validateFactionId = (value: string) => {
  if (!FACTION_NAMES.includes(value as typeof FACTION_NAMES[number])) {
    return `Invalid faction id: ${value}. See Factions.ts for a list of valid values.`;
  }
  return true;
}

const fields: Field[] = [
  {
    name: 'id',
    type: 'text',
    required: true,
    unique: true,
    validate: validateFactionId
  },
  {
    name: 'name',
    type: 'text',
    required: true,
    unique: true,
    label: 'What the player sees.'
  },
  {
    name: 'factionRelationships',
    type: 'array',
    fields: [{
      name: 'factionId',
      type: 'select',
      options: [...FACTION_NAMES],
      required: true,
    }, {
      name: 'reputation',
      type: 'select',
      options: [...FACTION_REPUTATIONS],
      defaultValue: REPUTATIONS.AMBIVALENCE,
      hooks: {
        // The game expects a number here:
        afterRead: [({ value }) => {
          return Number(value);
        }]
      },
      validate: validateFactionId
    }]
  }
];

export const factions = {
  admin: {
    useAsTitle: 'name',
    group: 'Narrative',
  },
  slug: 'factions',
  fields,
};


Oof, okay, the issue above was that

defaultValue

for the reputation field needed to also be a string.

  • discord user avatar
    denolfe
    3 years ago

    There should be no issue with recursive relationships, as we've done this before. An example of that is in the pages collection of the public demo:

    https://github.com/payloadcms/public-demo/blob/master/src/collections/Pages/index.ts#L145
  • default discord avatar
    anodyne_one3 years ago

    Thank you for the example

    @967118574445547650

    ! Could the issue have been with the hook I defined rather than the recursiveness?

  • discord user avatar
    denolfe
    3 years ago

    The error you saw specifically comes from us validation slugs on relationships, so it's odd that you were seeing it.

  • default discord avatar
    anodyne_one3 years ago

    Agreed. I did see the same error in some spots where I had a

    relationship

    type with

    defaultValue: []

    -- the defaultValue was not only unnecessary but caused an error. But that wasn't the case here. Possible that I had an incorrect slug at one point, though. I did have some copy and paste errors along those lines.

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.