3

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?

Molomby
  • 5,859
  • 2
  • 34
  • 27
Jayd
  • 880
  • 1
  • 12
  • 16

3 Answers3

2

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.

Matt Saunders
  • 3,538
  • 2
  • 22
  • 30
  • 1
    Nice. So you add the missing middle table as a whole new list. I hadn't thought of that. I guess the downside is that now instead of 3 tables (2 tables and a join) it now has 5 tables (2 tables, the joining table and the actual joining tables between them), so performance might become an issue, but you are totally right. this seems like a good way to work around the limitations. Thank you. – Jayd Feb 13 '21 at 02:01
  • NP. You might be able to avoid a performance hit by copying the attributes from your button templates to each button as it's created, so you only need to reference the templates once for each button - example of copying data here https://www.keystonejs.com/keystonejs/server-side-graphql-client/#use-cases – Matt Saunders Feb 15 '21 at 10:12
1

I had a similar challenge once, so after some research and found this answer, I implemented a solution to a project using PostgreSQL TRIGGER.

So you can add a trigger where on an update, it should shift the buttonOrder.

Here is the SQL I had on me, this was the test code, I regex replaced the terms to fit your question :)


    // Assign order
    await knex.raw(`
        do $$
        DECLARE form_id text;
        begin
            CREATE SEQUENCE buttons_order_seq;
            CREATE VIEW buttons_view AS SELECT * FROM "buttons" ORDER BY "createdAt" ASC, "formId";
            CREATE RULE buttons_rule AS ON UPDATE TO buttons_view DO INSTEAD UPDATE buttons SET order = NEW.order WHERE id = NEW.id;
        
            FOR form_id IN SELECT id FROM form LOOP
                ALTER SEQUENCE buttons_order_seq RESTART;
        
                UPDATE buttons_view SET order = nextval('buttons_order_seq') WHERE "formId" = form_id;
            END LOOP;
        
            DROP SEQUENCE buttons_order_seq;
            DROP RULE buttons_rule ON buttons_view;
            DROP VIEW buttons_view;
        END; $$`);

    // Create function that shifts orders
    await knex.raw(`
        CREATE FUNCTION shift_buttons_order()
            RETURNS trigger AS
            $$
            BEGIN
                IF NEW.order < OLD.order THEN
                    UPDATE buttons SET order = order + 1, "shiftOrderFlag" = NOT "shiftOrderFlag"
                        WHERE order >= NEW.order AND order < OLD.order AND "formId" = OLD."formId";
                ELSE 
                    UPDATE buttons SET order = order - 1, "shiftOrderFlag" = NOT "shiftOrderFlag"
                        WHERE order <= NEW.order AND order > OLD.order AND "formId" = OLD."formId";
                END IF;
            RETURN NEW;
            END;
            $$
            LANGUAGE 'plpgsql'`);

    // Create trigger to shift orders on update
    await knex.raw(`
        CREATE TRIGGER shift_buttons_order BEFORE UPDATE OF order ON buttons FOR EACH ROW
            WHEN (OLD."shiftOrderFlag" = NEW."shiftOrderFlag" AND OLD.order <> NEW.order)
            EXECUTE PROCEDURE shift_buttons_order()`);
Karim
  • 415
  • 6
  • 14
0

One option that we came up with is to add the order to the form table.

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

This new field buttonOrder could contain a string representation of the order of the button Ids, like in a JSON stringified array. The main issue with this is that it will be difficult to keep this field in-sync with the actual linked buttons.

Jayd
  • 880
  • 1
  • 12
  • 16