Mapping non-trivial Postgres columns

I guess this is not an issue but more of a question so I am not using the template.

I have a field in one of my tables that is of type int4range. When I try to access this table in the Forest Admin UI I get

Cannot reach your data
Your server may be down or your database connection broken.

That’s obviously not the case. All the other tables work just fine. Furthermore, I can see in the logs that two requests (and thus two queries) have been made towards that table. They both result in 304 though. I’m guessing that the problem is the value of the column of int4range type. If so, the question is if this type is supported OOTB? Perhaps there is a configurable mapping somewhere? If it is not supported, what are my options for working around this?

It turns out it wasn’t the int4range type that was causing the error. It was that the table does not have a primary key. After adding primaryKey: true in one of the unique columns in the JS model, I can see the table’s data in the UI. Unfortunately, the value of the int4range column is displayed as [object Object],[object Object].

I tried to change the model from type: DataTypes.INTEGER (generated) to DataTypes.RANGE(DataTypes.INTEGER) or Sequelize.RANGE(Sequelize.INTEGER) but both result in the field completely disappearing in the UI. So the question remains, what are my options to view/edit such a column type via the UI?

Hello @MilenDyankov :wave: ,

I’m able to reproduce your use case.
Currently the Forest Admin web app does not support int4range by default.
I’m looking if overriding the result from the route could help.

I will let you know :wink:

I’ve come with a solution that could suits you.

Would overriding your route and the serializing part could be an option for you?

// Get a list of Periods
router.get('/periods', permissionMiddlewareCreator.list(), (request, response, next) => {
  const recordsGetter = new RecordsGetter(periods, request.user, request.query);
  recordsGetter.getAll(request.query)
      .then(records => recordsGetter.serialize(records))
      .then(recordsSerialized => {
        recordsSerialized['data'].forEach(record => {
          record['attributes']['ranges'] = record['attributes']['ranges'].map(x => x['value']);
        })
          return response.send(recordsSerialized);
      })
      .catch(next);
});

Here my collection is named periods and the int4range attribute is names ranges.

It will give you a result like:

Let me know if it helps :wink:

Thank you @Guillaume_Cisco

That almost works! Except I can only see one number of the range, not both. I guess that is because the generated field type is DataTypes.INTEGER. Did you also change the type to see the range displayed?

Yes @MilenDyankov,

I forgot to mention this.
Can you try with:

const Periods = sequelize.define('periods', {
    ranges: {
      type: DataTypes.ARRAY(DataTypes.INTEGER),
    },
  }, {
    tableName: 'periods',
    underscored: true,
    timestamps: false,
    schema: process.env.DATABASE_SCHEMA,
  });

Tell me if it solves your issue :wink:

I took a slightly different route. I have the following to convert the JSON to a number range notation:

function range2string (range) {
    var display = range[0]['inclusive'] ? "[" : ")";
    display += range[0]['value'];
    display += ",";
    display += range[1]['value'];
    display += range[1]['inclusive'] ? "]" : ")";
    return display;
}

and then I have

 record['attributes']['myField'] = range2string(record['attributes']['myField']);

The advantage is that editing and saving data formatted this way works as Postgres recognizes the notation.

The issue I have now is with displaying a single record. There it still shows [object Object],[object Object]. I tried to edit the “get” route for the individual record, but it never calls my custom code. I can see in the logs that the response is 304, which suggests something in front of my customization returns cached results.

Interesting @MilenDyankov

I will test it on my side and tell you if I succeed to have something exploitable :wink:

Alright, I have been able to override the list and the details view for displaying correct data.
First of all, the definition looks like:

const Periods = sequelize.define('periods', {
    ranges: {
      type: DataTypes.ARRAY(DataTypes.INTEGER),
    },
  }

Then I override the list and details route:

const getRange = (range) => {
  return `${range[0]['inclusive'] ? '[': '('}${range[0]['value']}, ${range[1]['value']}${range[1]['inclusive'] ? ']': ')'}`;
}

// Get a list of Periods
router.get('/periods', permissionMiddlewareCreator.list(), (request, response, next) => {
  const recordsGetter = new RecordsGetter(periods, request.user, request.query);
  recordsGetter.getAll(request.query)
      .then(records => recordsGetter.serialize(records))
      .then(recordsSerialized => {
        recordsSerialized['data'].forEach((record) => {
          const ranges = record['attributes']['ranges'];
          record['attributes']['ranges'] = getRange(ranges);
        })
        return response.send(recordsSerialized)
      })
      .catch(next);
});

// Get a Period
//router.get('/periods/:recordId(?!count)', permissionMiddlewareCreator.details(), (request, response, next) => {
router.get('/periods/\\b(?!count\\b):recordId', permissionMiddlewareCreator.details(), (request, response, next) => {
  const recordGetter = new RecordGetter(periods, request.user, request.query);
  recordGetter.get(request.params.recordId)
      .then(record => recordGetter.serialize(record))
      .then(recordSerialized => {
        recordSerialized['data']['attributes']['ranges'] = getRange(recordSerialized['data']['attributes']['ranges']);
        return response.send(recordSerialized);
      })
      .catch(next);
});

Be careful with the path of the detail view, you need to exclude the count word thanks to \\b(?!count\\b):recordId

Does it help?

Let me know :rocket:

Thank you @Guillaume_Cisco !

The \\b(?!count\\b) did the trick. I have no idea what it actually does, but it was the missing piece.

I had to do two things different than you:

  1. I kept the type DataTypes.INTEGER to have only the range displayed in the UI: [1,100).
    If I change it to DataTypes.ARRAY(DataTypes.INTEGER) then it is displayed as array: [[1,100)]. But the bigger issue is that editing becomes tricky. In fact, I couldn’t make it work.

  2. I had to add a similar code to the put route. Otherwise, it goes back to [object Object],[object Object] after the update.

With those in place, it now works nice. Obviously, it would be great if you at some point introduce a nice UI for ranges, but the way it is now is sufficient for what I need.

Thank you for taking the time to help me with that!

1 Like

That’s great! :rocket: :star_struck:

I’m very happy it helps :wink:
Have a great journey with Forest Admin :tada: