AWS SDK (Node.js) + TypeScriptでAmazon PersonalizeのPutEventが実行できないバグとそのワークアラウンド

SDK側にレポートしてるんですが音沙汰ないのでワークアラウンド含めてここに記録。
https://github.com/aws/aws-sdk-js/pull/2821

## 要約
- AWS SDK(Node.js)のAmazon Personalize PutEventで型定義が間違っている
- `EventPropertiesJSON`の型を`string`から`{[key: string]: any}`に変更すれば解決する
- Pull Requestを出してレポートしているが反応がない
- マージされるまでは`any`や`as`で回避推奨

## 環境

```bash
$ node -v
v10.16.0

$ npm -v
6.9.0

$ npm view aws-sdk version
2.546.0

$ npm view typescript version
3.6.4

$ cat tsconfig.json
$ cat tsconfig.json 
{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "declaration": true,
    "outDir": "./dist/",
    "rootDir": "./libs/",
    "lib": [ "esnext", "dom"],
    "strict": true,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "alwaysStrict": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    // "noUnusedLocals": true,
    // "noUnusedParameters": true,
    "strictNullChecks": true,
    "pretty": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noEmitHelpers": true,
    "importHelpers": true,
    "moduleResolution": "node",
    "esModuleInterop": true
  },
  "include": [
    "libs/"
  ],
  "exclude": [
    "__tests__",
    "docs",
    "node_modules",
    "./dist",
    "./index.js",
    "examples/"
  ]
```

## 現象

AWS SDK(Node.js)を使ってTypeScriptでAmazon PersonalizeにputEventしようとすると、TypeScriptのエラーが発生する。

### Code Sample1: 型定義に従って記述する

```typescript
import {
  PersonalizeEvents,
} from 'aws-sdk'
import { v4 } from 'uuid'
import moment from 'moment'

const trackEvent = async () => {
  const client = new PersonalizeEvents({
    region: 'ap-northeast-1'
  })
  const params: PersonalizeEvents.Types.PutEventsRequest = {
    trackingId: 'xxx-xxx-xxx-xxx',
    userId: '1',
    sessionId: v4(),
    eventList: [{
      eventId: v4(),
      sentAt: moment().toDate(),
      eventType: 'rating',
      properties: JSON.stringify({
        itemId: '1',
        eventValue: 1.0
      })
    }]
  }
  console.log(params)
  const result = await client.putEvents(params).promise()
  console.log(result)
  return
}
trackEvent()
```

#### Code Sample1はAPIバリデーションエラーが発生する

```bash
$  ./node_modules/.bin/ts-node libs/index.ts 
{ trackingId: '52c88f57-7d7f-4941-8396-4c6a7434ec0d',
  userId: '1',
  sessionId: '4d58f851-6c8e-4ae8-8038-56f86f5f40ee',
  eventList:
   [ { eventId: 'd6ed382a-999e-4728-9895-98612a959506',
       sentAt: 2019-08-25T14:57:30.307Z,
       eventType: 'rating',
       properties: '{"itemId":"1","eventValue":1}' } ] }

{ InvalidInputException: Error parsing properties json
    at Object.extractError (/Users/develop/sandbox/aws-playground/node_modules/aws-sdk/lib/protocol/json.js:51:27)
    at Request.extractError (/Users/develop/sandbox/aws-playground/node_modules/aws-sdk/lib/protocol/rest_json.js:55:8)
    at Request.callListeners (/Users/develop/sandbox/aws-playground/node_modules/aws-sdk/lib/sequential_executor.js:106:20)
    at Request.emit (/Users/develop/sandbox/aws-playground/node_modules/aws-sdk/lib/sequential_executor.js:78:10)
    at Request.emit (/Users/develop/sandbox/aws-playground/node_modules/aws-sdk/lib/request.js:683:14)
    at Request.transition (/Users/develop/sandbox/aws-playground/node_modules/aws-sdk/lib/request.js:22:10)
    at AcceptorStateMachine.runTo (/Users/develop/sandbox/aws-playground/node_modules/aws-sdk/lib/state_machine.js:14:12)
    at /Users/develop/sandbox/aws-playground/node_modules/aws-sdk/lib/state_machine.js:26:10
    at Request.<anonymous> (/Users/develop/sandbox/aws-playground/node_modules/aws-sdk/lib/request.js:38:9)
    at Request.<anonymous> (/Users/develop/sandbox/aws-playground/node_modules/aws-sdk/lib/request.js:685:12)
  message: 'Error parsing properties json',
  code: 'InvalidInputException',
  time: 2019-08-25T14:57:30.537Z,
  requestId: 'd6766b7e-69b0-4889-83cf-18171e509b4a',
  statusCode: 400,
  retryable: false,
  retryDelay: 65.34243647640349 }
```

