Relation field not autocompleting

Hello,

I have an issue with my project sponsoplus-admin. I can’t load elements with auto-completion from ‘user’ relation to create a new cashbackTransaction entity. Strangely, it’s working for the ‘club’ relation.

I’m working with forest-express-sequelize 7.0.0 and sequelize ~5.15.1.

Here is my t-e-cashback-transaction.js model where I want to create the element:

// This model was generated by Forest CLI. 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 TECashbackTransaction = sequelize.define('tECashbackTransaction', {
    createdAt: {
      type: DataTypes.DATE,
      defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
    },
    updatedAt: {
      type: DataTypes.DATE,
      defaultValue: Sequelize.literal('now()'),
    },
    deletedAt: {
      type: DataTypes.DATE,
    },
    id: {
      type: DataTypes.UUID,
      primaryKey: true,
      defaultValue: Sequelize.literal('uuid_generate_v4()'),
      allowNull: false,
    },
    transactionId: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    amount: {
      type: DataTypes.DOUBLE,
      allowNull: false,
    },
    currency: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    userCashbackAmount: {
      type: DataTypes.DOUBLE,
      allowNull: false,
    },
    clubCashbackAmount: {
      type: DataTypes.DOUBLE,
      allowNull: false,
    },
    transactionDate: {
      type: DataTypes.DATE,
      allowNull: false,
    },
    label: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    origin: {
      type: DataTypes.ENUM(
        'Azela',
      ),
      allowNull: false,
    },
    metadata: {
      type: DataTypes.JSONB,
      allowNull: false,
    },
    status: {
      type: DataTypes.ENUM(
        'YES',
        'NO',
        'PENDING',
      ),
      allowNull: false,
    },
  }, {
    tableName: 't_e_cashback_transaction',
    underscored: true,
  });

  // This section contains the relationships for this model. See: https://docs.forestadmin.com/documentation/v/v6/reference-guide/relationships#adding-relationships.
  TECashbackTransaction.associate = (models) => {
    TECashbackTransaction.belongsTo(models.tEUserWithdraw, {
      foreignKey: {
        name: 'userWithdrawIdKey',
        field: 'user_withdraw_id',
      },
      as: 'userWithdraw',
    });
    TECashbackTransaction.belongsTo(models.tEClubWithdraw, {
      foreignKey: {
        name: 'clubWithdrawIdKey',
        field: 'club_withdraw_id',
      },
      as: 'clubWithdraw',
    });
    TECashbackTransaction.belongsTo(models.tEUser, {
      foreignKey: {
        name: 'userIdKey',
        field: 'user_id',
      },
      as: 'user',
    });
    TECashbackTransaction.belongsTo(models.tEClub, {
      foreignKey: {
        name: 'clubIdKey',
        field: 'club_id',
      },
      as: 'club',
    });
  };

  return TECashbackTransaction;
};

Here is my t-e-club.js model:

// This model was generated by Forest CLI. 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 TEClub = sequelize.define('tEClub', {
    id: {
      type: DataTypes.UUID,
      primaryKey: true,
      defaultValue: Sequelize.literal('uuid_generate_v4()'),
      allowNull: false,
    },
    name: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    beSportId: {
      type: DataTypes.STRING,
    },
    logoUrl: {
      type: DataTypes.STRING,
    },
    sportId: {
      type: DataTypes.INTEGER,
    },
    clubUrl: {
      type: DataTypes.STRING,
    },
  }, {
    tableName: 't_e_club',
    underscored: true,
    timestamps: false,
  });

  // This section contains the relationships for this model. See: https://docs.forestadmin.com/documentation/v/v6/reference-guide/relationships#adding-relationships.
  TEClub.associate = (models) => {
    TEClub.hasMany(models.tEUser, {
      foreignKey: {
        name: 'selectedClubIdKey',
        field: 'selected_club_id',
      },
      as: 'selectedClubTEUsers',
    });
    TEClub.hasMany(models.tECashbackTransaction, {
      foreignKey: {
        name: 'clubIdKey',
        field: 'club_id',
      },
      as: 'clubTECashbackTransactions',
    });
    TEClub.hasMany(models.tEClubWithdraw, {
      foreignKey: {
        name: 'clubIdKey',
        field: 'club_id',
      },
      as: 'clubTEClubWithdraws',
    });
  };

  return TEClub;
};

And here is my t-e-user.js model:

