Pagination Issue

Hi Team,

I added a custom code to display Subscription Orders data from my subscriptions.orders document using mongoose the data is rendering fine but the issue is in the pagination. Its only displaying a 25 records which I set in the code but unable to display the pagination link. Please help to resolve this. Check the attached screenshot

Hello,

What do you mean by “displaying a 25 records which I set in the code” ? Could you share your code?

Pagination can be configured in directly on your layout. Please follow this documentation.

Best regards,

Alban

Hi,

I am using custom hosting and want to display my custom data in the layout. I have a document called subscriptions, which contains a column named orders (as an object). I want to display all the orders in a table format using Node.js. The table and pagination are working fine, and the arrows work correctly. However, the total number of records is not being displayed in the table.

Please check the screenshot. The count API is displaying {“meta”:{“count”:“deactivated”}}

My Code

const {
  BaseCollection,
  BaseDataSource,
} = require('@forestadmin/datasource-toolkit');

const Subscriptions = require('../models/subscriptions');

class SubscriptionOrders extends BaseCollection {
  constructor(dataSource) {
    //super("subscriptionOrders", dataSource);
    super('subscriptionsOrders', dataSource, {
      capabilities: ['List', 'Count'],
    });

    this.addField('id', {
      type: 'Column',
      columnType: 'String',
      isPrimaryKey: true,
    });

    this.addField('subscriptionId', { type: 'Column', columnType: 'String' });
    this.addField('customerEmail', { type: 'Column', columnType: 'String' });
    this.addField('deliveryDate', { type: 'Column', columnType: 'String' });
    this.addField('isPlaced', { type: 'Column', columnType: 'Boolean' });
    this.addField('isSkipped', { type: 'Column', columnType: 'Boolean' });
    this.addField('shopifyOrderId', { type: 'Column', columnType: 'Number' });
    this.addField('shopifyOrderNumber', {
      type: 'Column',
      columnType: 'String',
    });
    this.addField('productTitle', { type: 'Column', columnType: 'String' });
  }

  async list(caller, filter = {}, projection) {
    console.log(filter, 'filter');

    const skip = filter.page?.skip ?? 0;
    const limit = filter.page?.limit ?? 15;
    const pageNumber = Math.floor(skip / limit) + 1;

    const pipeline = [
      { $unwind: { path: '$orders', preserveNullAndEmptyArrays: false } },
      {
        $project: {
          subscriptionId: '$_id',
          customerEmail: 1,
          productTitle: 1,
          deliveryDate: '$orders.deliveryDate',
          isPlaced: '$orders.isPlaced',
          isSkipped: '$orders.isSkipped',
          shopifyOrderId: '$orders.shopifyOrderId',
          shopifyOrderNumber: '$orders.shopifyOrderNumber',
        },
      },
      { $skip: skip },
      { $limit: limit },
    ];

    const results = await Subscriptions.aggregate(pipeline).exec();

    const records = results.map((r, idx) => ({
      id: `${r.subscriptionId}_${skip + idx}`,
      subscriptionId: r.subscriptionId?.toString() || '',
      customerEmail: r.customerEmail || '',
      //deliveryDate: r.deliveryDate || null,
      deliveryDate: r.deliveryDate || '',
      isPlaced: !!r.isPlaced,
      isSkipped: !!r.isSkipped,
      shopifyOrderId: r.shopifyOrderId || null,
      shopifyOrderNumber: r.shopifyOrderNumber || '',
      productTitle: r.productTitle || '',
    }));

    return projection.apply(records);
  }

  async count(caller, filter = {}) {
    console.log('count called with filter:', filter);

    const pipeline = [
      { $unwind: { path: '$orders', preserveNullAndEmptyArrays: false } },
      { $count: 'count' },
    ];

    const res = await Subscriptions.aggregate(pipeline).exec();

    return res?.[0]?.count ?? 0;
  }

  async aggregate() {
    const pipeline = [
      { $unwind: { path: '$orders', preserveNullAndEmptyArrays: false } },
      { $count: 'count' },
    ];

    const res = await Subscriptions.aggregate(pipeline).exec();

    return [{ result: res?.[0]?.count ?? 0 }];
  }
}

class SubscriptionOrdersDataSource extends BaseDataSource {
  constructor() {
    super();

    this.addCollection(new SubscriptionOrders(this));
  }
}

module.exports = () => new SubscriptionOrdersDataSource();


You can find the relevant documentation for implementing the count yourself here: Capabilities declaration | Node.js Developer Guide

You are just missing a this.enableCount() in your constructor :wink:

Hi,

Now I am following this documentation and based on it the count value is coming but still I am getting an error like

error: [500] GET /forest/subscriptionsOrders/count - 2975ms

===== An exception was raised =====
GET /forest/subscriptionsOrders/count?{
fields[subscriptionsOrders]: customerEmail,deliveryDate,id,isPlaced,isSkipped,productTitle,shopifyOrderId,shopifyOrderNumber,subscriptionId,
timezone: Asia/Calcutta
}

Cannot convert undefined or null to object

TypeError: Cannot convert undefined or null to object