### Code Sample2: プロパティをObjectで渡す

```typescript
import {
  PersonalizeEvents,
} from 'aws-sdk'
import { v4 } from 'uuid'
import moment from 'moment'

const trackEvent = async () => {
  const client = new PersonalizeEvents({
    region: 'ap-northeast-1'
  })
  const params: PersonalizeEvents.Types.PutEventsRequest = {
    trackingId: 'xxx-xxx-xxx-xxx',
    userId: '1',
    sessionId: v4(),
    eventList: [{
      eventId: v4(),
      sentAt: moment().toDate(),
      eventType: 'rating',
      properties: {
        itemId: '1',
        eventValue: 1.0
      }
    }]
  }
  console.log(params)
  const result = await client.putEvents(params).promise()
  console.log(result)
  return
}
trackEvent()
```

#### Code Sample2はTypeScriptの型エラーが発生する

```bash
$  ./node_modules/.bin/ts-node libs/index.ts 

/Users/develop/sandbox//node_modules/ts-node/src/index.ts:245
    return new TSError(diagnosticText, diagnosticCodes)
           ^
TSError: ⨯ Unable to compile TypeScript:
libs/index.ts:34:7 - error TS2322: Type '{ itemId: string; eventValue: number; }' is not assignable to type 'string'.

34       properties: {
         ~~~~~~~~~~

  node_modules/aws-sdk/clients/personalizeevents.d.ts:37:5
    37     properties: EventPropertiesJSON;
           ~~~~~~~~~~
    The expected type comes from property 'properties' which is declared here on type 'Event'
```

### Code Sample3: Code Sample2をanyで強行突破

```typescript
import {
  PersonalizeEvents,
} from 'aws-sdk'
import { v4 } from 'uuid'
import moment from 'moment'

const trackEvent = async () => {
  const client = new PersonalizeEvents({
    region: 'ap-northeast-1'
  })
  const params: PersonalizeEvents.Types.PutEventsRequest = {
    trackingId: 'xxx-xxx-xxx-xxx',
    userId: '1',
    sessionId: v4(),
    eventList: [{
      eventId: v4(),
      sentAt: moment().toDate(),
      eventType: 'rating',
      properties: {
        itemId: '1',
        eventValue: 1.0
      } as any as PersonalizeEvents.Types.EventPropertiesJSON
    }]
  }
  console.log(params)
  const result = await client.putEvents(params).promise()
  console.log(result)
  return
}
trackEvent()
```

#### Code Sample3はAPIバリデーションを通過する

```bash
$  ./node_modules/.bin/ts-node libs/index.ts 
{ trackingId: '52c88f57-7d7f-4941-8396-4c6a7434ec0d',
  userId: '1',
  sessionId: 'dcaf9abd-db3b-4793-a43e-6bb9ce6a1f9a',
  eventList:
   [ { eventId: 'b938051a-bf9c-4e3c-8c9a-b0453bee46e5',
       sentAt: 2019-08-25T14:59:26.639Z,
       eventType: 'rating',
       properties: [Object] } ] }
{}
```

## 問題

`eventList[].properties`の型がTypeScriptの定義とAPI側の想定とで不一致。

APIはObjectを想定しているが、TypeScriptの定義ファイルでは`string`で`JSON.stringify`したものを想定している。

## ワークアラウンド
https://github.com/aws/aws-sdk-js/pull/2821/files
のように型ファイルを書き換えれば一応動きます。
ただしnode_modules配下を直接触るのでnpm installやyarn addで消える可能性が非常に高いです。

現状以下のようにasを使ってTypeScriptのチェックを誤魔化す方法が無難そうです。

```typescript
const properties = {
        itemId: '1',
        eventValue: 1.0
} as any as PersonalizeEvents.Types.EventPropertiesJSON
```

## AWS Amplify SDKへの懸念


テストコードをみると、`putEvents`メソッドをモックしていることから、AWS Amplify SDKのAmazon Personalizeインテグレーションも内部で同じメソッドを使用している様子です。

https://github.com/aws-amplify/amplify-js/blob/d96eece301bf28a59ae320a83a18b8fc8f091e10/packages/analytics/__tests__/Providers/AmazonPersonalizeProvider-unit-test.ts

```typescript
jest.mock('aws-sdk/clients/personalizeevents', () => {
    const PersonalizeEvents = () => {
        var personalizeEvents = null;
        return personalizeEvents;
    }

    PersonalizeEvents.prototype.putEvents = (params, callback) => {
        callback(null, 'data');
    }

    return PersonalizeEvents;
});
```

こちらは試してないですが、もしAmplify SDK + TypeScriptでAmazon PersonalizeのputEventやろうとしてエラー踏んだ場合は、こいつを疑ってください。