Search code examples
postgresqlgraphqlkeystonejs

KeystoneJs user-defined order for Relationship


I am using KeystoneJS with PostgreSQL as my backend and Apollo on the frontend for my app. I have a schema that has a list that is linked to another list. I want to be able to allow users to change the order of the second list.

This is a simplified version of my schema

keystone.createList(
    'forms',
    {
      fields: {
        name: {
          type: Text,
          isRequired: true,
        },
        buttons: {
          type: Relationship,
          ref: 'buttons.attached_forms',
          many: true,
        },
      },
    }
);

keystone.createList(
    'buttons',
    {
      fields: {
        name: {
          type: Text,
          isRequired: true,
        },
        attached_forms: {
          type: Relationship,
          ref: 'forms.buttons',
          many: true,
        },
      },
    }
);

So what I would like to do, is allow users to change the order of buttons so when I fetch them in the future from forms:

const QUERY = gql`
  query getForms($formId: ID!) {
    allforms(where: {
      id: $formId,
    }) {
      id
      name
      buttons {
        id
        name
      }
    }
  }
`;

The buttons should come back from the backend in a predefined order.

{
    id: 1,
    name: 'Form 1',
    buttons: [
        {
            id: 1,
            name: 'Button 1',
        },
        {
            id: 3,
            name: 'Button 3',
        },
        {
            id: 2,
            name: 'Button 2',
        }
    ]
}

Or even just have some data on that returns with the query that will allow for sorting according to the user-defined sort order on the frontend.

The catch is that this relationship is many to many.

So it wouldn't be enough to add a column to the buttons schema as the ordering needs to be relationship-specific. In other words, if a user puts a particular button last on a particular form, it shouldn't change the order of that same button on other forms.

In a backend that I was creating myself, I would add something to the joining table, like a sortOrder field or similar and then change those values to change the order, or even order them on the frontend using that information. Something like this answer here. The many-to-many join table would have columns like formId, buttonId, sortOrder.

I have been diving into the docs for KeystoneJS and I can't figure out a way to make this work without getting into the weeds of overriding the KnexAdapter that we are using.

I am using:

{
    "@keystonejs/adapter-knex": "^11.0.7",
    "@keystonejs/app-admin-ui": "^7.3.11",
    "@keystonejs/app-graphql": "^6.2.1",
    "@keystonejs/fields": "^20.1.2",
    "@keystonejs/keystone": "^17.1.2",
    "@keystonejs/server-side-graphql-client": "^1.1.2",
}

Any thoughts on how I can achieve this?


Solution

  • One approach would be to have two "button" lists, one with a template for a button (buttonTemplate below) with common data such as name etc, and another (button below) which references one buttonTemplate and one form. This allows you to assign a formIndex property to each button, which dictates its position on the corresponding form.

    (Untested) example code:

    keystone.createList(
      'Form',
      {
        fields: {
          name: {
            type: Text,
            isRequired: true,
          },
          buttons: {
            type: Relationship,
            ref: 'Button.form',
            many: true,
          },
        },
      }
    );
    
    keystone.createList(
      'Button',
      {
        fields: {
          buttonTemplate: {
            type: Relationship,
            ref: 'ButtonTemplate.buttons',
            many: false,
          },
          form: {
            type: Relationship,
            ref: 'Form.buttons',
            many: false,
          },
          formIndex: {
            type: Integer,
            isRequired: true,
          },
        },
      }
    );
    
    keystone.createList(
      'ButtonTemplate',
      {
        fields: {
          name: {
            type: Text,
            isRequired: true,
          },
          buttons: {
            type: Relationship,
            ref: 'Button.buttonTemplate',
            many: true,
          },
        },
      }
    );
    

    I think this is less likely to cause you headaches (which I'm sure you can see coming) down the line than your buttonOrder solution, e.g. users deleting buttons that are referenced by this field.

    If you do decide to go with this approach, you can guard against such issues with the hook functionality in Keystone. E.g. before a button is deleted, go through all the forms and rewrite the buttonOrder field, removing any references to the deleted button.