Repository: Yoctol/bottender
Pull request #848 (2020-07-22 04:36:12)

feat(FacebookClient): fix getComment and add getComments
- fix some options in `getComment`
- add `getComments`
- improve types

codecov-commenter: # [Codecov](https://codecov.io/gh/Yoctol/bottender/pull/848?src=pr&el=h1) Report
> Merging [#848](https://codecov.io/gh/Yoctol/bottender/pull/848?src=pr&el=desc) into [release/1.5](https://codecov.io/gh/Yoctol/bottender/commit/cd5bc7a933f5bb94af89250e2456cda6fb1088ba&el=desc) will **increase** coverage by `0.01%`.
> The diff coverage is `90.90%`.

[![Impacted file tree graph](https://codecov.io/gh/Yoctol/bottender/pull/848/graphs/tree.svg?width=650&height=150&src=pr&token=JX3EGtxYH3)](https://codecov.io/gh/Yoctol/bottender/pull/848?src=pr&el=tree)

```diff
@@               Coverage Diff               @@
##           release/1.5     #848      +/-   ##
===============================================
+ Coverage        84.02%   84.04%   +0.01%     
===============================================
  Files              120      120              
  Lines             6236     6243       +7     
  Branches          1554     1560       +6     
===============================================
+ Hits              5240     5247       +7     
  Misses             992      992              
  Partials             4        4              
```


| [Impacted Files](https://codecov.io/gh/Yoctol/bottender/pull/848?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/bottender-facebook/src/FacebookContext.ts](https://codecov.io/gh/Yoctol/bottender/pull/848/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyLWZhY2Vib29rL3NyYy9GYWNlYm9va0NvbnRleHQudHM=) | `74.69% <75.00%> (+0.30%)` | :arrow_up: |
| [packages/bottender-facebook/src/FacebookClient.ts](https://codecov.io/gh/Yoctol/bottender/pull/848/diff?src=pr&el=tree#diff-cGFja2FnZXMvYm90dGVuZGVyLWZhY2Vib29rL3NyYy9GYWNlYm9va0NsaWVudC50cw==) | `84.44% <100.00%> (+2.39%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Yoctol/bottender/pull/848?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Yoctol/bottender/pull/848?src=pr&el=footer). Last update [cd5bc7a...d51386f](https://codecov.io/gh/Yoctol/bottender/pull/848?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/packages/bottender-facebook/src/FacebookClient.ts b/packages/bottender-facebook/src/FacebookClient.ts
index 170245ea..19bf8dab 100644
--- a/packages/bottender-facebook/src/FacebookClient.ts
+++ b/packages/bottender-facebook/src/FacebookClient.ts
@@ -66,17 +66,98 @@ export default class FacebookClient extends MessengerClient {
    * @param commentId - ID of the comment.
    * @param options -
    */
-  public getComment(
+  public getComment<
+    T extends Types.CommentField = 'id' | 'message' | 'created_time'
+  >(
     commentId: string,
-    { summary, filter, fields }: Types.GetCommentOptions = {}
-  ): Promise<Types.Comment> {
+    {
+      fields = ['id' as T, 'message' as T, 'created_time' as T],
+    }: { fields?: T[] } = {}
+  ): Promise<
+    Pick<
+      Types.Comment,
+      Types.CamelCaseUnion<Types.CommentKeyMap, typeof fields[number]>
+    >
+  > {
+    const conjunctFields = Array.isArray(fields) ? fields.join(',') : fields;
+
+    return this.axios
+      .get<
+        Pick<
+          Types.Comment,
+          Types.CamelCaseUnion<Types.CommentKeyMap, typeof fields[number]>
+        >
+      >(`/${commentId}`, {
+        params: {
+          fields: conjunctFields,
+          access_token: this.accessToken,
+        },
+      })
+      .then((res) => res.data, handleError);
+  }
+
+  /**
+   * Get comments of the object.
+   *
+   * @see https://developers.facebook.com/docs/graph-api/reference/v7.0/object/comments
+   *
+   * @param objectId - ID of the object.
+   * @param options -
+   */
+  public getComments<
+    T extends Types.CommentField = 'id' | 'message' | 'created_time',
+    U extends boolean = false
+  >(
+    objectId: string,
+    {
+      limit,
+      summary,
+      filter,
+      order,
+      fields = ['id' as T, 'message' as T, 'created_time' as T],
+    }: Types.GetCommentsOptions<T, U> = {}
+  ): Promise<
+    Types.PagingData<
+      Pick<
+        Types.Comment,
+        Types.CamelCaseUnion<Types.CommentKeyMap, typeof fields[number]>
+      >[]
+    > &
+      (U extends true
+        ? {
+            summary: {
+              order: 'ranked' | 'chronological' | 'reverse_chronological';
+              totalCount: number;
+              canComment: boolean;
+            };
+          }
+        : any)
+  > {
     const conjunctFields = Array.isArray(fields) ? fields.join(',') : fields;
 
     return this.axios
-      .get<Types.Comment>(`/${commentId}`, {
+      .get<
+        Types.PagingData<
+          Pick<
+            Types.Comment,
+            Types.CamelCaseUnion<Types.CommentKeyMap, typeof fields[number]>
+          >[]
+        > &
+          (U extends true
+            ? {
+                summary: {
+                  order: 'ranked' | 'chronological' | 'reverse_chronological';
+                  totalCount: number;
+                  canComment: boolean;
+                };
+              }
+            : any)
+      >(`/${objectId}/comments`, {
         params: {
+          limit,
           summary: summary ? 'true' : undefined,
           filter,
+          order,
           fields: conjunctFields,
           access_token: this.accessToken,
         },
diff --git a/packages/bottender-facebook/src/FacebookContext.ts b/packages/bottender-facebook/src/FacebookContext.ts
index dfe3bbb8..bc5db42d 100644
--- a/packages/bottender-facebook/src/FacebookContext.ts
+++ b/packages/bottender-facebook/src/FacebookContext.ts
@@ -232,9 +232,14 @@ export default class FacebookContext extends Context<
    *
    * @see https://developers.facebook.com/docs/graph-api/reference/comment
    */
-  public async getComment(
-    options: Types.GetCommentOptions
-  ): Promise<Types.Comment | null> {
+  public async getComment<
+    T extends Types.CommentField = 'id' | 'message' | 'created_time'
+  >({
+    fields = ['id' as T, 'message' as T, 'created_time' as T],
+  }: Types.GetCommentOptions<T> = {}): Promise<Pick<
+    Types.Comment,
+    Types.CamelCaseUnion<Types.CommentKeyMap, typeof fields[number]>
+  > | null> {
     const commentId = (this._event.rawEvent.value as Types.FeedComment)
       .commentId;
 
@@ -244,14 +249,19 @@ export default class FacebookContext extends Context<
     }
 
     if (this._batchQueue) {
-      return this._batchQueue.push<Types.Comment>(
+      return this._batchQueue.push<
+        Pick<
+          Types.Comment,
+          Types.CamelCaseUnion<Types.CommentKeyMap, typeof fields[number]>
+        >
+      >(
         FacebookBatch.getComment(commentId, {
           accessToken: this._customAccessToken,
-          ...options,
+          fields,
         })
       );
     }
-    return this._client.getComment(commentId, options);
+    return this._client.getComment(commentId, { fields });
   }
 
   /**
diff --git a/packages/bottender-facebook/src/FacebookTypes.ts b/packages/bottender-facebook/src/FacebookTypes.ts
index f8a9aa16..21443330 100644
--- a/packages/bottender-facebook/src/FacebookTypes.ts
+++ b/packages/bottender-facebook/src/FacebookTypes.ts
@@ -3,6 +3,10 @@ import { MessengerTypes } from 'bottender';
 export { FacebookConnectorOptions } from './FacebookConnector';
 export { FacebookContextOptions } from './FacebookContext';
 
+export type CamelCaseUnion<KM, U extends keyof KM> = U extends string
+  ? KM[U]
+  : never;
+
 export type FeedStatus = {
   from: {
     id: string;
@@ -261,6 +265,26 @@ export type CommentField =
   | 'private_reply_conversation'
   | 'user_likes';
 
+export type CommentKeyMap = {
+  id: 'id';
+  attachment: 'attachment';
+  can_comment: 'canComment';
+  can_remove: 'canRemove';
+  can_hide: 'canHide';
+  can_like: 'canLike';
+  can_reply_privately: 'canReplyPrivately';
+  comment_count: 'commentCount';
+  created_time: 'createdTime';
+  from: 'from';
+  like_count: 'likeCount';
+  message: 'message';
+  message_tags: 'messageTags';
+  object: 'object';
+  parent: 'parent';
+  private_reply_conversation: 'privateReplyConversation';
+  user_likes: 'userLikes';
+};
+
 export type InputComment =
   | {
       /**
@@ -295,67 +319,67 @@ export type Comment = {
   /**
    * Link, video, sticker, or photo attached to the comment
    */
-  attachment?: StoryAttachment;
+  attachment: StoryAttachment;
   /**
    * Whether the viewer can reply to this comment
    */
-  canComment?: boolean;
+  canComment: boolean;
   /**
    * Whether the viewer can remove this comment
    */
-  canRemove?: boolean;
+  canRemove: boolean;
   /**
    * Whether the viewer can hide this comment. Only visible to a page admin
    */
-  canHide?: boolean;
+  canHide: boolean;
   /**
    * Whether the viewer can like this comment
    */
-  canLike?: boolean;
+  canLike: boolean;
   /**
    * Whether the viewer can send a private reply to this comment (Page viewers only)
    */
-  canReplyPrivately?: boolean;
+  canReplyPrivately: boolean;
   /**
    * Number of replies to this comment
    */
-  commentCount?: number;
+  commentCount: number;
   /**
    * The time this comment was made
    */
-  createdTime?: string;
+  createdTime: string;
   /**
    * The person that made this comment
    */
-  from?: User;
+  from: User;
   /**
    * Number of times this comment was liked
    */
-  likeCount?: number;
+  likeCount: number;
   /**
    * The comment text
    */
-  message?: string;
+  message: string;
   /**
    * An array of Profiles tagged in message.
    */
-  messageTags?: MessageTag[];
+  messageTags: MessageTag[];
   /**
    * For comments on a photo or video, this is that object. Otherwise, this is empty.
    */
-  object?: any;
+  object: any;
   /**
    * For comment replies, this the comment that this is a reply to.
    */
-  parent?: Comment;
+  parent: Comment;
   /**
    * For comments with private replies, gets conversation between the Page and author of the comment (Page viewers only)
    */
-  privateReplyConversation?: Conversation;
+  privateReplyConversation: Conversation;
   /**
    * Whether the viewer has liked this comment.
    */
-  userLikes?: boolean;
+  userLikes: boolean;
 };
 
 /**
@@ -923,10 +947,49 @@ export type Like = {
   createdTime: string;
 };
 
-export type GetCommentOptions = {
-  summary?: boolean;
+export type CommonPaginationOptions = {
+  limit?: number;
+};
+
+export type CursorBasedPaginationOptions = CommonPaginationOptions & {
+  before?: string;
+  after?: string;
+};
+
+export type TimeBasedPaginationOptions = CommonPaginationOptions & {
+  until?: string;
+  since?: string;
+};
+
+export type OffsetBasedPaginationOptions = CommonPaginationOptions & {
+  offset?: number;
+};
+
+export type PaginationOptions =
+  | CursorBasedPaginationOptions
+  | TimeBasedPaginationOptions
+  | OffsetBasedPaginationOptions;
+
+export type PagingData<T extends object> = {
+  data: T;
+  paging: {
+    previous?: string;
+    next?: string;
+  };
+};
+
+export type GetCommentOptions<T extends CommentField> = {
+  fields?: T[];
+};
+
+export type GetCommentsOptions<
+  T extends CommentField,
+  U extends boolean
+> = PaginationOptions & {
+  summary?: U;
   filter?: 'toplevel' | 'stream';
-  fields?: string | CommentField[];
+  order?: 'chronological' | 'reverse_chronological';
+  fields?: T[];
 };
 
 export type GetLikesOptions = {


Test patch:
diff --git a/packages/bottender-facebook/src/__tests__/FacebookClient.spec.ts b/packages/bottender-facebook/src/__tests__/FacebookClient.spec.ts
index 12c61b73..c0c8b537 100644
--- a/packages/bottender-facebook/src/__tests__/FacebookClient.spec.ts
+++ b/packages/bottender-facebook/src/__tests__/FacebookClient.spec.ts
@@ -208,7 +208,10 @@ describe('send api', () => {
       const res = await client.getComment(COMMENT_ID);
 
       expect(url).toEqual(`/${COMMENT_ID}`);
-      expect(params).toEqual({ access_token: ACCESS_TOKEN });
+      expect(params).toEqual({
+        access_token: ACCESS_TOKEN,
+        fields: 'id,message,created_time',
+      });
       expect(res).toEqual(reply);
     });
 
@@ -229,19 +232,153 @@ describe('send api', () => {
       });
 
       const res = await client.getComment(COMMENT_ID, {
+        fields: ['can_reply_privately'],
+      });
+
+      expect(url).toEqual(`/${COMMENT_ID}`);
+      expect(params).toEqual({
+        access_token: ACCESS_TOKEN,
+        fields: 'can_reply_privately',
+      });
+      expect(res).toEqual(reply);
+    });
+  });
+
+  describe('#getComments', () => {
+    it('should call comments api', async () => {
+      const { client, mock } = createMock();
+
+      const reply = {
+        data: [
+          {
+            created_time: '2020-07-02T15:10:26+0000',
+            message: 'hello world!',
+            id: '128744825536666_128747222206666',
+          },
+        ],
+        paging: {
+          cursors: {
+            before: 'MQZDZD',
+            after: 'MQZDZD',
+          },
+        },
+        summary: {
+          order: 'ranked',
+          total_count: 1,
+          can_comment: true,
+        },
+      };
+
+      let url;
+      let params;
+      mock.onGet().reply((config) => {
+        url = config.url;
+        params = config.params;
+        return [200, reply];
+      });
+
+      const res = await client.getComments(OBJECT_ID);
+
+      expect(url).toEqual(`/${OBJECT_ID}/comments`);
+      expect(params).toEqual({
+        access_token: ACCESS_TOKEN,
+        fields: 'id,message,created_time',
+        filter: undefined,
+        limit: undefined,
+        order: undefined,
+        summary: undefined,
+      });
+      expect(res).toEqual({
+        data: [
+          {
+            createdTime: '2020-07-02T15:10:26+0000',
+            message: 'hello world!',
+            id: '128744825536666_128747222206666',
+          },
+        ],
+        paging: {
+          cursors: {
+            before: 'MQZDZD',
+            after: 'MQZDZD',
+          },
+        },
+        summary: {
+          order: 'ranked',
+          totalCount: 1,
+          canComment: true,
+        },
+      });
+    });
+
+    it('should support other options', async () => {
+      const { client, mock } = createMock();
+
+      const reply = {
+        data: [
+          {
+            created_time: '2020-07-02T15:10:26+0000',
+            message: 'hello world!',
+            id: '128744825536666_128747222206666',
+            can_reply_privately: true,
+          },
+        ],
+        paging: {
+          cursors: {
+            before: 'MQZDZD',
+            after: 'MQZDZD',
+          },
+        },
+        summary: {
+          order: 'reverse_chronological',
+          total_count: 1,
+          can_comment: true,
+        },
+      };
+
+      let url;
+      let params;
+      mock.onGet().reply((config) => {
+        url = config.url;
+        params = config.params;
+        return [200, reply];
+      });
+
+      const res = await client.getComments(OBJECT_ID, {
         summary: true,
         filter: 'toplevel',
+        order: 'reverse_chronological',
         fields: ['can_reply_privately'],
       });
 
-      expect(url).toEqual(`/${COMMENT_ID}`);
+      expect(url).toEqual(`/${OBJECT_ID}/comments`);
       expect(params).toEqual({
         summary: 'true',
         filter: 'toplevel',
+        order: 'reverse_chronological',
         fields: 'can_reply_privately',
         access_token: ACCESS_TOKEN,
       });
-      expect(res).toEqual(reply);
+      expect(res).toEqual({
+        data: [
+          {
+            createdTime: '2020-07-02T15:10:26+0000',
+            message: 'hello world!',
+            id: '128744825536666_128747222206666',
+            canReplyPrivately: true,
+          },
+        ],
+        paging: {
+          cursors: {
+            before: 'MQZDZD',
+            after: 'MQZDZD',
+          },
+        },
+        summary: {
+          order: 'reverse_chronological',
+          totalCount: 1,
+          canComment: true,
+        },
+      });
     });
   });
 
diff --git a/packages/bottender-facebook/src/__tests__/FacebookContext.spec.ts b/packages/bottender-facebook/src/__tests__/FacebookContext.spec.ts
index 39f6031a..68ce6bf6 100644
--- a/packages/bottender-facebook/src/__tests__/FacebookContext.spec.ts
+++ b/packages/bottender-facebook/src/__tests__/FacebookContext.spec.ts
@@ -301,7 +301,7 @@ describe('#getComment', () => {
 
     expect(client.getComment).toBeCalledWith(
       '139560936744456_139620233405726',
-      undefined
+      { fields: ['id', 'message', 'created_time'] }
     );
   });