// This model was generated by Forest CLI. 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 TEUser = sequelize.define('tEUser', {
    createdAt: {
      type: DataTypes.DATE,
      defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
    },
    updatedAt: {
      type: DataTypes.DATE,
      defaultValue: Sequelize.literal('now()'),
    },
    deletedAt: {
      type: DataTypes.DATE,
    },
    id: {
      type: DataTypes.UUID,
      primaryKey: true,
      defaultValue: Sequelize.literal('uuid_generate_v4()'),
      allowNull: false,
    },
    email: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    login: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    password: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    lang: {
      type: DataTypes.STRING,
      defaultValue: "fr",
      allowNull: false,
    },
    isActive: {
      type: DataTypes.BOOLEAN,
      defaultValue: true,
      allowNull: false,
    },
    roles: {
      type: DataTypes.JSONB,
      allowNull: false,
    },
    resetPasswordToken: {
      type: DataTypes.STRING,
    },
    emailValidationCode: {
      type: DataTypes.STRING,
    },
    isEmailValidated: {
      type: DataTypes.BOOLEAN,
      allowNull: false,
    },
    isFullDonation: {
      type: DataTypes.BOOLEAN,
      defaultValue: true,
      allowNull: false,
    },
    isSubscribedToClubNewsletter: {
      type: DataTypes.BOOLEAN,
      defaultValue: false,
      allowNull: false,
    },
    isSubscribedToNewsletter: {
      type: DataTypes.BOOLEAN,
      defaultValue: false,
      allowNull: false,
    },
    firstName: {
      type: DataTypes.STRING,
    },
    lastName: {
      type: DataTypes.STRING,
    },
    budgetInsightId: {
      type: DataTypes.STRING,
    },
    budgetInsightToken: {
      type: DataTypes.STRING,
    },
    budgetInsightSynchroStart: {
      type: DataTypes.DATE,
    },
    paymentInformation: {
      type: DataTypes.JSONB,
    },
    address: {
      type: DataTypes.STRING,
    },
    postalCode: {
      type: DataTypes.STRING,
    },
    city: {
      type: DataTypes.STRING,
    },
    decathlonId: {
      type: DataTypes.STRING,
    },
    decathlonPurchaseSynchroStart: {
      type: DataTypes.DATE,
    },
  }, {
    tableName: 't_e_user',
    underscored: true,
  });

  // This section contains the relationships for this model. See: https://docs.forestadmin.com/documentation/v/v6/reference-guide/relationships#adding-relationships.
  TEUser.associate = (models) => {
    TEUser.belongsTo(models.tEClub, {
      foreignKey: {
        name: 'selectedClubIdKey',
        field: 'selected_club_id',
      },
      as: 'selectedClub',
    });
    TEUser.hasMany(models.tERefreshToken, {
      foreignKey: {
        name: 'userIdKey',
        field: 'user_id',
      },
      as: 'userTERefreshTokens',
    });
    TEUser.hasMany(models.tEUserDevice, {
      foreignKey: {
        name: 'userIdKey',
        field: 'user_id',
      },
      as: 'userTEUserDevices',
    });
    TEUser.hasMany(models.tECashbackTransaction, {
      foreignKey: {
        name: 'userIdKey',
        field: 'user_id',
      },
      as: 'userTECashbackTransactions',
    });
    TEUser.hasMany(models.tEUserWithdraw, {
      foreignKey: {
        name: 'userIdKey',
        field: 'user_id',
      },
      as: 'userTEUserWithdraws',
    });
    TEUser.hasOne(models.tEToken, {
      foreignKey: {
        name: 'userIdKey',
        field: 'user_id',
      },
      as: 'userTEToken',
    });
  };

  return TEUser;
};

Thanks for the help,

Best regards,
Hugo

Hello @Hsohm,

No sure to fully understand what’s cause the problem.

  • What do you mean by auto-completion ? (When you select a record from another table using the dropdown during record creation?)

  • Can you check your widget definition ? Maybe something is misconfigured on the front side.

I take a look at your model definition. The relation between TECashbackTransaction and TEUser is a One-To-Many but all seems good at first glance but I’m wondering why you always specify the foreignKey (sequelize handle this by default if you have the right namming).

As you override the default sequelize comportement maybe the error is caused by a missing targetKey when declaring belongsTo relationship. You can find more information on the relationship here.

 TECashbackTransaction.belongsTo(models.tEUser, {
      foreignKey: {
        name: 'userIdKey',
        field: 'user_id',
      },
      targetKey: 'id', // Add this with the field name in the related table
      as: 'user',
    });

Tell me if this helps.

Kind regards,
Morgan

Hello @morganperre,

I mean getting a dropdown menu with the entities list for the userId input (When I select a record from another table using the dropdown during record creation, yes).

My widget definition is the same as the club table which is working perfectly.

I tried to define a targetKey on the relation but It doesn’t fix the problem, the club relation doesn’t have this key and it’s working fine.

I don’t understand what I’m missing, the relation is well defined on the database with a foreignKey.
Also, the foreignKey in the model is auto-generated, I didn’t wrote any lines of this models.

I’m wondering why you always specify the foreignKey

