Repository: scimmyjs/scimmy (js)

Issue:
No way to modify `totalResults` and `itemsPerPage` in the `ListResponse` result
I am using the `ListResponse` class to return a paginated response for `User` resources, which are retrieved from my application's database.
Is there a way to modify the `itemsPerPage` and `totalResults` properties of the `ListResponse`?
I am passing the `itemsPerPage` value from the `params` to the `ListResponse` to my SQL query which should correctly return the specified number of records from the database but the `totalResults` is meant to be a number showing count of the total records which satisfy the given conditions.

In my case the `ListResponse` returns the records which my database query returns.

```javascript

new ListResponse(
  resultRows,
  {
    sortBy: sortBy,
    sortOrder: sortOrder,
    startIndex: pgStart,
    itemsPerPage: limit,
  }
)

// response
{
  schemas: [ 'urn:ietf:params:scim:api:messages:2.0:ListResponse' ],
  totalResults: 1,
  Resources: [
    {
      id: 1,
      email: 'some@user.com',
    }
  ],
  startIndex: 1,
  itemsPerPage: 20
}
    
```

here the `totalResults` is showing the length of the `Resources` property of the `ListResponse`, but I am not able to find a way to change it to the calculated value of 8.
And the `itemsPerPage` value in the result is always 20, I provided the `itemsPerPage` param value as `1`.


Pull request description:
Add `totalResults` parameter to `SCIMMY.Messages.ListResponse` constructor
Currently, the `totalResults` parameter of the `ListResponse` class is sourced from the length of the resources array supplied to the constructor at instantiation. This can only be changed by manipulating the length of the resources array, taking advantage of JavaScript's sparse array functionality.

