Dynamic smart relation

Hi everybody,

I have an Order table which has a hasMany Attributes relation. Until then nothing complex …
But my Attributes relationship changes depending on the type of Service.

If my service is “ServiceA” then Attributes corresponds to the “AttributeA” table, If my service is “ServiceB” then Attributes corresponds to the “AttributeB” table, etc…

I was thinking of trying something like this:

Order.associate = (models) => {
    Order.belongsTo(models.service, {
      foreignKey: 'serviceId',
      as: 'service',
    });

    Order.hasMany(models.{order.service.className}, {
      foreignKey: 'orderId',
      as: 'attributes',
    });
  };

But I’m not sure how to implement it.
Can you direct me on the right way to do it?

Thank’s

Hello @Benjamin,

You cannot do this kind of dynamic relationship with sequelize nor any SQL ORM.

The only proposal is to use JSON to store any kind of nested data…
In my knowledge only PostgreSQL support sequelize datatype DataTypes.JSONB.

May the force be with you. :pray:

Official documentation

Example

Best regards,
Morgan

OK this turns out to be more complicated than expected :thinking:

Is it possible to make a smart collection with dynamic fields one can in the same way?

There is the Smart Fields in the Forest Admin ecosystem. So you should be able to change the field value with your own logic.

There is also the Smart Segments. Maybe you can create a smart collection with all the available fields then make your magic dynamic thing with Smart Segments only displaying the one you need for each Segment (SegmentServiceA will display AttributesA1, AttributesA2, AttributesA3 and SegmentServiceB will display AttributesB1, AttributesB2, AttributesB3). You get the idea.

If you’re willing to share your use case (or a simpler version) I should provide you a simple example.

I hope it will help.

Kind regards,
Morgan

Yes ok great, let’s try to work together on the subject then !

I have 8 services.

  • BabySitting
  • BikeRepair
  • CarWash
  • ElderlyAssist
  • Flower
  • Gardening
  • HouseWork
  • PressingAtCompany

In my order i have a serviceId that corresponds to a service.
I’ve also 8 table “Attributes” :

  • OrderAttributeBabySitting
  • OrderAttributeBikeRepair
  • OrderAttributeCarWash
  • OrderAttributeElderlyAssist
  • OrderAttributeFlower
  • OrderAttributeGardening
  • OrderAttributeHouseWork
  • OrderAttributePressingAtCompany

Each attribute table contains different element that corresponds to the information of the order. specific to its corresponding service.Obviously for an order of Flower I do not need the attributes of the service HouseWork, PressingAtCompany, …

The idea would therefore be to link the right attribute on each of the services.
Another small specificity, I lied above … In fact I have more than 8 services! Certain service does not have any attribute, the idea in which it would be to display nothing.

Here is some example between two services :

models/order.js

// This model was generated by Lumber. However, you remain in control of your models.
// Learn how here: https://docs.forestadmin.com/documentation/v/v6/reference-guide/models/enrich-your-models
module.exports = (sequelize, DataTypes) => {
  const { Sequelize } = sequelize;
  // This section contains the fields of your model, mapped to your table's columns.
  // Learn more here: https://docs.forestadmin.com/documentation/v/v6/reference-guide/models/enrich-your-models#declaring-a-new-field-in-a-model
  const Order = sequelize.define('order', {
    id: {
      type: DataTypes.BIGINT,
      autoIncrement: true,
      primaryKey: true
    },
    userId: {
      type: DataTypes.BIGINT,
      allowNull: false,
    },
    companyId: {
      type: DataTypes.BIGINT,
      allowNull: false,
    },
    serviceId: {
      type: DataTypes.BIGINT,
      allowNull: false,
    },
    providerId: {
      type: DataTypes.BIGINT,
      allowNull: true,
      defaultValue: null
    },
    status: {
      type: DataTypes.INTEGER,
      allowNull: false,
    },
    userComment: {
      type: DataTypes.STRING,
      allowNull: true,
      defaultValue: null
    },
    amount: {
      type: DataTypes.INTEGER,
      defaultValue: 0,
      allowNull: false,
    },
    deliveryDate: {
      type: DataTypes.DATE,
      allowNull: true,
      defaultValue: null
    },
    createdAt: {
      type: DataTypes.DATE,
      defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
    },
    updatedAt: {
      type: DataTypes.DATE,
      defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
      onUpdate: Sequelize.literal('CURRENT_TIMESTAMP')
    },
  }, {
    tableName: 'Order',
  });

  // This section contains the relationships for this model. See: https://docs.forestadmin.com/documentation/v/v6/reference-guide/relationships#adding-relationships.
  Order.associate = (models) => {
    Order.belongsTo(models.user, {
      foreignKey: 'userId',
    });

    Order.hasMany(models.orderDetail, {
      foreignKey: 'orderId',
      as: 'details',
    });

    Order.belongsTo(models.service, {
      foreignKey: 'serviceId',
      as: 'service',
    });

    Order.hasOne(models.orderPayment, {
      foreignKey: 'orderId',
      as: 'payment',
    });
  };

  return Order;
};