Here is an error log when I try to load the user entities from the dropdown menu autocompleting :

sponsoplus_admin  | [forest] 🌳🌳🌳  Unexpected error: function lower(integer) does not exist
sponsoplus_admin  | {
sponsoplus_admin  |   "name": "SequelizeDatabaseError",
sponsoplus_admin  |   "parent": {
sponsoplus_admin  |     "length": 209,
sponsoplus_admin  |     "name": "error",
sponsoplus_admin  |     "severity": "ERROR",
sponsoplus_admin  |     "code": "42883",
sponsoplus_admin  |     "hint": "No function matches the given name and argument types. You might need to add explicit type casts.",
sponsoplus_admin  |     "position": "1393",
sponsoplus_admin  |     "file": "parse_func.c",
sponsoplus_admin  |     "line": "627",
sponsoplus_admin  |     "routine": "ParseFuncOrColumn",
sponsoplus_admin  |     "sql": "SELECT \"created_at\" AS \"createdAt\", \"updated_at\" AS \"updatedAt\", \"deleted_at\" AS \"deletedAt\", \"id\", \"email\", \"login\", \"password\", \"lang\", \"is_active\" AS \"isActive\", \"roles\", \"reset_password_token\" AS \"resetPasswordToken\", \"email_validation_code\" AS \"emailValidationCode\", \"is_email_validated\" AS \"isEmailValidated\", \"is_full_donation\" AS \"isFullDonation\", \"is_subscribed_to_club_newsletter\" AS \"isSubscribedToClubNewsletter\", \"is_subscribed_to_newsletter\" AS \"isSubscribedToNewsletter\", \"first_name\" AS \"firstName\", \"last_name\" AS \"lastName\", \"budget_insight_id\" AS \"budgetInsightId\", \"budget_insight_token\" AS \"budgetInsightToken\", \"budget_insight_synchro_start\" AS \"budgetInsightSynchroStart\", \"payment_information\" AS \"paymentInformation\", \"address\", \"postal_code\" AS \"postalCode\", \"city\", \"decathlon_id\" AS \"decathlonId\", \"decathlon_purchase_synchro_start\" AS \"decathlonPurchaseSynchroStart\", \"selected_club_id\" AS \"selectedClubIdKey\" FROM \"t_e_user\" AS \"tEUser\" WHERE (lower(\"tEUser\".\"email\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"login\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"password\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"lang\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"reset_password_token\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"email_validation_code\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"first_name\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"last_name\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"budget_insight_id\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"budget_insight_token\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"address\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"postal_code\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"city\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"decathlon_id\")  LIKE  lower('%f%')) LIMIT 10 OFFSET 0;"
sponsoplus_admin  |   },
sponsoplus_admin  |   "original": {
sponsoplus_admin  |     "length": 209,
sponsoplus_admin  |     "name": "error",
sponsoplus_admin  |     "severity": "ERROR",
sponsoplus_admin  |     "code": "42883",
sponsoplus_admin  |     "hint": "No function matches the given name and argument types. You might need to add explicit type casts.",
sponsoplus_admin  |     "position": "1393",
sponsoplus_admin  |     "file": "parse_func.c",
sponsoplus_admin  |     "line": "627",
sponsoplus_admin  |     "routine": "ParseFuncOrColumn",
sponsoplus_admin  |     "sql": "SELECT \"created_at\" AS \"createdAt\", \"updated_at\" AS \"updatedAt\", \"deleted_at\" AS \"deletedAt\", \"id\", \"email\", \"login\", \"password\", \"lang\", \"is_active\" AS \"isActive\", \"roles\", \"reset_password_token\" AS \"resetPasswordToken\", \"email_validation_code\" AS \"emailValidationCode\", \"is_email_validated\" AS \"isEmailValidated\", \"is_full_donation\" AS \"isFullDonation\", \"is_subscribed_to_club_newsletter\" AS \"isSubscribedToClubNewsletter\", \"is_subscribed_to_newsletter\" AS \"isSubscribedToNewsletter\", \"first_name\" AS \"firstName\", \"last_name\" AS \"lastName\", \"budget_insight_id\" AS \"budgetInsightId\", \"budget_insight_token\" AS \"budgetInsightToken\", \"budget_insight_synchro_start\" AS \"budgetInsightSynchroStart\", \"payment_information\" AS \"paymentInformation\", \"address\", \"postal_code\" AS \"postalCode\", \"city\", \"decathlon_id\" AS \"decathlonId\", \"decathlon_purchase_synchro_start\" AS \"decathlonPurchaseSynchroStart\", \"selected_club_id\" AS \"selectedClubIdKey\" FROM \"t_e_user\" AS \"tEUser\" WHERE (lower(\"tEUser\".\"email\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"login\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"password\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"lang\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"reset_password_token\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"email_validation_code\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"first_name\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"last_name\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"budget_insight_id\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"budget_insight_token\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"address\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"postal_code\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"city\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"decathlon_id\")  LIKE  lower('%f%')) LIMIT 10 OFFSET 0;"
sponsoplus_admin  |   },
sponsoplus_admin  |   "sql": "SELECT \"created_at\" AS \"createdAt\", \"updated_at\" AS \"updatedAt\", \"deleted_at\" AS \"deletedAt\", \"id\", \"email\", \"login\", \"password\", \"lang\", \"is_active\" AS \"isActive\", \"roles\", \"reset_password_token\" AS \"resetPasswordToken\", \"email_validation_code\" AS \"emailValidationCode\", \"is_email_validated\" AS \"isEmailValidated\", \"is_full_donation\" AS \"isFullDonation\", \"is_subscribed_to_club_newsletter\" AS \"isSubscribedToClubNewsletter\", \"is_subscribed_to_newsletter\" AS \"isSubscribedToNewsletter\", \"first_name\" AS \"firstName\", \"last_name\" AS \"lastName\", \"budget_insight_id\" AS \"budgetInsightId\", \"budget_insight_token\" AS \"budgetInsightToken\", \"budget_insight_synchro_start\" AS \"budgetInsightSynchroStart\", \"payment_information\" AS \"paymentInformation\", \"address\", \"postal_code\" AS \"postalCode\", \"city\", \"decathlon_id\" AS \"decathlonId\", \"decathlon_purchase_synchro_start\" AS \"decathlonPurchaseSynchroStart\", \"selected_club_id\" AS \"selectedClubIdKey\" FROM \"t_e_user\" AS \"tEUser\" WHERE (lower(\"tEUser\".\"email\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"login\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"password\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"lang\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"reset_password_token\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"email_validation_code\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"first_name\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"last_name\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"budget_insight_id\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"budget_insight_token\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"address\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"postal_code\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"city\")  LIKE  lower('%f%') OR lower(\"tEUser\".\"decathlon_id\")  LIKE  lower('%f%')) LIMIT 10 OFFSET 0;",
sponsoplus_admin  |   "stack": "SequelizeDatabaseError: function lower(integer) does not exist\n    at Query.formatError (/usr/src/app/node_modules/sequelize/lib/dialects/postgres/query.js:354:16)\n    at query.catch.err (/usr/src/app/node_modules/sequelize/lib/dialects/postgres/query.js:71:18)\n    at tryCatcher (/usr/src/app/node_modules/bluebird/js/release/util.js:16:23)\n    at Promise._settlePromiseFromHandler (/usr/src/app/node_modules/bluebird/js/release/promise.js:547:31)\n    at Promise._settlePromise (/usr/src/app/node_modules/bluebird/js/release/promise.js:604:18)\n    at Promise._settlePromise0 (/usr/src/app/node_modules/bluebird/js/release/promise.js:649:10)\n    at Promise._settlePromises (/usr/src/app/node_modules/bluebird/js/release/promise.js:725:18)\n    at _drainQueueStep (/usr/src/app/node_modules/bluebird/js/release/async.js:93:12)\n    at _drainQueue (/usr/src/app/node_modules/bluebird/js/release/async.js:86:9)\n    at Async._drainQueues (/usr/src/app/node_modules/bluebird/js/release/async.js:102:5)\n    at Immediate.Async.drainQueues [as _onImmediate] (/usr/src/app/node_modules/bluebird/js/release/async.js:15:14)\n    at runCallback (timers.js:705:18)\n    at tryOnImmediate (timers.js:676:5)\n    at processImmediate (timers.js:658:5)"
sponsoplus_admin  | }

Best regards,
Hugo

Hello @Hsohm,

Sorry for being late. I tried to reproduce with forest-express-sequelize 8.4.8 but I couldn’t.

In case you didn’t know. You can change the userId to a more friendly referenceField in ForestAdmin.

Ok, I see that it try to filter some column that can be filtered (eg. lower(\"tEUser\".\"budget_insight_id\"), budget_insight_id is an integer so it can’t be lower).

If I’m right you are in forest-express-sequelize 7.12.2. Can you try to upgrade to the lastest forest-express-sequelize 7.12.3 ? The last patch fixes some bug related to required fields for filters/search, it can help.

Also, I recommend you to set a referenceField for all collections. Then see if it fixes the issue.

Tell me if this helps.

Kind regards,
Morgan

1 Like

Hey again,

I just saw you use DataTypes.UUID. I remember we had some troubles with them.

We introduced a fix in forest-express-sequelize v8.3.0. So if my previous answer doesn’t help I think you will have to upgrade to v8. :eyes:

Keep me updated. :pray:

Kind regards,
Morgan