This change adds a `totalResults` property to the `ListResponse` constructor parameters, allowing the total number of results in a list response to be explicitly set without needing to manipulate the length of the resources array. Usage examples and further documentation for the `ListResponse` class have also been included, and test fixtures have been updated to reflect this change (fixes #64).

Patch:
diff --git a/src/lib/messages/listresponse.js b/src/lib/messages/listresponse.js
index 3797938..f49939d 100644
--- a/src/lib/messages/listresponse.js
+++ b/src/lib/messages/listresponse.js
@@ -12,6 +12,29 @@
  * > **Tip:**  
  * > When using the {@link SCIMMY.Resources.User|`User`} and {@link SCIMMY.Resources.Group|`Group`} resource classes, their instance `read()` methods will wrap the set of matching query results in a `ListResponse` instance.
  * > This happens automatically when retrieving multiple resources, meaning their {@link SCIMMY.Types.Resource~EgressHandler|egress handlers} need only return the array of matching resources, not a `ListResponse` instance of their own.
+ * 
+ * @example <caption>Basic usage with no constraints</caption>
+ * // Retrieve the list of resources from somewhere, and pass it to the ListResponse constructor
+ * const resources = [{id: "1", userName: "AdeleV"}, {id: "2", userName: "GradyA"}];
+ * const response = new SCIMMY.Messages.ListResponse(resources);
+ * 
+ * @example <caption>Basic usage with specified total results</caption>
+ * // Retrieve the list of resources from somewhere...
+ * const resources = [{id: "1", userName: "AdeleV"}, {id: "2", userName: "GradyA"}];
+ * // ...and separately retrieve the total number of matching results
+ * const totalResults = 1005;
+ * 
+ * // Instantiate the list response with the resources array, specifying totalResults
+ * const response = new SCIMMY.Messages.ListResponse(resources, {totalResults});
+ * 
+ * @example <caption>Advanced usage with manipulation of array length</caption>
+ * // Retrieve the list of resources from somewhere...
+ * const resources = [{id: "1", userName: "AdeleV"}, {id: "2", userName: "GradyA"}];
+ * // ...and set the length of the list to match the desired total
+ * resources.length = 1005;
+ * 
+ * // Instantiate the list response with the (mostly) sparse resources array
+ * const response = new SCIMMY.Messages.ListResponse(resources, {itemsPerPage: 2});
  */
 export class ListResponse {
     /**
@@ -34,8 +57,12 @@ export class ListResponse {
      * Instantiate a new SCIM List Response Message with relevant details
      * @param {SCIMMY.Messages.ListResponse<T>|T[]} request - contents of the ListResponse message, or items to include in the list response
      * @param {SCIMMY.Messages.ListResponse~ListConstraints} [params] - parameters for the list response (i.e. sort details, start index, and items per page)
+     * @param {String} [params.sortBy] - the attribute to sort results by, if any
+     * @param {String} [params.sortOrder="ascending"] - the direction to sort results in, if sortBy is specified
+     * @param {Number} [params.startIndex=1] - offset index that items start from
      * @param {Number} [params.count=20] - alias property for itemsPerPage, used only if itemsPerPage is unset
      * @param {Number} [params.itemsPerPage=20] - maximum number of items returned in this list response
+     * @param {Number} [params.totalResults] - the total number of resources matching a given request
      * @property {Array<T>} Resources - resources included in the list response
      * @property {Number} totalResults - the total number of resources matching a given request
      * @property {Number} startIndex - index within total results that included resources start from
@@ -44,7 +71,7 @@ export class ListResponse {
     constructor(request = [], params = {}) {
         const outbound = Array.isArray(request);
         const resources = (outbound ? request : request?.Resources ?? []);
-        const totalResults = (outbound ? resources.totalResults ?? resources.length : request.totalResults);
+        const totalResults = (outbound ? params.totalResults ?? resources.length : request.totalResults);
         const {sortBy, sortOrder = "ascending"} = (outbound ? params : {});
         const {startIndex = 1, count = 20, itemsPerPage = count} = (outbound ? params : request);
         
@@ -57,7 +84,7 @@ export class ListResponse {
             throw new TypeError("Expected 'sortOrder' parameter to be either 'ascending' or 'descending' in ListResponse message constructor");
         
         // Check supplied itemsPerPage and startIndex are valid integers...
-        for (let [key, val, min] of Object.entries({itemsPerPage, startIndex}).map(([key, val], index) => ([key, val, index]))) {
+        for (let [key, val, min] of Object.entries({totalResults, itemsPerPage, startIndex}).map(([key, val], index) => ([key, val, Math.max(index - 1, 0)]))) {
             // ...but only expect actual number primitives when preparing an outbound list response
             if (Number.isNaN(Number.parseInt(val)) || !`${val}`.match(/^-?\d*$/) || (outbound && (typeof val !== "number" || !Number.isInteger(val)))) {
                 throw new TypeError(`Expected '${key}' parameter to be a ${min ? "positive" : "non-negative"} integer in ListResponse message constructor`);
@@ -66,11 +93,11 @@ export class ListResponse {
         
         // Construct the ListResponse message
         this.schemas = [ListResponse.#id];
-        this.totalResults = totalResults;
         this.Resources = resources.filter(r => r);
         // Constrain integer properties to their minimum values
         this.startIndex = Math.max(Number.parseInt(startIndex), 1);
         this.itemsPerPage = Math.max(Number.parseInt(itemsPerPage), 0);
+        this.totalResults = Math.max(totalResults, 0);
         
         // Handle sorting if sortBy is defined
         if (sortBy !== undefined) {


Test patch:
diff --git a/test/lib/messages/listresponse.js b/test/lib/messages/listresponse.js
index 8137f34..5900ebf 100644
--- a/test/lib/messages/listresponse.js
+++ b/test/lib/messages/listresponse.js
@@ -37,13 +37,21 @@ describe("SCIMMY.Messages.ListResponse", () => {
         });
         
         it("should expect 'itemsPerPage' parameter to be a number", () => {
-            for (let value of ["a string", false, {}]) {
+            for (let value of ["a string", false, {}, "1"]) {
                 assert.throws(() => new ListResponse([], {itemsPerPage: value}),
                     {name: "TypeError", message: "Expected 'itemsPerPage' parameter to be a non-negative integer in ListResponse message constructor"},
                     `ListResponse instantiated with invalid 'itemsPerPage' parameter value '${value}'`);
             }
         });
         
+        it("should expect 'totalResults' parameter to be a number", () => {
+            for (let value of ["a string", false, {}, "1"]) {
+                assert.throws(() => new ListResponse([], {totalResults: value}),
+                    {name: "TypeError", message: "Expected 'totalResults' parameter to be a non-negative integer in ListResponse message constructor"},
+                    `ListResponse instantiated with invalid 'totalResults' parameter value '${value}'`);
+            }
+        });
+        
         it("should expect 'sortBy' parameter to be a string", () => {
             assert.throws(() => new ListResponse([], {sortBy: 1}),
                 {name: "TypeError", message: "Expected 'sortBy' parameter to be a string in ListResponse message constructor"},
@@ -278,19 +286,19 @@ describe("SCIMMY.Messages.ListResponse", () => {
             }
         });
         
-        it("should equal 'totalResults' property of resources array when preparing outbound messages", () => {
-            assert.strictEqual(new ListResponse(Object.assign([], {totalResults: 100})).totalResults, 100,
-                "Instance member 'totalResults' did not equal 'totalResults' property of resources array when preparing outbound messages");
+        it("should equal 'totalResults' parameter when preparing outbound messages", () => {
+            assert.strictEqual(new ListResponse([], {totalResults: 100}).totalResults, 100,
+                "Instance member 'totalResults' did not equal 'totalResults' parameter when preparing outbound messages");
         });
         
-        it("should equal 'length' property of resources array when 'totalResults' property is not specified", () => {
+        it("should equal 'length' property of resources array when 'totalResults' parameter is not specified", () => {
             assert.strictEqual(new ListResponse(Object.assign([], {length: 100})).totalResults, 100,
-                "Instance member 'totalResults' did not equal 'length' property of resources array when 'totalResults' property was not specified");
+                "Instance member 'totalResults' did not equal 'length' property of resources array when 'totalResults' parameter and property were not specified");
         });
         
-        it("should prefer 'totalResults' property over 'length' property of resources array when preparing outbound messages", () => {
-            assert.strictEqual(new ListResponse(Object.assign([], {length: 1000, totalResults: 100})).totalResults, 100,
-                "Instance member 'totalResults' did not prefer 'totalResults' property over 'length' property of resources array when preparing outbound messages");
+        it("should prefer 'totalResults' parameter over 'length' property of resources array when preparing outbound messages", () => {
+            assert.strictEqual(new ListResponse(Object.assign([], {length: 1000}), {totalResults: 100}).totalResults, 100,
+                "Instance member 'totalResults' did not prefer 'totalResults' parameter over 'length' property of resources array when preparing outbound messages");
         });
     });
 });
\ No newline at end of file