at Function.entries ()
at /home/alfaiz/Downloads/projects/shade-of-spring/sos-new-agent/node_modules/@forestadminforestadmin/datasource-customizer/dist/decorators/binary/collection.js:50:36
at Array.map ()
at BinaryCollectionDecorator.aggregate (/home/alfaiz/Downloads/projects/shade-of-spring/sos-new-agent/no@forestadmine_modules/@forestadmin/datasource-customizer/dist/decorators/binary/collection.js:49:31)
at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
at async RenameFieldCollectionDecorator.aggregate (/home/alfaiz/Downloads/projects/shade-of-spring/sos-n@forestadminw-agent/node_modules/@forestadmin/datasource-customizer/dist/decorators/rename-field/collection.js:91:22)
at async CountRoute.handleCount (/home/alfaiz/Downloads/projects/shade-of-s@forestadminring/sos-new-agent/node_modules/@forestadmin/agent/dist/routes/access/count.js:22:39)
at async ErrorHandling.errorHandler (/home/alfaiz/Downloads/projects@forestadminshade-of-spring/sos-new-agent/node_modules/@forestadmin/agent/dist/routes/system/error-handling.js:20:13)
at async Logger.logger (/home/alfaiz/Downloa@forestadmins/projects/shade-of-spring/sos-new-agent/node_modules/@forestadmin/agent/dist/routes/system/logger.js:20:13)
at async cors (/home/alf@koaiz/Downloads/projects/shade-of-spring/sos-new-agent/node_modules/@koa/cors/index.js:109:16)

H@forestadminre is my code

const {
  BaseCollection,
  BaseDataSource,
} = require('@forestadmin/datasource-toolkit');
const Subscriptions = require('../models/subscriptions');

class SubscriptionOrders extends BaseCollection {
  constructor(dataSource) {
    //super("subscriptionOrders", dataSource);super("subscriptionsOrders", dataSource, {capabilities: ["List", "Count"],});

    this.enableCount();
    this.enableSearch();

    this.addField('id', {
      type: 'Column',
      columnType: 'String',
      isPrimaryKey: true,
    });
    this.addField('subscriptionId', { type: 'Column', columnType: 'String' });
    this.addField('customerEmail', { type: 'Column', columnType: 'String' });
    this.addField('deliveryDate', { type: 'Column', columnType: 'String' });
    this.addField('isPlaced', { type: 'Column', columnType: 'Boolean' });
    this.addField('isSkipped', { type: 'Column', columnType: 'Boolean' });
    this.addField('shopifyOrderId', { type: 'Column', columnType: 'Number' });
    this.addField('shopifyOrderNumber', {
      type: 'Column',
      columnType: 'String',
    });
    this.addField('productTitle', { type: 'Column', columnType: 'String' });
  }

  async list(caller, filter = {}, projection) {
    console.log(filter, 'filter');
    const skip = filter.page?.skip ?? 0;
    const limit = filter.page?.limit ?? 15;
    const pageNumber = Math.floor(skip / limit) + 1;

    const pipeline = [
      { $unwind: { path: '$orders', preserveNullAndEmptyArrays: false } },
      {
        $project: {
          subscriptionId: '$_id',
          customerEmail: 1,
          productTitle: 1,
          deliveryDate: '$orders.deliveryDate',
          isPlaced: '$orders.isPlaced',
          isSkipped: '$orders.isSkipped',
          shopifyOrderId: '$orders.shopifyOrderId',
          shopifyOrderNumber: '$orders.shopifyOrderNumber',
        },
      },
      { $skip: skip },
      { $limit: limit },
    ];

    const results = await Subscriptions.aggregate(pipeline).exec();

    const records = results.map((r, idx) => ({
      id: `${r.subscriptionId}_${skip + idx}`,
      subscriptionId: r.subscriptionId?.toString() || '',
      customerEmail: r.customerEmail || '',
      //deliveryDate: r.deliveryDate || null,
      deliveryDate: r.deliveryDate || '',
      isPlaced: !!r.isPlaced,
      isSkipped: !!r.isSkipped,
      shopifyOrderId: r.shopifyOrderId || null,
      shopifyOrderNumber: r.shopifyOrderNumber || '',
      productTitle: r.productTitle || '',
    }));

    //return projection.apply(records);
    return projection ? projection.apply(records) : records;
  }

  async count(caller, filter = {}) {
    console.log('count called with filter:', filter);

    const pipeline = [
      { $unwind: { path: '$orders', preserveNullAndEmptyArrays: false } },
      { $count: 'count' },
    ];

    const res = await Subscriptions.aggregate(pipeline).exec();
    return res?.[0]?.count ?? 0; // Forest expects just a number
  }

  async aggregate(caller, filter = {}, aggregation = {}, limit) {
    const { operation, fields, groups = null } = aggregation || {};
    console.log('aggregate called:', aggregation);

    // Handle COUNT
    if (operation === 'Count' && groups.length === 0 && !fields) {
      const count = await this.count(caller, filter);
      console.log(count, 'my count result');
      return [{ count }]; // ✅ Forest expects { count: <number> }
    }

    // Fallback pipeline if needed
    const pipeline = [
      { $unwind: { path: '$orders', preserveNullAndEmptyArrays: false } },
      { $count: 'count' },
    ];
    const res = await Subscriptions.aggregate(pipeline).exec();
    return [{ count: res?.[0]?.count ?? 0 }];
  }
}