models/service.js

// This model was generated by Lumber. However, you remain in control of your models.
// Learn how here: https://docs.forestadmin.com/documentation/v/v6/reference-guide/models/enrich-your-models
module.exports = (sequelize, DataTypes) => {
  const { Sequelize } = sequelize;
  // This section contains the fields of your model, mapped to your table's columns.
  // Learn more here: https://docs.forestadmin.com/documentation/v/v6/reference-guide/models/enrich-your-models#declaring-a-new-field-in-a-model
  const Service = sequelize.define('service', {
    name: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    className: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    categoryId: {
      type: DataTypes.INTEGER,
      allowNull: false,
    },
    externalLink: {
      type: DataTypes.STRING,
    },
    createdAt: {
      type: DataTypes.DATE,
      defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
    },
    updatedAt: {
      type: DataTypes.DATE,
      defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
      onUpdate: Sequelize.literal('CURRENT_TIMESTAMP')
    },
  }, {
    tableName: 'Service',
  });

  // This section contains the relationships for this model. See: https://docs.forestadmin.com/documentation/v/v6/reference-guide/relationships#adding-relationships.
  Service.associate = (models) => {
  };

  return Service;
};

models/order-attribute-baby-sitting.js

// This model was generated by Lumber. However, you remain in control of your models.
// Learn how here: https://docs.forestadmin.com/documentation/v/v6/reference-guide/models/enrich-your-models
module.exports = (sequelize, DataTypes) => {
  const { Sequelize } = sequelize;
  // This section contains the fields of your model, mapped to your table's columns.
  // Learn more here: https://docs.forestadmin.com/documentation/v/v6/reference-guide/models/enrich-your-models#declaring-a-new-field-in-a-model
  const Service = sequelize.define('service', {
    name: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    className: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    categoryId: {
      type: DataTypes.INTEGER,
      allowNull: false,
    },
    externalLink: {
      type: DataTypes.STRING,
    },
    createdAt: {
      type: DataTypes.DATE,
      defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
    },
    updatedAt: {
      type: DataTypes.DATE,
      defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
      onUpdate: Sequelize.literal('CURRENT_TIMESTAMP')
    },
  }, {
    tableName: 'Service',
  });

  // This section contains the relationships for this model. See: https://docs.forestadmin.com/documentation/v/v6/reference-guide/relationships#adding-relationships.
  Service.associate = (models) => {
  };

  return Service;
};

models/order-attribute-bike-repair.js

// This model was generated by Lumber. However, you remain in control of your models.
// Learn how here: https://docs.forestadmin.com/documentation/v/v6/reference-guide/models/enrich-your-models
module.exports = (sequelize, DataTypes) => {
  const { Sequelize } = sequelize;
  // This section contains the fields of your model, mapped to your table's columns.
  // Learn more here: https://docs.forestadmin.com/documentation/v/v6/reference-guide/models/enrich-your-models#declaring-a-new-field-in-a-model
  const OrderAttributeBikeRepair = sequelize.define('orderAttributeBikeRepair', {
    orderId: {
      type: DataTypes.BIGINT,
      primaryKey: true,
      allowNull: false,
    },
    firstName: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    lastName: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    email: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    indicMobile: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    mobile: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    streetNumber: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    street: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    locality: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    postalCode: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    country: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    additionalAddress: {
      type: DataTypes.STRING,
    },
    vehicle: {
      type: DataTypes.INTEGER,
      allowNull: false,
    },
    slots: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    idOrderApi: {
      type: DataTypes.INTEGER,
      allowNull: false,
    },
    providerOrderNumber: {
      type: DataTypes.STRING,
    },
    providerComment: {
      type: DataTypes.STRING,
    },
    createdAt: {
      type: DataTypes.DATE,
      defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
    },
    updatedAt: {
      type: DataTypes.DATE,
      defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
      onUpdate: Sequelize.literal('CURRENT_TIMESTAMP')
    },
  }, {
    tableName: 'OrderAttributeBikeRepair',
  });

  // This section contains the relationships for this model. See: https://docs.forestadmin.com/documentation/v/v6/reference-guide/relationships#adding-relationships.
  OrderAttributeBikeRepair.associate = (models) => {
  };

  return OrderAttributeBikeRepair;
};

