Repository: miragejs/miragejs
Pull request #1027 (2022-03-09 21:19:19)

Updates typing for `Request.queryParams`
This uses the type from https://github.com/tildeio/route-recognizer/blob/0a44fd86081ef425071839d3184e72fd2916d6fe/lib/route-recognizer.ts#L432 / https://github.com/tildeio/route-recognizer/blob/0a44fd86081ef425071839d3184e72fd2916d6fe/lib/route-recognizer.ts#L454

I think that https://github.com/tildeio/route-recognizer/blob/0a44fd86081ef425071839d3184e72fd2916d6fe/lib/route-recognizer.ts#L161 might contain the correct type for `params` a la #1026 but that seems to need better verification

IanVS: @jasikpark could you force-push to this branch to trigger a new run?  I'm curious if it will still fail, and I can't trigger a re-run (maybe it's too old)
jasikpark: @IanVS mind approving the CI run?
IanVS: @cah-brian-gantzler how will we handle types when we swap out MSW for pretender?  For instance, in this PR, we can see that the `queryParams` comes from pretender, which uses [`route-recognzier`](https://github.com/tildeio/route-recognizer), which has a type: https://github.com/tildeio/route-recognizer/blob/0a44fd86081ef425071839d3184e72fd2916d6fe/lib/route-recognizer.ts#L433

If one were to swap out MSW, would the types here change, I guess?
jasikpark: thx for the approval! i don't have merge perms, so merge when you think it's ready to be
cah-brian-gantzler: @IanVS 
The query params are the params at the end of the url. The url is is the same wether its pretender or msw. I do not believe this would change at all. 
jasikpark: so this can merge then?
cah-brian-gantzler: I am ok with this being merged
jasikpark: mind clicking the button for me then? I don't have the permissions for merging my own PR
IanVS: I will do it in a minute after giving one more quick look.

Patch:
diff --git a/lib/serializer.js b/lib/serializer.js
index 43c1467..e79eb30 100644
--- a/lib/serializer.js
+++ b/lib/serializer.js
@@ -340,6 +340,48 @@ class Serializer {
       @public
     */
     this.serializeIds = this.serializeIds || undefined; // this is just here so I can add the doc comment. Better way?
+
+    /**
+      Primary Key name of the model
+
+      Defaults to 'id', so a request for an author looks like:
+
+      ```
+      GET /authors/1
+
+      {
+        author: {
+          id: 1,
+          name: 'Link'
+        }
+      }
+      ```
+
+      Setting `primaryKey` to 'authorId changes this:
+
+      ```js
+      Serializer.extend({
+        primaryKey: 'authorId'
+      });
+      ```
+
+      Now the response looks like:
+
+      ```
+      GET /authors/1
+
+      {
+        author: {
+          authorId: 1,
+          name: 'Link'
+        }
+      }
+      ```
+
+      @property primaryKey
+      @public
+    */
+    this.primaryKey = this.primaryKey || undefined; // this is just here so I can add the doc comment. Better way?
   }
 
   /**
@@ -844,6 +886,10 @@ class Serializer {
     @public
    */
   keyForAttribute(attr) {
+    if (attr === "id") {
+      return this.keyForId();
+    }
+
     return attr;
   }
 
@@ -1146,6 +1192,103 @@ class Serializer {
     return this.registry.schema;
   }
 
+  /**
+    Used to customize how a model's primary key is formatted in your JSON payload.
+
+    By default, this is 'id'
+
+    ```
+    GET /authors/1
+
+    {
+      author: {
+        id: '1',
+        firstName: 'Link',
+        lastName: 'The WoodElf'
+      }
+    }
+    ```
+
+    If your API expects a different primary key, you could write the following:
+
+    ```js
+    // serializers/application.js
+    export default Serializer.extend({
+      keyForId() {
+        return 'authorId';
+      }
+    });
+    ```
+
+    Now the response would look like:
+
+    ```
+    {
+      author: {
+        authorId: '1',
+        firstName: 'Link',
+        lastName: 'The WoodElf'
+      }
+    }
+    ```
+
+    See the property `primaryKey` for a shorthand way of doing this on a model serializer
+
+    @method keyForId
+    @public
+   */
+  keyForId() {
+    return this.primaryKey;
+  }
+
+  /**
+    Used to customize how a model's primary key value is formatted in your JSON payload.
+
+    By default, the primary key is a string
+
+    ```
+    GET /authors/1
+
+    {
+      author: {
+        id: '1',
+        firstName: 'Link',
+        lastName: 'The WoodElf'
+      }
+    }
+    ```
+
+    If your API expects a integers, you could write the following:
+
+    ```js
+    // serializers/application.js
+    export default Serializer.extend({
+      valueForId(value) {
+        return parseInt(value);
+      }
+    });
+    ```
+
+    Now the response would look like:
+
+    ```
+    {
+      author: {
+        authorId: 1,
+        firstName: 'Link',
+        lastName: 'The WoodElf'
+      }
+    }
+    ```
+
+    @method valueForId
+    @param value
+    @public
+   */
+  valueForId(value) {
+    return value;
+  }
+
   /**
     @method _formatAttributeKeys
     @param attrs
@@ -1156,8 +1299,13 @@ class Serializer {
     let formattedAttrs = {};
 
     for (let key in attrs) {
+      let formattedValue = attrs[key];
+      if (key === "id") {
+        formattedValue = this.valueForId(formattedValue);
+      }
+
       let formattedKey = this.keyForAttribute(key);
-      formattedAttrs[formattedKey] = attrs[key];
+      formattedAttrs[formattedKey] = formattedValue;
     }
 
     return formattedAttrs;
@@ -1170,6 +1318,7 @@ class Serializer {
 Serializer.prototype.include = [];
 Serializer.prototype.root = true;
 Serializer.prototype.embed = false;
+Serializer.prototype.primaryKey = "id";
 Serializer.prototype.serializeIds = "included"; // can be 'included', 'always', or 'never'
 
 Serializer.extend = extend;
diff --git a/lib/serializers/active-model-serializer.js b/lib/serializers/active-model-serializer.js
index 5bdbc0f..fa59ea7 100644
--- a/lib/serializers/active-model-serializer.js
+++ b/lib/serializers/active-model-serializer.js
@@ -10,6 +10,7 @@ export default Serializer.extend({
   },
 
   keyForAttribute(attr) {
+    attr = Serializer.prototype.keyForAttribute.apply(this, arguments);
     return underscore(attr);
   },
 
@@ -46,6 +47,11 @@ export default Serializer.extend({
     let belongsToKeys = Object.keys(belongsToAssociations);
     let hasManyKeys = Object.keys(hasManyAssociations);
 
+    if (this.primaryKey !== "id") {
+      attrs.id = attrs[this.primaryKey];
+      delete attrs[this.primaryKey];
+    }
+
     let jsonApiPayload = {
       data: {
         type: this._container.inflector.pluralize(type),
diff --git a/lib/serializers/rest-serializer.js b/lib/serializers/rest-serializer.js
index e010f88..316c2bb 100644
--- a/lib/serializers/rest-serializer.js
+++ b/lib/serializers/rest-serializer.js
@@ -9,6 +9,10 @@ export default ActiveModelSerializer.extend({
   },
 
   keyForAttribute(attr) {
+    attr = ActiveModelSerializer.prototype.keyForAttribute.apply(
+      this,
+      arguments
+    );
     return camelize(attr);
   },
 
diff --git a/types/index.d.ts b/types/index.d.ts
index cf2cb4b..c851677 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -41,7 +41,7 @@ declare module "miragejs" {
     readonly params: Record<string, string>;
 
     /** Any query parameters associated with the request */
-    readonly queryParams: Record<string, string>;
+    readonly queryParams: Record<string, string[] | string | null | undefined>;
   }
 
   /**


Test patch:
diff --git a/__tests__/external/shared/serializers/base/id-key-formatting-test.js b/__tests__/external/shared/serializers/base/id-key-formatting-test.js
new file mode 100644
index 0000000..1ae5280
--- /dev/null
+++ b/__tests__/external/shared/serializers/base/id-key-formatting-test.js
@@ -0,0 +1,109 @@
+import { Server, Model, Serializer } from "miragejs";
+
+describe("External | Shared | Serializers | Base | Id Key Formatting", function () {
+  test(`keyForId formats the id of a model`, () => {
+    let server = new Server({
+      models: {
+        wordSmith: Model,
+      },
+
+      serializers: {
+        wordSmith: Serializer.extend({
+          keyForId() {
+            return "wordSmithId";
+          },
+        }),
+      },
+    });
+
+    let wordSmith = server.schema.wordSmiths.create({
+      id: 1,
+      firstName: "Link",
+      lastName: "Jackson",
+      age: 323,
+    });
+
+    let result = server.serializerOrRegistry.serialize(wordSmith);
+
+    expect(result).toEqual({
+      wordSmith: {
+        wordSmithId: "1",
+        firstName: "Link",
+        lastName: "Jackson",
+        age: 323,
+      },
+    });
+
+    server.shutdown();
+  });
+
+  test(`primaryKey formats the id of a model`, () => {
+    let server = new Server({
+      models: {
+        wordSmith: Model,
+      },
+
+      serializers: {
+        wordSmith: Serializer.extend({
+          primaryKey: "wordSmithId",
+        }),
+      },
+    });
+
+    let wordSmith = server.schema.wordSmiths.create({
+      id: 1,
+      firstName: "Link",
+      lastName: "Jackson",
+      age: 323,
+    });
+
+    let result = server.serializerOrRegistry.serialize(wordSmith);
+
+    expect(result).toEqual({
+      wordSmith: {
+        wordSmithId: "1",
+        firstName: "Link",
+        lastName: "Jackson",
+        age: 323,
+      },
+    });
+
+    server.shutdown();
+  });
+
+  test(`valueForId formats the id of a model`, () => {
+    let server = new Server({
+      models: {
+        wordSmith: Model,
+      },
+
+      serializers: {
+        wordSmith: Serializer.extend({
+          valueForId(value) {
+            return parseInt(value);
+          },
+        }),
+      },
+    });
+
+    let wordSmith = server.schema.wordSmiths.create({
+      id: 1,
+      firstName: "Link",
+      lastName: "Jackson",
+      age: 323,
+    });
+
+    let result = server.serializerOrRegistry.serialize(wordSmith);
+
+    expect(result).toEqual({
+      wordSmith: {
+        id: 1,
+        firstName: "Link",
+        lastName: "Jackson",
+        age: 323,
+      },
+    });
+
+    server.shutdown();
+  });
+});
diff --git a/__tests__/internal/unit/serializers/active-model-serializer-test.js b/__tests__/internal/unit/serializers/active-model-serializer-test.js
index 40bb901..087b1b1 100644
--- a/__tests__/internal/unit/serializers/active-model-serializer-test.js
+++ b/__tests__/internal/unit/serializers/active-model-serializer-test.js
@@ -44,6 +44,29 @@ describe("Unit | Serializers | ActiveModelSerializer", function () {
     });
   });
 
+  test("normalize primaryKey Works", () => {
+    let payload = {
+      contact: {
+        contactId: 1,
+        name: "Link",
+      },
+    };
+
+    serializer.primaryKey = "contactId";
+
+    let jsonApiDoc = serializer.normalize(payload);
+
+    expect(jsonApiDoc).toEqual({
+      data: {
+        type: "contacts",
+        id: 1,
+        attributes: {
+          name: "Link",
+        },
+      },
+    });
+  });
+
   test("it hyphenates snake_cased words", () => {
     let payload = {
       contact: {
diff --git a/types/tests/server-test.ts b/types/tests/server-test.ts
index f862bef..6f75c55 100644
--- a/types/tests/server-test.ts
+++ b/types/tests/server-test.ts
@@ -58,7 +58,7 @@ export default function config(this: Server): void {
     schema.db; // $ExpectType Db
 
     request.params; // $ExpectType Record<string, string>
-    request.queryParams; // $ExpectType Record<string, string>
+    request.queryParams; // $ExpectType Record<string, string | string[] | null | undefined>
     request.requestBody; // $ExpectType string
     request.requestHeaders; // $ExpectType Record<string, string>
     request.url; // $ExpectType string