class SubscriptionOrdersDataSource extends BaseDataSource {
  constructor() {
    super();
    this.addCollection(new SubscriptionOrders(this));
  }
}

module.exports = () => new SubscriptionOrdersDataSource();

In your aggregate function, try the following:

return [{ count, group: {} }]

The pagination issue has been resolved. Now I want to display Filter option beside the search bar which is not currently displaying. Please check the attached screenshot and code

const {
BaseCollection,
BaseDataSource,
} = require(“@forestadmin/datasource-toolkit”);
const Subscriptions = require(“../models/subscriptions”);

class SubscriptionOrders extends BaseCollection {
constructor(dataSource) {
//super(“subscriptionOrders”, dataSource);
super(“subscriptionsOrders”, dataSource, {
capabilities: [“List”, “Count”, “Filter”, “Search”, “Sort”],
});

this.enableCount();
this.enableSearch();

this.addField("id", {
  type: "Column",
  columnType: "String",
  isPrimaryKey: true,
});
this.addField("subscriptionId", { type: "Column", columnType: "String" });
this.addField("customerEmail", { type: "Column", columnType: "String" });
this.addField("deliveryDate", { type: "Column", columnType: "String" });
this.addField("isPlaced", { type: "Column", columnType: "Boolean" });
this.addField("isSkipped", { type: "Column", columnType: "Boolean" });
this.addField("shopifyOrderId", { type: "Column", columnType: "Number" });
this.addField("shopifyOrderNumber", {
  type: "Column",
  columnType: "String",
});
this.addField("productTitle", { type: "Column", columnType: "String" });

}

async list(caller, filter = {}, projection) {
//console.log(filter, “filter”);

const skip = filter.page?.skip ?? 0;
const limit = filter.page?.limit ?? 15;
const pageNumber = Math.floor(skip / limit) + 1;

const pipeline = [
  { $unwind: { path: "$orders", preserveNullAndEmptyArrays: false } },
  {
    $project: {
      subscriptionId: "$_id",
      customerEmail: 1,
      productTitle: 1,
      deliveryDate: "$orders.deliveryDate",
      isPlaced: "$orders.isPlaced",
      isSkipped: "$orders.isSkipped",
      shopifyOrderId: "$orders.shopifyOrderId",
      shopifyOrderNumber: "$orders.shopifyOrderNumber",
    },
  },
  { $skip: skip },
  { $limit: limit },
];

const results = await Subscriptions.aggregate(pipeline).exec();

const records = results.map((r, idx) => ({
  id: `${r.subscriptionId}_${skip + idx}`,
  subscriptionId: r.subscriptionId?.toString() || "",
  customerEmail: r.customerEmail || "",
  //deliveryDate: r.deliveryDate || null,
  deliveryDate: r.deliveryDate || "",
  isPlaced: !!r.isPlaced,
  isSkipped: !!r.isSkipped,
  shopifyOrderId: r.shopifyOrderId || null,
  shopifyOrderNumber: r.shopifyOrderNumber || "",
  productTitle: r.productTitle || "",
}));

//return projection.apply(records);
return projection ? projection.apply(records) : records;

}

async count(caller, filter = {}) {
console.log(“count called with filter:”, filter);

const pipeline = [
  { $unwind: { path: "$orders", preserveNullAndEmptyArrays: false } },
  { $count: "count" },
];

const res = await Subscriptions.aggregate(pipeline).exec();
return res?.[0]?.count ?? 0; // Forest expects just a number

}

async aggregate(caller, filter = {}, aggregation = {}, limit) {
const { operation, fields, groups = } = aggregation || {};
console.log(“aggregate called:”, aggregation);

// Handle COUNT
if (operation === "Count" && groups.length === 0 && !fields) {
  const count = await this.count(caller, filter);
  console.log(count, "my count result");
  //return [{ count }]; // ✅ Forest expects { count: <number> }
  return [{ value: count, group: {} }];
}

// Fallback pipeline if needed
const pipeline = [
  { $unwind: { path: "$orders", preserveNullAndEmptyArrays: false } },
  { $count: "count" },
];
const res = await Subscriptions.aggregate(pipeline).exec();
return [{ count: res?.[0]?.count ?? 0 }];

}
}

class SubscriptionOrdersDataSource extends BaseDataSource {
constructor() {
super();
this.addCollection(new SubscriptionOrders(this));
}
}

module.exports = () => new SubscriptionOrdersDataSource();

Hi @alfaizm,

If you continue to look the documentation I send you you will see it’’s talking about the filter, you can find it here: Capabilities declaration | Node.js Developer Guide

Hi @vince

I checked the documentation and tried implementing it in different ways, but I’m still unable to see the filter option in the UI. Please schedule a call to look into this issue.

Could you please share your code ?

What’s the name of your project ?