Do not hesitate if you need clarification on the subject, or additional information. I will also start to try various things :point_right:

Hey @Benjamin,

I think I misunderstood your problem. You just have to create a clear relationship between your tables.
I don’t see the complete relation between Order and Service ?

// You have
Order.belongsTo(Service)

// You need to define
Service.hasOne(Order);
// or
Service.hasMany(Order);

The same goes for the relation between Service and order-attribute-xxx.

You are the only one that can decide. But with the right relationship you don’t need the dynamic magic.

:skull_and_crossbones:

I let a fast explanation of my first thought.

First create an “Abstract” Smart Collection reprensenting the union between all the order-attribute-xxx fields.

const { collection } = require('forest-express-sequelize');

collection('all-order-attribute', {
  isSearchable: false,
  actions: [],
  fields: [     
      {
          field: 'id',
          type: 'String',
      }, {
        field: 'name',
         type: 'String',
      }, {
          field: 'className',
          type: 'String',
      }, {
          field: 'categoryId',
          type: 'Number',
      }, {
          field: 'externalLink',
          type: 'String',
      },
      // AttributeBikeRepair
      {
          field: 'orderId',
          type: 'Number',
      }, {
          field: 'firstName',
          type: 'String',
      }, 
      // ... add all fields 
      ],
  segments: [],
});

Then create Smart Segment for all order-attribute-xxx.

segments: [{
    name: 'OrderBikeRepair',
    where: (order) => {
    // Your logic to query the right Attribute table
    // You just fill the field you need add only display those fields in the UI (for each Smart Segment).
    }
  }]

Hi @morganperre,

Yes I forgot to complete the relationship, I do now.
There is no relation between Service and order-attribute-xxx .

I’m not sure I understand, the point here is not to “sort” orders by services. But display the attributes (In the details of the order) of the order. Attribute that changes depending on the service.

I totally get your use case. I’m just not convinced about the relationship/schema you use in the SQL world.

If every OrderAttributeXXX have a relation One-To-One with order and the foreignKey in OrderAttributeXXX. You can’t access the right OrderAttributeXXX from Order. That a limitation from SQL langague.

You can create a custom logic Order > Service > service.attributteTableName then use this to query the right table and return the right data (using orderId). Then use the “Abstract” Smart Collection to display those data but it’s little bit overkill I think…

I will let a colleague take over this thread. Maybe he will have a better solution.

Kind regards,
Morgan

Hello, Thanks for the help so far ! I’m still having trouble figuring out how to make it. So what you are saying is that there is no way to make the relationship without having a “link” between my services and my OrderAttributeXXX? Also, i was trying to make it another way and was wondering if it was possible to make the reference field in a collection dynamic (for each order make a specific reference) ? I’ve seen that it was possible to create a relationship in my routes and get the correct model for the actual order (ex: order 1 will have the correct OrderAttributeFlower and order 2 will have the OrderAttributeBikeRepair) but is there a way to set the correct table fields ? other than writing it in the collection reference field?

collection('order', {
    actions: [],
    fields: [
        {
            field: 'attribute',
            type: ['String'],
            // can we change the reference for each order ? 
            reference: 'orderAttributeFlower'
        },
    ],
    segments: [],
});

Hey @Benjamin :wave:

I’m taking the discussion from here. I’ve discussed with @morganperre just to get the overall idea of this.

To answer your last question, reference is defined to be a String, so you can’t really make it dynamic as far as I know.

Also, your use-case seems very specific, and looks like a Single Table Inheritence issue, which we don’t currently handle well with forest-express-sequelize. Depending on the state of your project (If you are still designing your database or if you already have code running with this setup), I would suggest to either store your OrderAttributeXXX as a JSON column (With validation on save), or look into the STI pattern.

Let me know if that helps :pray:

Hello ! Yes my database is already designed. Uhm about the suggestion, not sure to understand … is it about adding a column in my order table with my OrderAttributeXXX information ?

Thanks

That a potential idea, but I think it will require a lot of work.

After thinking a lot about this actually, the “best” solution we can provide will take advantage of the smart collection feature + reference field.

My idea is to make something similar to an STI table as a smart collection, and then use the smart field feature to make a link to your individual OrderAttributeXXX.

  • Create a smart collection PartialOrderAttribute with an id field (mandatory) & a set of smart fields OrderAttributeXXX1, OrderAttributeXXX2, OrderAttributeXXX3
  • Add a smart relationship on your Order table, that will reference PartialOrderAttribute

