attach extra information to field definitions

discord user avatar
alessiogr
Payload Team
5 months ago
14

Anyone knows a good idea to attach extra info to field definitions?



The goal is to be able to read that information through a plugin.



E.g. something like:


{
            name: 'textArea',
            type: 'textarea',
            extraInfo: {
              someKey: "val"
            }
},


So I can do stuff like:


Find all fields of a

CollectionConfig

which have value XX for key XY



It's possible to "mark" fields using hooks like @exo3718 did in his plugin here:

https://github.com/TimHal/pcms-backpop/blob/main/demo/src/collections/Foo.collection.ts#L22

which is then "read out" here:

https://github.com/TimHal/pcms-backpop/blob/main/demo/src/backpopulated-relationship.plugin.ts#L17

But I cannot add any extra information to that which I can later extract



This PR would help here:

https://github.com/payloadcms/payload/pull/2449
  • default discord avatar
    Teun
    5 months ago

    I created a pr for exactly this 2 days ago:

    https://github.com/payloadcms/payload/pull/2436

    , after creating a feature request 2 weeks ago here:

    https://github.com/payloadcms/payload/discussions/2369
  • discord user avatar
    alessiogr
    Payload Team
    5 months ago

    Thank you!! Just closed my PR. I completely missed that one. Funny that we both thought of making the same PR in a span of 2 days (I didn't even know yours existed) when this feature was missing for years



    Really looking forward to it getting merged!

  • default discord avatar
    Teun
    5 months ago

    yes. I need it for my payload-swagger plugin to properly document custom endpoint, but thought it wouldn't hurt to also add it to the other levels in the config.



    for instance for adding example data

  • discord user avatar
    alessiogr
    Payload Team
    5 months ago

    Definitely, thank you for that! So you wanna allow users to document their own custom endpoint? Or use it inside your own plugin to programmatically mark stuff or something like that?

  • default discord avatar
    Teun
    5 months ago

    I'd like for the developer of an endpoint to be able to document it. I can already derive the endpoint, method and route parameters, but I cannot e.g. generate a proper description, supported query parameters, returntype. And you, what did you intend to use this for?

  • discord user avatar
    alessiogr
    Payload Team
    5 months ago

    That's interesting! Makes sense since endpoints don't have a field like "description".



    I'd like to use it for my new

    https://github.com/AlessioGr/payload-plugin-ai

    plugin. Currently using hooks to mark fields where "embeddings" should be generated for a field after it's been saved. Apart from being ugly, I also want to let users specify additional options. E.g. should the new embeddings field be hidden or not?



    So something like


    custom: {
       ...genEmbeddings({hidden: false}),
    }


    instead of just



    hooks: {beforeChange: [genEmbeddings]}


    Or for the future, allow users to run GPT prompts for fields. E.g.



    custom: {
       ...runPrompt({
    when: 'update',
    prompt: """
    Summarize this text:
    %fieldvalue%
    """,
    outputField: '%fieldname%_summary'
    }),
    }


    So, would be quite a useful feature to me 😄

  • default discord avatar
    Teun
    5 months ago

    Nice one 🙂



    I see that using this custom field would be nice, but since you are already looping through all the fields, you can already just add a field and then just remove it in your plugin. Also, did you know that since your hook is a function, you can allready add properties to it and the joy validation does not fail on it or strip them?

  • discord user avatar
    alessiogr
    Payload Team
    5 months ago
    did you know that since your hook is a function, you can allready add properties to it and the joy validation does not fail on it or strip them?

    Hmm could you make an example? I experimented with it a lot. Only solution I found was returning the data I wanna specify from the hook. But that way I need to call the hook of every single field which is not optimal



    you can already just add a field and then just remove it in your plugin

    Wouldn't that have typescript and/or joy complain?

  • default discord avatar
    Teun
    5 months ago

    I think this would be the way to go for you:



    type EmbeddingsHook = FieldHook & Required<Options>;
    type AllowedField = TextField; // assuming not all fields support this
    
    export const genEmbeddings = ({ hidden = false }: Options = {}): EmbeddingsHook => {
      const hook = (args => {
        return args.value;
      }) as EmbeddingsHook;
      // So here we add the options to the hook. You'll be able to read them in your plugin, yup ignores them
      hook.hidden = hidden;
    
      return hook;
    };
    
    // Users of your plugin can use the below function as a wrapper where they create the field.
    // The typesystem just sees a field that is returned from the function and expects a function, so you're good there
    export const createEmbeddingsField = (field: AllowedField & { options: Options }): Field => {
      const { options, ...payloadField } = field;
      return {
        ...payloadField,
        hooks: {
          ...field.hooks,
          beforeChange: [...(field.hooks?.beforeChange || []), genEmbeddings(options)],
        },
      };
    };


    Of course if you have the

    custom

    field you don't need to use the trick of adding the options to the function. But then only users with a Payload version higher than the one where that get's merged can use your plugin.



    Alternatively you can just cleanup the config in your plugin and take the approach below:



    You could do this, but that's not ideal, but users of you plugin wouldn't be able to do use multiple libraries with config extensions at the same time.



    import { CollectionConfig as PayloadCollectionConfig, Field as PayloadField } from 'payload/types';
    
    export type Field = PayloadField & { description: string };
    
    export type CollectionConfig = Omit<PayloadCollectionConfig, 'fields'> & { fields: Field[] };


    You can solve this by passing the config types as generic params, but it's a bit ugly



    import { CollectionConfig as PayloadCollectionConfig, Field as PayloadField } from 'payload/types';
    
    export type Field<BaseField extends PayloadField = PayloadField> = BaseField & { description: string };
    
    export type CollectionConfig<BaseCollection extends PayloadCollectionConfig = PayloadCollectionConfig> = Omit<BaseCollection, 'fields'> & { fields: Field[] };


    Hope this makes sense

  • discord user avatar
    alessiogr
    Payload Team
    5 months ago

    Thank you!!! Those are very creative solutions! And people say programmers aren't creative... The wrapper for creating a function is certainly an interesting solution.



    Honestly I'm not sure what to prefer. The first one with the wrapper for the field itself might be a bit annoying for the user. Especially when eventually there would be multiple plugins doing that. Similar to the if-condition hell (if(){if(){if(){ } } })



    Last one would be nicer - but then I'd rather wait for your PR to be approved. In the end, it would have the same effect

  • default discord avatar
    Teun
    5 months ago

    Don't understand what you think would cause an

    if-condition hell

    . You could just use it like

    createEmbeddingsField(createDocumentedField(createFancyField({ type: 'text', name: 'bla' })))
  • discord user avatar
    alessiogr
    Payload Team
    5 months ago

    Yeah that's what I mean - nested functions like these. I think those would make it look quite confusing/complex, especially when there are multiple functions and plugins like in your example.



    Also not sure if there's any benefit compared to your

    custom

    field

  • default discord avatar
    Teun
    5 months ago

    It's just a basic decorator pattern. Don't think that's confusing. Maybe naming can be better. Like

    withEmbeddings(withDocumentation(withFancyStuff({ type: 'text', name: 'bla' })))


    It can just be used like this:

    withEmbeddings(withDocumentation(withFancyStuff({ type: 'text', name: 'bla', hidden: true, description: 'bla bla', fancy: 'something' })))


    And I don't think this is instead of the

    custom

    field. When that field is available, you can still apply this pattern, but then instead of putting the

    hidden

    property on the function, you remap it to the custom object.

  • discord user avatar
    alessiogr
    Payload Team
    5 months ago

    Hm I'm trying to implement it like that right now, but it doesn't allow me to do the following things:


    - Return multiple fields (well it does, but then you cannot combine multiple of those)


    - Modify another field to add the hook to it



    Imma try extending the collectionconfig! Hope that doesn't make joi go mad during validation 😅



    hm and yep, that 2nd solution does cause joi to complain

    Code_2023-04-08_at_22.02.562x.jpg
Open the post
Continue the discussion in Discord
Like what we're doing?
Star us on GitHub!

Star

Connect with the Payload Community on Discord

Discord

online

Can't find what you're looking for?

Get help straight from the Payload team with an Enterprise License.