With this, you should have related data on your Order table referencing PartialOrderAttribute, which will be linked with your OrderAttributeXXX. From what I know, this is the best solution we can propose that will not require any database changes, but still allow you to indirectly link Order with your OrderAttributeXXX

The associated documentation is here:
For smart collection
For smart field
And finally, for smart relationships

To be clear, dynamic reference is not something supported right now, so if my solution does not suit your need, the best I can do is push your suggestion as a feature request on our product board.

Let me know if that helps :pray:

1 Like

Thanks for the reply ! So is it something like this ? :

const {collection} = require('forest-express-sequelize');

collection('PartialOrderAttribute', {
    actions: [],
    fields: [
        {
            field: 'orderId',
            type: 'String',
        },
        // OrderAttributeFlowers
        {
            field: 'receiverFirstName',
            type: 'String',
        },
        {
            field: 'receiverLastName',
            type: 'String',
        },
        {
            field: 'receiverGender',
            type: 'String',
        },
        ....
        // OrderAttributeBikeRepair
        {
            field: 'firstName',
            type: 'String',
        },
        {
            field: 'lastName',
            type: 'String',
        },
        {
            field: 'slots',
            type: 'String',
        }
        // all other columns and other OrderAttributeXXX ....
    ],
    segments: [],
});

Order collection :

collection('order', {
    actions: [],
    fields: [
        {
            field: 'attribute',
            type: ['String'],
            reference: 'PartialOrderAttribute'
        }
    ],
    segments: [],
});

and one route that will get the corresponding table data (which works) :

router.get('/order/:recordId/relationships/attribute')

So if i’m correct this “method” will fill each column with the returned data and keep unnecessary empty columns displayed ? Isn’t there a way to hide them ? Because if not i will end up with a very long table, i have 8 models for now but i might need to add some more in the future and each OrderAttributeXXX model can have common columns for exemple “orderId” or “mobile” (so it’s fine because it will always be filled) but a model can also have more or less than 5 specific (to the model) column :

Thanks!

A lot of questions here :smiley:

Thanks for the reply ! So is it something like this ? :

Definitely what I had in mind yes!

So if i’m correct this “method” will fill each column with the returned data and keep unnecessary empty columns displayed

That’s the downside of this sadly, until we do support this kind of features. The summary view feature might help a bit to re-organize the fields properly. Maybe you could try (instead of duplicating all the fields) use a smart relationship instead of copying all the fields? I can’t really be sure that would work, but that would avoid the # number of fields you would need to display otherwise.

Isn’t there a way to hide them?

No, not in a dynamic way like you may want to. Hiding fields in related data would also hide them in the table view, which is IMO not something you may want.

Because if not i will end up with a very long table, i have 8 models for now but i might need to add some more in the future and each OrderAttributeXXX model can have common columns for exemple “orderId” or “mobile”

This part is totally achievable though. You can either dynamically generate the fields list before the collection(...) is computed. It will also recompte your schema on server start, so you can computed both your OrderAttributesXXX list & the list of fields associated (Not sure if I was clear enough here, tell me if I wasn’t)

Again, to be clear, this is definitely far from being a perfect solution, and I pushed it as a suggestion on product board. To me, your pattern is definitely a STI use case, even though there is not a direct integration of this within the Sequelize framework.

Let me know if that helps :pray:

1 Like

This part is totally achievable though. You can either dynamically generate the fields list before the collection(...) is computed. It will also recompte your schema on server start, so you can computed both your OrderAttributesXXX list & the list of fields associated (Not sure if I was clear enough here, tell me if I wasn’t)

Uhm i’m not sure to understand it, could i have a quick example ?

Not sure I have a project on hand with an example that would make sense here, but as long as you are able to compute the list of fields required synchronously, the fields attribute of a smart collection could be anything you want - As long as you are able to compute it before Liana.init in the forestadmin.js middleware.

Maybe something like

  // On start, get the schema of the address table
  const addressSchema = await connections.default.queryInterface.describeTable('Address');
  // Initialize the list of fields
  const fields = [];
  // Compute the list of fields from the schema
  Object.keys(addressSchema).map(key => { fields.push({ field: `smart-address-field-${key}`, type: 'String' }); })
  global.smartAddressFields = fields;

Then using global.smartAddressFields in your smart collection (Not very elegant here because of global/the async nature of sequelize, but that’s what I had in mind)

1 Like

Thanks for all the help provided ! And sorry for this late answer. It wasn’t something i could work with unfortunately so i found another way around using a smart view and a collection to get the corresponding data and it is working the way i wanted.

Thanks again!

2 Likes

Good to know you found a workaround then, and that the smart view feature could help in your case :pray:

I’ll mark the thread as solved then, since you seems to have found a solution that suits your need :smiley: